nx_chacha20_wasm.nx source
↩ module page · 191 lines · 6607 B
1// nx_chacha20_wasm.nx -- ChaCha20 stream cipher (RFC 7539) for WAT target.
2//
3// State is a 4x4 matrix of 32-bit words:
4//
5// "expand 32-byte k" (constants 0..3)
6// key words 0..7 (key bytes 0..31)
7// counter (word 12)
8// nonce words 13..15 (nonce bytes 0..11)
9//
10// 20 rounds of quarter-round operations, then add original state,
11// produce 64 bytes of keystream per block. XOR keystream with
12// plaintext byte-for-byte to encrypt; same operation decrypts.
13//
14// API for the embedder:
15//
16// nx_chacha20_xor(key_ptr, nonce_ptr, counter,
17// in_ptr, in_len, scratch_ptr, out_ptr) -> i64
18// key_ptr -- 32 bytes (256-bit key)
19// nonce_ptr -- 12 bytes (96-bit nonce per RFC 7539)
20// counter -- starting block counter (1 for RFC 7539; 0 for some
21// flows; caller's choice)
22// in_ptr -- plaintext bytes
23// in_len -- input length
24// scratch_ptr -- >= 256 bytes; holds state + working matrix
25// out_ptr -- ciphertext bytes (may alias in_ptr for in-place)
26//
27// Memory layout of scratch_ptr:
28// bytes 0..63 : initial state (16 i32 words, little-endian packed)
29// bytes 64..127 : working state during one block compute
30// bytes 128..191 : keystream output bytes for one block
31// bytes 192.. : unused
32//
33// Verified against RFC 7539 §2.4.2 test vector.
34//
35// license_tier: INDEPENDENT_REDERIVE
36// genealogy_id: international-research-sources/ietf/rfc_7539
37// lineage_id: nishi_chacha20_wasm_q11
38
39const M32: i64 = 0xFFFFFFFF
40
41// Read a 32-bit little-endian word from byte buffer at offset.
42func _le32_read(buf: *u8, off: i64) -> i64 {
43 return ((buf[off] as i64) |
44 ((buf[off + 1] as i64) << 8) |
45 ((buf[off + 2] as i64) << 16) |
46 ((buf[off + 3] as i64) << 24)) & M32
47}
48
49// Write a 32-bit little-endian word to byte buffer at offset.
50func _le32_write(buf: *u8, off: i64, v: i64) -> i64 {
51 buf[off] = v & 0xFF
52 buf[off + 1] = (v >> 8) & 0xFF
53 buf[off + 2] = (v >> 16) & 0xFF
54 buf[off + 3] = (v >> 24) & 0xFF
55 return 0
56}
57
58// Left-rotate a 32-bit value by n bits.
59func _rotl32(x: i64, n: i64) -> i64 {
60 let nn: i64 = n & 31
61 let lo: i64 = (x << nn) & M32
62 let hi: i64 = (x >> (32 - nn)) & M32
63 return (lo | hi) & M32
64}
65
66// Quarter-round operating on 4 i32 lanes stored at given offsets in
67// the working state byte buffer. Each lane is a 4-byte LE i32.
68//
69// QR(a, b, c, d):
70// a += b; d ^= a; d <<<= 16
71// c += d; b ^= c; b <<<= 12
72// a += b; d ^= a; d <<<= 8
73// c += d; b ^= c; b <<<= 7
74func _qr(state: *u8, ai: i64, bi: i64, ci: i64, di: i64) -> i64 {
75 var a: i64 = _le32_read(state, ai * 4)
76 var b: i64 = _le32_read(state, bi * 4)
77 var c: i64 = _le32_read(state, ci * 4)
78 var d: i64 = _le32_read(state, di * 4)
79
80 a = (a + b) & M32; d = d ^ a; d = _rotl32(d, 16)
81 c = (c + d) & M32; b = b ^ c; b = _rotl32(b, 12)
82 a = (a + b) & M32; d = d ^ a; d = _rotl32(d, 8)
83 c = (c + d) & M32; b = b ^ c; b = _rotl32(b, 7)
84
85 _le32_write(state, ai * 4, a)
86 _le32_write(state, bi * 4, b)
87 _le32_write(state, ci * 4, c)
88 _le32_write(state, di * 4, d)
89 return 0
90}
91
92// Run 20 rounds (10 column-rounds + 10 diagonal-rounds) on the state
93// at scratch[64..127], starting from a freshly-copied state at
94// scratch[0..63]. Emit 64 bytes of keystream into scratch[128..191].
95func _chacha20_block(init: *u8, work: *u8, keystream: *u8) -> i64 {
96 // Copy initial state into working state.
97 var i: i64 = 0
98 while i < 64 { work[i] = init[i]; i = i + 1 }
99
100 // 10 iterations of (column-round + diagonal-round) = 20 rounds.
101 var r: i64 = 0
102 while r < 10 {
103 // Column rounds.
104 _qr(work, 0, 4, 8, 12)
105 _qr(work, 1, 5, 9, 13)
106 _qr(work, 2, 6, 10, 14)
107 _qr(work, 3, 7, 11, 15)
108 // Diagonal rounds.
109 _qr(work, 0, 5, 10, 15)
110 _qr(work, 1, 6, 11, 12)
111 _qr(work, 2, 7, 8, 13)
112 _qr(work, 3, 4, 9, 14)
113 r = r + 1
114 }
115
116 // Add the initial state to the working state, write as LE bytes
117 // into keystream.
118 var w: i64 = 0
119 while w < 16 {
120 let init_w: i64 = _le32_read(init, w * 4)
121 let work_w: i64 = _le32_read(work, w * 4)
122 let sum: i64 = (init_w + work_w) & M32
123 _le32_write(keystream, w * 4, sum)
124 w = w + 1
125 }
126 return 0
127}
128
129// Initialise the 64-byte ChaCha20 state matrix with constants + key +
130// counter + nonce.
131// state[0..15] = "expand 32-byte k" (4 LE constants)
132// state[16..47] = key (32 bytes)
133// state[48..51] = counter (LE i32)
134// state[52..63] = nonce (12 bytes)
135func _chacha20_init_state(init: *u8, key: *u8, nonce: *u8, counter: i64) -> i64 {
136 // RFC 7539 §2.3 constants: "expand 32-byte k"
137 // sigma[0] = 0x61707865 = 'expa'
138 // sigma[1] = 0x3320646e = 'nd 3'
139 // sigma[2] = 0x79622d32 = '2-by'
140 // sigma[3] = 0x6b206574 = 'te k'
141 _le32_write(init, 0, 0x61707865)
142 _le32_write(init, 4, 0x3320646e)
143 _le32_write(init, 8, 0x79622d32)
144 _le32_write(init, 12, 0x6b206574)
145 // Copy 32 bytes of key.
146 var i: i64 = 0
147 while i < 32 { init[16 + i] = key[i]; i = i + 1 }
148 // Counter (one LE i32).
149 _le32_write(init, 48, counter & M32)
150 // Copy 12 bytes of nonce.
151 var j: i64 = 0
152 while j < 12 { init[52 + j] = nonce[j]; j = j + 1 }
153 return 0
154}
155
156// Public: XOR `in_len` bytes from in_ptr with the ChaCha20 keystream
157// generated from (key, nonce, counter), writing to out_ptr.
158// in_ptr and out_ptr may alias (in-place encryption is safe).
159// Exported to JS as `nx_chacha20_xor`.
160func nx_chacha20_xor(key_ptr: *u8, nonce_ptr: *u8, counter: i64,
161 in_ptr: *u8, in_len: i64,
162 scratch_ptr: *u8, out_ptr: *u8) -> i64 {
163 let init: *u8 = scratch_ptr
164 let work: *u8 = (scratch_ptr as i64 + 64) as *u8
165 let ks: *u8 = (scratch_ptr as i64 + 128) as *u8
166
167 _chacha20_init_state(init, key_ptr, nonce_ptr, counter)
168
169 var pos: i64 = 0
170 var blk: i64 = counter
171 while pos < in_len {
172 // Update counter in state for this block.
173 _le32_write(init, 48, blk & M32)
174 _chacha20_block(init, work, ks)
175
176 // XOR up to 64 bytes from ks with in_ptr -> out_ptr.
177 var b: i64 = 0
178 while b < 64 {
179 if pos + b >= in_len { b = 64 }
180 else {
181 let pb: i64 = in_ptr[pos + b]
182 let kb: i64 = ks[b]
183 out_ptr[pos + b] = (pb ^ kb) & 0xFF
184 b = b + 1
185 }
186 }
187 pos = pos + 64
188 blk = blk + 1
189 }
190 return 0
191}