code wiki / hub / nx_bip39.nx

nx_bip39.nx source

↩ module page · 257 lines · 9299 B

1// nx_bip39.nx -- V-MODAUTH-3: BIP39 mnemonic encode/decode (the recovery-phrase rung). 2// 3// BIP39: entropy (ENT bits, 128..256, multiple of 32) + checksum (first ENT/32 bits 4// of SHA-256(entropy)) -> (ENT+CS)/11 words from the 2048-word English list. 5// 24 words = 256-bit entropy = what nx_modern_auth_register issues as the 6// forgot-passphrase credential (the mnemonic entropy keys a SECOND OPAQUE record, 7// so recovery stores nothing offline-crackable either -- exceeds the charter's 8// recovery_hash sketch). 9// 10// Decode validates the checksum: a mistyped word fails loudly (1-in-2^CS miss rate 11// per BIP39 design; 256 for 24 words). 12// 13// COMPOSES: nx_bip39_wordlist (generated from canonical bitcoin/bips english.txt, 14// sha256-verified at staging), sha256, nx_csprng 15// COMPOSED BY: hub/nx_modern_auth_flow (register issues, recover redeems) 16// SPEC: BIP-0039 (bitcoin/bips); gate vectors = trezor/python-mnemonic vectors.json 17// license_tier: ORIGINAL 18import "nx_syscalls.nx" 19import "nx_csprng.nx" 20import "sha256.nx" 21import "nx_bip39_wordlist.nx" 22 23// ===== Sealed verdict surface (codes 1480-1489) ================================================= 24const NX_B39_OK: i64 = 0 25const NX_B39_BAD_INPUT: i64 = 1480 26const NX_B39_BUF_OVERFLOW: i64 = 1481 27const NX_B39_UNKNOWN_WORD: i64 = 1482 28const NX_B39_BAD_CHECKSUM: i64 = 1483 29const NX_B39_BAD_WORD_COUNT: i64 = 1484 30 31const NX_B39_MAX_ENT_BYTES: i64 = 32 // 256-bit entropy = 24 words 32const NX_B39_MIN_ENT_BYTES: i64 = 16 // 128-bit entropy = 12 words 33const NX_B39_MAX_WORDS: i64 = 24 34const NX_B39_MAX_MNEMONIC_LEN: i64 = 256 // 24 * (8+1) worst case + slack 35 36// ===== word table access ================================================= 37 38// copy word at index 0..2047 into out; returns word length, or negative. 39func nx_b39_word_at(idx: i64, out: *u8, out_cap: i64) -> i64 { 40 if idx < 0 { return 0 - NX_B39_BAD_INPUT } 41 if idx > 2047 { return 0 - NX_B39_BAD_INPUT } 42 let chunk: *u8 = _b39_chunk(idx / 32) 43 let want: i64 = idx % 32 44 // walk to the want-th space-separated word 45 var p: i64 = 0 46 var w: i64 = 0 47 while w < want { 48 while chunk[p] != (32 as u8) { p = p + 1 } 49 p = p + 1 50 w = w + 1 51 } 52 var n: i64 = 0 53 var go: i64 = 1 54 while go == 1 { 55 let c: i64 = chunk[p + n] as i64 56 if c == 32 { go = 0 } 57 if c == 0 { go = 0 } 58 if go == 1 { 59 if n >= out_cap { return 0 - NX_B39_BUF_OVERFLOW } 60 out[n] = c as u8 61 n = n + 1 62 } 63 } 64 return n 65} 66 67// find index of word[0..n) in the table; -1 if unknown. Linear scan over the 68// 64 chunks (2048 short string compares; recovery-frequency path, microseconds). 69func nx_b39_word_index(word: *u8, word_n: i64) -> i64 { 70 if word_n < 1 { return 0 - 1 } 71 if word_n > 8 { return 0 - 1 } 72 var c: i64 = 0 73 while c < 64 { 74 let chunk: *u8 = _b39_chunk(c) 75 var p: i64 = 0 76 var w: i64 = 0 77 while w < 32 { 78 // current word spans [p, p+len) 79 var len: i64 = 0 80 var go: i64 = 1 81 while go == 1 { 82 let ch: i64 = chunk[p + len] as i64 83 if ch == 32 { go = 0 } 84 if ch == 0 { go = 0 } 85 if go == 1 { len = len + 1 } 86 } 87 if len == word_n { 88 var m: i64 = 1 89 var i: i64 = 0 90 while i < len { 91 if (chunk[p + i] as i64) != (word[i] as i64) { m = 0; i = len } 92 if i < len { i = i + 1 } 93 } 94 if m == 1 { return c * 32 + w } 95 } 96 p = p + len + 1 97 w = w + 1 98 } 99 c = c + 1 100 } 101 return 0 - 1 102} 103 104// ===== bit helpers ================================================= 105// 106// BIP39 bitstream = entropy bytes MSB-first, then checksum bits MSB-first. 107// Word index i = bits [11i, 11i+11). 108 109func _b39_bit_at(entropy: *u8, cs_byte: i64, ent_bytes: i64, bit: i64) -> i64 { 110 let byte_idx: i64 = bit / 8 111 let bit_in: i64 = 7 - (bit % 8) 112 var b: i64 = 0 113 if byte_idx < ent_bytes { b = entropy[byte_idx] as i64 } 114 if byte_idx >= ent_bytes { b = cs_byte } 115 return (b >> bit_in) & 1 116} 117 118// ===== encode: entropy -> mnemonic ================================================= 119// 120// out_str receives space-joined lowercase words; out_n the length. 121 122func nx_bip39_encode( 123 entropy: *u8, ent_bytes: i64, 124 out_str: *u8, out_cap: i64, 125 out_n: *i64 126) -> i64 { 127 if (entropy as i64) == 0 { return 0 - NX_B39_BAD_INPUT } 128 if (out_str as i64) == 0 { return 0 - NX_B39_BAD_INPUT } 129 if (out_n as i64) == 0 { return 0 - NX_B39_BAD_INPUT } 130 if ent_bytes < NX_B39_MIN_ENT_BYTES { return 0 - NX_B39_BAD_INPUT } 131 if ent_bytes > NX_B39_MAX_ENT_BYTES { return 0 - NX_B39_BAD_INPUT } 132 if (ent_bytes % 4) != 0 { return 0 - NX_B39_BAD_INPUT } 133 134 let hash: *u8 = sys_mmap(32) 135 sha256_digest(entropy, ent_bytes, hash) 136 let cs_byte: i64 = hash[0] as i64 137 let cs_bits: i64 = (ent_bytes * 8) / 32 138 let total_bits: i64 = ent_bytes * 8 + cs_bits 139 let n_words: i64 = total_bits / 11 140 141 var pos: i64 = 0 142 var w: i64 = 0 143 while w < n_words { 144 var idx: i64 = 0 145 var b: i64 = 0 146 while b < 11 { 147 idx = (idx << 1) | _b39_bit_at(entropy, cs_byte, ent_bytes, w * 11 + b) 148 b = b + 1 149 } 150 if w > 0 { 151 if pos + 1 > out_cap { return 0 - NX_B39_BUF_OVERFLOW } 152 out_str[pos] = 32 as u8 153 pos = pos + 1 154 } 155 let wn: i64 = nx_b39_word_at(idx, (out_str as i64 + pos) as *u8, out_cap - pos) 156 if wn < 0 { return wn } 157 pos = pos + wn 158 w = w + 1 159 } 160 out_n[0] = pos 161 return NX_B39_OK 162} 163 164// random 24-word mnemonic (256-bit entropy) + the entropy itself. 165func nx_bip39_generate_24( 166 out_entropy_32: *u8, 167 out_str: *u8, out_cap: i64, 168 out_n: *i64 169) -> i64 { 170 if (out_entropy_32 as i64) == 0 { return 0 - NX_B39_BAD_INPUT } 171 if nx_csprng_fill(out_entropy_32, 32) != 0 { return 0 - NX_B39_BAD_INPUT } 172 return nx_bip39_encode(out_entropy_32, 32, out_str, out_cap, out_n) 173} 174 175// ===== decode: mnemonic -> entropy ================================================= 176// 177// Whitespace-tolerant (any run of spaces); words must be lowercase exact. 178// Verifies the checksum; BAD_CHECKSUM on any bit wrong. 179 180func nx_bip39_decode( 181 mnemonic: *u8, mnemonic_n: i64, 182 out_entropy: *u8, out_entropy_cap: i64, 183 out_ent_bytes: *i64 184) -> i64 { 185 if (mnemonic as i64) == 0 { return 0 - NX_B39_BAD_INPUT } 186 if mnemonic_n < 1 { return 0 - NX_B39_BAD_INPUT } 187 if mnemonic_n > NX_B39_MAX_MNEMONIC_LEN { return 0 - NX_B39_BAD_INPUT } 188 if (out_entropy as i64) == 0 { return 0 - NX_B39_BAD_INPUT } 189 if (out_ent_bytes as i64) == 0 { return 0 - NX_B39_BAD_INPUT } 190 191 // ---- split words -> indices ---- 192 let idxs: *i64 = sys_mmap(NX_B39_MAX_WORDS * 8) as *i64 193 var n_words: i64 = 0 194 var p: i64 = 0 195 while p < mnemonic_n { 196 // skip spaces 197 var go_s: i64 = 1 198 while go_s == 1 { 199 if p >= mnemonic_n { go_s = 0 } 200 if go_s == 1 { if (mnemonic[p] as i64) != 32 { go_s = 0 } } 201 if go_s == 1 { p = p + 1 } 202 } 203 if p < mnemonic_n { 204 let start: i64 = p 205 var go_w: i64 = 1 206 while go_w == 1 { 207 if p >= mnemonic_n { go_w = 0 } 208 if go_w == 1 { if (mnemonic[p] as i64) == 32 { go_w = 0 } } 209 if go_w == 1 { p = p + 1 } 210 } 211 if n_words >= NX_B39_MAX_WORDS { return 0 - NX_B39_BAD_WORD_COUNT } 212 let idx: i64 = nx_b39_word_index((mnemonic as i64 + start) as *u8, p - start) 213 if idx < 0 { return 0 - NX_B39_UNKNOWN_WORD } 214 idxs[n_words] = idx 215 n_words = n_words + 1 216 } 217 } 218 219 // valid counts: 12, 15, 18, 21, 24 220 var ent_bytes: i64 = 0 221 if n_words == 12 { ent_bytes = 16 } 222 if n_words == 15 { ent_bytes = 20 } 223 if n_words == 18 { ent_bytes = 24 } 224 if n_words == 21 { ent_bytes = 28 } 225 if n_words == 24 { ent_bytes = 32 } 226 if ent_bytes == 0 { return 0 - NX_B39_BAD_WORD_COUNT } 227 if ent_bytes > out_entropy_cap { return 0 - NX_B39_BUF_OVERFLOW } 228 229 // ---- bitstream -> entropy bytes + checksum bits ---- 230 let cs_bits: i64 = (ent_bytes * 8) / 32 231 let total_bits: i64 = ent_bytes * 8 + cs_bits 232 var i: i64 = 0 233 while i < ent_bytes { out_entropy[i] = 0 as u8; i = i + 1 } 234 var cs_got: i64 = 0 235 var bit: i64 = 0 236 while bit < total_bits { 237 let w: i64 = bit / 11 238 let b_in: i64 = 10 - (bit % 11) 239 let v: i64 = (idxs[w] >> b_in) & 1 240 if bit < ent_bytes * 8 { 241 let by: i64 = bit / 8 242 let sh: i64 = 7 - (bit % 8) 243 out_entropy[by] = ((out_entropy[by] as i64) | (v << sh)) as u8 244 } 245 if bit >= ent_bytes * 8 { cs_got = (cs_got << 1) | v } 246 bit = bit + 1 247 } 248 249 // ---- verify checksum ---- 250 let hash: *u8 = sys_mmap(32) 251 sha256_digest(out_entropy, ent_bytes, hash) 252 let cs_want: i64 = (hash[0] as i64) >> (8 - cs_bits) 253 if cs_got != cs_want { return 0 - NX_B39_BAD_CHECKSUM } 254 255 out_ent_bytes[0] = ent_bytes 256 return NX_B39_OK 257}