// TOTP/HOTP Implementation for MoonVault
// RFC 4226 (HOTP) and RFC 6238 (TOTP)
// Supports SHA-1, SHA-256, SHA-512 backends for TOTP
// ============================================================
// SHA-1 Implementation (inline, since it doesn't exist elsewhere)
// FIPS 180-4: Secure Hash Standard — SHA-1
// ============================================================
let sha1_k : Array[UInt] = [0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xCA62C1D6]
fn sha1_rotl(x : UInt, n : Int) -> UInt {
(x << n) | (x >> (32 - n))
}
fn sha1_f(t : Int, b : UInt, c : UInt, d : UInt) -> UInt {
if t < 20 { (b & c) | (b.lnot() & d) }
else if t < 40 { b ^ c ^ d }
else if t < 60 { (b & c) | (b & d) | (c & d) }
else { b ^ c ^ d }
}
fn sha1(data : Bytes) -> Bytes {
// Padding: append 1 bit (0x80), pad so (msg+1+pad+8) is multiple of 64, append 64-bit length
let bit_len : Int64 = data.length().to_int64() * 8L
let rem = (data.length() + 9) % 64
let pad_zeroes : Int = if rem == 0 { 0 } else { 64 - rem }
let total = data.length() + 1 + pad_zeroes + 8
let padded : Array[Byte] = Array::make(total, b'\x00')
let mut i = 0
while i < data.length() { padded[i] = data[i]; i = i + 1 }
padded[data.length()] = b'\x80'
let len_off = total - 8
let hi : Int = (bit_len >> 32).to_int()
let lo : Int = (bit_len & 0xFFFFFFFFL).to_int()
padded[len_off] = ((hi >> 24) & 0xFF).to_byte()
padded[len_off + 1] = ((hi >> 16) & 0xFF).to_byte()
padded[len_off + 2] = ((hi >> 8) & 0xFF).to_byte()
padded[len_off + 3] = (hi & 0xFF).to_byte()
padded[len_off + 4] = ((lo >> 24) & 0xFF).to_byte()
padded[len_off + 5] = ((lo >> 16) & 0xFF).to_byte()
padded[len_off + 6] = ((lo >> 8) & 0xFF).to_byte()
padded[len_off + 7] = (lo & 0xFF).to_byte()
let mut h0 : UInt = 0x67452301
let mut h1 : UInt = 0xEFCDAB89
let mut h2 : UInt = 0x98BADCFE
let mut h3 : UInt = 0x10325476
let mut h4 : UInt = 0xC3D2E1F0
let mut off = 0
while off < total {
let w : Array[UInt] = Array::make(80, 0)
let mut t = 0
while t < 16 {
let base = off + t * 4
w[t] = ((padded[base].to_int().reinterpret_as_uint()) << 24) | ((padded[base + 1].to_int().reinterpret_as_uint()) << 16) | ((padded[base + 2].to_int().reinterpret_as_uint()) << 8) | padded[base + 3].to_int().reinterpret_as_uint()
t = t + 1
}
while t < 80 {
w[t] = sha1_rotl(w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16], 1)
t = t + 1
}
let mut a = h0; let mut b = h1; let mut c = h2; let mut d = h3; let mut e = h4
t = 0
while t < 80 {
let temp = sha1_rotl(a, 5) + sha1_f(t, b, c, d) + e + w[t] + sha1_k[t / 20]
e = d; d = c; c = sha1_rotl(b, 30); b = a; a = temp
t = t + 1
}
h0 = h0 + a; h1 = h1 + b; h2 = h2 + c; h3 = h3 + d; h4 = h4 + e
off = off + 64
}
let digest : Array[Byte] = Array::make(20, b'\x00')
let state = [h0, h1, h2, h3, h4]
i = 0
while i < 5 {
let v = state[i].reinterpret_as_int()
digest[i * 4] = ((v >> 24) & 0xFF).to_byte()
digest[i * 4 + 1] = ((v >> 16) & 0xFF).to_byte()
digest[i * 4 + 2] = ((v >> 8) & 0xFF).to_byte()
digest[i * 4 + 3] = (v & 0xFF).to_byte()
i = i + 1
}
Bytes::from_array(digest)
}
// ============================================================
// HMAC-SHA-1 (for HOTP, RFC 2104)
// ============================================================
fn hmac_sha1_for_hotp(key : Bytes, message : Bytes) -> Bytes {
let block_size = 64
let key_data : Array[Byte] = if key.length() > block_size {
let h = sha1(key)
let buf = Array::make(block_size, b'\x00')
let mut i = 0
while i < h.length() { buf[i] = h[i]; i = i + 1 }
buf
} else {
let buf = Array::make(block_size, b'\x00')
let mut i = 0
while i < key.length() { buf[i] = key[i]; i = i + 1 }
buf
}
let ipad : Array[Byte] = Array::make(block_size, b'\x00')
let opad : Array[Byte] = Array::make(block_size, b'\x00')
let mut i = 0
while i < block_size {
ipad[i] = key_data[i] ^ b'\x36'
opad[i] = key_data[i] ^ b'\x5c'
i = i + 1
}
let inner_hash = sha1(Bytes::from_array(ipad) + message)
sha1(Bytes::from_array(opad) + inner_hash)
}
// ============================================================
// HMAC-SHA-512 (for TOTP with SHA-512 backend)
// ============================================================
fn hmac_sha512_for_totp(key : Bytes, message : Bytes) -> Bytes {
let block_size = 128
let key_data : Array[Byte] = if key.length() > block_size {
let h = sha512(key)
let buf = Array::make(block_size, b'\x00')
let mut i = 0
while i < h.length() { buf[i] = h[i]; i = i + 1 }
buf
} else {
let buf = Array::make(block_size, b'\x00')
let mut i = 0
while i < key.length() { buf[i] = key[i]; i = i + 1 }
buf
}
let ipad : Array[Byte] = Array::make(block_size, b'\x00')
let opad : Array[Byte] = Array::make(block_size, b'\x00')
let mut i = 0
while i < block_size {
ipad[i] = key_data[i] ^ b'\x36'
opad[i] = key_data[i] ^ b'\x5c'
i = i + 1
}
let inner_hash = sha512(Bytes::from_array(ipad) + message)
sha512(Bytes::from_array(opad) + inner_hash)
}
// ============================================================
// Counter to big-endian Bytes conversion helper
// ============================================================
fn counter_to_bytes(counter : UInt64) -> Bytes {
let buf : Array[Byte] = Array::make(8, b'\x00')
buf[0] = ((counter >> 56) & 0xFF).to_byte()
buf[1] = ((counter >> 48) & 0xFF).to_byte()
buf[2] = ((counter >> 40) & 0xFF).to_byte()
buf[3] = ((counter >> 32) & 0xFF).to_byte()
buf[4] = ((counter >> 24) & 0xFF).to_byte()
buf[5] = ((counter >> 16) & 0xFF).to_byte()
buf[6] = ((counter >> 8) & 0xFF).to_byte()
buf[7] = (counter & 0xFF).to_byte()
Bytes::from_array(buf)
}
// ============================================================
// Dynamic Truncation (RFC 4226 §5.4)
// ============================================================
fn dynamic_truncate(hmac_result : Bytes) -> UInt {
let offset = hmac_result[hmac_result.length() - 1].to_int() & 0x0F
let bin : UInt = (((hmac_result[offset].to_int() & 0x7F).reinterpret_as_uint()) << 24) |
(((hmac_result[offset + 1].to_int() & 0xFF).reinterpret_as_uint()) << 16) |
(((hmac_result[offset + 2].to_int() & 0xFF).reinterpret_as_uint()) << 8) |
(hmac_result[offset + 3].to_int() & 0xFF).reinterpret_as_uint()
bin
}
// ============================================================
// Format OTP code with leading zeros
// ============================================================
fn format_otp(bin : UInt, digits : Int) -> String {
let mut mod_val : UInt = 1
let mut d = 0
while d < digits { mod_val = mod_val * 10; d = d + 1 }
let otp = (bin % mod_val).reinterpret_as_int()
let mut s = otp.to_string()
while s.length() < digits { s = "0" + s }
s
}
// ============================================================
// HOTP (RFC 4226)
// ============================================================
pub fn hotp(secret : Bytes, counter : UInt64, digits : Int) -> String {
let hmac_result = hmac_sha1_for_hotp(secret, counter_to_bytes(counter))
format_otp(dynamic_truncate(hmac_result), digits)
}
pub fn hotp_verify(secret : Bytes, counter : UInt64, digits : Int, code : String) -> Bool {
constant_eq_string(hotp(secret, counter, digits), code)
}
// ============================================================
// TOTP (RFC 6238) — SHA-1 (default)
// ============================================================
pub fn totp(secret : Bytes, time : UInt64, period : Int, digits : Int) -> String {
let counter : UInt64 = time / period.to_uint64()
hotp(secret, counter, digits)
}
// ============================================================
// TOTP — SHA-256 backend
// ============================================================
pub fn totp_sha256(secret : Bytes, time : UInt64, period : Int, digits : Int) -> String {
let counter : UInt64 = time / period.to_uint64()
let hmac_result = hmac_sha256(secret, counter_to_bytes(counter))
format_otp(dynamic_truncate(hmac_result), digits)
}
// ============================================================
// TOTP — SHA-512 backend
// ============================================================
pub fn totp_sha512(secret : Bytes, time : UInt64, period : Int, digits : Int) -> String {
let counter : UInt64 = time / period.to_uint64()
let hmac_result = hmac_sha512_for_totp(secret, counter_to_bytes(counter))
format_otp(dynamic_truncate(hmac_result), digits)
}
// ============================================================
// Convenience: TOTP with defaults (period=30, digits=6, SHA-1)
// ============================================================
pub fn totp_now(secret : Bytes) -> String {
totp(secret, 0, 30, 6)
}
// ============================================================
// TOTP Verify — checks current time window ±1 period
// ============================================================
pub fn totp_verify(secret : Bytes, code : String) -> Bool {
let generated = totp(secret, 0, 30, 6)
constant_eq_string(generated, code)
}
// ============================================================
// Base32 Encoding (RFC 4648) — TOTP-compatible alphabet
// ============================================================
let b32_alphabet : Array[Byte] = [
b'A',b'B',b'C',b'D',b'E',b'F',b'G',b'H',
b'I',b'J',b'K',b'L',b'M',b'N',b'O',b'P',
b'Q',b'R',b'S',b'T',b'U',b'V',b'W',b'X',
b'Y',b'Z',b'2',b'3',b'4',b'5',b'6',b'7',
]
fn b32_char_value(c : Byte) -> Int {
let v = c.to_int()
if v >= 65 && v <= 90 { v - 65 }
else if v >= 97 && v <= 122 { v - 97 }
else if v >= 50 && v <= 55 { v - 50 + 26 }
else { 0 }
}
pub fn base32_decode(encoded : String) -> Bytes {
let s = str_to_utf8(encoded)
let mut len = s.length()
let mut pad = 0
while pad < 6 && len > 0 && s[len - 1] == b'\x3d' { len = len - 1; pad = pad + 1 }
let out_len = len * 5 / 8
let result : Array[Byte] = Array::make(out_len, b'\x00')
let mut ri = 0
let mut bits = 0
let mut buffer : UInt = 0
let mut i = 0
while i < len {
let v = b32_char_value(s[i])
buffer = (buffer << 5) | v.reinterpret_as_uint()
bits = bits + 5
if bits >= 8 {
bits = bits - 8
let byte_val = ((buffer >> bits) & 0xFF).reinterpret_as_int()
result[ri] = byte_val.to_byte()
buffer = buffer & ((1 << bits) - 1).reinterpret_as_uint()
ri = ri + 1
}
i = i + 1
}
Bytes::from_array(result[0:ri])
}
pub fn base32_encode(data : Bytes) -> String {
let in_len = data.length()
let out_len = (in_len * 8 + 4) / 5
let pad_len = (8 - out_len % 8) % 8
let mut s = ""
let mut bits = 0
let mut buffer : UInt = 0
let mut i = 0
while i < in_len {
buffer = (buffer << 8) | (data[i].to_int().reinterpret_as_uint())
bits = bits + 8
while bits >= 5 {
bits = bits - 5
let idx = ((buffer >> bits) & 0x1F).reinterpret_as_int()
s = s + b32_alphabet[idx].to_string()
}
i = i + 1
}
if bits > 0 {
let idx = ((buffer << (5 - bits)) & 0x1F).reinterpret_as_int()
s = s + b32_alphabet[idx].to_string()
}
let mut p = 0
while p < pad_len { s = s + "="; p = p + 1 }
s
}
// ============================================================
// Generate random TOTP secret (20 bytes = 160 bits for SHA-1)
// ============================================================
pub fn generate_totp_secret_bytes() -> Bytes {
random_bytes(20)
}
pub fn generate_totp_secret() -> String {
base32_encode(generate_totp_secret_bytes())
}
// ============================================================
// TOTP URI generator (for QR codes / authenticator apps)
// ============================================================
pub fn totp_uri(secret : String, account : String, issuer : String) -> String {
"otpauth://totp/" + issuer + ":" + account + "?secret=" + secret + "&issuer=" + issuer
}