code wiki / (root) / nx_totp_uri.nx

nx_totp_uri.nx source

↩ module page · 182 lines · 6149 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 29// nx_safety_envelope: 30// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 31// sil_target: SIL1 32// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 33// verdict: NOT_YET_EVALUATED 34 35import "nx_syscalls.nx" 36import "nx_base32.nx" 37import "nx_url_canonical.nx" 38 39const TU_ERR_SHORT: i64 = -1 40 41// Append literal bytes. Returns new offset. 42func tu_put(out: *u8, cap: i64, off: i64, src: *u8, n: i64) -> i64 { 43 if off + n > cap { return TU_ERR_SHORT } 44 var i: i64 = 0 45 while i < n { 46 out[off + i] = src[i] 47 i = i + 1 48 } 49 return off + n 50} 51 52// Percent-encode and append. Uses url_encode_component (full 53// reserved-char escape). 54func tu_put_enc(out: *u8, cap: i64, off: i64, 55 src: *u8, n: i64) -> i64 { 56 // Bound: worst-case each byte encodes to %HH (3x). Bail if 57 // insufficient room; conservative check so we can pass the 58 // caller buffer tail safely. 59 if off + n * 3 > cap { return TU_ERR_SHORT } 60 let w: i64 = url_encode_component(src, n, out + off) 61 if w < 0 { return TU_ERR_SHORT } 62 return off + w 63} 64 65// Emit a decimal integer. Used for digits + period. 66func tu_put_dec(out: *u8, cap: i64, off: i64, v: i64) -> i64 { 67 if v == 0 { 68 if off + 1 > cap { return TU_ERR_SHORT } 69 out[off] = 0x30 70 return off + 1 71 } 72 let digits_raw: *u8 = sys_mmap(32) 73 var tmp: i64 = v 74 var n: i64 = 0 75 while tmp > 0 { 76 digits_raw[n] = 0x30 + (tmp % 10) 77 tmp = tmp / 10 78 n = n + 1 79 } 80 if off + n > cap { return TU_ERR_SHORT } 81 var i: i64 = n - 1 82 var cur: i64 = off 83 while i >= 0 { 84 out[cur] = digits_raw[i] 85 cur = cur + 1 86 i = i - 1 87 } 88 return cur 89} 90 91// Build an otpauth://totp URI. 92// 93// label_bytes: typically "Issuer:user@example.com" 94// secret_bytes: raw secret (NOT base32; encoded internally) 95// issuer_bytes: optional brand name; pass 0-length to skip 96// digits: typically 6 97// period: typically 30 seconds 98// 99// Caller supplies ASCII/UTF-8 label + issuer as raw bytes; we 100// percent-encode them. algorithm is hardcoded SHA1 (the default 101// Google Authenticator uses; SHA256/SHA512 variants are available 102// via separate builders). 103func totp_uri_build(out: *u8, cap: i64, 104 label: *u8, label_len: i64, 105 secret: *u8, secret_len: i64, 106 issuer: *u8, issuer_len: i64, 107 digits: i64, period: i64) -> i64 { 108 var cur: i64 = 0 109 cur = tu_put(out, cap, cur, "otpauth://totp/", 15) 110 if cur < 0 { return cur } 111 cur = tu_put_enc(out, cap, cur, label, label_len) 112 if cur < 0 { return cur } 113 cur = tu_put(out, cap, cur, "?secret=", 8) 114 if cur < 0 { return cur } 115 116 // Base32-encode the secret directly into out[cur..]. The 117 // base32 encoder writes into a caller-provided buffer. We 118 // need to compute the length first so we can check cap. 119 // base32 is 8 chars per 5 bytes; ceil(secret_len/5)*8 with 120 // padding; we skip padding below. 121 let sec_scratch: *u8 = sys_mmap(secret_len * 2 + 16) 122 let sec_len: i64 = base32_encode(secret, secret_len, sec_scratch) 123 // Strip '=' padding. 124 var stripped: i64 = sec_len 125 while stripped > 0 { 126 if sec_scratch[stripped - 1] != 0x3D { break } 127 stripped = stripped - 1 128 } 129 cur = tu_put(out, cap, cur, sec_scratch, stripped) 130 if cur < 0 { return cur } 131 132 if issuer_len > 0 { 133 cur = tu_put(out, cap, cur, "&issuer=", 8) 134 if cur < 0 { return cur } 135 cur = tu_put_enc(out, cap, cur, issuer, issuer_len) 136 if cur < 0 { return cur } 137 } 138 139 cur = tu_put(out, cap, cur, "&algorithm=SHA1&digits=", 23) 140 if cur < 0 { return cur } 141 cur = tu_put_dec(out, cap, cur, digits) 142 if cur < 0 { return cur } 143 cur = tu_put(out, cap, cur, "&period=", 8) 144 if cur < 0 { return cur } 145 cur = tu_put_dec(out, cap, cur, period) 146 if cur < 0 { return cur } 147 148 return cur 149} 150 151// Compile-only smoke. 152func main() -> i64 { 153 let out: *u8 = sys_mmap(256) 154 let secret: *u8 = "12345678901234567890" // 20 bytes 155 let n: i64 = totp_uri_build(out, 256, 156 "Nishi:elder", 11, 157 secret, 20, 158 "Nishi", 5, 159 6, 30) 160 if n <= 0 { return 1 } 161 // URI must start with "otpauth://totp/". 162 if out[0] != 0x6F { return 2 } // 'o' 163 if out[7] != 0x2F { return 3 } // '/' 164 if out[14] != 0x2F { return 4 } // '/' 165 166 // Secret query param must appear. 167 var i: i64 = 0 168 var found_secret: i64 = 0 169 while i < n - 7 { 170 if out[i] == 0x3F { // '?' 171 if out[i+1] == 0x73 { // 's' 172 if out[i+2] == 0x65 { // 'e' 173 found_secret = 1 174 break 175 } 176 } 177 } 178 i = i + 1 179 } 180 if found_secret != 1 { return 5 } 181 return 0 182}