-
-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add experimental Argon2 implementation from RFC9106.
- Loading branch information
Showing
4 changed files
with
802 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,377 @@ | ||
import assert from './_assert.js'; | ||
import { Input, toBytes, u8, u32 } from './utils'; | ||
import { blake2b } from './blake2b'; | ||
import u64 from './_u64.js'; | ||
|
||
// Experimental implementation of argon2. | ||
// Could be broken & slow. May be removed at a later time. | ||
// RFC 9106 | ||
|
||
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 = u64.add3L(Al, Bl, Cl); | ||
return { h: u64.add3H(Rll, Ah, Bh, Ch), l: Rll | 0 }; | ||
} | ||
|
||
// Temporary block buffer | ||
const BUF = new Uint32Array(256); | ||
|
||
function G(a: number, b: number, c: number, d: number) { | ||
let Al = BUF[2*a], Ah = BUF[2*a + 1]; // prettier-ignore | ||
let Bl = BUF[2*b], Bh = BUF[2*b + 1]; // prettier-ignore | ||
let Cl = BUF[2*c], Ch = BUF[2*c + 1]; // prettier-ignore | ||
let Dl = BUF[2*d], Dh = 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: u64.rotr32H(Dh, Dl), Dl: u64.rotr32L(Dh, Dl) }); | ||
|
||
({ h: Ch, l: Cl } = blamka(Ch, Cl, Dh, Dl)); | ||
({ Bh, Bl } = { Bh: Bh ^ Ch, Bl: Bl ^ Cl }); | ||
({ Bh, Bl } = { Bh: u64.rotrSH(Bh, Bl, 24), Bl: u64.rotrSL(Bh, Bl, 24) }); | ||
|
||
({ h: Ah, l: Al } = blamka(Ah, Al, Bh, Bl)); | ||
({ Dh, Dl } = { Dh: Dh ^ Ah, Dl: Dl ^ Al }); | ||
({ Dh, Dl } = { Dh: u64.rotrSH(Dh, Dl, 16), Dl: u64.rotrSL(Dh, Dl, 16) }); | ||
|
||
({ h: Ch, l: Cl } = blamka(Ch, Cl, Dh, Dl)); | ||
({ Bh, Bl } = { Bh: Bh ^ Ch, Bl: Bl ^ Cl }); | ||
({ Bh, Bl } = { Bh: u64.rotrBH(Bh, Bl, 63), Bl: u64.rotrBL(Bh, Bl, 63) }); | ||
|
||
(BUF[2 * a] = Al), (BUF[2 * a + 1] = Ah); | ||
(BUF[2 * b] = Bl), (BUF[2 * b + 1] = Bh); | ||
(BUF[2 * c] = Cl), (BUF[2 * c + 1] = Ch); | ||
(BUF[2 * d] = Dl), (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++) 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] ^= BUF[i] ^ x[xPos + i] ^ x[yPos + i]; | ||
else for (let i = 0; i < 256; i++) x[outPos + i] = 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 | ||
assert.number(p); | ||
assert.number(dkLen); | ||
assert.number(m); | ||
assert.number(t); | ||
assert.number(version); | ||
if (dkLen < 4 || dkLen >= 2 ** 32) throw new Error('Argon2: dkLen should be at least 4 bytes'); | ||
if (dkLen < 1 || p >= 2 ** 32) throw new Error('Argon2: p (paralllelism) should be at least 1'); | ||
if (dkLen < 1 || p >= 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); |
Oops, something went wrong.