code wiki / (root) / totp_uri.nx

totp_uri.nx source

↩ module page · 176 lines · 6055 B

1// totp_uri.nx -- build otpauth:// enrollment URIs. 2// 3// Google Authenticator Key URI Format (de-facto standard, used by 4// Authy / 1Password / Microsoft Authenticator / Duo). A QR code 5// encodes this URI; scanning provisions a new OTP account. 6// 7// Shape: 8// otpauth://totp/Label?secret=BASE32SECRET&issuer=Name&algorithm=SHA1 9// &digits=6&period=30 10// 11// Label is typically \"Issuer:user@example.com\" (colon-separated). 12// Secret is a base32-encoded arbitrary-length key -- 20 bytes is 13// the RFC 6238 recommended length for SHA-1. 14// 15// Composes base32.nx (for secret encoding) + url.nx (for 16// percent-encoding the label + issuer). 17// 18// Invariants: 19// TU1 Secret bytes are base32-encoded with '=' padding stripped 20// (most authenticator apps tolerate padding but some 21// reject it). 22// TU2 Label / issuer percent-encoded so `@`, spaces, `:` are 23// safe in the URI. 24// TU3 Default params (digits=6, period=30, algo=SHA1) match 25// what Google Authenticator assumes on missing params. 26// We emit them anyway for clarity + compat with strict 27// parsers. 28 29import "syscalls.nx" 30import "base32.nx" 31import "url.nx" 32 33const TU_ERR_SHORT: i64 = -1 34 35// Append literal bytes. Returns new offset. 36func tu_put(out: *u8, cap: i64, off: i64, src: *u8, n: i64) -> i64 { 37 if off + n > cap { return TU_ERR_SHORT } 38 var i: i64 = 0 39 while i < n { 40 out[off + i] = src[i] 41 i = i + 1 42 } 43 return off + n 44} 45 46// Percent-encode and append. Uses url_encode_component (full 47// reserved-char escape). 48func tu_put_enc(out: *u8, cap: i64, off: i64, 49 src: *u8, n: i64) -> i64 { 50 // Bound: worst-case each byte encodes to %HH (3x). Bail if 51 // insufficient room; conservative check so we can pass the 52 // caller buffer tail safely. 53 if off + n * 3 > cap { return TU_ERR_SHORT } 54 let w: i64 = url_encode_component(src, n, out + off) 55 if w < 0 { return TU_ERR_SHORT } 56 return off + w 57} 58 59// Emit a decimal integer. Used for digits + period. 60func tu_put_dec(out: *u8, cap: i64, off: i64, v: i64) -> i64 { 61 if v == 0 { 62 if off + 1 > cap { return TU_ERR_SHORT } 63 out[off] = 0x30 64 return off + 1 65 } 66 let digits_raw: *u8 = sys_mmap(32) 67 var tmp: i64 = v 68 var n: i64 = 0 69 while tmp > 0 { 70 digits_raw[n] = 0x30 + (tmp % 10) 71 tmp = tmp / 10 72 n = n + 1 73 } 74 if off + n > cap { return TU_ERR_SHORT } 75 var i: i64 = n - 1 76 var cur: i64 = off 77 while i >= 0 { 78 out[cur] = digits_raw[i] 79 cur = cur + 1 80 i = i - 1 81 } 82 return cur 83} 84 85// Build an otpauth://totp URI. 86// 87// label_bytes: typically "Issuer:user@example.com" 88// secret_bytes: raw secret (NOT base32; encoded internally) 89// issuer_bytes: optional brand name; pass 0-length to skip 90// digits: typically 6 91// period: typically 30 seconds 92// 93// Caller supplies ASCII/UTF-8 label + issuer as raw bytes; we 94// percent-encode them. algorithm is hardcoded SHA1 (the default 95// Google Authenticator uses; SHA256/SHA512 variants are available 96// via separate builders). 97func totp_uri_build(out: *u8, cap: i64, 98 label: *u8, label_len: i64, 99 secret: *u8, secret_len: i64, 100 issuer: *u8, issuer_len: i64, 101 digits: i64, period: i64) -> i64 { 102 var cur: i64 = 0 103 cur = tu_put(out, cap, cur, "otpauth://totp/", 15) 104 if cur < 0 { return cur } 105 cur = tu_put_enc(out, cap, cur, label, label_len) 106 if cur < 0 { return cur } 107 cur = tu_put(out, cap, cur, "?secret=", 8) 108 if cur < 0 { return cur } 109 110 // Base32-encode the secret directly into out[cur..]. The 111 // base32 encoder writes into a caller-provided buffer. We 112 // need to compute the length first so we can check cap. 113 // base32 is 8 chars per 5 bytes; ceil(secret_len/5)*8 with 114 // padding; we skip padding below. 115 let sec_scratch: *u8 = sys_mmap(secret_len * 2 + 16) 116 let sec_len: i64 = base32_encode(secret, secret_len, sec_scratch) 117 // Strip '=' padding. 118 var stripped: i64 = sec_len 119 while stripped > 0 { 120 if sec_scratch[stripped - 1] != 0x3D { break } 121 stripped = stripped - 1 122 } 123 cur = tu_put(out, cap, cur, sec_scratch, stripped) 124 if cur < 0 { return cur } 125 126 if issuer_len > 0 { 127 cur = tu_put(out, cap, cur, "&issuer=", 8) 128 if cur < 0 { return cur } 129 cur = tu_put_enc(out, cap, cur, issuer, issuer_len) 130 if cur < 0 { return cur } 131 } 132 133 cur = tu_put(out, cap, cur, "&algorithm=SHA1&digits=", 23) 134 if cur < 0 { return cur } 135 cur = tu_put_dec(out, cap, cur, digits) 136 if cur < 0 { return cur } 137 cur = tu_put(out, cap, cur, "&period=", 8) 138 if cur < 0 { return cur } 139 cur = tu_put_dec(out, cap, cur, period) 140 if cur < 0 { return cur } 141 142 return cur 143} 144 145// Compile-only smoke. 146func main() -> i64 { 147 let out: *u8 = sys_mmap(256) 148 let secret: *u8 = "12345678901234567890" // 20 bytes 149 let n: i64 = totp_uri_build(out, 256, 150 "Nishi:elder", 11, 151 secret, 20, 152 "Nishi", 5, 153 6, 30) 154 if n <= 0 { return 1 } 155 // URI must start with "otpauth://totp/". 156 if out[0] != 0x6F { return 2 } // 'o' 157 if out[7] != 0x2F { return 3 } // '/' 158 if out[14] != 0x2F { return 4 } // '/' 159 160 // Secret query param must appear. 161 var i: i64 = 0 162 var found_secret: i64 = 0 163 while i < n - 7 { 164 if out[i] == 0x3F { // '?' 165 if out[i+1] == 0x73 { // 's' 166 if out[i+2] == 0x65 { // 'e' 167 found_secret = 1 168 break 169 } 170 } 171 } 172 i = i + 1 173 } 174 if found_secret != 1 { return 5 } 175 return 0 176}