code wiki / (root) / nx_chacha20_wasm.nx

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}