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}