nx_argon2_block.nx source
↩ module page · 258 lines · 8937 B
1// nx_argon2_block.nx -- RFC 9106 sec 3.5/3.6 Argon2 G compression
2// + P permutation.
3//
4// The third and final brick in the Argon2id arc (before the
5// orchestrator that ties indexing + memory matrix together). G is
6// what gives Argon2 its memory hardness: it's intentionally slow on
7// GPU/ASIC because it requires 1024-byte block-at-a-time mixing
8// that can't be unrolled or pipelined deeply.
9//
10// ===== G compression function (RFC 9106 sec 3.5) =================
11//
12// G(X, Y) -> Z where X, Y, Z are each 1024-byte blocks.
13//
14// 1. R = X XOR Y (1024 bytes)
15// 2. Save R as R_save for the final XOR.
16// 3. View R as 8x8 matrix of u128 registers (each u128 = 2 u64).
17// Equivalently: 8 rows of 16 u64 words (a row = 128 bytes).
18// 4. Row-wise: apply P to each of 8 rows.
19// 5. Column-wise: apply P to each of 8 "columns" (gathered slice
20// of 16 u64 from rows i = 0..7 at columns 2j, 2j+1).
21// 6. Output Z = R XOR R_save.
22//
23// ===== P permutation (RFC 9106 sec 3.6) ==========================
24//
25// P operates on 16 u64 words v_0..v_15 in-place. Same call pattern
26// as BLAKE2b's round but using GB (modified G with 2*L(a)*L(b)
27// instead of message words):
28//
29// GB(v_0, v_4, v_8, v_12)
30// GB(v_1, v_5, v_9, v_13)
31// GB(v_2, v_6, v_10, v_14)
32// GB(v_3, v_7, v_11, v_15)
33// GB(v_0, v_5, v_10, v_15)
34// GB(v_1, v_6, v_11, v_12)
35// GB(v_2, v_7, v_8, v_13)
36// GB(v_3, v_4, v_9, v_14)
37//
38// ===== GB function (RFC 9106 sec 3.6) ============================
39//
40// GB(a, b, c, d):
41// a = (a + b + 2*L(a)*L(b)) mod 2^64
42// d = rotr64(d XOR a, 32)
43// c = (c + d + 2*L(c)*L(d)) mod 2^64
44// b = rotr64(b XOR c, 24)
45// a = (a + b + 2*L(a)*L(b)) mod 2^64
46// d = rotr64(d XOR a, 16)
47// c = (c + d + 2*L(c)*L(d)) mod 2^64
48// b = rotr64(b XOR c, 63)
49//
50// where L(x) = x mod 2^32 (lower 32 bits of x). The MULTIPLY
51// is critical -- it's what makes G expensive on hardware that
52// has weak 32-bit-integer multiplication (older GPUs). Modern
53// GPUs DO have fast int-mul but the memory-traffic pattern (a
54// different 1024-byte block per G call) is what blunts them.
55//
56// ===== Caller-owns-memory pattern ================================
57//
58// Caller supplies:
59// - r: 128 i64 array = 1024 bytes for R working space
60// - rsave: 128 i64 array = 1024 bytes for R_save
61// - col_tmp: 16 i64 array = column gather scratch
62//
63// license_tier: ORIGINAL
64
65// nx_safety_envelope:
66// intended_use: Argon2 G compression / P permutation
67// sil_target: SIL2
68// evidence: [g_x_x_returns_zero_block,
69// g_symmetric_x_y_equals_y_x,
70// determinism_two_runs_same_output,
71// regression_capture_known_input]
72// verdict: NOT_YET_EVALUATED (gating: cross-validate
73// against argon2_ref C output on a real Argon2id
74// end-to-end run, next session)
75
76import "nx_blake2b.nx" // for shared rotr64 / load/store helpers
77const NX_MAGIC_4294967295: i64 = 4294967295
78
79// ===== Verdict ====================================================
80
81const NX_A2B_OK: i64 = 0
82const NX_A2B_BAD_ARG: i64 = 1
83const NX_A2B_VERDICT_N: i64 = 2
84
85func nx_a2b_verdict_name(v: i64) -> *u8 {
86 if v == NX_A2B_OK { return "OK" }
87 if v == NX_A2B_BAD_ARG { return "BAD_ARG" }
88 return "UNKNOWN"
89}
90
91const NX_A2B_BLOCK_BYTES: i64 = 1024
92const NX_A2B_BLOCK_WORDS: i64 = 128 // 1024 / 8
93
94// ===== Local 64-bit helpers (avoid name-clashing across modules) ====
95
96func _a2b_shr64(x: i64, n: i64) -> i64 {
97 let nn: i64 = n & 63
98 if nn == 0 { return x }
99 let mask_lo: i64 = (1 << (64 - nn)) - 1
100 return (x >> nn) & mask_lo
101}
102
103func _a2b_rotr64(x: i64, n: i64) -> i64 {
104 let nn: i64 = n & 63
105 if nn == 0 { return x }
106 let lo: i64 = _a2b_shr64(x, nn)
107 let hi: i64 = x << (64 - nn)
108 return lo | hi
109}
110
111func _a2b_load_u64_le(buf: *u8, off: i64) -> i64 {
112 let b0: i64 = (buf[off + 0] as i64) & 255
113 let b1: i64 = (buf[off + 1] as i64) & 255
114 let b2: i64 = (buf[off + 2] as i64) & 255
115 let b3: i64 = (buf[off + 3] as i64) & 255
116 let b4: i64 = (buf[off + 4] as i64) & 255
117 let b5: i64 = (buf[off + 5] as i64) & 255
118 let b6: i64 = (buf[off + 6] as i64) & 255
119 let b7: i64 = (buf[off + 7] as i64) & 255
120 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
121 | (b4 << 32) | (b5 << 40) | (b6 << 48) | (b7 << 56)
122}
123
124func _a2b_store_u64_le(buf: *u8, off: i64, v: i64) -> i64 {
125 buf[off + 0] = (v & 255) as u8
126 buf[off + 1] = (_a2b_shr64(v, 8) & 255) as u8
127 buf[off + 2] = (_a2b_shr64(v, 16) & 255) as u8
128 buf[off + 3] = (_a2b_shr64(v, 24) & 255) as u8
129 buf[off + 4] = (_a2b_shr64(v, 32) & 255) as u8
130 buf[off + 5] = (_a2b_shr64(v, 40) & 255) as u8
131 buf[off + 6] = (_a2b_shr64(v, 48) & 255) as u8
132 buf[off + 7] = (_a2b_shr64(v, 56) & 255) as u8
133 return 0
134}
135
136// ===== GB function =================================================
137//
138// Operates in-place on v[ia], v[ib], v[ic], v[id].
139
140func _a2b_gb(v: *i64, ia: i64, ib: i64, ic: i64, id: i64) -> i64 {
141 let mask32: i64 = NX_MAGIC_4294967295 // 0xFFFFFFFF
142
143 // Step 1: a = a + b + 2*L(a)*L(b)
144 let la1: i64 = v[ia] & mask32
145 let lb1: i64 = v[ib] & mask32
146 let prod1: i64 = la1 * lb1
147 v[ia] = v[ia] + v[ib] + (prod1 << 1)
148 // d = rotr(d XOR a, 32)
149 v[id] = _a2b_rotr64(v[id] ^ v[ia], 32)
150 // Step 2: c = c + d + 2*L(c)*L(d)
151 let lc2: i64 = v[ic] & mask32
152 let ld2: i64 = v[id] & mask32
153 let prod2: i64 = lc2 * ld2
154 v[ic] = v[ic] + v[id] + (prod2 << 1)
155 // b = rotr(b XOR c, 24)
156 v[ib] = _a2b_rotr64(v[ib] ^ v[ic], 24)
157 // Step 3: a = a + b + 2*L(a)*L(b)
158 let la3: i64 = v[ia] & mask32
159 let lb3: i64 = v[ib] & mask32
160 let prod3: i64 = la3 * lb3
161 v[ia] = v[ia] + v[ib] + (prod3 << 1)
162 // d = rotr(d XOR a, 16)
163 v[id] = _a2b_rotr64(v[id] ^ v[ia], 16)
164 // Step 4: c = c + d + 2*L(c)*L(d)
165 let lc4: i64 = v[ic] & mask32
166 let ld4: i64 = v[id] & mask32
167 let prod4: i64 = lc4 * ld4
168 v[ic] = v[ic] + v[id] + (prod4 << 1)
169 // b = rotr(b XOR c, 63)
170 v[ib] = _a2b_rotr64(v[ib] ^ v[ic], 63)
171 return 0
172}
173
174// ===== P permutation on 16-word slice starting at base ============
175
176func _a2b_p(v: *i64, base: i64) -> i64 {
177 // Column step.
178 _a2b_gb(v, base + 0, base + 4, base + 8, base + 12)
179 _a2b_gb(v, base + 1, base + 5, base + 9, base + 13)
180 _a2b_gb(v, base + 2, base + 6, base + 10, base + 14)
181 _a2b_gb(v, base + 3, base + 7, base + 11, base + 15)
182 // Diagonal step.
183 _a2b_gb(v, base + 0, base + 5, base + 10, base + 15)
184 _a2b_gb(v, base + 1, base + 6, base + 11, base + 12)
185 _a2b_gb(v, base + 2, base + 7, base + 8, base + 13)
186 _a2b_gb(v, base + 3, base + 4, base + 9, base + 14)
187 return 0
188}
189
190// ===== G compression function =====================================
191//
192// nx_argon2_g(x, y, out, r, rsave, col_tmp) -> verdict
193//
194// All buffer pointers are 1024-byte blocks unless noted:
195// x, y, out: *u8, 1024 bytes each
196// r, rsave: *i64, 128 entries each (1024 bytes each)
197// col_tmp: *i64, 16 entries (column gather scratch)
198
199func nx_argon2_g(x: *u8, y: *u8, out: *u8,
200 r: *i64, rsave: *i64, col_tmp: *i64) -> i64 {
201 if x == (0 as *u8) { return NX_A2B_BAD_ARG }
202 if y == (0 as *u8) { return NX_A2B_BAD_ARG }
203 if out == (0 as *u8) { return NX_A2B_BAD_ARG }
204
205 // 1. Load R = X XOR Y as 128 i64 LE words.
206 var i: i64 = 0
207 while i < NX_A2B_BLOCK_WORDS {
208 let xi: i64 = _a2b_load_u64_le(x, i * 8)
209 let yi: i64 = _a2b_load_u64_le(y, i * 8)
210 r[i] = xi ^ yi
211 i = i + 1
212 }
213
214 // 2. Save R for final XOR.
215 i = 0
216 while i < NX_A2B_BLOCK_WORDS {
217 rsave[i] = r[i]
218 i = i + 1
219 }
220
221 // 3. Row-wise P (8 rows of 16 u64).
222 var row: i64 = 0
223 while row < 8 {
224 _a2b_p(r, row * 16)
225 row = row + 1
226 }
227
228 // 4. Column-wise P.
229 // Column j (0..7) consists of u64 indices [2j, 2j+1, 16+2j, 16+2j+1, ..., 112+2j, 112+2j+1].
230 // Gather, run P, scatter back.
231 var col: i64 = 0
232 while col < 8 {
233 var rr: i64 = 0
234 while rr < 8 {
235 col_tmp[rr * 2 + 0] = r[rr * 16 + col * 2 + 0]
236 col_tmp[rr * 2 + 1] = r[rr * 16 + col * 2 + 1]
237 rr = rr + 1
238 }
239 _a2b_p(col_tmp, 0)
240 rr = 0
241 while rr < 8 {
242 r[rr * 16 + col * 2 + 0] = col_tmp[rr * 2 + 0]
243 r[rr * 16 + col * 2 + 1] = col_tmp[rr * 2 + 1]
244 rr = rr + 1
245 }
246 col = col + 1
247 }
248
249 // 5. Z = R XOR R_save, write LE to out.
250 i = 0
251 while i < NX_A2B_BLOCK_WORDS {
252 let z: i64 = r[i] ^ rsave[i]
253 _a2b_store_u64_le(out, i * 8, z)
254 i = i + 1
255 }
256
257 return NX_A2B_OK
258}