diff --git a/README.md b/README.md
index 26821d0..09f7932 100644
--- a/README.md
+++ b/README.md
@@ -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.**
diff --git a/index.d.ts b/index.d.ts
index 1878084..28d87ea 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -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.
@@ -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;
+export function loadWasm(options?: Argon2idSetupOptions): Promise;
export default loadWasm;
-export type { Argon2idParams, computeHash };
+export type { Argon2idParams, computeHash, Argon2idAsyncParams, Argon2idSetupOptions };
diff --git a/index.js b/index.js
index f6d395e..192d48d 100644
--- a/index.js
+++ b/index.js
@@ -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;
diff --git a/lib/argon2id.js b/lib/argon2id.js
index 0bdbb8d..890e4c6 100644
--- a/lib/argon2id.js
+++ b/lib/argon2id.js
@@ -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])
@@ -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);
@@ -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 = {};
@@ -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]
@@ -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');
+ }
+ }
+ }
+}
diff --git a/lib/setup.d.ts b/lib/setup.d.ts
index 51c1428..3b11386 100644
--- a/lib/setup.d.ts
+++ b/lib/setup.d.ts
@@ -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;
export type computeHash = typeof argon2id;
type MaybePromise = T | Promise;
@@ -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;
diff --git a/lib/setup.js b/lib/setup.js
index 65c1670..cf16a7e 100644
--- a/lib/setup.js
+++ b/lib/setup.js
@@ -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`
@@ -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;
}
diff --git a/test/argon2id.spec.ts b/test/argon2id.spec.ts
index f925b92..35ba426 100644
--- a/test/argon2id.spec.ts
+++ b/test/argon2id.spec.ts
@@ -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 () {
@@ -23,6 +22,7 @@ describe('argon2id tests', () => {
ad: hexToUint8Array('040404040404040404040404'),
tagLength: 32
});
+ expect(tagT3).to.be.instanceOf(Uint8Array);
expect(uint8ArrayToHex(tagT3)).to.equal(expected);
});
@@ -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 | null = null;
+ const tick = (resolve: () => void) => {
+ setTimeout(() => {
+ ticks++;
+ resolve();
+
+ if (!completed) {
+ timerPromise = new Promise(tick);
+ }
+ }, 0);
+
+ };
+
+ timerPromise = new Promise(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);
+ });
})
diff --git a/test/helpers/node-loader.ts b/test/helpers/node-loader.ts
index ec69038..c78ecc5 100644
--- a/test/helpers/node-loader.ts
+++ b/test/helpers/node-loader.ts
@@ -1,5 +1,5 @@
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';
@@ -7,12 +7,14 @@ 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
);
}