| 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 | |
| 15 | import { concat, u32le } from "./bytes.js"; |
| 16 | import { blake2b } from "./blake2b.js"; |
| 17 | |
| 18 | const ARGON2ID = 2; |
| 19 | const VERSION = 0x13; |
| 20 | const BLOCK_U32 = 256; // 1024 bytes |
| 21 | const SYNC_POINTS = 4; |
| 22 | const ADDRESSES_PER_BLOCK = 128; |
| 23 | |
| 24 | // H', the variable-length hash of RFC 9106 §3.2. |
| 25 | function 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. |
| 43 | const 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). |
| 51 | function 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. |
| 64 | function 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. |
| 88 | const fma2 = fma; |
| 89 | |
| 90 | function 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 | |
| 97 | function 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 | |
| 104 | function 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). |
| 112 | function 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 | |
| 119 | function 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 | |
| 124 | const R = new Uint32Array(BLOCK_U32); |
| 125 | const 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). |
| 130 | function 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. |
| 171 | function 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. |
| 186 | export 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 | |