Initial LisgloSIPS V2 implementation

This commit is contained in:
hectorzhao
2026-06-22 10:56:38 +08:00
commit 5fa1bd35e9
303 changed files with 35644 additions and 0 deletions
+354
View File
@@ -0,0 +1,354 @@
import blake2b from "./blake2b.js"
const TYPE = 2; // Argon2id
const VERSION = 0x13;
const TAGBYTES_MAX = 0xFFFFFFFF; // Math.pow(2, 32) - 1;
const TAGBYTES_MIN = 4; // Math.pow(2, 32) - 1;
const SALTBYTES_MAX = 0xFFFFFFFF; // Math.pow(2, 32) - 1;
const SALTBYTES_MIN = 8;
const passwordBYTES_MAX = 0xFFFFFFFF;// Math.pow(2, 32) - 1;
const passwordBYTES_MIN = 8;
const MEMBYTES_MAX = 0xFFFFFFFF;// Math.pow(2, 32) - 1;
const ADBYTES_MAX = 0xFFFFFFFF; // Math.pow(2, 32) - 1; // associated data (optional)
const SECRETBYTES_MAX = 32; // key (optional)
const ARGON2_BLOCK_SIZE = 1024;
const ARGON2_PREHASH_DIGEST_LENGTH = 64;
const isLittleEndian = new Uint8Array(new Uint16Array([0xabcd]).buffer)[0] === 0xcd;
// store n as a little-endian 32-bit Uint8Array inside buf (at buf[i:i+3])
function LE32(buf, n, i) {
buf[i+0] = n;
buf[i+1] = n >> 8;
buf[i+2] = n >> 16;
buf[i+3] = n >> 24;
return buf;
}
/**
* Store n as a 64-bit LE number in the given buffer (from buf[i] to buf[i+7])
* @param {Uint8Array} buf
* @param {Number} n
* @param {Number} i
*/
function LE64(buf, n, i) {
if (n > Number.MAX_SAFE_INTEGER) throw new Error("LE64: large numbers unsupported");
// ECMAScript standard has engines convert numbers to 32-bit integers for bitwise operations
// shifting by 32 or more bits is not supported (https://stackoverflow.com/questions/6729122/javascript-bit-shift-number-wraps)
// so we manually extract each byte
let remainder = n;
for (let offset = i; offset < i+7; offset++) { // last byte can be ignored as it would overflow MAX_SAFE_INTEGER
buf[offset] = remainder; // implicit & 0xff
remainder = (remainder - buf[offset]) / 256;
}
return buf;
}
/**
* Variable-Length Hash Function H'
* @param {Number} outlen - T
* @param {Uint8Array} X - value to hash
* @param {Uint8Array} res - output buffer, of length `outlength` or larger
*/
function H_(outlen, X, res) {
const V = new Uint8Array(64); // no need to keep around all V_i
const V1_in = new Uint8Array(4 + X.length);
LE32(V1_in, outlen, 0);
V1_in.set(X, 4);
if (outlen <= 64) {
// H'^T(A) = H^T(LE32(T)||A)
blake2b(outlen).update(V1_in).digest(res);
return res
}
const r = Math.ceil(outlen / 32) - 2;
// Let V_i be a 64-byte block and W_i be its first 32 bytes.
// V_1 = H^(64)(LE32(T)||A)
// V_2 = H^(64)(V_1)
// ...
// V_r = H^(64)(V_{r-1})
// V_{r+1} = H^(T-32*r)(V_{r})
// H'^T(X) = W_1 || W_2 || ... || W_r || V_{r+1}
for (let i = 0; i < r; i++) {
blake2b(64).update(i === 0 ? V1_in : V).digest(V);
// store W_i in result buffer already
res.set(V.subarray(0, 32), i*32)
}
// V_{r+1}
const V_r1 = new Uint8Array(blake2b(outlen - 32*r).update(V).digest());
res.set(V_r1, r*32);
return res;
}
// compute buf = xs ^ ys
function XOR(wasmContext, buf, xs, ys) {
wasmContext.fn.XOR(
buf.byteOffset,
xs.byteOffset,
ys.byteOffset,
);
return buf
}
/**
* @param {Uint8Array} X (read-only)
* @param {Uint8Array} Y (read-only)
* @param {Uint8Array} R - output buffer
* @returns
*/
function G(wasmContext, X, Y, R) {
wasmContext.fn.G(
X.byteOffset,
Y.byteOffset,
R.byteOffset,
wasmContext.refs.gZ.byteOffset
);
return R;
}
function G2(wasmContext, X, Y, R) {
wasmContext.fn.G2(
X.byteOffset,
Y.byteOffset,
R.byteOffset,
wasmContext.refs.gZ.byteOffset
);
return R;
}
// Generator for data-independent J1, J2. Each `next()` invocation returns a new pair of values.
function* makePRNG(wasmContext, pass, lane, slice, m_, totalPasses, segmentLength, segmentOffset) {
// For each segment, we do the following. First, we compute the value Z as:
// Z= ( LE64(r) || LE64(l) || LE64(sl) || LE64(m') || LE64(t) || LE64(y) )
wasmContext.refs.prngTmp.fill(0);
const Z = wasmContext.refs.prngTmp.subarray(0, 6 * 8);
LE64(Z, pass, 0);
LE64(Z, lane, 8);
LE64(Z, slice, 16);
LE64(Z, m_, 24);
LE64(Z, totalPasses, 32);
LE64(Z, TYPE, 40);
// Then we compute q/(128*SL) 1024-byte values
// G( ZERO(1024),
// G( ZERO(1024), Z || LE64(1) || ZERO(968) ) ),
// ...,
// G( ZERO(1024),
// G( ZERO(1024), Z || LE64(q/(128*SL)) || ZERO(968) )),
for(let i = 1; i <= segmentLength; i++) {
// tmp.set(Z); // no need to re-copy
LE64(wasmContext.refs.prngTmp, i, Z.length); // tmp.set(ZER0968) not necessary, memory already zeroed
const g2 = G2(wasmContext, wasmContext.refs.ZERO1024, wasmContext.refs.prngTmp, wasmContext.refs.prngR );
// each invocation of G^2 outputs 1024 bytes that are to be partitioned into 8-bytes values, take as X1 || X2
// NB: the first generated pair must be used for the first block of the segment, and so on.
// Hence, if some blocks are skipped (e.g. during the first pass), the corresponding J1J2 are discarded based on the given segmentOffset.
for(let k = i === 1 ? segmentOffset*8 : 0; k < g2.length; k += 8) {
yield g2.subarray(k, k+8);
}
}
return [];
}
function validateParams({ type, version, tagLength, password, salt, ad, secret, parallelism, memorySize, passes }) {
const assertLength = (name, value, min, max) => {
if (value < min || value > max) { throw new Error(`${name} size should be between ${min} and ${max} bytes`); }
}
if (type !== TYPE || version !== VERSION) throw new Error('Unsupported type or version');
assertLength('password', password, passwordBYTES_MIN, passwordBYTES_MAX);
assertLength('salt', salt, SALTBYTES_MIN, SALTBYTES_MAX);
assertLength('tag', tagLength, TAGBYTES_MIN, TAGBYTES_MAX);
assertLength('memory', memorySize, 8*parallelism, MEMBYTES_MAX);
// optional fields
ad && assertLength('associated data', ad, 0, ADBYTES_MAX);
secret && assertLength('secret', secret, 0, SECRETBYTES_MAX);
return { type, version, tagLength, password, salt, ad, secret, lanes: parallelism, memorySize, passes };
}
const KB = 1024;
const WASM_PAGE_SIZE = 64 * KB;
export default function argon2id(params, { memory, instance: wasmInstance }) {
if (!isLittleEndian) throw new Error('BigEndian system not supported'); // optmisations assume LE system
const ctx = validateParams({ type: TYPE, version: VERSION, ...params });
const { G:wasmG, G2:wasmG2, xor:wasmXOR, getLZ:wasmLZ } = wasmInstance.exports;
const wasmRefs = {};
const wasmFn = {};
wasmFn.G = wasmG;
wasmFn.G2 = wasmG2;
wasmFn.XOR = wasmXOR;
// The actual number of blocks is m', which is m rounded down to the nearest multiple of 4*p.
const m_ = 4 * ctx.lanes * Math.floor(ctx.memorySize / (4 * ctx.lanes));
const requiredMemory = m_ * ARGON2_BLOCK_SIZE + 10 * KB; // Additional KBs for utility references
if (memory.buffer.byteLength < requiredMemory) {
const missing = Math.ceil((requiredMemory - memory.buffer.byteLength) / WASM_PAGE_SIZE)
// If enough memory is available, the `memory.buffer` is internally detached and the reference updated.
// Otherwise, the operation fails, and the original memory can still be used.
memory.grow(missing)
}
let offset = 0;
// Init wasm memory needed in other functions
wasmRefs.gZ = new Uint8Array(memory.buffer, offset, ARGON2_BLOCK_SIZE); offset+= wasmRefs.gZ.length;
wasmRefs.prngR = new Uint8Array(memory.buffer, offset, ARGON2_BLOCK_SIZE); offset+=wasmRefs.prngR.length;
wasmRefs.prngTmp = new Uint8Array(memory.buffer, offset, ARGON2_BLOCK_SIZE); offset+=wasmRefs.prngTmp.length;
wasmRefs.ZERO1024 = new Uint8Array(memory.buffer, offset, 1024); offset+=wasmRefs.ZERO1024.length;
// Init wasm memory needed locally
const lz = new Uint32Array(memory.buffer, offset, 2); offset+=lz.length * Uint32Array.BYTES_PER_ELEMENT;
const wasmContext = { fn: wasmFn, refs: wasmRefs };
const newBlock = new Uint8Array(memory.buffer, offset, ARGON2_BLOCK_SIZE); offset+=newBlock.length;
const blockMemory = new Uint8Array(memory.buffer, offset, ctx.memorySize * ARGON2_BLOCK_SIZE);
const allocatedMemory = new Uint8Array(memory.buffer, 0, offset);
// 1. Establish H_0
const H0 = getH0(ctx);
// 2. Allocate the memory as m' 1024-byte blocks
// For p lanes, the memory is organized in a matrix B[i][j] of blocks with p rows (lanes) and q = m' / p columns.
const q = m_ / ctx.lanes;
const B = new Array(ctx.lanes).fill(null).map(() => new Array(q));
const initBlock = (i, j) => {
B[i][j] = blockMemory.subarray(i*q*1024 + j*1024, (i*q*1024 + j*1024) + ARGON2_BLOCK_SIZE);
return B[i][j];
}
for (let i = 0; i < ctx.lanes; i++) {
// const LEi = LE0; // since p = 1 for us
const tmp = new Uint8Array(H0.length + 8);
// 3. Compute B[i][0] for all i ranging from (and including) 0 to (not including) p
// B[i][0] = H'^(1024)(H_0 || LE32(0) || LE32(i))
tmp.set(H0); LE32(tmp, 0, H0.length); LE32(tmp, i, H0.length + 4);
H_(ARGON2_BLOCK_SIZE, tmp, initBlock(i, 0));
// 4. Compute B[i][1] for all i ranging from (and including) 0 to (not including) p
// B[i][1] = H'^(1024)(H_0 || LE32(1) || LE32(i))
LE32(tmp, 1, H0.length);
H_(ARGON2_BLOCK_SIZE, tmp, initBlock(i, 1));
}
// 5. Compute B[i][j] for all i ranging from (and including) 0 to (not including) p and for all j ranging from (and including) 2
// to (not including) q. The computation MUST proceed slicewise (Section 3.4) : first, blocks from slice 0 are computed for all lanes
// (in an arbitrary order of lanes), then blocks from slice 1 are computed, etc.
const SL = 4; // vertical slices
const segmentLength = q / SL;
for (let pass = 0; pass < ctx.passes; pass++) {
// The intersection of a slice and a lane is called a segment, which has a length of q/SL. Segments of the same slice can be computed in parallel
for (let sl = 0; sl < SL; sl++) {
const isDataIndependent = pass === 0 && sl <= 1;
for (let i = 0; i < ctx.lanes; i++) { // lane
// On the first slice of the first pass, blocks 0 and 1 are already filled
let segmentOffset = sl === 0 && pass === 0 ? 2 : 0;
// no need to generate all J1J2s, use iterator/generator that creates the value on the fly (to save memory)
const PRNG = isDataIndependent ? makePRNG(wasmContext, pass, i, sl, m_, ctx.passes, segmentLength, segmentOffset) : null;
for (segmentOffset; segmentOffset < segmentLength; segmentOffset++) {
const j = sl * segmentLength + segmentOffset;
const prevBlock = j > 0 ? B[i][j-1] : B[i][q-1]; // B[i][(j-1) mod q]
// we can assume the PRNG is never done
const J1J2 = isDataIndependent ? PRNG.next().value : prevBlock; // .subarray(0, 8) not required since we only pass the byteOffset to wasm
// The block indices l and z are determined for each i, j differently for Argon2d, Argon2i, and Argon2id.
wasmLZ(lz.byteOffset, J1J2.byteOffset, i, ctx.lanes, pass, sl, segmentOffset, SL, segmentLength)
const l = lz[0]; const z = lz[1];
// for (let i = 0; i < p; i++ )
// B[i][j] = G(B[i][j-1], B[l][z])
// The block indices l and z are determined for each i, j differently for Argon2d, Argon2i, and Argon2id.
if (pass === 0) initBlock(i, j);
G(wasmContext, prevBlock, B[l][z], pass > 0 ? newBlock : B[i][j]);
// 6. If the number of passes t is larger than 1, we repeat step 5. However, blocks are computed differently as the old value is XORed with the new one
if (pass > 0) XOR(wasmContext, B[i][j], newBlock, B[i][j])
}
}
}
}
// 7. After t steps have been iterated, the final block C is computed as the XOR of the last column:
// C = B[0][q-1] XOR B[1][q-1] XOR ... XOR B[p-1][q-1]
const C = B[0][q-1];
for(let i = 1; i < ctx.lanes; i++) {
XOR(wasmContext, C, C, B[i][q-1])
}
const tag = H_(ctx.tagLength, C, new Uint8Array(ctx.tagLength));
// clear memory since the module might be cached
allocatedMemory.fill(0) // clear sensitive contents
memory.grow(0) // allow deallocation
// 8. The output tag is computed as H'^T(C).
return tag;
}
function getH0(ctx) {
const H = blake2b(ARGON2_PREHASH_DIGEST_LENGTH);
const ZERO32 = new Uint8Array(4);
const params = new Uint8Array(24);
LE32(params, ctx.lanes, 0);
LE32(params, ctx.tagLength, 4);
LE32(params, ctx.memorySize, 8);
LE32(params, ctx.passes, 12);
LE32(params, ctx.version, 16);
LE32(params, ctx.type, 20);
const toHash = [params];
if (ctx.password) {
toHash.push(LE32(new Uint8Array(4), ctx.password.length, 0))
toHash.push(ctx.password)
} else {
toHash.push(ZERO32) // context.password.length
}
if (ctx.salt) {
toHash.push(LE32(new Uint8Array(4), ctx.salt.length, 0))
toHash.push(ctx.salt)
} else {
toHash.push(ZERO32) // context.salt.length
}
if (ctx.secret) {
toHash.push(LE32(new Uint8Array(4), ctx.secret.length, 0))
toHash.push(ctx.secret)
// todo clear secret?
} else {
toHash.push(ZERO32) // context.secret.length
}
if (ctx.ad) {
toHash.push(LE32(new Uint8Array(4), ctx.ad.length, 0))
toHash.push(ctx.ad)
} else {
toHash.push(ZERO32) // context.ad.length
}
H.update(concatArrays(toHash))
const outputBuffer = H.digest();
return new Uint8Array(outputBuffer);
}
function concatArrays(arrays) {
if (arrays.length === 1) return arrays[0];
let totalLength = 0;
for (let i = 0; i < arrays.length; i++) {
if (!(arrays[i] instanceof Uint8Array)) {
throw new Error('concatArrays: Data must be in the form of a Uint8Array');
}
totalLength += arrays[i].length;
}
const result = new Uint8Array(totalLength);
let pos = 0;
arrays.forEach((element) => {
result.set(element, pos);
pos += element.length;
});
return result;
}
+256
View File
@@ -0,0 +1,256 @@
// Adapted from the reference implementation in RFC7693
// Initial port to Javascript by https://github.com/dcposch and https://github.com/emilbayes
// Uint64 values are represented using two Uint32s, stored as little endian
// NB: Uint32Arrays endianness depends on the underlying system, so for interoperability, conversions between Uint8Array and Uint32Arrays
// need to be manually handled
// 64-bit unsigned addition (little endian, in place)
// Sets a[i,i+1] += b[j,j+1]
// `a` and `b` must be Uint32Array(2)
function ADD64 (a, i, b, j) {
a[i] += b[j];
a[i+1] += b[j+1] + (a[i] < b[j]); // add carry
}
// Increment 64-bit little-endian unsigned value by `c` (in place)
// `a` must be Uint32Array(2)
function INC64 (a, c) {
a[0] += c;
a[1] += (a[0] < c);
}
// G Mixing function
// The ROTRs are inlined for speed
function G (v, m, a, b, c, d, ix, iy) {
ADD64(v, a, v, b) // v[a,a+1] += v[b,b+1]
ADD64(v, a, m, ix) // v[a, a+1] += x ... x0
// v[d,d+1] = (v[d,d+1] xor v[a,a+1]) rotated to the right by 32 bits
let xor0 = v[d] ^ v[a]
let xor1 = v[d + 1] ^ v[a + 1]
v[d] = xor1
v[d + 1] = xor0
ADD64(v, c, v, d)
// v[b,b+1] = (v[b,b+1] xor v[c,c+1]) rotated right by 24 bits
xor0 = v[b] ^ v[c]
xor1 = v[b + 1] ^ v[c + 1]
v[b] = (xor0 >>> 24) ^ (xor1 << 8)
v[b + 1] = (xor1 >>> 24) ^ (xor0 << 8)
ADD64(v, a, v, b)
ADD64(v, a, m, iy)
// v[d,d+1] = (v[d,d+1] xor v[a,a+1]) rotated right by 16 bits
xor0 = v[d] ^ v[a]
xor1 = v[d + 1] ^ v[a + 1]
v[d] = (xor0 >>> 16) ^ (xor1 << 16)
v[d + 1] = (xor1 >>> 16) ^ (xor0 << 16)
ADD64(v, c, v, d)
// v[b,b+1] = (v[b,b+1] xor v[c,c+1]) rotated right by 63 bits
xor0 = v[b] ^ v[c]
xor1 = v[b + 1] ^ v[c + 1]
v[b] = (xor1 >>> 31) ^ (xor0 << 1)
v[b + 1] = (xor0 >>> 31) ^ (xor1 << 1)
}
// Initialization Vector
const BLAKE2B_IV32 = new Uint32Array([
0xF3BCC908, 0x6A09E667, 0x84CAA73B, 0xBB67AE85,
0xFE94F82B, 0x3C6EF372, 0x5F1D36F1, 0xA54FF53A,
0xADE682D1, 0x510E527F, 0x2B3E6C1F, 0x9B05688C,
0xFB41BD6B, 0x1F83D9AB, 0x137E2179, 0x5BE0CD19
])
// These are offsets into a Uint64 buffer.
// Multiply them all by 2 to make them offsets into a Uint32 buffer
const SIGMA = new Uint8Array([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3,
11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4,
7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8,
9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13,
2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9,
12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11,
13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10,
6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5,
10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3
].map(x => x * 2))
// Compression function. 'last' flag indicates last block.
// Note: we're representing 16 uint64s as 32 uint32s
function compress(S, last) {
const v = new Uint32Array(32)
const m = new Uint32Array(S.b.buffer, S.b.byteOffset, 32)
// init work variables
for (let i = 0; i < 16; i++) {
v[i] = S.h[i]
v[i + 16] = BLAKE2B_IV32[i]
}
// low 64 bits of offset
v[24] ^= S.t0[0]
v[25] ^= S.t0[1]
// high 64 bits not supported (`t1`), offset may not be higher than 2**53-1
// if last block
const f0 = last ? 0xFFFFFFFF : 0;
v[28] ^= f0;
v[29] ^= f0;
// twelve rounds of mixing
for (let i = 0; i < 12; i++) {
// ROUND(r)
const i16 = i << 4;
G(v, m, 0, 8, 16, 24, SIGMA[i16 + 0], SIGMA[i16 + 1])
G(v, m, 2, 10, 18, 26, SIGMA[i16 + 2], SIGMA[i16 + 3])
G(v, m, 4, 12, 20, 28, SIGMA[i16 + 4], SIGMA[i16 + 5])
G(v, m, 6, 14, 22, 30, SIGMA[i16 + 6], SIGMA[i16 + 7])
G(v, m, 0, 10, 20, 30, SIGMA[i16 + 8], SIGMA[i16 + 9])
G(v, m, 2, 12, 22, 24, SIGMA[i16 + 10], SIGMA[i16 + 11])
G(v, m, 4, 14, 16, 26, SIGMA[i16 + 12], SIGMA[i16 + 13])
G(v, m, 6, 8, 18, 28, SIGMA[i16 + 14], SIGMA[i16 + 15])
}
for (let i = 0; i < 16; i++) {
S.h[i] ^= v[i] ^ v[i + 16]
}
}
// Creates a BLAKE2b hashing context
// Requires an output length between 1 and 64 bytes
// Takes an optional Uint8Array key
class Blake2b {
constructor(outlen, key, salt, personal) {
const params = new Uint8Array(64)
// 0: outlen, keylen, fanout, depth
// 4: leaf length, sequential mode
// 8: node offset
// 12: node offset
// 16: node depth, inner length, rfu
// 20: rfu
// 24: rfu
// 28: rfu
// 32: salt
// 36: salt
// 40: salt
// 44: salt
// 48: personal
// 52: personal
// 56: personal
// 60: personal
// init internal state
this.S = {
b: new Uint8Array(BLOCKBYTES),
h: new Uint32Array(OUTBYTES_MAX / 4),
t0: new Uint32Array(2), // input counter `t`, lower 64-bits only
c: 0, // `fill`, pointer within buffer, up to `BLOCKBYTES`
outlen // output length in bytes
}
// init parameter block
params[0] = outlen
if (key) params[1] = key.length
params[2] = 1 // fanout
params[3] = 1 // depth
if (salt) params.set(salt, 32)
if (personal) params.set(personal, 48)
const params32 = new Uint32Array(params.buffer, params.byteOffset, params.length / Uint32Array.BYTES_PER_ELEMENT);
// initialize hash state
for (let i = 0; i < 16; i++) {
this.S.h[i] = BLAKE2B_IV32[i] ^ params32[i];
}
// key the hash, if applicable
if (key) {
const block = new Uint8Array(BLOCKBYTES)
block.set(key)
this.update(block)
}
}
// Updates a BLAKE2b streaming hash
// Requires Uint8Array (byte array)
update(input) {
if (!(input instanceof Uint8Array)) throw new Error('Input must be Uint8Array or Buffer')
// for (let i = 0; i < input.length; i++) {
// if (this.S.c === BLOCKBYTES) { // buffer full
// INC64(this.S.t0, this.S.c) // add counters
// compress(this.S, false)
// this.S.c = 0 // empty buffer
// }
// this.S.b[this.S.c++] = input[i]
// }
let i = 0
while(i < input.length) {
if (this.S.c === BLOCKBYTES) { // buffer full
INC64(this.S.t0, this.S.c) // add counters
compress(this.S, false)
this.S.c = 0 // empty buffer
}
let left = BLOCKBYTES - this.S.c
this.S.b.set(input.subarray(i, i + left), this.S.c) // end index can be out of bounds
const fill = Math.min(left, input.length - i)
this.S.c += fill
i += fill
}
return this
}
/**
* Return a BLAKE2b hash, either filling the given Uint8Array or allocating a new one
* @param {Uint8Array} [prealloc] - optional preallocated buffer
* @returns {ArrayBuffer} message digest
*/
digest(prealloc) {
INC64(this.S.t0, this.S.c) // mark last block offset
// final block, padded
this.S.b.fill(0, this.S.c);
this.S.c = BLOCKBYTES;
compress(this.S, true)
const out = prealloc || new Uint8Array(this.S.outlen);
for (let i = 0; i < this.S.outlen; i++) {
// must be loaded individually since default Uint32 endianness is platform dependant
out[i] = this.S.h[i >> 2] >> (8 * (i & 3))
}
this.S.h = null; // prevent calling `update` after `digest`
return out.buffer;
}
}
export default function createHash(outlen, key, salt, personal) {
if (outlen > OUTBYTES_MAX) throw new Error(`outlen must be at most ${OUTBYTES_MAX} (given: ${outlen})`)
if (key) {
if (!(key instanceof Uint8Array)) throw new Error('key must be Uint8Array or Buffer')
if (key.length > KEYBYTES_MAX) throw new Error(`key size must be at most ${KEYBYTES_MAX} (given: ${key.length})`)
}
if (salt) {
if (!(salt instanceof Uint8Array)) throw new Error('salt must be Uint8Array or Buffer')
if (salt.length !== SALTBYTES) throw new Error(`salt must be exactly ${SALTBYTES} (given: ${salt.length}`)
}
if (personal) {
if (!(personal instanceof Uint8Array)) throw new Error('personal must be Uint8Array or Buffer')
if (personal.length !== PERSONALBYTES) throw new Error(`salt must be exactly ${PERSONALBYTES} (given: ${personal.length}`)
}
return new Blake2b(outlen, key, salt, personal)
}
const OUTBYTES_MAX = 64;
const KEYBYTES_MAX = 64;
const SALTBYTES = 16;
const PERSONALBYTES = 16;
const BLOCKBYTES = 128;
+35
View File
@@ -0,0 +1,35 @@
export interface Argon2idParams {
password: Uint8Array;
salt: Uint8Array;
/** Degree of parallelism (number of lanes) */
parallelism: number;
/** Number of iterations */
passes: number;
/** Memory cost in kibibytes */
memorySize: number;
/** Output tag length */
tagLength: number;
/** Associated Data */
ad?: Uint8Array;
/** Secret Data */
secret?: Uint8Array;
}
declare function argon2id(params: Argon2idParams): Uint8Array;
export type computeHash = typeof argon2id;
type MaybePromise<T> = T | Promise<T>;
declare function customInstanceLoader(importObject: WebAssembly.Imports): MaybePromise<WebAssembly.WebAssemblyInstantiatedSource>;
/**
* Load Wasm module and return argon2id wrapper.
* It is platform-independent and it relies on the two functions given in input to instatiate the Wasm instances.
* @param getSIMD - function instantiating and returning the SIMD Wasm instance
* @param getNonSIMD - function instantiating and returning the non-SIMD Wasm instance
* @returns {computeHash}
*/
export default function setupWasm(
getSIMD: typeof customInstanceLoader,
getNonSIMD: typeof customInstanceLoader,
): Promise<computeHash>;
+46
View File
@@ -0,0 +1,46 @@
import argon2id from "./argon2id.js";
let isSIMDSupported;
async function wasmLoader(memory, getSIMD, getNonSIMD) {
const importObject = { env: { memory } };
if (isSIMDSupported === undefined) {
try {
const loaded = await getSIMD(importObject);
isSIMDSupported = true;
return loaded;
} catch(e) {
isSIMDSupported = false;
}
}
const loader = isSIMDSupported ? getSIMD : getNonSIMD;
return loader(importObject);
}
export default async function setupWasm(getSIMD, getNonSIMD) {
const memory = new WebAssembly.Memory({
// in pages of 64KiB each
// these values need to be compatible with those declared when building in `build-wasm`
initial: 1040, // 65MB
maximum: 65536, // 4GB
});
const wasmModule = await wasmLoader(memory, getSIMD, getNonSIMD);
/**
* Argon2id hash function
* @callback computeHash
* @param {Object} params
* @param {Uint8Array} params.password - password
* @param {Uint8Array} params.salt - salt
* @param {Integer} params.parallelism
* @param {Integer} params.passes
* @param {Integer} params.memorySize - in kibibytes
* @param {Integer} params.tagLength - output tag length
* @param {Uint8Array} [params.ad] - associated data (optional)
* @param {Uint8Array} [params.secret] - secret data (optional)
* @return {Uint8Array} argon2id hash
*/
const computeHash = (params) => argon2id(params, { instance: wasmModule.instance, memory });
return computeHash;
}
+308
View File
@@ -0,0 +1,308 @@
/**
* Vectorised code mostly taken from: Argon2 reference C implementations (www.github.com/P-H-C/phc-winner-argon2)
* Copyright 2015 Daniel Dinu, Dmitry Khovratovich, Jean-Philippe Aumasson, and Samuel Neves
* Licence: CC0 1.0 Universal (https://creativecommons.org/publicdomain/zero/1.0)
*/
#include <stdint.h>
// #include <stdio.h>
#undef EMSCRIPTEN_KEEPALIVE
#define EMSCRIPTEN_KEEPALIVE __attribute__((used)) __attribute__((retain))
#if defined(__SSSE3__) || defined(__SSE2__)
#include <emmintrin.h>
#if defined(__SSSE3__)
#include <tmmintrin.h>
#define r16 \
(_mm_setr_epi8(2, 3, 4, 5, 6, 7, 0, 1, 10, 11, 12, 13, 14, 15, 8, 9))
#define r24 \
(_mm_setr_epi8(3, 4, 5, 6, 7, 0, 1, 2, 11, 12, 13, 14, 15, 8, 9, 10))
#define _mm_roti_epi64(x, c) \
(-(c) == 32) \
? _mm_shuffle_epi32((x), _MM_SHUFFLE(2, 3, 0, 1)) \
: (-(c) == 24) \
? _mm_shuffle_epi8((x), r24) \
: (-(c) == 16) \
? _mm_shuffle_epi8((x), r16) \
: (-(c) == 63) \
? _mm_xor_si128(_mm_srli_epi64((x), -(c)), \
_mm_add_epi64((x), (x))) \
: _mm_xor_si128(_mm_srli_epi64((x), -(c)), \
_mm_slli_epi64((x), 64 - (-(c))))
#else /* SSE2 */
#define _mm_roti_epi64(r, c) \
_mm_xor_si128(_mm_srli_epi64((r), -(c)), _mm_slli_epi64((r), 64 - (-(c))))
#endif
static __m128i fBlaMka(__m128i x, __m128i y) {
const __m128i z = _mm_mul_epu32(x, y);
return _mm_add_epi64(_mm_add_epi64(x, y), _mm_add_epi64(z, z));
}
#define GB1(A0, B0, C0, D0, A1, B1, C1, D1) \
do { \
A0 = fBlaMka(A0, B0); \
A1 = fBlaMka(A1, B1); \
\
D0 = _mm_xor_si128(D0, A0); \
D1 = _mm_xor_si128(D1, A1); \
\
D0 = _mm_roti_epi64(D0, -32); \
D1 = _mm_roti_epi64(D1, -32); \
\
C0 = fBlaMka(C0, D0); \
C1 = fBlaMka(C1, D1); \
\
B0 = _mm_xor_si128(B0, C0); \
B1 = _mm_xor_si128(B1, C1); \
\
B0 = _mm_roti_epi64(B0, -24); \
B1 = _mm_roti_epi64(B1, -24); \
} while ((void)0, 0)
#define GB2(A0, B0, C0, D0, A1, B1, C1, D1) \
do { \
A0 = fBlaMka(A0, B0); \
A1 = fBlaMka(A1, B1); \
\
D0 = _mm_xor_si128(D0, A0); \
D1 = _mm_xor_si128(D1, A1); \
\
D0 = _mm_roti_epi64(D0, -16); \
D1 = _mm_roti_epi64(D1, -16); \
\
C0 = fBlaMka(C0, D0); \
C1 = fBlaMka(C1, D1); \
\
B0 = _mm_xor_si128(B0, C0); \
B1 = _mm_xor_si128(B1, C1); \
\
B0 = _mm_roti_epi64(B0, -63); \
B1 = _mm_roti_epi64(B1, -63); \
} while ((void)0, 0)
#if defined(__SSSE3__)
#define DIAGONALIZE(A0, B0, C0, D0, A1, B1, C1, D1) \
do { \
__m128i t0 = _mm_alignr_epi8(B1, B0, 8); \
__m128i t1 = _mm_alignr_epi8(B0, B1, 8); \
B0 = t0; \
B1 = t1; \
\
t0 = C0; \
C0 = C1; \
C1 = t0; \
\
t0 = _mm_alignr_epi8(D1, D0, 8); \
t1 = _mm_alignr_epi8(D0, D1, 8); \
D0 = t1; \
D1 = t0; \
} while ((void)0, 0)
#define UNDIAGONALIZE(A0, B0, C0, D0, A1, B1, C1, D1) \
do { \
__m128i t0 = _mm_alignr_epi8(B0, B1, 8); \
__m128i t1 = _mm_alignr_epi8(B1, B0, 8); \
B0 = t0; \
B1 = t1; \
\
t0 = C0; \
C0 = C1; \
C1 = t0; \
\
t0 = _mm_alignr_epi8(D0, D1, 8); \
t1 = _mm_alignr_epi8(D1, D0, 8); \
D0 = t1; \
D1 = t0; \
} while ((void)0, 0)
#else /* SSE2 */
#define DIAGONALIZE(A0, B0, C0, D0, A1, B1, C1, D1) \
do { \
__m128i t0 = D0; \
__m128i t1 = B0; \
D0 = C0; \
C0 = C1; \
C1 = D0; \
D0 = _mm_unpackhi_epi64(D1, _mm_unpacklo_epi64(t0, t0)); \
D1 = _mm_unpackhi_epi64(t0, _mm_unpacklo_epi64(D1, D1)); \
B0 = _mm_unpackhi_epi64(B0, _mm_unpacklo_epi64(B1, B1)); \
B1 = _mm_unpackhi_epi64(B1, _mm_unpacklo_epi64(t1, t1)); \
} while ((void)0, 0)
#define UNDIAGONALIZE(A0, B0, C0, D0, A1, B1, C1, D1) \
do { \
__m128i t0, t1; \
t0 = C0; \
C0 = C1; \
C1 = t0; \
t0 = B0; \
t1 = D0; \
B0 = _mm_unpackhi_epi64(B1, _mm_unpacklo_epi64(B0, B0)); \
B1 = _mm_unpackhi_epi64(t0, _mm_unpacklo_epi64(B1, B1)); \
D0 = _mm_unpackhi_epi64(D0, _mm_unpacklo_epi64(D1, D1)); \
D1 = _mm_unpackhi_epi64(D1, _mm_unpacklo_epi64(t1, t1)); \
} while ((void)0, 0)
#endif
// BLAKE2_ROUND in reference code
#define P(A0, A1, B0, B1, C0, C1, D0, D1) \
do { \
GB1(A0, B0, C0, D0, A1, B1, C1, D1); \
GB2(A0, B0, C0, D0, A1, B1, C1, D1); \
\
DIAGONALIZE(A0, B0, C0, D0, A1, B1, C1, D1); \
\
GB1(A0, B0, C0, D0, A1, B1, C1, D1); \
GB2(A0, B0, C0, D0, A1, B1, C1, D1); \
\
UNDIAGONALIZE(A0, B0, C0, D0, A1, B1, C1, D1); \
} while ((void)0, 0)
EMSCRIPTEN_KEEPALIVE void xor(__m128i* out, __m128i* x, __m128i* y){
for(uint8_t i = 0; i < 64; i++) { // ARGON2_BLOCK_SIZE (1024) / 16 bytes (128bits) = 64
out[i] = _mm_xor_si128(x[i], y[i]);
}
}
// G will be given uint64_t* values by JS, which can be automatically casted to _m128i*:
// see https://stackoverflow.com/questions/11034302/sse-difference-between-mm-load-store-vs-using-direct-pointer-access
EMSCRIPTEN_KEEPALIVE void G(__m128i* X, __m128i* Y, __m128i* R, __m128i* Z) {
for (uint8_t i = 0; i < 64; i++) { // inlined `xor` to set both R and Z
R[i] = Z[i] = _mm_xor_si128(X[i], Y[i]);
}
for (uint8_t i = 0; i < 8; ++i) {
P(Z[8 * i + 0], Z[8 * i + 1], Z[8 * i + 2],
Z[8 * i + 3], Z[8 * i + 4], Z[8 * i + 5],
Z[8 * i + 6], Z[8 * i + 7]);
}
for (uint8_t i = 0; i < 8; ++i) {
P(Z[8 * 0 + i], Z[8 * 1 + i], Z[8 * 2 + i],
Z[8 * 3 + i], Z[8 * 4 + i], Z[8 * 5 + i],
Z[8 * 6 + i], Z[8 * 7 + i]);
}
xor(R, R, Z);
}
// G^2
EMSCRIPTEN_KEEPALIVE void G2(__m128i* X, __m128i* Y, __m128i* R, __m128i* Z) {
G( X, Y, R, Z );
G( X, R, R, Z );
}
#else // no vectorization
uint64_t rotr64(uint64_t x, uint64_t n) { return (x >> n) ^ (x << (64 - n)); }
#define LSB(x) ((x) & 0xffffffff)
EMSCRIPTEN_KEEPALIVE void xor(uint64_t* out, uint64_t* x, uint64_t* y){for(uint8_t i = 0; i < 128; i++) out[i] = x[i] ^ y[i];}
void GB(uint64_t* v, int a, int b, int c, int d) {
// a = (a + b + 2 * trunc(a) * trunc(b)) mod 2^(64)
v[a] += v[b] + 2 * LSB(v[a]) * LSB(v[b]);
// d = (d XOR a) >>> 32, where >>> is a rotation
v[d] = rotr64(v[d] ^ v[a], 32);
// c = (c + d + 2 * trunc(c) * trunc(d)) mod 2^(64)
v[c] += v[d] + 2 * LSB(v[c]) * LSB(v[d]);
// b = (b XOR c) >>> 24
v[b] = rotr64(v[b] ^ v[c], 24);
// a = (a + b + 2 * trunc(a) * trunc(b)) mod 2^(64)
v[a] += v[b] + 2 * LSB(v[a]) * LSB(v[b]);
// d = (d XOR a) >>> 16
v[d] = rotr64(v[d] ^ v[a], 16);
// c = (c + d + 2 * trunc(c) * trunc(d)) mod 2^(64)
v[c] += v[d] + 2 * LSB(v[c]) * LSB(v[d]);
// b = (b XOR c) >>> 63
v[b] = rotr64(v[b] ^ v[c], 63);
}
void P(uint64_t* v, uint16_t i0,uint16_t i1,uint16_t i2,uint16_t i3,uint16_t i4,uint16_t i5,uint16_t i6,uint16_t i7, uint16_t i8,uint16_t i9,uint16_t i10,uint16_t i11,uint16_t i12,uint16_t i13,uint16_t i14,uint16_t i15) {
// v stores 16 64-bit values
GB(v, i0, i4, i8, i12);
GB(v, i1, i5, i9, i13);
GB(v, i2, i6, i10, i14);
GB(v, i3, i7, i11, i15);
GB(v, i0, i5, i10, i15);
GB(v, i1, i6, i11, i12);
GB(v, i2, i7, i8, i13);
GB(v, i3, i4, i9, i14);
}
// given a copy of R, compute Z (in-place)
EMSCRIPTEN_KEEPALIVE void G(uint64_t* X, uint64_t* Y, uint64_t* R, uint64_t* Z) {
xor(R, X, Y);
// // we need to store S_i = (v_{2*i+1} || v_{2*i}), for v[i] of 64 bits
// // S[0] = R[8:15] || R[0:7]
for(uint8_t i = 0; i < 128; i+=16) {
Z[i+0] = R[i+0]; Z[i+1] = R[i+1]; Z[i+2] = R[i+2]; Z[i+3] = R[i+3];
Z[i+4] = R[i+4]; Z[i+5] = R[i+5]; Z[i+6] = R[i+6]; Z[i+7] = R[i+7];
Z[i+8] = R[i+8]; Z[i+9] = R[i+9]; Z[i+10] = R[i+10]; Z[i+11] = R[i+11];
Z[i+12] = R[i+12]; Z[i+13] = R[i+13]; Z[i+14] = R[i+14]; Z[i+15] = R[i+15];
// const ids = [0, 1, 2, 3, 4, 5, 6, 7].map(j => i*128 + j*16); // 0, 16, .. 112 | 128, 144...
// ( Q_0, Q_1, Q_2, ... , Q_7) <- P( R_0, R_1, R_2, ... , R_7) of 16-bytes each
P(Z,i+0, 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);
}
for(uint8_t i = 0; i < 16; i+=2) {
// Q_0 = Q[8:15] || Q[0:7]
// const ids = [0, 1, 2, 3, 4, 5, 6, 7].map(j => i*16 + j*128); // 128 .. 896 | 16, 144 .. 912 | ..
// ( Z_0, Z_8, Z_16, ... , Z_56) <- P( Q_0, Q_8, Q_16, ... , Q_56) of 16-bytes each
// ( Z_1, Z_9, Z_17, ... , Z_57) <- P( Q_1, Q_9, Q_17, ... , Q_57) ...
P(Z, i+0, 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); // store one column of Z at a time
}
xor(R, R, Z);
}
// G^2
EMSCRIPTEN_KEEPALIVE void G2(uint64_t* X, uint64_t* Y, uint64_t* R, uint64_t* Z) {
G( X, Y, R, Z );
G( X, R, R, Z );
}
#endif
// Returns out = [l, z]
EMSCRIPTEN_KEEPALIVE uint32_t* getLZ(uint32_t* out, uint32_t* J1J2, uint32_t currentLane, uint32_t p, uint32_t pass, uint32_t slice, uint32_t segmentOffset, uint32_t SL, uint32_t segmentLength) {
// For the first pass (r=0) and the first slice (sl=0), the block is taken from the current lane.
uint32_t l = (pass == 0 && slice == 0) ? currentLane : J1J2[1] % p;
// W includes the indices of all blocks in the last SL - 1 = 3 segments computed and finished (possibly from previous pass, if any).
// Plus, if `l` is on the current lane, we can also reference the finished blocks in the current segment (up to 'offset')
uint32_t offset = l == currentLane
? segmentOffset - 1
: segmentOffset == 0 ? -1 : 0; // If B[i][j] is the first block of a segment, then the very last index from W is excluded.
uint32_t segmentCount = pass == 0 ? slice : SL-1;
uint64_t W_area = segmentCount * segmentLength + offset;
// cast to uint64_t since we don't want the multiplication to be in uint32_t space
uint32_t x = ((uint64_t)J1J2[0] * J1J2[0]) >> 32;
uint32_t y = (W_area * x) >> 32;
uint32_t zz = W_area - 1 - y;
uint32_t startPos = pass == 0 ? 0 : (slice + 1) * segmentLength; // next segment (except for first pass)
// TODO (?) possible optimisation: zz < 2 * (SL * segmentLength) so we can use an if instead of %
uint32_t z = (startPos + zz) % (SL * segmentLength);
out[0] = l;
out[1] = z;
return out;
}