import { number as assertNumber } from './_assert.js'; import { Input, toBytes, u8, u32 } from './utils.js'; import { blake2b } from './blake2b.js'; import { add3H, add3L, rotr32H, rotr32L, rotrBH, rotrBL, rotrSH, rotrSL } from './_u64.js'; // Experimental Argon2 RFC 9106 implementation. It may be removed at any time. const enum Types { Argond2d = 0, Argon2i = 1, Argon2id = 2, } const ARGON2_SYNC_POINTS = 4; const toBytesOptional = (buf?: Input) => (buf !== undefined ? toBytes(buf) : new Uint8Array([])); function mul(a: number, b: number) { const aL = a & 0xffff; const aH = a >>> 16; const bL = b & 0xffff; const bH = b >>> 16; const ll = Math.imul(aL, bL); const hl = Math.imul(aH, bL); const lh = Math.imul(aL, bH); const hh = Math.imul(aH, bH); const BUF = ((ll >>> 16) + (hl & 0xffff) + lh) | 0; const h = ((hl >>> 16) + (BUF >>> 16) + hh) | 0; return { h, l: (BUF << 16) | (ll & 0xffff) }; } function relPos(areaSize: number, relativePos: number) { // areaSize - 1 - ((areaSize * ((relativePos ** 2) >>> 32)) >>> 32) return areaSize - 1 - mul(areaSize, mul(relativePos, relativePos).h).h; } function mul2(a: number, b: number) { // 2 * a * b (via shifts) const { h, l } = mul(a, b); return { h: ((h << 1) | (l >>> 31)) & 0xffff_ffff, l: (l << 1) & 0xffff_ffff }; } function blamka(Ah: number, Al: number, Bh: number, Bl: number) { const { h: Ch, l: Cl } = mul2(Al, Bl); // A + B + (2 * A * B) const Rll = add3L(Al, Bl, Cl); return { h: add3H(Rll, Ah, Bh, Ch), l: Rll | 0 }; } // Temporary block buffer const A2_BUF = new Uint32Array(256); function G(a: number, b: number, c: number, d: number) { let Al = A2_BUF[2*a], Ah = A2_BUF[2*a + 1]; // prettier-ignore let Bl = A2_BUF[2*b], Bh = A2_BUF[2*b + 1]; // prettier-ignore let Cl = A2_BUF[2*c], Ch = A2_BUF[2*c + 1]; // prettier-ignore let Dl = A2_BUF[2*d], Dh = A2_BUF[2*d + 1]; // prettier-ignore ({ h: Ah, l: Al } = blamka(Ah, Al, Bh, Bl)); ({ Dh, Dl } = { Dh: Dh ^ Ah, Dl: Dl ^ Al }); ({ Dh, Dl } = { Dh: rotr32H(Dh, Dl), Dl: rotr32L(Dh, Dl) }); ({ h: Ch, l: Cl } = blamka(Ch, Cl, Dh, Dl)); ({ Bh, Bl } = { Bh: Bh ^ Ch, Bl: Bl ^ Cl }); ({ Bh, Bl } = { Bh: rotrSH(Bh, Bl, 24), Bl: rotrSL(Bh, Bl, 24) }); ({ h: Ah, l: Al } = blamka(Ah, Al, Bh, Bl)); ({ Dh, Dl } = { Dh: Dh ^ Ah, Dl: Dl ^ Al }); ({ Dh, Dl } = { Dh: rotrSH(Dh, Dl, 16), Dl: rotrSL(Dh, Dl, 16) }); ({ h: Ch, l: Cl } = blamka(Ch, Cl, Dh, Dl)); ({ Bh, Bl } = { Bh: Bh ^ Ch, Bl: Bl ^ Cl }); ({ Bh, Bl } = { Bh: rotrBH(Bh, Bl, 63), Bl: rotrBL(Bh, Bl, 63) }); (A2_BUF[2 * a] = Al), (A2_BUF[2 * a + 1] = Ah); (A2_BUF[2 * b] = Bl), (A2_BUF[2 * b + 1] = Bh); (A2_BUF[2 * c] = Cl), (A2_BUF[2 * c + 1] = Ch); (A2_BUF[2 * d] = Dl), (A2_BUF[2 * d + 1] = Dh); } // prettier-ignore function P( v00: number, v01: number, v02: number, v03: number, v04: number, v05: number, v06: number, v07: number, v08: number, v09: number, v10: number, v11: number, v12: number, v13: number, v14: number, v15: number, ) { G(v00, v04, v08, v12); G(v01, v05, v09, v13); G(v02, v06, v10, v14); G(v03, v07, v11, v15); G(v00, v05, v10, v15); G(v01, v06, v11, v12); G(v02, v07, v08, v13); G(v03, v04, v09, v14); } function block(x: Uint32Array, xPos: number, yPos: number, outPos: number, needXor: boolean) { for (let i = 0; i < 256; i++) A2_BUF[i] = x[xPos + i] ^ x[yPos + i]; // columns for (let i = 0; i < 128; i += 16) { // prettier-ignore P( i, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7, i + 8, i + 9, i + 10, i + 11, i + 12, i + 13, i + 14, i + 15 ); } // rows for (let i = 0; i < 16; i += 2) { // prettier-ignore P( i, i + 1, i + 16, i + 17, i + 32, i + 33, i + 48, i + 49, i + 64, i + 65, i + 80, i + 81, i + 96, i + 97, i + 112, i + 113 ); } if (needXor) for (let i = 0; i < 256; i++) x[outPos + i] ^= A2_BUF[i] ^ x[xPos + i] ^ x[yPos + i]; else for (let i = 0; i < 256; i++) x[outPos + i] = A2_BUF[i] ^ x[xPos + i] ^ x[yPos + i]; } // Variable-Length Hash Function H' function Hp(A: Uint32Array, dkLen: number) { const A8 = u8(A); const T = new Uint32Array(1); const T8 = u8(T); T[0] = dkLen; // Fast path if (dkLen <= 64) return blake2b.create({ dkLen }).update(T8).update(A8).digest(); const out = new Uint8Array(dkLen); let V = blake2b.create({}).update(T8).update(A8).digest(); let pos = 0; // First block out.set(V.subarray(0, 32)); pos += 32; // Rest blocks for (; dkLen - pos > 64; pos += 32) out.set((V = blake2b(V)).subarray(0, 32), pos); // Last block out.set(blake2b(V, { dkLen: dkLen - pos }), pos); return u32(out); } function indexAlpha( r: number, s: number, laneLen: number, segmentLen: number, index: number, randL: number, sameLane: boolean = false ) { let area; if (0 == r) { if (0 == s) area = index - 1; else if (sameLane) area = s * segmentLen + index - 1; else area = s * segmentLen + (index == 0 ? -1 : 0); } else if (sameLane) area = laneLen - segmentLen + index - 1; else area = laneLen - segmentLen + (index == 0 ? -1 : 0); const startPos = r !== 0 && s !== ARGON2_SYNC_POINTS - 1 ? (s + 1) * segmentLen : 0; const rel = relPos(area, randL); // NOTE: check about overflows here // absPos = (startPos + relPos) % laneLength; return (startPos + rel) % laneLen; } // RFC 9106 export type ArgonOpts = { t: number; // Time cost, iterations count m: number; // Memory cost (in KB) p: number; // Parallelization parameter version?: number; // Default: 0x13 (19) key?: Input; // Optional key personalization?: Input; // Optional arbitrary extra data dkLen?: number; // Desired number of returned bytes asyncTick?: number; // Maximum time in ms for which async function can block execution maxmem?: number; onProgress?: (progress: number) => void; }; function argon2Init(type: Types, password: Input, salt: Input, opts: ArgonOpts) { password = toBytes(password); salt = toBytes(salt); let { p, dkLen, m, t, version, key, personalization, maxmem, onProgress } = { ...opts, version: opts.version || 0x13, dkLen: opts.dkLen || 32, maxmem: 2 ** 32, }; // Validation assertNumber(p); assertNumber(dkLen); assertNumber(m); assertNumber(t); assertNumber(version); if (dkLen < 4 || dkLen >= 2 ** 32) throw new Error('Argon2: dkLen should be at least 4 bytes'); if (p < 1 || p >= 2 ** 32) throw new Error('Argon2: p (parallelism) should be at least 1'); if (t < 1 || t >= 2 ** 32) throw new Error('Argon2: t (iterations) should be at least 1'); if (m < 8 * p) throw new Error(`Argon2: memory should be at least 8*p bytes`); if (version !== 16 && version !== 19) throw new Error(`Argon2: unknown version=${version}`); password = toBytes(password); if (password.length < 0 || password.length >= 2 ** 32) throw new Error('Argon2: password should be less than 4 GB'); salt = toBytes(salt); if (salt.length < 8) throw new Error('Argon2: salt should be at least 8 bytes'); key = toBytesOptional(key); personalization = toBytesOptional(personalization); if (onProgress !== undefined && typeof onProgress !== 'function') throw new Error('progressCb should be function'); // Params const lanes = p; // m' = 4 * p * floor (m / 4p) const mP = 4 * p * Math.floor(m / (ARGON2_SYNC_POINTS * p)); //q = m' / p columns const laneLen = Math.floor(mP / p); const segmentLen = Math.floor(laneLen / ARGON2_SYNC_POINTS); // H0 const h = blake2b.create({}); const BUF = new Uint32Array(1); const BUF8 = u8(BUF); for (const i of [p, dkLen, m, t, version, type]) { if (i < 0 || i >= 2 ** 32) throw new Error(`Argon2: wrong parameter=${i}, expected uint32`); BUF[0] = i; h.update(BUF8); } for (let i of [password, salt, key, personalization]) { BUF[0] = i.length; h.update(BUF8).update(i); } const H0 = new Uint32Array(18); const H0_8 = u8(H0); h.digestInto(H0_8); // 256 u32 = 1024 (BLOCK_SIZE) const memUsed = mP * 256; if (memUsed < 0 || memUsed >= 2 ** 32 || memUsed > maxmem) { throw new Error( `Argon2: wrong params (memUsed=${memUsed} maxmem=${maxmem}), should be less than 2**32` ); } const B = new Uint32Array(memUsed); // Fill first blocks for (let l = 0; l < p; l++) { const i = 256 * laneLen * l; // B[i][0] = H'^(1024)(H_0 || LE32(0) || LE32(i)) H0[17] = l; H0[16] = 0; B.set(Hp(H0, 1024), i); // B[i][1] = H'^(1024)(H_0 || LE32(1) || LE32(i)) H0[16] = 1; B.set(Hp(H0, 1024), i + 256); } let perBlock = () => {}; if (onProgress) { const totalBlock = t * ARGON2_SYNC_POINTS * p * segmentLen; // Invoke callback if progress changes from 10.01 to 10.02 // Allows to draw smooth progress bar on up to 8K screen const callbackPer = Math.max(Math.floor(totalBlock / 10000), 1); let blockCnt = 0; perBlock = () => { blockCnt++; if (onProgress && (!(blockCnt % callbackPer) || blockCnt === totalBlock)) onProgress(blockCnt / totalBlock); }; } return { type, mP, p, t, version, B, laneLen, lanes, segmentLen, dkLen, perBlock }; } function argon2Output(B: Uint32Array, p: number, laneLen: number, dkLen: number) { const B_final = new Uint32Array(256); for (let l = 0; l < p; l++) for (let j = 0; j < 256; j++) B_final[j] ^= B[256 * (laneLen * l + laneLen - 1) + j]; return u8(Hp(B_final, dkLen)); } function processBlock( B: Uint32Array, address: Uint32Array, l: number, r: number, s: number, index: number, laneLen: number, segmentLen: number, lanes: number, offset: number, prev: number, dataIndependent: boolean, needXor: boolean ) { if (offset % laneLen) prev = offset - 1; let randL, randH; if (dataIndependent) { if (index % 128 === 0) { address[256 + 12]++; block(address, 256, 2 * 256, 0, false); block(address, 0, 2 * 256, 0, false); } randL = address[2 * (index % 128)]; randH = address[2 * (index % 128) + 1]; } else { const T = 256 * prev; randL = B[T]; randH = B[T + 1]; } // address block const refLane = r === 0 && s === 0 ? l : randH % lanes; const refPos = indexAlpha(r, s, laneLen, segmentLen, index, randL, refLane == l); const refBlock = laneLen * refLane + refPos; // B[i][j] = G(B[i][j-1], B[l][z]) block(B, 256 * prev, 256 * refBlock, offset * 256, needXor); } function argon2(type: Types, password: Input, salt: Input, opts: ArgonOpts) { const { mP, p, t, version, B, laneLen, lanes, segmentLen, dkLen, perBlock } = argon2Init( type, password, salt, opts ); // Pre-loop setup // [address, input, zero_block] format so we can pass single U32 to block function const address = new Uint32Array(3 * 256); address[256 + 6] = mP; address[256 + 8] = t; address[256 + 10] = type; for (let r = 0; r < t; r++) { const needXor = r !== 0 && version === 0x13; address[256 + 0] = r; for (let s = 0; s < ARGON2_SYNC_POINTS; s++) { address[256 + 4] = s; const dataIndependent = type == Types.Argon2i || (type == Types.Argon2id && r === 0 && s < 2); for (let l = 0; l < p; l++) { address[256 + 2] = l; address[256 + 12] = 0; let startPos = 0; if (r === 0 && s === 0) { startPos = 2; if (dataIndependent) { address[256 + 12]++; block(address, 256, 2 * 256, 0, false); block(address, 0, 2 * 256, 0, false); } } // current block postion let offset = l * laneLen + s * segmentLen + startPos; // previous block position let prev = offset % laneLen ? offset - 1 : offset + laneLen - 1; for (let index = startPos; index < segmentLen; index++, offset++, prev++) { perBlock(); processBlock( B, address, l, r, s, index, laneLen, segmentLen, lanes, offset, prev, dataIndependent, needXor ); } } } } return argon2Output(B, p, laneLen, dkLen); } export const argon2d = (password: Input, salt: Input, opts: ArgonOpts) => argon2(Types.Argond2d, password, salt, opts); export const argon2i = (password: Input, salt: Input, opts: ArgonOpts) => argon2(Types.Argon2i, password, salt, opts); export const argon2id = (password: Input, salt: Input, opts: ArgonOpts) => argon2(Types.Argon2id, password, salt, opts);