Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ const argon2id = await setupWasm(

Using the same principle, for browsers you can use bundlers with simple base-64 file loaders.

### Async usage

If you need a Promise-based and non-blocking method, you can pass the `async: true` option to `argon2id`:

```js
import loadArgon2idWasm from 'argon2id';

const argon2id = await loadArgon2idWasm();
const hash = await argon2id({
async: true,
password: new Uint8Array(...),
salt: crypto.getRandomValues(new Uint8Array(32)),
parallelism: 4,
passes: 3,
memorySize: 2**16,
});
```

The resolution of the yielding frequency can be controlled by passing the `async.yieldResolution` option to `argon2id` or to `loadArgon2idWasm` (default is 2048). The `async` option can be passed as `true` or as an object with async options, e.g. `{ async: { yieldResolution: 1024 } }`. The `yieldResolution` option of `loadArgon2idWasm` will be used as default for all calls to `argon2id`, unless overridden by the `yieldResolution` option of `argon2id`.

## Compiling

**The npm package already includes the compiled binaries.**<br>
Expand Down
6 changes: 3 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { computeHash, Argon2idParams } from "./lib/setup";
import type { computeHash, Argon2idParams, Argon2idAsyncParams, Argon2idSetupOptions } from "./lib/setup";

/**
* Setup an `argon2id` instance, by loading the Wasm module that is used under the hood. The SIMD version is used as long as the platform supports it.
Expand All @@ -8,6 +8,6 @@ import type { computeHash, Argon2idParams } from "./lib/setup";
* and some of them require considerably more memory than the rest.
* @returns argon2id function
*/
export function loadWasm(): Promise<computeHash>;
export function loadWasm(options?: Argon2idSetupOptions): Promise<computeHash>;
export default loadWasm;
export type { Argon2idParams, computeHash };
export type { Argon2idParams, computeHash, Argon2idAsyncParams, Argon2idSetupOptions };
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import setupWasm from './lib/setup.js';
import wasmSIMD from './dist/simd.wasm';
import wasmNonSIMD from './dist/no-simd.wasm';

const loadWasm = async () => setupWasm(
const loadWasm = async (options) => setupWasm(
(instanceObject) => wasmSIMD(instanceObject),
(instanceObject) => wasmNonSIMD(instanceObject),
options,
);

export default loadWasm;
102 changes: 97 additions & 5 deletions lib/argon2id.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const SECRETBYTES_MAX = 32; // key (optional)
const ARGON2_BLOCK_SIZE = 1024;
const ARGON2_PREHASH_DIGEST_LENGTH = 64;

const DEFAULT_ASYNC_YIELD_RESOLUTION = 2048;

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])
Expand Down Expand Up @@ -153,11 +155,13 @@ function* makePRNG(wasmContext, pass, lane, slice, m_, totalPasses, segmentLengt
return [];
}

function validateParams({ type, version, tagLength, password, salt, ad, secret, parallelism, memorySize, passes }) {
function validateParams({ type, version, tagLength, password, salt, ad, secret, parallelism, memorySize, passes, async }, { async: globalAsync } = {}) {
const assertLength = (name, value, min, max) => {
if (value < min || value > max) { throw new Error(`${name} size should be between ${min} and ${max} bytes`); }
}

if (!isLittleEndian) throw new Error('BigEndian system not supported'); // optmisations assume LE system

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);
Expand All @@ -167,17 +171,85 @@ function validateParams({ type, version, tagLength, password, salt, ad, secret,
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 };
// async options
let abortSignal;
let yieldResolution;
if (async) {
let asyncOptions = { ...globalAsync, ...async };
async = true;

abortSignal = asyncOptions.abortSignal;
if (abortSignal) {
abortSignal = normalizeAbortSignal(abortSignal);
}

yieldResolution = asyncOptions.yieldResolution ?? DEFAULT_ASYNC_YIELD_RESOLUTION;
if (yieldResolution < 1) throw new Error('yieldResolution must be at least 1');
}

return { type, version, tagLength, password, salt, ad, secret, lanes: parallelism, memorySize, passes, async, abortSignal, yieldResolution };
}

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
export default function argon2id(params, options) {
// Validate parameters and set up the context.
const ctx = validateParams({ type: TYPE, version: VERSION, ...params }, options);

return ctx.async
// Call the asynchronous version.
? argon2idAsync(ctx, options)
// Call the synchronous version.
: argon2idSync(ctx, options)
;
}

function argon2idSync(ctx, options) {
// Run the generator to completion synchronously.
const tasks = argon2idTasksGenerator(ctx, options);
let step = tasks.next();
while (!step.done) {
step = tasks.next();
}

// Return the final result of the generator, which is the computed Argon2id hash.
return step.value;
}

const ctx = validateParams({ type: TYPE, version: VERSION, ...params });
async function argon2idAsync(ctx, options) {
const { abortSignal, yieldResolution } = ctx;

abortSignal?.throwIfAborted();

// Operation counter to control yielding.
let operations = 0;

// Run the generator and yield to the event loop as needed to avoid blocking
// the main thread.
const tasks = argon2idTasksGenerator(ctx, options);
let step = tasks.next();
while (!step.done) {
// Yield resolution control: yield to the event loop every `yieldResolution` operations.
if (++operations >= yieldResolution) {
// Check for abort signal before yielding to the event loop.
abortSignal?.throwIfAborted();
// Reset the operation counter.
operations = 0;
// yield to the event loop to allow other tasks to run
await new Promise(resolve => setTimeout(resolve, 0));
}
step = tasks.next();
}

// Return the final result of the generator, which is the computed Argon2id hash.
return step.value;
}

// Internal generator that contains the single shared implementation of the Argon2id algorithm.
// Each iteration of the innermost block-computation loop yields once, allowing the scheduler to
// decide whether (and how) to pause execution between blocks.
function* argon2idTasksGenerator(ctx, { memory, instance: wasmInstance }) {
const { G:wasmG, G2:wasmG2, xor:wasmXOR, getLZ:wasmLZ } = wasmInstance.exports;
const wasmRefs = {};
const wasmFn = {};
Expand Down Expand Up @@ -248,6 +320,9 @@ export default function argon2id(params, { memory, instance: wasmInstance }) {
// 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++) {
// Yield to the loop control to allow other tasks to run.
yield;

const j = sl * segmentLength + segmentOffset;
const prevBlock = j > 0 ? B[i][j-1] : B[i][q-1]; // B[i][(j-1) mod q]

Expand Down Expand Up @@ -352,3 +427,20 @@ function concatArrays(arrays) {

return result;
}

function normalizeAbortSignal(abortSignal) {
if (!(abortSignal instanceof AbortSignal)) throw new Error('abortSignal must be an instance of AbortSignal');

if (abortSignal.throwIfAborted) {
return abortSignal;
}

// Legacy support.
return {
throwIfAborted: () => {
if (abortSignal.aborted) {
throw abortSignal.reason ?? new Error('Aborted');
}
}
}
}
16 changes: 16 additions & 0 deletions lib/setup.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,21 @@ export interface Argon2idParams {
secret?: Uint8Array;
}

export interface Argon2idAsyncParams extends Argon2idParams {
async: true | {
/** AbortSignal to cancel the computation */
abortSignal?: AbortSignal,
/** Number of iterations after which the function yields control to the event loop */
yieldResolution?: number
}
}

export interface Argon2idSetupOptions {
async?: { yieldResolution?: number };
}

declare function argon2id(params: Argon2idParams): Uint8Array;
declare function argon2id(params: Argon2idAsyncParams): Promise<Uint8Array>;
export type computeHash = typeof argon2id;

type MaybePromise<T> = T | Promise<T>;
Expand All @@ -27,9 +41,11 @@ declare function customInstanceLoader(importObject: WebAssembly.Imports): MaybeP
* 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
* @param options - optional parameters
* @returns {computeHash}
*/
export default function setupWasm(
getSIMD: typeof customInstanceLoader,
getNonSIMD: typeof customInstanceLoader,
options?: Argon2idSetupOptions,
): Promise<computeHash>;
4 changes: 2 additions & 2 deletions lib/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async function wasmLoader(memory, getSIMD, getNonSIMD) {
return loader(importObject);
}

export default async function setupWasm(getSIMD, getNonSIMD) {
export default async function setupWasm(getSIMD, getNonSIMD, options) {
const memory = new WebAssembly.Memory({
// in pages of 64KiB each
// these values need to be compatible with those declared when building in `build-wasm`
Expand All @@ -40,7 +40,7 @@ export default async function setupWasm(getSIMD, getNonSIMD) {
* @param {Uint8Array} [params.secret] - secret data (optional)
* @return {Uint8Array} argon2id hash
*/
const computeHash = (params) => argon2id(params, { instance: wasmModule.instance, memory });
const computeHash = (params) => argon2id(params, { ...options, instance: wasmModule.instance, memory });

return computeHash;
}
81 changes: 79 additions & 2 deletions test/argon2id.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ describe('argon2id tests', () => {
// Node tests do not use a bundler, so we need an alternative entry-point
// @ts-ignore
const { default: loadWasm } = isNode ? await import(/* webpackIgnore: true */ './helpers/node-loader.ts') : await import('../index.js');
argon2id = await loadWasm();

argon2id = await loadWasm({ async: { yieldResolution: 64 } });
});

it('Test vector, 3 passes', function () {
Expand All @@ -23,6 +22,7 @@ describe('argon2id tests', () => {
ad: hexToUint8Array('040404040404040404040404'),
tagLength: 32
});
expect(tagT3).to.be.instanceOf(Uint8Array);
expect(uint8ArrayToHex(tagT3)).to.equal(expected);
});

Expand Down Expand Up @@ -62,6 +62,83 @@ describe('argon2id tests', () => {

expect(uint8ArrayToHex(tag)).to.equal(expected);
});

it('Async test vector, 3 passes', async function () {
const expected = '0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659';
const tagT3 = await argon2id({
async: true,
password: hexToUint8Array('0101010101010101010101010101010101010101010101010101010101010101'),
salt: hexToUint8Array('02020202020202020202020202020202'),
passes: 3, memorySize:32, parallelism: 4,
secret: hexToUint8Array('0303030303030303'),
ad: hexToUint8Array('040404040404040404040404'),
tagLength: 32
});
expect(uint8ArrayToHex(tagT3)).to.equal(expected);
});

it('Async method yields back to the event loop', async function () {
this.timeout(10000);
let ticks = 0;
let completed = false;

let timerPromise: Promise<void> | null = null;
const tick = (resolve: () => void) => {
setTimeout(() => {
ticks++;
resolve();

if (!completed) {
timerPromise = new Promise<void>(tick);
}
}, 0);

};

timerPromise = new Promise<void>(tick);

const hashPromise = argon2id({
async: { yieldResolution: 1024 },
password: hexToUint8Array('0101010101010101010101010101010101010101010101010101010101010101'),
salt: hexToUint8Array('0202020202020202020202020202020202020202020202020202020202020202'),
passes: 2, memorySize: Math.pow(2, 15), parallelism: 4, tagLength: 32,
});

expect(hashPromise).to.be.a('promise');

const asyncTag = await hashPromise;
completed = true;

expect(ticks).to.be.greaterThan(0);
expect(asyncTag.length).to.equal(32);

await timerPromise;

expect(ticks).to.be.equal(64);
});

it('Async test vector, abort', async function () {
const abortController = new AbortController();

setTimeout(() => {
abortController.abort();
}, 10)

let error;
try {
await argon2id({
async: { abortSignal: abortController.signal },
password: hexToUint8Array('0101010101010101010101010101010101010101010101010101010101010101'),
salt: hexToUint8Array('02020202020202020202020202020202'),
passes: 2, memorySize: Math.pow(2, 15), parallelism: 4, tagLength: 32,
});
}
catch (e) {
error = e;
}

expect(error).to.equal(abortController.signal.reason);
});
})


Expand Down
6 changes: 4 additions & 2 deletions test/helpers/node-loader.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { readFileSync } from 'fs';
import setupWasm from '../../lib/setup.js';
import setupWasm, { Argon2idSetupOptions } from '../../lib/setup.js';

const SIMD_FILENAME = './dist/simd.wasm';
const NON_SIMD_FILENAME = './dist/no-simd.wasm';

/**
* Simple wasm loader for Node, that does not require bundlers:
* it reads the wasm binaries from disk and instantiates them.
* @param options - optional parameters
* @returns argon2id function
*/
export default async function load() {
export default async function load(options?: Argon2idSetupOptions) {
return setupWasm(
(importObject) => WebAssembly.instantiate(readFileSync(SIMD_FILENAME), importObject),
(importObject) => WebAssembly.instantiate(readFileSync(NON_SIMD_FILENAME), importObject),
options
);
}