carlos / contra Public

Clone
git clone https://amadan.net/carlos/contra

Plain git — no account needed to clone.

Download

Download this file

1// Argon2id (RFC 9106), in the browser, with no build step and no vendored
2// binary.
3//
4// The passphrase branch is the one derivation WebCrypto cannot do, and the
5// alternatives were vendoring somebody's .wasm — a third-party binary blob in
6// a password manager, unreadable in review — or this. This is ~250 lines you
7// can read against the RFC, and it is pinned byte-for-byte by
8// vectors/contra-v1.json and by the published PHC reference vectors.
9//
10// Speed lives in fillBlock, so the 64-bit arithmetic there is done as pairs
11// of 32-bit words rather than BigInt: a derivation runs about 200,000 block
12// compressions, and BigInt would turn a one-second unlock into minutes.
13// blake2b.js takes the opposite trade for the same reason in reverse.
14
15import { concat, u32le } from "./bytes.js";
16import { blake2b } from "./blake2b.js";
17
18const ARGON2ID = 2;
19const VERSION = 0x13;
20const BLOCK_U32 = 256; // 1024 bytes
21const SYNC_POINTS = 4;
22const ADDRESSES_PER_BLOCK = 128;
23
24// H', the variable-length hash of RFC 9106 §3.2.
25function hPrime(outLen, input) {
26 const prefixed = concat(u32le(outLen), input);
27 if (outLen <= 64) return blake2b(outLen, prefixed);
28
29 const r = Math.ceil(outLen / 32) - 2;
30 const out = new Uint8Array(outLen);
31 let v = blake2b(64, prefixed);
32 out.set(v.subarray(0, 32), 0);
33 for (let i = 1; i < r; i++) {
34 v = blake2b(64, v);
35 out.set(v.subarray(0, 32), i * 32);
36 }
37 out.set(blake2b(outLen - 32 * r, v), r * 32);
38 return out;
39}
40
41// The 16 64-bit registers the permutation works on, as (lo, hi) pairs. Module
42// scope so the hot loop allocates nothing.
43const v = new Uint32Array(32);
44
45// gb is Argon2's BlaMka variant of the BLAKE2b mixing function: the plain
46// additions gain a 2*lo(a)*lo(b) term, which is what makes the compression
47// latency-bound and so resistant to cheap parallel hardware.
48//
49// a, b, c and d are register numbers; each occupies v[2n] (low) and
50// v[2n+1] (high).
51function gb(a, b, c, d) {
52 a <<= 1; b <<= 1; c <<= 1; d <<= 1;
53 fma(a, b);
54 rot32(d, a);
55 fma2(c, d);
56 rot24(b, c);
57 fma(a, b);
58 rot16(d, a);
59 fma2(c, d);
60 rot63(b, c);
61}
62
63// fma: x = x + y + 2 * lo32(x) * lo32(y), modulo 2^64.
64function fma(x, y) {
65 const xl = v[x], yl = v[y];
66 const x0 = xl & 0xffff, x1 = xl >>> 16;
67 const y0 = yl & 0xffff, y1 = yl >>> 16;
68 const p00 = x0 * y0;
69 const p01 = x0 * y1;
70 const p10 = x1 * y0;
71 const mid = p10 + (p00 >>> 16) + (p01 & 0xffff);
72 // The doubled product, as (ml, mh).
73 let ml = (((mid & 0xffff) << 16) | (p00 & 0xffff)) >>> 0;
74 let mh = (x1 * y1 + ((mid / 0x10000) | 0) + (p01 >>> 16)) >>> 0;
75 mh = ((mh << 1) | (ml >>> 31)) >>> 0;
76 ml = (ml << 1) >>> 0;
77
78 let lo = xl + yl;
79 let hi = v[x + 1] + v[y + 1] + (lo > 0xffffffff ? 1 : 0);
80 lo = (lo >>> 0) + ml;
81 v[x] = lo >>> 0;
82 v[x + 1] = (hi + mh + (lo > 0xffffffff ? 1 : 0)) >>> 0;
83}
84
85// fma2 is fma under a different name, kept separate only so gb reads in the
86// order the RFC writes it. Inlining by hand here made no measurable
87// difference and cost the resemblance.
88const fma2 = fma;
89
90function rot32(x, y) {
91 const lo = (v[x] ^ v[y]) >>> 0;
92 const hi = (v[x + 1] ^ v[y + 1]) >>> 0;
93 v[x] = hi;
94 v[x + 1] = lo;
95}
96
97function rot24(x, y) {
98 const lo = (v[x] ^ v[y]) >>> 0;
99 const hi = (v[x + 1] ^ v[y + 1]) >>> 0;
100 v[x] = ((lo >>> 24) | (hi << 8)) >>> 0;
101 v[x + 1] = ((hi >>> 24) | (lo << 8)) >>> 0;
102}
103
104function rot16(x, y) {
105 const lo = (v[x] ^ v[y]) >>> 0;
106 const hi = (v[x + 1] ^ v[y + 1]) >>> 0;
107 v[x] = ((lo >>> 16) | (hi << 16)) >>> 0;
108 v[x + 1] = ((hi >>> 16) | (lo << 16)) >>> 0;
109}
110
111// rotr(x, 63) is rotl(x, 1).
112function rot63(x, y) {
113 const lo = (v[x] ^ v[y]) >>> 0;
114 const hi = (v[x + 1] ^ v[y + 1]) >>> 0;
115 v[x] = ((lo << 1) | (hi >>> 31)) >>> 0;
116 v[x + 1] = ((hi << 1) | (lo >>> 31)) >>> 0;
117}
118
119function permute() {
120 gb(0, 4, 8, 12); gb(1, 5, 9, 13); gb(2, 6, 10, 14); gb(3, 7, 11, 15);
121 gb(0, 5, 10, 15); gb(1, 6, 11, 12); gb(2, 7, 8, 13); gb(3, 4, 9, 14);
122}
123
124const R = new Uint32Array(BLOCK_U32);
125const Z = new Uint32Array(BLOCK_U32);
126
127// fillBlock is G: R = prev ^ ref, then the permutation over R's eight rows
128// and then its eight columns, then out = Z ^ R (XORed into out on later
129// passes rather than replacing it).
130function fillBlock(mem, prevOff, refOff, outOff, withXor) {
131 for (let i = 0; i < BLOCK_U32; i++) {
132 const x = (mem[prevOff + i] ^ mem[refOff + i]) >>> 0;
133 R[i] = x;
134 Z[i] = x;
135 }
136 // Rows: registers 8i..8i+7, contiguous as 32 words of 32 bits.
137 for (let i = 0; i < 8; i++) {
138 const o = i * 32;
139 for (let k = 0; k < 32; k++) v[k] = Z[o + k];
140 permute();
141 for (let k = 0; k < 32; k++) Z[o + k] = v[k];
142 }
143 // Columns: register i, i+8, i+16, ... — two 32-bit words each, striding 32.
144 for (let i = 0; i < 8; i++) {
145 const o = i * 4;
146 for (let k = 0; k < 8; k++) {
147 const s = o + k * 32;
148 v[k * 4] = Z[s];
149 v[k * 4 + 1] = Z[s + 1];
150 v[k * 4 + 2] = Z[s + 2];
151 v[k * 4 + 3] = Z[s + 3];
152 }
153 permute();
154 for (let k = 0; k < 8; k++) {
155 const s = o + k * 32;
156 Z[s] = v[k * 4];
157 Z[s + 1] = v[k * 4 + 1];
158 Z[s + 2] = v[k * 4 + 2];
159 Z[s + 3] = v[k * 4 + 3];
160 }
161 }
162 if (withXor) {
163 for (let i = 0; i < BLOCK_U32; i++) mem[outOff + i] ^= (Z[i] ^ R[i]) >>> 0;
164 } else {
165 for (let i = 0; i < BLOCK_U32; i++) mem[outOff + i] = (Z[i] ^ R[i]) >>> 0;
166 }
167}
168
169// mulHi32 is the high half of a 32x32 -> 64 multiply, which is all the
170// reference-index mapping needs.
171function mulHi32(a, b) {
172 const a0 = a & 0xffff, a1 = a >>> 16;
173 const b0 = b & 0xffff, b1 = b >>> 16;
174 const p00 = a0 * b0;
175 const p01 = a0 * b1;
176 const mid = a1 * b0 + (p00 >>> 16) + (p01 & 0xffff);
177 return (a1 * b1 + ((mid / 0x10000) | 0) + (p01 >>> 16)) >>> 0;
178}
179
180// argon2id derives a tag from a password and salt.
181//
182// Single-lane only, and that is not a shortcut: Argon2id's output depends on
183// p, WASM without SharedArrayBuffer cannot parallelise, and any other value
184// would break byte-identity with the Go and Swift sides. kdf.js's floor
185// refuses p != 1 before we ever get here; this is the belt.
186export function argon2id(password, salt, params, tagLen = 32) {
187 const { m_kib: mKiB, t, p } = params;
188 if (p !== 1) throw new Error("contra: argon2id is single-lane here (see PROTOCOL.md)");
189 if (t < 1) throw new Error("contra: argon2id needs at least one pass");
190
191 const blocks = 4 * Math.floor(mKiB / 4);
192 if (blocks < 8) throw new Error("contra: argon2id memory too small");
193 const laneLength = blocks;
194 const segmentLength = laneLength / SYNC_POINTS;
195
196 const h0 = blake2b(64, concat(
197 u32le(p), u32le(tagLen), u32le(mKiB), u32le(t), u32le(VERSION), u32le(ARGON2ID),
198 u32le(password.length), password,
199 u32le(salt.length), salt,
200 u32le(0), // secret
201 u32le(0), // associated data
202 ));
203
204 const mem = new Uint32Array(blocks * BLOCK_U32);
205 for (const j of [0, 1]) {
206 const b = hPrime(1024, concat(h0, u32le(j), u32le(0)));
207 for (let i = 0; i < BLOCK_U32; i++) {
208 mem[j * BLOCK_U32 + i] =
209 (b[i * 4] | (b[i * 4 + 1] << 8) | (b[i * 4 + 2] << 16) | (b[i * 4 + 3] << 24)) >>> 0;
210 }
211 }
212
213 const zero = new Uint32Array(BLOCK_U32);
214 const input = new Uint32Array(BLOCK_U32);
215 const address = new Uint32Array(BLOCK_U32);
216 const scratch = new Uint32Array(BLOCK_U32 * 3); // zero | input | address
217
218 const nextAddresses = () => {
219 // input.v[6]++ — the low word of the seventh 64-bit slot.
220 input[12] = (input[12] + 1) >>> 0;
221 if (input[12] === 0) input[13] = (input[13] + 1) >>> 0;
222 scratch.set(zero, 0);
223 scratch.set(input, BLOCK_U32);
224 fillBlock(scratch, 0, BLOCK_U32, BLOCK_U32 * 2, false);
225 address.set(scratch.subarray(BLOCK_U32 * 2, BLOCK_U32 * 3));
226 scratch.set(address, BLOCK_U32);
227 fillBlock(scratch, 0, BLOCK_U32, BLOCK_U32 * 2, false);
228 address.set(scratch.subarray(BLOCK_U32 * 2, BLOCK_U32 * 3));
229 };
230
231 for (let pass = 0; pass < t; pass++) {
232 for (let slice = 0; slice < SYNC_POINTS; slice++) {
233 // Argon2id is Argon2i for the first two slices of the first pass and
234 // Argon2d thereafter: data-independent while an attacker could still
235 // profit from a side channel, data-dependent once they could not.
236 const independent = pass === 0 && slice < 2;
237 if (independent) {
238 input.fill(0);
239 input[0] = pass;
240 input[2] = 0; // lane
241 input[4] = slice;
242 input[6] = blocks;
243 input[8] = t;
244 input[10] = ARGON2ID;
245 }
246
247 let i = 0;
248 if (pass === 0 && slice === 0) {
249 i = 2; // the two blocks h0 already filled
250 if (independent) nextAddresses();
251 }
252
253 for (; i < segmentLength; i++) {
254 const cur = slice * segmentLength + i;
255 const prev = cur === 0 ? laneLength - 1 : cur - 1;
256
257 let j1;
258 if (independent) {
259 if (i % ADDRESSES_PER_BLOCK === 0) nextAddresses();
260 j1 = address[(i % ADDRESSES_PER_BLOCK) * 2];
261 } else {
262 j1 = mem[prev * BLOCK_U32];
263 }
264
265 // index_alpha: the reference area is everything already written in
266 // this lane that is not the block we just used.
267 let refAreaSize;
268 if (pass === 0) {
269 refAreaSize = slice === 0 ? i - 1 : slice * segmentLength + i - 1;
270 } else {
271 refAreaSize = laneLength - segmentLength + i - 1;
272 }
273 let rel = mulHi32(j1, j1);
274 rel = refAreaSize - 1 - mulHi32(refAreaSize, rel);
275 const start = pass === 0 ? 0 : (slice === SYNC_POINTS - 1 ? 0 : (slice + 1) * segmentLength);
276 const ref = (start + rel) % laneLength;
277
278 fillBlock(mem, prev * BLOCK_U32, ref * BLOCK_U32, cur * BLOCK_U32, pass !== 0);
279 }
280 }
281 }
282
283 const last = new Uint8Array(1024);
284 for (let i = 0; i < BLOCK_U32; i++) {
285 const w = mem[(laneLength - 1) * BLOCK_U32 + i];
286 last[i * 4] = w & 255;
287 last[i * 4 + 1] = (w >>> 8) & 255;
288 last[i * 4 + 2] = (w >>> 16) & 255;
289 last[i * 4 + 3] = (w >>> 24) & 255;
290 }
291 return hPrime(tagLen, last);
292}
293