From 8d4e8f6e6d66076355f36ca2b2274788987ac5c7 Mon Sep 17 00:00:00 2001 From: lonerapier Date: Mon, 1 Dec 2025 21:51:44 +0530 Subject: [PATCH 1/2] add docs for typescript bindings --- mfkdf2-web/src/api.ts | 1620 ++++++++++++++--- mfkdf2-web/src/utils.ts | 90 + .../test/features/reconstitution.test.ts | 2 - mfkdf2-web/test/mfkdf2/security.test.ts | 8 - mfkdf2-web/test/setup/factors/totp.test.ts | 2 +- mfkdf2/src/policy/mod.rs | 4 + 6 files changed, 1478 insertions(+), 248 deletions(-) create mode 100644 mfkdf2-web/src/utils.ts diff --git a/mfkdf2-web/src/api.ts b/mfkdf2-web/src/api.ts index 08a58748..238d73c0 100644 --- a/mfkdf2-web/src/api.ts +++ b/mfkdf2-web/src/api.ts @@ -1,147 +1,52 @@ // Facade over autogenerated bindings to provide ergonomic API matching reference implementation import crypto from 'crypto'; import * as raw from './generated/web/mfkdf2.js'; +import { toArrayBuffer, deepParse, stringifyFactorParams } from './utils.js'; export { uniffiInitAsync } from './index.web.js'; export { initLog, LogLevel } from './generated/web/mfkdf2.js'; // Re-export types export type { - Mfkdf2DerivedKey, - Policy, PolicyFactor, Mfkdf2Options, - Mfkdf2Factor, } from './generated/web/mfkdf2.js'; -// Helper to convert Buffer/Uint8Array to ArrayBuffer for UniFFI -function toArrayBuffer(input: ArrayBuffer | Buffer | Uint8Array | undefined): ArrayBuffer | undefined { - if (input === undefined) return undefined; - if (input instanceof ArrayBuffer) return input; - // Buffer and Uint8Array have .buffer property, but may be a view with offset - const view = input as Uint8Array; - return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength) as ArrayBuffer; -} - -function deepParse(value: any): any { - if (typeof value === 'string') { - try { - return deepParse(JSON.parse(value)); - } catch { - return value; - } - } - - if (Array.isArray(value)) { - return value.map(deepParse); - } - - if (value && typeof value === 'object') { - const parsed: any = {}; - for (const [key, nested] of Object.entries(value)) { - parsed[key] = deepParse(nested); - } - return parsed; - } - - return value; -} -// Deterministically stringify by sorting object keys at all depths -function stableStringify(value: any): any { - function normalize(val: any): any { - if (val === undefined || val === null || typeof val === 'string') { - return val; - } - if (Array.isArray(val)) return val.map(normalize) - if (val && typeof val === 'object') { - const out = {} - for (const k of Object.keys(val).sort()) { - if (val[k] === undefined) continue - out[k] = k === 'params' && typeof val.params !== 'string' - ? JSON.stringify(normalize(val.params)) - : normalize(val[k]) - } - return out - } - return val - } - return JSON.stringify(normalize(value)) -} - -function stringifyFactorParams(value: any): any { - if (value === undefined || value === null || typeof value === 'string') { - return value; - } - - const POLICY_ORDER = ['$id', '$schema', 'factors', 'key', 'memory', 'salt', 'threshold', 'time']; - const FACTOR_ORDER = ['id', 'pad', 'params', 'salt', 'secret', 'type', 'hint']; - - const stringifyPolicy = (input: any): string => JSON.stringify(orderValue(input, 'policy')); - - function orderValue(input: any, context?: 'policy' | 'factor'): any { - if (Array.isArray(input)) { - if (context === 'policy') { - return input.map((item) => orderValue(item, 'factor')); - } - return input.map((item) => orderValue(item)); - } - - if (input && typeof input === 'object') { - const baseOrder = context === 'policy' ? POLICY_ORDER : context === 'factor' ? FACTOR_ORDER : []; - const extras = Object.keys(input).filter((key) => !baseOrder.includes(key)).sort(); - const keys = [...baseOrder, ...extras]; - const ordered: any = {}; - - for (const key of keys) { - if (!(key in input)) continue; - - if (context === 'factor' && key === 'params') { - ordered.params = input[key]; - continue; - } - - if (key === 'factors' && Array.isArray(input[key])) { - ordered.factors = input[key].map((item: any) => orderValue(item, 'factor')); - continue; - } - - ordered[key] = orderValue(input[key]); - } - - return ordered; - } - - return input; - } - - return stringifyPolicy(value); -} - -// Wrap factor to add ergonomic API -function wrapFactor(factor: raw.Mfkdf2Factor): any { +// Wrap factor to add type and data properties +function wrapFactor(factor: raw.Mfkdf2Factor) { const getKind = () => raw.factorTypeKind(factor.factorType); return { ...factor, - // Add type property that returns the factor kind (reference implementation compatibility) + /** + * Factor type + */ get type() { return getKind(); }, - // Add data property that returns bytes as Buffer + /** + * Raw factor bytes as a `Buffer`. + */ get data() { return Buffer.from(raw.factorTypeBytes(factor.factorType)); }, }; } -function wrapSetupFactor(factor: raw.Mfkdf2Factor): any { +function wrapSetupFactor(factor: raw.Mfkdf2Factor) { const wrapped = wrapFactor(factor); return { ...wrapped, + /** + * Setup public parameters for the factor. + */ async params(key?: ArrayBuffer) { const result = raw.setupFactorTypeParams(factor.factorType, key); // Parse JSON string returned by UniFFI (Value is serialized as string) return typeof result === 'string' ? JSON.parse(result) : result; }, + /** + * Setup public outputs for the factor. + */ async output() { const result = raw.setupFactorTypeOutput(factor.factorType); // Parse JSON string returned by UniFFI (Value is serialized as string) @@ -150,15 +55,21 @@ function wrapSetupFactor(factor: raw.Mfkdf2Factor): any { } } -function wrapDeriveFactor(factor: raw.Mfkdf2Factor): any { +function wrapDeriveFactor(factor: raw.Mfkdf2Factor) { const wrapped = wrapFactor(factor); return { ...wrapped, + /** + * Derive public parameters for the factor. + */ async params(key?: ArrayBuffer) { const result = raw.deriveFactorParams(factor.factorType, key); // Parse JSON string returned by UniFFI (Value is serialized as string) return typeof result === 'string' ? JSON.parse(result) : result; }, + /** + * Derive public outputs for the factor. + */ async output() { const result = raw.deriveFactorOutput(factor.factorType); // Parse JSON string returned by UniFFI (Value is serialized as string) @@ -167,24 +78,29 @@ function wrapDeriveFactor(factor: raw.Mfkdf2Factor): any { } } -function wrapPolicy(policy: any): any { - const wrapped = { - ...policy, - $id: policy.id, - $schema: policy.schema - }; - delete wrapped.id; - delete wrapped.schema; - - for (const factor of wrapped.factors) { +function wrapPolicy(policy: raw.Policy) { + const factors = policy.factors.map((f) => { + const factor: any = { ...f }; // use `type` instead of `kind` - factor.type = factor.type ?? factor.kind; + factor.type = f.kind; delete factor.kind; // parse params from string to object factor.params = deepParse(factor.params); - } + return factor; + }); - return wrapped; + return { + ...policy, + factors, + /** + * Unique identifier for this policy. + */ + $id: policy.id, + /** + * JSON schema URL used to validate this policy. + */ + $schema: policy.schema, + }; } // Unwrap policy to remove $id and $schema (non-mutating) @@ -211,7 +127,7 @@ function unwrapPolicy(policy: any): raw.Policy { } // Wrap derived key to add $id to policy -function wrapDerivedKey(key: raw.Mfkdf2DerivedKey): any { +function wrapDerivedKey(key: raw.Mfkdf2DerivedKey) { const outputsToObject = () => Object.fromEntries(Array.from(key.outputs.entries()).map(([entryKey, value]) => [entryKey, JSON.parse(value)])); @@ -237,13 +153,83 @@ function wrapDerivedKey(key: raw.Mfkdf2DerivedKey): any { return wrapped; }; - const wrapped: any = { + const wrapped = { + /** + * Authentication policy describing factors, threshold, and integrity configuration associated + * with this key. + */ policy: wrapPolicy(key.policy), + /** + * Final 32‑byte key output of the KDF + */ key: Buffer.from(key.key), + /** + * Internal secret material that is split into per‑factor shares for threshold recovery + */ secret: Buffer.from(key.secret), + /** + * Shamir‑style shares of `secret`, one per factor, used by reconstitution and + * threshold‑management routines. + */ shares: key.shares.map(share => Buffer.from(share)), + /** + * Measured and theoretical entropy estimates for the derived key, useful for auditing and + * security analysis. + */ entropyBits: key.entropy, + /** Per‑factor public outputs produced during setup or derive (such as strength metrics or + * factor‑specific metadata). + */ outputs: outputsToObject(), + /** + * Create a 256-bit sub-key for specified purpose using HKDF + * + * @example + * // setup multi-factor derived key + * const key = await mfkdf.setup.key([ await mfkdf.setup.factors.password('password') ]) + * + * // get sub-key for "eth" + * const subkey = key.getSubkey('eth') + * subkey.toString('hex') // -> 97cb…bac5 + * + * @param purpose - Unique purpose value for this sub-key + * @param salt - Unique salt value for this sub-key + * @returns Derived sub-key + */ + async getSubkey(purpose: string, salt: string) { + const buffer = toArrayBuffer(Buffer.from(salt)); + if (!buffer) { + throw new TypeError('salt must be a Buffer'); + } + const updated = raw.derivedKeyGetSubkey(key, purpose, buffer); + return updated; + }, + /** + * Change the threshold of factors needed to derive a multi-factor derived key + * + * @example + * // setup 3-factor multi-factor derived key + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password1', { id: 'password1' }), + * await mfkdf.setup.factors.password('password2', { id: 'password2' }), + * await mfkdf.setup.factors.password('password3', { id: 'password3' }) + * ]) + * + * // change threshold to 2/3 + * await setup.setThreshold(2) + * + * // derive key with 2 factors + * const derived = await mfkdf.derive.key(setup.policy, { + * password1: mfkdf.derive.factors.password('password1'), + * password3: mfkdf.derive.factors.password('password3') + * }) + * + * setup.key.toString('hex') // -> 6458…dc3c + * derived.key.toString('hex') // -> 6458…dc3c + * + * @param threshold - New threshold for key derivation + * @throws {TypeError} If the threshold is not an integer. + */ async setThreshold(threshold: number) { if (threshold && !Number.isInteger(threshold)) { throw new TypeError('threshold must be an integer'); @@ -253,72 +239,349 @@ function wrapDerivedKey(key: raw.Mfkdf2DerivedKey): any { const updated = raw.derivedKeySetThreshold(key, threshold); return applyUpdate(updated); }, + /** + * Remove a factor used to derive a multi-factor derived key + * + * @example + * // setup 2-of-3-factor multi-factor derived key + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password1', { id: 'password1' }), + * await mfkdf.setup.factors.password('password2', { id: 'password2' }), + * await mfkdf.setup.factors.password('password3', { id: 'password3' }) + * ], {threshold: 2}) + * + * // remove one of the factors + * await setup.removeFactor('password2') + * + * // derive key with remaining 2 factors + * const derived = await mfkdf.derive.key(setup.policy, { + * password1: mfkdf.derive.factors.password('password1'), + * password3: mfkdf.derive.factors.password('password3') + * }) + * + * setup.key.toString('hex') // -> 6458…dc3c + * derived.key.toString('hex') // -> 6458…dc3c + * + * @param factorId - ID of existing factor to remove + */ async removeFactor(factorId: string) { key.policy = unwrapPolicy(key.policy); const updated = raw.derivedKeyRemoveFactor(key, factorId); return applyUpdate(updated); }, + /** + * Remove factors used to derive a multi-factor derived key + * + * @example + * // setup 1-of-3-factor multi-factor derived key + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password1', { id: 'password1' }), + * await mfkdf.setup.factors.password('password2', { id: 'password2' }), + * await mfkdf.setup.factors.password('password3', { id: 'password3' }) + * ], {threshold: 1}) + * + * // remove two factors + * await setup.removeFactors(['password1', 'password2']) + * + * // derive key with remaining factor + * const derived = await mfkdf.derive.key(setup.policy, { + * password3: mfkdf.derive.factors.password('password3') + * }) + * + * setup.key.toString('hex') // -> 6458…dc3c + * derived.key.toString('hex') // -> 6458…dc3c + * + * @param factorIds - Array of IDs of existing factors to remove + */ async removeFactors(factorIds: string[]) { key.policy = unwrapPolicy(key.policy); const updated = raw.derivedKeyRemoveFactors(key, factorIds); return applyUpdate(updated); }, + /** + * Add a factor used to derive a multi-factor derived key + * + * @example + * // setup 2-of-3-factor multi-factor derived key + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password1', { id: 'password1' }), + * await mfkdf.setup.factors.password('password2', { id: 'password2' }), + * await mfkdf.setup.factors.password('password3', { id: 'password3' }) + * ], {threshold: 2}) + * + * // add fourth factor + * await setup.addFactor( + * await mfkdf.setup.factors.password('password4', { id: 'password4' }) + * ) + * + * // derive key with any 2 factors + * const derived = await mfkdf.derive.key(setup.policy, { + * password2: mfkdf.derive.factors.password('password2'), + * password4: mfkdf.derive.factors.password('password4') + * }) + * + * setup.key.toString('hex') // -> 6458…dc3c + * derived.key.toString('hex') // -> 6458…dc3c + * + * @param factor - Factor to add + */ async addFactor(factor: raw.Mfkdf2Factor) { key.policy = unwrapPolicy(key.policy); const updated = raw.derivedKeyAddFactor(key, factor); return applyUpdate(updated); }, + /** + * Add new factors to derive a multi-factor derived key + * + * @example + * // setup 2-of-3-factor multi-factor derived key + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password1', { id: 'password1' }), + * await mfkdf.setup.factors.password('password2', { id: 'password2' }), + * await mfkdf.setup.factors.password('password3', { id: 'password3' }) + * ], {threshold: 2}) + * + * // add two more factors + * await setup.addFactors([ + * await mfkdf.setup.factors.password('password4', { id: 'password4' }), + * await mfkdf.setup.factors.password('password5', { id: 'password5' }) + * ]) + * + * // derive key with any 2 factors + * const derived = await mfkdf.derive.key(setup.policy, { + * password3: mfkdf.derive.factors.password('password3'), + * password5: mfkdf.derive.factors.password('password5') + * }) + * + * setup.key.toString('hex') // -> 6458…dc3c + * derived.key.toString('hex') // -> 6458…dc3c + * + * @param factors - Array of factors to add + */ async addFactors(factors: raw.Mfkdf2Factor[]) { key.policy = unwrapPolicy(key.policy); const updated = raw.derivedKeyAddFactors(key, factors); return applyUpdate(updated); }, + /** + * Update a factor used to derive a multi-factor derived key + * + * @example + * // setup 3-factor multi-factor derived key + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password1', { id: 'password1' }), + * await mfkdf.setup.factors.password('password2', { id: 'password2' }), + * await mfkdf.setup.factors.password('password3', { id: 'password3' }) + * ]) + * + * // change the 2nd factor + * await setup.recoverFactor( + * await mfkdf.setup.factors.password('newPassword2', { id: 'password2' }) + * ) + * + * // derive key with new factors + * const derived = await mfkdf.derive.key(setup.policy, { + * password1: mfkdf.derive.factors.password('password1'), + * password2: mfkdf.derive.factors.password('newPassword2'), + * password3: mfkdf.derive.factors.password('password3') + * }) + * + * setup.key.toString('hex') // -> 6458…dc3c + * derived.key.toString('hex') // -> 6458…dc3c + * + * @param factor - Factor to replace + */ async recoverFactor(factor: raw.Mfkdf2Factor) { key.policy = unwrapPolicy(key.policy); const updated = raw.derivedKeyRecoverFactor(key, factor); return applyUpdate(updated); }, + /** + * Update the factors used to derive a multi-factor derived key + * + * @example + * // setup 3-factor multi-factor derived key + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password1', { id: 'password1' }), + * await mfkdf.setup.factors.password('password2', { id: 'password2' }), + * await mfkdf.setup.factors.password('password3', { id: 'password3' }) + * ]) + * + * // change 2 factors + * await setup.recoverFactors([ + * await mfkdf.setup.factors.password('newPassword2', { id: 'password2' }), + * await mfkdf.setup.factors.password('newPassword3', { id: 'password3' }) + * ]) + * + * // derive key with new factors + * const derived = await mfkdf.derive.key(setup.policy, { + * password1: mfkdf.derive.factors.password('password1'), + * password2: mfkdf.derive.factors.password('newPassword2'), + * password3: mfkdf.derive.factors.password('newPassword3') + * }) + * + * setup.key.toString('hex') // -> 6458…dc3c + * derived.key.toString('hex') // -> 6458…dc3c + * + * @param factors - Array of factors to replace + */ async recoverFactors(factors: raw.Mfkdf2Factor[]) { key.policy = unwrapPolicy(key.policy); const updated = raw.derivedKeyRecoverFactors(key, factors); return applyUpdate(updated); }, /** - * Reconstitutes the key with the given factors. - * @param remove_factors - The factors ids to remove from the key. - * @param add_factors - The factors to add to the key. - * @param threshold - The threshold for the key. - * @returns The updated key. + * Reconstitute the factors used to derive a multi-factor derived key + * + * @example + * // setup 2-of-3-factor multi-factor derived key + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password1', { id: 'password1' }), + * await mfkdf.setup.factors.password('password2', { id: 'password2' }), + * await mfkdf.setup.factors.password('password3', { id: 'password3' }) + * ], {threshold: 2}) + * + * // remove 1 factor and add 1 new factor + * await setup.reconstitute( + * ['password1'], // remove + * [ await mfkdf.setup.factors.password('password4', { id: 'password4' }) ] // add + * ) + * + * // derive key with new factors + * const derived = await mfkdf.derive.key(setup.policy, { + * password3: mfkdf.derive.factors.password('password3'), + * password4: mfkdf.derive.factors.password('password4') + * }) + * + * setup.key.toString('hex') // -> 6458…dc3c + * derived.key.toString('hex') // -> 6458…dc3c + * + * @param removeFactors - Array of IDs of existing factors to remove + * @param addFactors - Array of factors to add or replace + * @param threshold - New threshold for key derivation; same as current by default * @throws {TypeError} If the threshold is not an integer. */ - async reconstitute(remove_factors?: string[], add_factors?: raw.Mfkdf2Factor[], threshold?: number) { + async reconstitute(removeFactors?: string[], addFactors?: raw.Mfkdf2Factor[], threshold?: number) { // check for integer otherwise uniffi will cast to integer if (threshold && !Number.isInteger(threshold)) { throw new TypeError('threshold must be an integer'); } key.policy = unwrapPolicy(key.policy); - const updated = raw.derivedKeyReconstitute(key, remove_factors ?? [], add_factors ?? [], threshold); + const updated = raw.derivedKeyReconstitute(key, removeFactors ?? [], addFactors ?? [], threshold); return applyUpdate(updated); }, - async strengthen(time: number, memory: number) { + /** + * Update the time and/or memory cost of an existing multi-factor derived key. + * (This can also be used to 'weaken' a key if necessary, but that is not recommended.) + * + * @example + * const setup = await mfkdf.setup.key( + * [ + * await mfkdf.setup.factors.password('password1', { + * id: 'password1' + * }) + * ], + * { time: 3, memory: 16384 } + * ) + * + * setup.policy.time.should.equal(3) + * setup.policy.memory.should.equal(16384) + * + * const derive = await mfkdf.derive.key(setup.policy, { + * password1: mfkdf.derive.factors.password('password1') + * }) + * + * derive.policy.time.should.equal(3) + * derive.policy.memory.should.equal(16384) + * + * derive.key.toString('hex').should.equal(setup.key.toString('hex')) + * + * @param time - Additional rounds of argon2 time cost to add; 0 by default + * @param memory - Additional argon2 memory cost to add (in KiB); 0 by default + * @returns The updated key. + * @throws {TypeError} If the time or memory is not a non-negative integer. + */ + async strengthen(time?: number, memory?: number) { // check for integer otherwise uniffi will cast to integer - if (time && !Number.isInteger(time) || time < 0) { + if (time && (!Number.isInteger(time) || time < 0)) { throw new TypeError('time must be a non-negative integer'); } - if (memory && !Number.isInteger(memory) || memory < 0) { + if (memory && (!Number.isInteger(memory) || memory < 0)) { throw new TypeError('memory must be a non-negative integer'); } key.policy = unwrapPolicy(key.policy); - const updated = raw.derivedKeyStrengthen(key, time, memory); + const updated = raw.derivedKeyStrengthen(key, time ?? 0, memory ?? 0); return applyUpdate(updated); }, - async persistFactor(factorId: string) { + /** + * Persist material from an MFKDF factor to bypass it in future derivation + * + * @example + * // setup 3-factor multi-factor derived key + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password1', { id: 'password1' }), + * await mfkdf.setup.factors.password('password2', { id: 'password2' }), + * await mfkdf.setup.factors.password('password3', { id: 'password3' }) + * ]) + * + * // persist one of the factors + * const factor2 = setup.persistFactor('password2') + * + * // derive key with 2 factors + * const derived = await mfkdf.derive.key(setup.policy, { + * password1: mfkdf.derive.factors.password('password1'), + * password2: mfkdf.derive.factors.persisted(factor2), + * password3: mfkdf.derive.factors.password('password3') + * }) + * + * setup.key.toString('hex') // -> 6458…dc3c + * derived.key.toString('hex') // -> 6458…dc3c + * + * @param factorId - ID of the factor to persist + * @returns The share which can be used to bypass the factor + */ + async persistFactor(factorId: string): Promise { key.policy = unwrapPolicy(key.policy); const updated = raw.derivedKeyPersistFactor(key, factorId); return Buffer.from(updated); }, + /** + * Add a (probabilistic) hint for a factor to (usually) help verify which factor is wrong. + * Permanently adds the hint to the key policy, and throws an error when the factor is wrong. + * Makes the key slightly easier to brute-force (about 2^bits times easier), so be careful. + * Overrides the existing hint if one already exists. + * + * @example + * const setup = await mfkdf.setup.key( + * [ + * await mfkdf.setup.factors.password('password1', { + * id: 'password1' + * }) + * ], + * { + * integrity: false + * } + * ) + * + * await setup.addHint('password1') + * + * await mfkdf.derive + * .key( + * setup.policy, + * { + * password1: mfkdf.derive.factors.password('password2') + * }, + * false + * ) + * .should.be.rejectedWith(RangeError) + * + * @param factorId - Factor ID to add hint for + * @param bits - Bits of entropy to reveal; 7 by default (more is risky) + * @returns The hint. + * @throws {TypeError} If the bits is not an integer. + */ async addHint(factorId: string, bits?: number) { // check for integer otherwise uniffi will cast to integer if (bits && !Number.isInteger(bits)) { @@ -329,6 +592,31 @@ function wrapDerivedKey(key: raw.Mfkdf2DerivedKey): any { const updated = raw.derivedKeyAddHint(key, factorId, bits); return applyUpdate(updated); }, + /** + * Get a (probabilistic) hint for a factor to (usually) help verify which factor is wrong. + * Makes the key slightly easier to brute-force (about 2^bits times easier), so be careful. + * + * @example + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password("password1", { + * id: "password1", + * }), + * ]); + * + * const hint = await setup.getHint("password1", 7); // -> 1011000 + * + * const derived = await mfkdf.derive.key(setup.policy, { + * password1: mfkdf.derive.factors.password("password1"), + * }); + * + * const hint2 = await derived.getHint("password1", 7); // -> 1011000 + * hint2.should.equal(hint); + * + * @param factorId - Factor ID to get hint for + * @param bits - Bits of entropy to reveal; 7 by default (more is risky) + * @returns The hint. + * @throws {TypeError} If the bits is not an integer. + */ async getHint(factorId: string, bits: number) { // check for integer otherwise uniffi will cast to integer if (bits && !Number.isInteger(bits)) { @@ -339,10 +627,40 @@ function wrapDerivedKey(key: raw.Mfkdf2DerivedKey): any { const updated = raw.derivedKeyGetHint(key, factorId, bits); return updated; }, - async derivePassword(purpose: string, salt: string, regex: RegExp) { + /** + * Generate a policy-compliant password for a given purpose. + * + * @example + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password1', { + * id: 'password1' + * }) + * ]) + * const password = setup.derivePassword( + * 'example.com', + * 'salt', + * /[a-zA-Z]{6,10}/ + * ) + * + * const password2 = setup.derivePassword( + * 'example.com', + * 'salt', + * /[a-zA-Z]{6,10}/ + * ) + * password.should.equal(password2) + * + * @param purpose - Unique purpose value for this password + * @param salt - Unique salt value for this salt + * @param regex - Regular expression defining password policy + * @returns The derived password. + * @throws {TypeError} If the purpose or salt is not a string, or the regex is not a valid regular expression. + */ + async derivePassword(purpose: string, salt: string, regex: RegExp): Promise { key.policy = unwrapPolicy(key.policy); - // TODO (@lonerapier): fix this type - let buffer = toArrayBuffer(Buffer.from(salt)); + const buffer = toArrayBuffer(Buffer.from(salt)); + if (!buffer) { + throw new TypeError('salt must be a Buffer'); + } const updated = raw.derivedKeyDerivePassword(key, purpose, buffer, regex.source); return updated; } @@ -355,19 +673,66 @@ export const mfkdf = { setup: { factors: { /** - * - * @param password - The password to setup. - * @param options - The options for the password factor. - * @param options.id - The id of the factor. - * @returns The setup factor. + * Setup a YubiKey-compatible MFKDF HMAC-SHA1 challenge-response factor + * + * @example + * // setup key with hmacsha1 factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.hmacsha1() + * ]) + * + * // calculate response; could be done using hardware device + * const secret = setup.outputs.hmacsha1.secret + * const challenge = Buffer.from(setup.policy.factors[0].params.challenge, 'hex') + * const response = crypto.createHmac('sha1', secret).update(challenge).digest() + * + * // derive key with hmacsha1 factor + * const derive = await mfkdf.derive.key(setup.policy, { + * hmacsha1: mfkdf.derive.factors.hmacsha1(response) + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param options - Configuration options + * @param options.id - Unique identifier for this factor; `hmacsha1` by default + * @param options.secret - HMAC secret to use; randomly generated by default + * @returns Setup HMAC-SHA1 factor */ - async password(password: string, options: { id?: string } = {}) { - const factor = await raw.setupPassword(password, { - id: options.id + async hmacsha1(options: { secret?: ArrayBuffer | Buffer, id?: string } = {}): Promise { + const factor = await raw.setupHmacsha1({ + id: options.id, + secret: toArrayBuffer(options.secret) }); return wrapSetupFactor(factor); }, - async hotp(options: { secret?: ArrayBuffer | Buffer, id?: string, digits?: number, hash?: raw.HashAlgorithm, issuer?: string, label?: string } = {}) { + /** + * Setup an MFKDF HOTP factor + * + * @example + * // setup key with hotp factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.hotp({ secret: Buffer.from('abcdefghijklmnopqrst') }) + * ]) + * + * // derive key with hotp factor + * const derive = await mfkdf.derive.key(setup.policy, { + * hotp: mfkdf.derive.factors.hotp(241063) + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param options - Configuration options + * @param options.id - Unique identifier for this factor; `hotp` by default + * @param options.hash - Hash algorithm to use; sha512, sha256, or sha1 + * @param options.digits - Number of digits to use + * @param options.secret - HOTP secret to use; randomly generated by default + * @param options.issuer - OTPAuth issuer string + * @param options.label - OTPAuth label string + * @returns Setup HOTP factor + */ + async hotp(options: { secret?: ArrayBuffer | Buffer, id?: string, digits?: number, hash?: raw.HashAlgorithm, issuer?: string, label?: string } = {}): Promise { const factor = await raw.setupHotp({ id: options.id, secret: toArrayBuffer(options.secret), @@ -378,72 +743,293 @@ export const mfkdf = { }); return wrapSetupFactor(factor); }, - async totp(options: { secret?: ArrayBuffer | Buffer, id?: string, digits?: number, hash?: raw.HashAlgorithm, issuer?: string, label?: string, window?: number, step?: number, time?: bigint | number, oracle?: Record } = {}) { - const factor = await raw.setupTotp({ + /** + * Setup an MFKDF Out-of-Band Authentication (OOBA) factor + * + * @example + * // setup RSA key pair (on out-of-band server) + * const keyPair = await crypto.webcrypto.subtle.generateKey({hash: 'SHA-256', modulusLength: 2048, name: 'RSA-OAEP', publicExponent: new Uint8Array([1, 0, 1])}, true, ['encrypt', 'decrypt']) + * + * // setup key with out-of-band authentication factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.ooba({ + * key: keyPair.publicKey, params: { email: 'test@mfkdf.com' } + * }) + * ]) + * + * // decrypt and send code (on out-of-band server) + * const next = setup.policy.factors[0].params.next + * const decrypted = await crypto.webcrypto.subtle.decrypt({name: 'RSA-OAEP'}, keyPair.privateKey, Buffer.from(next, 'hex')) + * const code = JSON.parse(Buffer.from(decrypted).toString()).code; + * + * // derive key with out-of-band factor + * const derive = await mfkdf.derive.key(setup.policy, { + * ooba: mfkdf.derive.factors.ooba(code) + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param options - Configuration options + * @param options.id - Unique identifier for this factor; `ooba` by default + * @param options.length - Number of characters to use in one-time codes + * @param options.key - Public key of out-of-band channel + * @param options.params - Parameters to provide out-of-band channel + * @returns Setup OOBA factor + */ + async ooba(options: { key?: crypto.webcrypto.CryptoKey, id?: string, length?: number, params?: Record }): Promise { + const key = options.key ? await crypto.webcrypto.subtle.exportKey('jwk', options.key) : undefined; + const keyString = JSON.stringify(key); + const factor = await raw.setupOoba({ id: options.id, - secret: toArrayBuffer(options.secret), - digits: options.digits, - hash: options.hash, - issuer: options.issuer, - label: options.label, - time: options?.time ? BigInt(options.time) : undefined, - window: options.window, - step: options.step, - oracle: options?.oracle ? new Map(Object.entries(options.oracle).map(([key, value]) => [BigInt(key), value])) : undefined, + key: keyString, + length: options.length ?? 6, + params: options.params ? JSON.stringify(options.params) : undefined }); return wrapSetupFactor(factor); }, - async uuid(options: { uuid?: string, id?: string } = {}) { - const factor = await raw.setupUuid({ + /** + * Setup an MFKDF passkey factor + * + * @example + * const prf = await crypto.randomBytes(32) + * + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.passkey(prf) + * ]) + * + * const derive = await mfkdf.derive.key(setup.policy, { + * passkey: mfkdf.derive.factors.passkey(prf) + * }) + * + * derive.key.toString('hex').should.equal(setup.key.toString('hex')) + * + * @param secret - The 256-bit PRF secret from which to derive an MFKDF factor + * @param options - Configuration options + * @param options.id - Unique identifier for this factor; `passkey` by default + * @returns Setup passkey factor information + */ + async passkey(secret: ArrayBuffer | Buffer, options: { id?: string } = {}): Promise { + const factor = await raw.setupPasskey(toArrayBuffer(secret) || new Uint8Array(32).buffer, { id: options.id, - uuid: options.uuid }); return wrapSetupFactor(factor); }, - async hmacsha1(options: { secret?: ArrayBuffer | Buffer, id?: string } = {}) { - const factor = await raw.setupHmacsha1({ - id: options.id, - secret: toArrayBuffer(options.secret) + /** + * Setup an MFKDF password factor + * + * @example + * // setup key with password factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password') + * ]) + * + * // derive key with password factor + * const derive = await mfkdf.derive.key(setup.policy, { + * password: mfkdf.derive.factors.password('password') + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param password - The password from which to derive an MFKDF factor + * @param options - Configuration options + * @param options.id - Unique identifier for this factor; `password` by default + * @returns Setup password factor information + */ + async password(password: string, options: { id?: string } = {}): Promise { + const factor = await raw.setupPassword(password, { + id: options.id }); return wrapSetupFactor(factor); }, - async question(answer: string, options: { question?: string, id?: string } = {}) { + /** + * Setup an MFKDF Security Question factor + * + * @example + * // setup key with security question factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.question('Fido') + * ]) + * + * // derive key with security question factor + * const derive = await mfkdf.derive.key(setup.policy, { + * question: mfkdf.derive.factors.question('Fido') + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param answer - The answer from which to derive an MFKDF factor + * @param options - Configuration options + * @param options.question - Security question corresponding to this factor + * @param options.id - Unique identifier for this factor; `question` by default + * @returns Setup security question factor information + */ + async question(answer: string, options: { question?: string, id?: string } = {}): Promise { const factor = await raw.setupQuestion(answer, { id: options.id, question: options.question }); return wrapSetupFactor(factor); }, - async ooba(options: { key?: crypto.webcrypto.CryptoKey, id?: string, length?: number, params?: Record }) { - const key = options.key ? await crypto.webcrypto.subtle.exportKey('jwk', options.key) : undefined; - const keyString = JSON.stringify(key); - const factor = await raw.setupOoba({ + /** + * Setup an MFKDF stacked key factor + * + * @example + * // setup key with stack factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.stack([ + * await mfkdf.setup.factors.password('password1', { + * id: 'password1' + * }), + * await mfkdf.setup.factors.password('password2', { + * id: 'password2' + * }) + * ]), + * await mfkdf.setup.factors.password('password3', { id: 'password3' }) + * ]) + * + * // derive key with stack factor + * const derive = await mfkdf.derive.key(setup.policy, { + * stack: mfkdf.derive.factors.stack({ + * password1: mfkdf.derive.factors.password('password1'), + * password2: mfkdf.derive.factors.password('password2') + * }), + * password3: mfkdf.derive.factors.password('password3') + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param factors - Array of factors used to derive this key + * @param options - Configuration options + * @param options.id - Unique identifier for this factor + * @param options.threshold - Number of factors required to derive key; factors.length by default (all required) + * @param options.salt - Cryptographic salt; generated via secure PRG by default (recommended) + * @returns Setup stack factor information + */ + async stack(factors: raw.Mfkdf2Factor[], options: { id?: string, threshold?: number, salt?: ArrayBuffer | Buffer | Uint8Array } = {}): Promise { + const factor = await raw.setupStack(factors, { id: options.id, - key: keyString, - length: options.length ?? 6, - params: options.params ? JSON.stringify(options.params) : undefined + threshold: options.threshold, + salt: toArrayBuffer(options.salt) }); return wrapSetupFactor(factor); }, - async passkey(secret: ArrayBuffer | Buffer, options: { id?: string } = {}) { - const factor = await raw.setupPasskey(toArrayBuffer(secret) || new Uint8Array(32).buffer, { + /** + * Setup an MFKDF TOTP factor + * + * @example + * // setup key with totp factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.totp({ + * secret: Buffer.from('abcdefghijklmnopqrst'), + * time: 1650430806597 + * }) + * ]) + * + * // derive key with totp factor + * const derive = await mfkdf.derive.key(setup.policy, { + * totp: mfkdf.derive.factors.totp(953265, { time: 1650430943604 }) + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param options - Configuration options + * @param options.id - Unique identifier for this factor; `totp` by default + * @param options.hash - Hash algorithm to use; `sha512`, `sha256`, or `sha1`; `sha1` by default + * @param options.digits - Number of digits to use; `6` by default + * @param options.secret - TOTP secret to use; randomly generated by default + * @param options.issuer - OTPAuth issuer string; `MFKDF` by default + * @param options.label - OTPAuth label string; `mfkdf.com` by default + * @param options.time - Current time for TOTP; defaults to `Date.now()` + * @param options.window - Maximum window between logins, in number of steps (1 month by default) + * @param options.step - TOTP step size; `30` by default + * @param options.oracle - Timing oracle offsets to use; none by default + * @returns Setup TOTP factor information + */ + async totp(options: { secret?: ArrayBuffer | Buffer, id?: string, digits?: number, hash?: raw.HashAlgorithm, issuer?: string, label?: string, window?: number, step?: number, time?: bigint | number, oracle?: Record } = {}): Promise { + const factor = await raw.setupTotp({ id: options.id, + secret: toArrayBuffer(options.secret), + digits: options.digits, + hash: options.hash, + issuer: options.issuer, + label: options.label, + time: options?.time ? BigInt(options.time) : undefined, + window: options.window, + step: options.step, + oracle: options?.oracle ? new Map(Object.entries(options.oracle).map(([key, value]) => [BigInt(key), value])) : undefined, }); return wrapSetupFactor(factor); }, - async stack(factors: raw.Mfkdf2Factor[], options: { id?: string, threshold?: number, salt?: ArrayBuffer | Buffer | Uint8Array } = {}) { - const factor = await raw.setupStack(factors, { + /** + * Setup an MFKDF UUID factor + * + * @example + * // setup key with uuid factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.uuid({ uuid: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d' }) + * ]) + * + * // derive key with uuid factor + * const derive = await mfkdf.derive.key(setup.policy, { + * uuid: mfkdf.derive.factors.uuid('9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d') + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param options - Configuration options + * @param options.uuid - UUID to use for this factor; random v4 UUID by default + * @param options.id - Unique identifier for this factor; `uuid` by default + * @returns Setup UUID factor information + */ + async uuid(options: { uuid?: string, id?: string } = {}): Promise { + const factor = await raw.setupUuid({ id: options.id, - threshold: options.threshold, - salt: toArrayBuffer(options.salt) + uuid: options.uuid }); return wrapSetupFactor(factor); - } + }, }, + /** + * Validate and setup a configuration for a multi-factor derived key. + * + * @example + * // Setup 16-byte, 2-of-3-factor multi-factor derived key using password, HOTP, and UUID recovery code: + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password'), + * await mfkdf.setup.factors.hotp({ secret: Buffer.from('abcdefghijklmnopqrst') }), + * await mfkdf.setup.factors.uuid({ id: 'recovery', uuid: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d' }) + * ], { threshold: 2 }); + * + * // Derive key using 2 of the 3 factors + * const derive = await mfkdf.derive.key(setup.policy, { + * password: mfkdf.derive.factors.password('password'), + * hotp: mfkdf.derive.factors.hotp(241063) + * }); + * + * setup.key.toString('hex') // => 34d2…5771 + * derive.key.toString('hex') // => 34d2…5771 + * + * @param factors Array of factors used to derive this key. + * @param options Optional configuration options. + * @param options.id Unique identifier for this key. A random UUIDv4 is generated by default. + * @param options.threshold Number of factors required to derive key. Defaults to factors.length (all required). + * @param options.salt Cryptographic salt. Generated by a secure PRG by default (recommended). + * @param options.integrity Whether to sign the resulting key policy. Defaults to true (recommended). + * @param options.stack Whether to use a stack key for key derivation. Defaults to false. + * @param options.time Additional rounds of argon2 time cost to add. Defaults to 0. + * @param options.memory Additional argon2 memory cost to add (in KiB). Defaults to 0. + * @returns The derived key. + */ async key( factors: raw.Mfkdf2Factor[], options: { id?: string; threshold?: number; salt?: ArrayBuffer | Buffer | Uint8Array, stack?: boolean, integrity?: boolean, time?: number, memory?: number } = {} - ) { + ): Promise { // BUG (@lonerapier): uniffi casts float to integer automatically so we need to check here if (options.time !== undefined && (!Number.isInteger(options.time) || options.time < 0)) { throw new TypeError('time must be a non-negative integer'); @@ -465,53 +1051,319 @@ export const mfkdf = { }, derive: { factors: { - async password(password: string) { - return wrapDeriveFactor(await raw.derivePassword(password)); - }, - async hotp(code: number) { - return wrapDeriveFactor(await raw.deriveHotp(code)); - }, - async uuid(uuid: string) { - return wrapDeriveFactor(await raw.deriveUuid(uuid)); - }, - async hmacsha1(response: Buffer) { + /** + * Derive a YubiKey-compatible MFKDF HMAC-SHA1 challenge-response factor + * + * @example + * // setup key with hmacsha1 factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.hmacsha1() + * ]) + * + * // calculate response; could be done using hardware device + * const secret = setup.outputs.hmacsha1.secret + * const challenge = Buffer.from(setup.policy.factors[0].params.challenge, 'hex') + * const response = crypto.createHmac('sha1', secret).update(challenge).digest() + * + * // derive key with hmacsha1 factor + * const derive = await mfkdf.derive.key(setup.policy, { + * hmacsha1: mfkdf.derive.factors.hmacsha1(response) + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param response - HMAC-SHA1 response + * @returns Derived HMAC-SHA1 factor + */ + async hmacsha1(response: Buffer): Promise { const buffer = toArrayBuffer(response); if (!buffer) throw new Error('Invalid response'); return wrapDeriveFactor(await raw.deriveHmacsha1(buffer)); }, - async question(answer: string) { - return wrapDeriveFactor(await raw.deriveQuestion(answer)); + /** + * Derive an MFKDF HOTP factor + * + * @example + * // setup key with hotp factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.hotp({ secret: Buffer.from('abcdefghijklmnopqrst') }) + * ]) + * + * // derive key with hotp factor + * const derive = await mfkdf.derive.key(setup.policy, { + * hotp: mfkdf.derive.factors.hotp(241063) + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param code - The HOTP code from which to derive an MFKDF factor + * @returns Derived HOTP factor + */ + async hotp(code: number): Promise { + return wrapDeriveFactor(await raw.deriveHotp(code)); }, - async ooba(code: string) { + /** + * Derive an MFKDF Out-of-Band Authentication (OOBA) factor + * + * @example + * // setup RSA key pair (on out-of-band server) + * const keyPair = await crypto.webcrypto.subtle.generateKey({hash: 'SHA-256', modulusLength: 2048, name: 'RSA-OAEP', publicExponent: new Uint8Array([1, 0, 1])}, true, ['encrypt', 'decrypt']) + * + * // setup key with out-of-band authentication factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.ooba({ + * key: keyPair.publicKey, params: { email: 'test@mfkdf.com' } + * }) + * ]) + * + * // decrypt and send code (on out-of-band server) + * const next = setup.policy.factors[0].params.next + * const decrypted = await crypto.webcrypto.subtle.decrypt({name: 'RSA-OAEP'}, keyPair.privateKey, Buffer.from(next, 'hex')) + * const code = JSON.parse(Buffer.from(decrypted).toString()).code; + * + * // derive key with out-of-band factor + * const derive = await mfkdf.derive.key(setup.policy, { + * ooba: mfkdf.derive.factors.ooba(code) + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param code - The one-time code from which to derive an MFKDF factor + * @returns Derived OOBA factor + */ + async ooba(code: string): Promise { return wrapDeriveFactor(await raw.deriveOoba(code)); }, - async passkey(secret: ArrayBuffer | Buffer) { + /** + * Derive an MFKDF passkey factor + * + * @example + * const prf = await crypto.randomBytes(32) + * + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.passkey(prf) + * ]) + * + * const derive = await mfkdf.derive.key(setup.policy, { + * passkey: mfkdf.derive.factors.passkey(prf) + * }) + * + * derive.key.toString('hex').should.equal(setup.key.toString('hex')) + * + * @param secret - The 256-bit PRF secret from which to derive an MFKDF factor + * @returns Derived passkey factor + */ + async passkey(secret: ArrayBuffer | Buffer): Promise { const buffer = toArrayBuffer(secret); if (!buffer) throw new Error('Invalid secret'); return wrapDeriveFactor(await raw.derivePasskey(buffer)); }, - async stack(factors: Record | Map) { + /** + * Derive an MFKDF password factor + * + * @example + * // setup key with password factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password') + * ]) + * + * // derive key with password factor + * const derive = await mfkdf.derive.key(setup.policy, { + * password: mfkdf.derive.factors.password('password') + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param password - The password from which to derive an MFKDF factor + * @returns Derived password factor + */ + async password(password: string): Promise { + return wrapDeriveFactor(await raw.derivePassword(password)); + }, + /** + * Use a persisted MFDKF factor + * + * @example + * // setup 3-factor multi-factor derived key + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password1', { id: 'password1' }), + * await mfkdf.setup.factors.password('password2', { id: 'password2' }), + * await mfkdf.setup.factors.password('password3', { id: 'password3' }) + * ]) + * + * // persist one of the factors + * const factor2 = setup.persistFactor('password2') + * + * // derive key with 2 factors + * const derived = await mfkdf.derive.key(setup.policy, { + * password1: mfkdf.derive.factors.password('password1'), + * password2: mfkdf.derive.factors.persisted(factor2), + * password3: mfkdf.derive.factors.password('password3') + * }) + * + * setup.key.toString('hex') // -> 6458…dc3c + * derived.key.toString('hex') // -> 6458…dc3c + * + * @param share - The share corresponding to the persisted factor + * @returns Derived persisted factor + */ + async persisted(share: Buffer): Promise { + const buffer = toArrayBuffer(share); + if (!buffer) throw new Error('Invalid share'); + + return wrapDeriveFactor(await raw.derivePersisted(buffer)); + }, + /** + * Derive an MFKDF Security Question factor + * + * @example + * // setup key with security question factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.question('Fido') + * ]) + * + * // derive key with security question factor + * const derive = await mfkdf.derive.key(setup.policy, { + * question: mfkdf.derive.factors.question('Fido') + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param answer - The answer from which to derive an MFKDF factor + * @returns Derived security question factor + */ + async question(answer: string): Promise { + return wrapDeriveFactor(await raw.deriveQuestion(answer)); + }, + /** + * Derive an MFKDF stacked key factor + * + * @example + * // setup key with stack factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.stack([ + * await mfkdf.setup.factors.password('password1', { + * id: 'password1' + * }), + * await mfkdf.setup.factors.password('password2', { + * id: 'password2' + * }) + * ]), + * await mfkdf.setup.factors.password('password3', { id: 'password3' }) + * ]) + * + * // derive key with stack factor + * const derive = await mfkdf.derive.key(setup.policy, { + * stack: mfkdf.derive.factors.stack({ + * password1: mfkdf.derive.factors.password('password1'), + * password2: mfkdf.derive.factors.password('password2') + * }), + * password3: mfkdf.derive.factors.password('password3') + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param factors - Factors used to derive this key + * @returns Derived stacked factor + */ + async stack(factors: Record | Map): Promise { // Convert object to Map if needed const factorMap = factors instanceof Map ? factors : new Map(Object.entries(factors)); return wrapDeriveFactor(await raw.deriveStack(factorMap)); }, - async totp(code: number, options?: { time?: bigint | number, oracle?: Record }) { + /** + * Derive an MFKDF TOTP factor + * + * @example + * // setup key with totp factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.totp({ + * secret: Buffer.from('abcdefghijklmnopqrst'), + * time: 1650430806597 + * }) + * ]) + * + * // derive key with totp factor + * const derive = await mfkdf.derive.key(setup.policy, { + * totp: mfkdf.derive.factors.totp(953265, { time: 1650430943604 }) + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param code - The TOTP code from which to derive an MFKDF factor + * @param options - Additional options for deriving the TOTP factor + * @param options.time - Current time for TOTP; defaults to Date.now() + * @param options.oracle - Timing oracle offsets to use; none by default + * @returns Derived TOTP factor + */ + async totp(code: number, options?: { time?: bigint | number, oracle?: Record }): Promise { const factor = await raw.deriveTotp(code, { time: options?.time ? BigInt(options.time) : undefined, oracle: options?.oracle ? new Map(Object.entries(options.oracle).map(([key, value]) => [BigInt(key), value])) : undefined, }); return wrapDeriveFactor(factor); }, - async persisted(share: ArrayBuffer | Buffer) { - const buffer = toArrayBuffer(share); - if (!buffer) throw new Error('Invalid share'); - - return wrapDeriveFactor(await raw.derivePersisted(buffer)); - } + /** + * Derive an MFKDF UUID factor + * + * @example + * // setup key with uuid factor + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.uuid({ uuid: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d' }) + * ]) + * + * // derive key with uuid factor + * const derive = await mfkdf.derive.key(setup.policy, { + * uuid: mfkdf.derive.factors.uuid('9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d') + * }) + * + * setup.key.toString('hex') // -> 01d0…2516 + * derive.key.toString('hex') // -> 01d0…2516 + * + * @param uuid - The uuid from which to derive an MFKDF factor + * @returns Derived UUID factor + */ + async uuid(uuid: string): Promise { + return wrapDeriveFactor(await raw.deriveUuid(uuid)); + }, }, - async key(policy: any, factors: Record | Map, verify?: boolean, stack?: boolean) { + /** + * Derive a key from multiple factors of input + * + * @example + * // setup 16 byte 2-of-3-factor multi-factor derived key with a password, HOTP code, and UUID recovery code + * const setup = await mfkdf.setup.key([ + * await mfkdf.setup.factors.password('password'), + * await mfkdf.setup.factors.hotp({ secret: Buffer.from('abcdefghijklmnopqrst') }), + * await mfkdf.setup.factors.uuid({ id: 'recovery', uuid: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d' }) + * ], { threshold: 2 }); + * + * // derive key using 2 of the 3 factors + * const derive = await mfkdf.derive.key(setup.policy, { + * password: mfkdf.derive.factors.password('password'), + * hotp: mfkdf.derive.factors.hotp(241063) + * }); + * + * setup.key.toString('hex') // => 34d2…5771 + * derive.key.toString('hex') // => 34d2…5771 + * + * @param policy - The key policy for the key being derived + * @param factors - Factors used to derive this key + * @param [verify=true] - Whether to verify the integrity of the policy after deriving (recommended) + * @returns The derived key. + * @async + * @memberOf derive + */ + async key(policy: any, factors: Record | Map, verify?: boolean, stack?: boolean): Promise { // Convert object to Map if needed const factorMap = factors instanceof Map ? factors @@ -521,61 +1373,355 @@ export const mfkdf = { return wrapDerivedKey(key); } }, - secrets: { - share(secret: ArrayBuffer, threshold: number, shares: number): ArrayBuffer[] { - // Placeholder - implement if needed in Rust - const result: ArrayBuffer[] = []; - for (let i = 0; i < shares; i++) { - result.push(secret); - } - return result; - }, - combine(shares: (ArrayBuffer | null)[], threshold: number, totalShares: number): ArrayBuffer { - const validShares = shares.filter(s => s !== null) as ArrayBuffer[]; - if (validShares.length < threshold) { - throw new Error('Not enough shares'); - } - return validShares[0]; - } - }, policy: { - async validate(policy: any) { + /** + * Get all ids of multi-factor derived key factors (including factors of stacked keys) + * + * @example + * // setup key that can be derived from passwordA AND (passwordB OR passwordC) + * const setup = await mfkdf.policy.setup( + * await mfkdf.policy.and( + * await mfkdf.setup.factors.password('passwordA', { id: 'passwordA' }), + * await mfkdf.policy.or( + * await mfkdf.setup.factors.password('passwordB', { id: 'passwordB' }), + * await mfkdf.setup.factors.password('passwordC', { id: 'passwordC' }) + * ) + * ) + * ) + * + * // get list of ids + * const ids = mfkdf.policy.ids(setup.policy) // -> ['passwordA', 'passwordB', 'passwordC', ...] + * + * @param policy - Policy used to derive a key + * @returns The ids of the provided factors + */ + async ids(policy: MFKDF2Policy): Promise { + return raw.policyIds(unwrapPolicy(policy)); + }, + /** + * Validate multi-factor derived key policy + * + * @example + * // setup key that can be derived from passwordA AND (passwordB OR passwordC) + * const setup = await mfkdf.policy.setup( + * await mfkdf.policy.and( + * await mfkdf.setup.factors.password('passwordA', { id: 'passwordA' }), + * await mfkdf.policy.or( + * await mfkdf.setup.factors.password('passwordB', { id: 'passwordB' }), + * await mfkdf.setup.factors.password('passwordC', { id: 'passwordC' }) + * ) + * ) + * ) + * + * // validate policy + * const valid = mfkdf.policy.validate(setup.policy) // -> true + * + * @param policy - Policy used to derive a key + * @returns Whether the policy is valid + */ + async validate(policy: MFKDF2Policy): Promise { return raw.policyValidate(unwrapPolicy(policy)); }, - async setup(factor: raw.Mfkdf2Factor, options?: { id?: string, threshold?: number, salt?: ArrayBuffer | Buffer | Uint8Array, integrity?: boolean }) { - return wrapDerivedKey(await raw.policySetup(factor, { + /** + * Validate and setup a policy-based multi-factor derived key + * + * @example + * // setup key that can be derived from passwordA AND (passwordB OR passwordC) + * const setup = await mfkdf.policy.setup( + * await mfkdf.policy.and( + * await mfkdf.setup.factors.password('passwordA', { id: 'passwordA' }), + * await mfkdf.policy.or( + * await mfkdf.setup.factors.password('passwordB', { id: 'passwordB' }), + * await mfkdf.setup.factors.password('passwordC', { id: 'passwordC' }) + * ) + * ) + * ) + * + * // derive key with passwordA and passwordC (or passwordA and passwordB) + * const derive = await mfkdf.policy.derive(setup.policy, { + * passwordA: mfkdf.derive.factors.password('passwordA'), + * passwordC: mfkdf.derive.factors.password('passwordC'), + * }) + * + * setup.key.toString('hex') // -> e16a…5263 + * derive.key.toString('hex') // -> e16a…5263 + * + * @param factor - Base factor used to derive this key + * @param options - Configuration options + * @param options.id - Unique identifier for this key; random UUIDv4 generated by default + * @param options.threshold - Number of factors required to derive key; factors.length by default (all required) + * @param options.salt - Cryptographic salt; generated via secure PRG by default (recommended) + * @param options.integrity - Whether to sign the resulting key policy; true by default + * @returns Setup policy-based multi-factor derived key + */ + async setup(factor: raw.Mfkdf2Factor, options?: { id?: string, threshold?: number, salt?: ArrayBuffer | Buffer | Uint8Array, integrity?: boolean }): Promise { + return wrapDerivedKey(raw.policySetup(factor, { id: options?.id, threshold: options?.threshold, salt: toArrayBuffer(options?.salt), integrity: options?.integrity })); }, - async derive(policy: any, factors: Record | Map, verify?: boolean) { + /** + * Derive a policy-based multi-factor derived key + * + * @example + * // setup key that can be derived from passwordA AND (passwordB OR passwordC) + * const setup = await mfkdf.policy.setup( + * await mfkdf.policy.and( + * await mfkdf.setup.factors.password('passwordA', { id: 'passwordA' }), + * await mfkdf.policy.or( + * await mfkdf.setup.factors.password('passwordB', { id: 'passwordB' }), + * await mfkdf.setup.factors.password('passwordC', { id: 'passwordC' }) + * ) + * ) + * ) + * + * // derive key with passwordA and passwordC (or passwordA and passwordB) + * const derive = await mfkdf.policy.derive(setup.policy, { + * passwordA: mfkdf.derive.factors.password('passwordA'), + * passwordC: mfkdf.derive.factors.password('passwordC'), + * }) + * + * setup.key.toString('hex') // -> e16a…5263 + * derive.key.toString('hex') // -> e16a…5263 + * + * @param policy - The key policy for the key being derived + * @param factors - Factors used to derive this key + * @param verify - Whether to verify the integrity of the policy after deriving (recommended); true by default + * @returns Derived key + */ + async derive(policy: MFKDF2Policy, factors: Record | Map, verify?: boolean): Promise { const factorMap = factors instanceof Map ? factors : new Map(Object.entries(factors)); - return wrapDerivedKey(await raw.policyDerive(unwrapPolicy(policy), factorMap, verify)); + return wrapDerivedKey(raw.policyDerive(unwrapPolicy(policy), factorMap, verify)); }, - async evaluate(policy: any, factorIds: string[]) { - return await raw.policyEvaluate(unwrapPolicy(policy), factorIds); + /** + * Evaluate a policy-based multi-factor derived key + * + * @example + * // setup key that can be derived from passwordA AND (passwordB OR passwordC) + * const setup = await mfkdf.policy.setup( + * await mfkdf.policy.and( + * await mfkdf.setup.factors.password('passwordA', { id: 'passwordA' }), + * await mfkdf.policy.or( + * await mfkdf.setup.factors.password('passwordB', { id: 'passwordB' }), + * await mfkdf.setup.factors.password('passwordC', { id: 'passwordC' }) + * ) + * ) + * ) + * + * // check if key can be derived with passwordA and passwordC + * const valid1 = await mfkdf.policy.evaluate(setup.policy, ['passwordA', 'passwordC']) // -> true + * + * // check if key can be derived with passwordB and passwordC + * const valid2 = await mfkdf.policy.evaluate(setup.policy, ['passwordB', 'passwordC']) // -> false + * + * @param policy - The key policy for the key being derived + * @param factorIds - Array of factor ids used to derive this key + * @returns Whether the key can be derived with given factor ids + */ + async evaluate(policy: MFKDF2Policy, factorIds: string[]): Promise { + return raw.policyEvaluate(unwrapPolicy(policy), factorIds); }, - async atLeast(n: number, factors: raw.Mfkdf2Factor[]) { + /** + * Create a MFKDF factor based on at least some number of the provided MFKDF factors + * + * @example + * // setup key that can be derived from at least 2 of (passwordA, passwordB, passwordC) + * const setup = await mfkdf.policy.setup( + * await mfkdf.policy.any([ + * await mfkdf.setup.factors.password('passwordA', { id: 'passwordA' }), + * await mfkdf.setup.factors.password('passwordB', { id: 'passwordB' }), + * await mfkdf.setup.factors.password('passwordC', { id: 'passwordC' }) + * ]) + * ) + * + * // derive key with passwordA and passwordB (or passwordA and passwordC, or passwordB and passwordC) + * const derive = await mfkdf.policy.derive(setup.policy, { + * passwordA: mfkdf.derive.factors.password('passwordA'), + * passwordB: mfkdf.derive.factors.password('passwordB') + * }) + * + * setup.key.toString('hex') // -> e16a…5263 + * derive.key.toString('hex') // -> e16a…5263 + * + * @param n - The number of factors to be required + * @param factors - The factor inputs to the atLeast(#) policy + * @returns Factor that can be derived with at least n of the given factors + */ + async atLeast(n: number, factors: raw.Mfkdf2Factor[]): Promise { return wrapSetupFactor(raw.policyAtLeast(n, factors)); }, - async all(factors: raw.Mfkdf2Factor[]) { + /** + * Create a MFKDF factor based on ALL of the provided MFKDF factors + * + * @example + * // setup key that can be derived from passwordA AND passwordB AND passwordC + * const setup = await mfkdf.policy.setup( + * await mfkdf.policy.all([ + * await mfkdf.setup.factors.password('passwordA', { id: 'passwordA' }), + * await mfkdf.setup.factors.password('passwordB', { id: 'passwordB' }), + * await mfkdf.setup.factors.password('passwordC', { id: 'passwordC' }) + * ]) + * ) + * + * // derive key with passwordA and passwordB and passwordC + * const derive = await mfkdf.policy.derive(setup.policy, { + * passwordA: mfkdf.derive.factors.password('passwordA'), + * passwordB: mfkdf.derive.factors.password('passwordB'), + * passwordC: mfkdf.derive.factors.password('passwordC'), + * }) + * + * setup.key.toString('hex') // -> e16a…5263 + * derive.key.toString('hex') // -> e16a…5263 + * + * @param factors - The factor inputs to the ALL policy + * @returns Factor that can be derived with all factors + */ + async all(factors: raw.Mfkdf2Factor[]): Promise { return wrapSetupFactor(raw.policyAll(factors)); }, - async any(factors: raw.Mfkdf2Factor[]) { + /** + * Create a MFKDF factor based on ANY of the provided MFKDF factors + * + * @example + * // setup key that can be derived from passwordA OR passwordB OR passwordC + * const setup = await mfkdf.policy.setup( + * await mfkdf.policy.any([ + * await mfkdf.setup.factors.password('passwordA', { id: 'passwordA' }), + * await mfkdf.setup.factors.password('passwordB', { id: 'passwordB' }), + * await mfkdf.setup.factors.password('passwordC', { id: 'passwordC' }) + * ]) + * ) + * + * // derive key with passwordA (or passwordB or passwordC) + * const derive = await mfkdf.policy.derive(setup.policy, { + * passwordB: mfkdf.derive.factors.password('passwordB') + * }) + * + * setup.key.toString('hex') // -> e16a…5263 + * derive.key.toString('hex') // -> e16a…5263 + * + * @param factors - The factor inputs to the ANY policy + * @returns Factor that can be derived with any factor + */ + async any(factors: raw.Mfkdf2Factor[]): Promise { return wrapSetupFactor(raw.policyAny(factors)); }, - async or(factor1: raw.Mfkdf2Factor, factor2: raw.Mfkdf2Factor) { + /** + * Create a MFKDF factor based on OR of two MFKDF factors + * + * @example + * // setup key that can be derived from passwordA AND (passwordB OR passwordC) + * const setup = await mfkdf.policy.setup( + * await mfkdf.policy.and( + * await mfkdf.setup.factors.password('passwordA', { id: 'passwordA' }), + * await mfkdf.policy.or( + * await mfkdf.setup.factors.password('passwordB', { id: 'passwordB' }), + * await mfkdf.setup.factors.password('passwordC', { id: 'passwordC' }) + * ) + * ) + * ) + * + * // derive key with passwordA and passwordC (or passwordA and passwordB) + * const derive = await mfkdf.policy.derive(setup.policy, { + * passwordA: mfkdf.derive.factors.password('passwordA'), + * passwordC: mfkdf.derive.factors.password('passwordC'), + * }) + * + * setup.key.toString('hex') // -> e16a…5263 + * derive.key.toString('hex') // -> e16a…5263 + * + * @param factor1 - The first factor input to the OR policy + * @param factor2 - The second factor input to the OR policy + * @returns Factor that can be derived with either factor + */ + async or(factor1: raw.Mfkdf2Factor, factor2: raw.Mfkdf2Factor): Promise { return wrapSetupFactor(raw.policyOr(factor1, factor2)); }, - async and(factor1: raw.Mfkdf2Factor, factor2: raw.Mfkdf2Factor) { + /** + * Create a MFKDF factor based on AND of two MFKDF factors + * + * @example + * // setup key that can be derived from passwordA AND (passwordB OR passwordC) + * const setup = await mfkdf.policy.setup( + * await mfkdf.policy.and( + * await mfkdf.setup.factors.password('passwordA', { id: 'passwordA' }), + * await mfkdf.policy.or( + * await mfkdf.setup.factors.password('passwordB', { id: 'passwordB' }), + * await mfkdf.setup.factors.password('passwordC', { id: 'passwordC' }) + * ) + * ) + * ) + * + * // derive key with passwordA and passwordC (or passwordA and passwordB) + * const derive = await mfkdf.policy.derive(setup.policy, { + * passwordA: mfkdf.derive.factors.password('passwordA'), + * passwordC: mfkdf.derive.factors.password('passwordC'), + * }) + * + * setup.key.toString('hex') // -> e16a…5263 + * derive.key.toString('hex') // -> e16a…5263 + * + * @param factor1 - The first factor input to the AND policy + * @param factor2 - The second factor input to the AND policy + * @returns Factor that can be derived with both factors + */ + async and(factor1: raw.Mfkdf2Factor, factor2: raw.Mfkdf2Factor): Promise { return wrapSetupFactor(raw.policyAnd(factor1, factor2)); } } }; +/** + * MFKDF2 factor instance. + * + * In MFKDF2 protocol, a factor combines a secret piece of data (the factor material, often derived + * from a password, hardware token response, TOTP code, etc.) with some public state stored on the + * server. The job of a factor is to turn this dynamic user input into stable key material that can + * be reused across multiple key derivations. + * + * Each factor has two core operations: + * - `setup`: creates an initial factor instance from a factor-specific configuration (e.g., password policy, TOTP parameters, hardware token IDs) + * - `derive`: given a fresh user "witness" (e.g., the current password or OTP) and the current public state, produces new factor material and updated public state for the next use. + * + * @property type - Factor type string (for example `'password'`, `'hotp'`, `'totp'`) + * @property data - Raw factor bytes as a `Buffer` + * @property params - Async helper returning setup/derive parameters for this factor + * @property output - Async helper returning setup/derive outputs for this factor + */ +export type MFKDF2Factor = ReturnType; + +/** + * Class representing a multi-factor derived key + * + * @property policy - The policy for deriving this key + * @property key - The value of this derived key + * @property secret - The secret (pre-KDF) value of this derived key + * @property shares - The shares corresponding to the factors of this key + * @property outputs - The outputs corresponding to the factors of this key + */ +export type MFKDF2DerivedKey = ReturnType; + +/** + * MFKDF policy is a set of all allowable factor combinations that can be used to derive the final + * key. MFKDF instance after i-th derivation consists of public construction parameters (threshold, + * salt, etc.), per-factor public parameters (encrypted shares, secret), and factor public state + * (params). + * + * @property schema - JSON schema URL to validate the key policy. + * @property id - Unique identifier for the policy. + * @property threshold - Threshold for the policy. + * @property salt - Base-64 encoded salt value used to derive the policy key. + * @property factors - [`PolicyFactor`] combination used to derive the key in the policy. + * @property hmac - Base-64 encoded HMAC value used to verify the policy [integrity](`crate::integrity`). + * @property time - Additional rounds of argon2 time cost to add, beyond OWASP minimums. + * @property memory - Additional argon2 memory cost to add (in KiB), beyond OWASP minimums. + * @property key - Base-64 encoded policy key encrypted using KEK (key encapsulation key). + */ +export type MFKDF2Policy = ReturnType; + export default mfkdf; \ No newline at end of file diff --git a/mfkdf2-web/src/utils.ts b/mfkdf2-web/src/utils.ts new file mode 100644 index 00000000..c5c389bb --- /dev/null +++ b/mfkdf2-web/src/utils.ts @@ -0,0 +1,90 @@ + +// Helper to convert Buffer/Uint8Array to ArrayBuffer for UniFFI +function toArrayBuffer(input: ArrayBuffer | Buffer | Uint8Array | undefined): ArrayBuffer | undefined { + if (input === undefined) return undefined; + if (input instanceof ArrayBuffer) return input; + // Buffer and Uint8Array have .buffer property, but may be a view with offset + const view = input as Uint8Array; + return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength) as ArrayBuffer; +} + +// Helper to deep parse JSON strings +function deepParse(value: any): any { + if (typeof value === 'string') { + try { + return deepParse(JSON.parse(value)); + } catch { + return value; + } + } + + if (Array.isArray(value)) { + return value.map(deepParse); + } + + if (value && typeof value === 'object') { + const parsed: any = {}; + for (const [key, nested] of Object.entries(value)) { + parsed[key] = deepParse(nested); + } + return parsed; + } + + return value; +} + +// Helper to stringify policy/factor params/outputs +function stringifyFactorParams(value: any): any { + if (value === undefined || value === null || typeof value === 'string') { + return value; + } + + const POLICY_ORDER = ['$id', '$schema', 'factors', 'key', 'memory', 'salt', 'threshold', 'time']; + const FACTOR_ORDER = ['id', 'pad', 'params', 'salt', 'secret', 'type', 'hint']; + + const stringifyPolicy = (input: any): string => JSON.stringify(orderValue(input, 'policy')); + + function orderValue(input: any, context?: 'policy' | 'factor'): any { + if (Array.isArray(input)) { + if (context === 'policy') { + return input.map((item) => orderValue(item, 'factor')); + } + return input.map((item) => orderValue(item)); + } + + if (input && typeof input === 'object') { + const baseOrder = context === 'policy' ? POLICY_ORDER : context === 'factor' ? FACTOR_ORDER : []; + const extras = Object.keys(input).filter((key) => !baseOrder.includes(key)).sort(); + const keys = [...baseOrder, ...extras]; + const ordered: any = {}; + + for (const key of keys) { + if (!(key in input)) continue; + + if (context === 'factor' && key === 'params') { + ordered.params = input[key]; + continue; + } + + if (key === 'factors' && Array.isArray(input[key])) { + ordered.factors = input[key].map((item: any) => orderValue(item, 'factor')); + continue; + } + + ordered[key] = orderValue(input[key]); + } + + return ordered; + } + + return input; + } + + return stringifyPolicy(value); +} + +export { + toArrayBuffer, + deepParse, + stringifyFactorParams, +}; \ No newline at end of file diff --git a/mfkdf2-web/test/features/reconstitution.test.ts b/mfkdf2-web/test/features/reconstitution.test.ts index cc156a01..c4aca579 100644 --- a/mfkdf2-web/test/features/reconstitution.test.ts +++ b/mfkdf2-web/test/features/reconstitution.test.ts @@ -358,7 +358,5 @@ suite('features/reconstitution', () => { await setup.reconstitute([], [], 4).should.be.rejectedWith(Mfkdf2Error.InvalidThreshold) }) - - // TODO: type error tests are not added }) }); diff --git a/mfkdf2-web/test/mfkdf2/security.test.ts b/mfkdf2-web/test/mfkdf2/security.test.ts index 2680dedc..478ac1fb 100644 --- a/mfkdf2-web/test/mfkdf2/security.test.ts +++ b/mfkdf2-web/test/mfkdf2/security.test.ts @@ -118,10 +118,6 @@ suite('mfkdf2/security', () => { await mfkdf.setup.factors.password('password2', { id: 'password2' }) ]) - // TODO (@lonerapier): current ts api doesn't return closure - // const materialp1 = await mfkdf.derive.factors.password('password1')( - // setup.policy.factors[0].params - // ) const materialp1 = await mfkdf.derive.factors.password('password1') const padp1 = Buffer.from(setup.policy.factors[0].pad, 'base64') const stretchedp1 = Buffer.from( @@ -159,10 +155,6 @@ suite('mfkdf2/security', () => { }) derive2.key.toString('hex').should.equal(setup.key.toString('hex')) - // TODO (@lonerapier): current ts api doesn't return closure - // const materialp3 = await mfkdf.derive.factors.password('newPassword1')( - // derive.policy.factors[0].params - // ) const materialp3 = await mfkdf.derive.factors.password('newPassword1') const padp3 = Buffer.from(derive.policy.factors[0].pad, 'base64') const stretchedp3 = Buffer.from( diff --git a/mfkdf2-web/test/setup/factors/totp.test.ts b/mfkdf2-web/test/setup/factors/totp.test.ts index 8eb10705..5f8e5821 100644 --- a/mfkdf2-web/test/setup/factors/totp.test.ts +++ b/mfkdf2-web/test/setup/factors/totp.test.ts @@ -57,7 +57,7 @@ suite('setup/factors/totp', () => { label: 'test@example.com', step: 60 }) - factor.id.should.equal('mytotp') + factor.id?.should.equal('mytotp') factor.type.should.equal('totp') const output = await factor.output() output.should.have.property('issuer', 'TestCorp') diff --git a/mfkdf2/src/policy/mod.rs b/mfkdf2/src/policy/mod.rs index f0faf31c..49839962 100644 --- a/mfkdf2/src/policy/mod.rs +++ b/mfkdf2/src/policy/mod.rs @@ -105,6 +105,10 @@ impl Policy { } } +#[cfg(feature = "bindings")] +#[cfg_attr(feature = "bindings", uniffi::export(name = "policy_ids"))] +fn policy_ids(policy: &Policy) -> Vec { policy.ids() } + #[cfg(feature = "bindings")] #[cfg_attr(feature = "bindings", uniffi::export(name = "policy_validate"))] fn validate(policy: &Policy) -> bool { policy.validate() } From 702c471dd5cbc25ec5e1c1071e8582c8a7da31ad Mon Sep 17 00:00:00 2001 From: lonerapier Date: Mon, 1 Dec 2025 23:46:49 +0530 Subject: [PATCH 2/2] fix type errors --- mfkdf2-web/src/api.ts | 6 +++--- mfkdf2-web/test/mfkdf2/hints.test.ts | 3 --- mfkdf2-web/test/setup/factors/hmacsha1.test.ts | 4 ++-- mfkdf2-web/test/setup/factors/hotp.test.ts | 2 +- mfkdf2-web/test/setup/factors/ooba.test.ts | 4 ++-- mfkdf2-web/test/setup/factors/passkey.test.ts | 6 +++--- mfkdf2-web/test/setup/factors/password.test.ts | 4 ++-- mfkdf2-web/test/setup/factors/question.test.ts | 4 ++-- mfkdf2-web/test/setup/factors/stack.test.ts | 4 ++-- 9 files changed, 17 insertions(+), 20 deletions(-) diff --git a/mfkdf2-web/src/api.ts b/mfkdf2-web/src/api.ts index 238d73c0..29d98bfd 100644 --- a/mfkdf2-web/src/api.ts +++ b/mfkdf2-web/src/api.ts @@ -78,7 +78,7 @@ function wrapDeriveFactor(factor: raw.Mfkdf2Factor) { } } -function wrapPolicy(policy: raw.Policy) { +function wrapPolicy(policy: raw.Policy): any { const factors = policy.factors.map((f) => { const factor: any = { ...f }; // use `type` instead of `kind` @@ -93,7 +93,7 @@ function wrapPolicy(policy: raw.Policy) { ...policy, factors, /** - * Unique identifier for this policy. + * unique identifier for this policy. */ $id: policy.id, /** @@ -617,7 +617,7 @@ function wrapDerivedKey(key: raw.Mfkdf2DerivedKey) { * @returns The hint. * @throws {TypeError} If the bits is not an integer. */ - async getHint(factorId: string, bits: number) { + async getHint(factorId: string, bits: number = 7) { // check for integer otherwise uniffi will cast to integer if (bits && !Number.isInteger(bits)) { throw new TypeError('bits must be an integer'); diff --git a/mfkdf2-web/test/mfkdf2/hints.test.ts b/mfkdf2-web/test/mfkdf2/hints.test.ts index 88fa1aac..8ad09f2b 100644 --- a/mfkdf2-web/test/mfkdf2/hints.test.ts +++ b/mfkdf2-web/test/mfkdf2/hints.test.ts @@ -104,10 +104,7 @@ suite('mfkdf2/hints', () => { integrity: false } ) - setup.getHint().should.be.rejectedWith(TypeError) - setup.getHint(123).should.be.rejectedWith(TypeError) setup.getHint('unknown').should.be.rejectedWith(RangeError) - setup.getHint('password1', 'string').should.be.rejectedWith(TypeError) setup.getHint('password1', 0).should.be.rejectedWith(TypeError) setup.getHint('password1', 300).should.be.rejectedWith(TypeError) }) diff --git a/mfkdf2-web/test/setup/factors/hmacsha1.test.ts b/mfkdf2-web/test/setup/factors/hmacsha1.test.ts index 72366744..3b1a85fe 100644 --- a/mfkdf2-web/test/setup/factors/hmacsha1.test.ts +++ b/mfkdf2-web/test/setup/factors/hmacsha1.test.ts @@ -23,7 +23,7 @@ suite('setup/factors/hmacsha1', () => { const factor = await mfkdf.setup.factors.hmacsha1() factor.type.should.equal('hmacsha1') factor.data.should.have.length(32) // 20 bytes + 12 bytes of padding - factor.id.should.equal('hmacsha1') + factor.id?.should.equal('hmacsha1') const params = await factor.params() params.should.have.property('challenge') params.should.have.property('pad') @@ -41,7 +41,7 @@ suite('setup/factors/hmacsha1', () => { test('valid - with id', async () => { const factor = await mfkdf.setup.factors.hmacsha1({ id: 'myhmac' }) - factor.id.should.equal('myhmac') + factor.id?.should.equal('myhmac') factor.type.should.equal('hmacsha1') const output = await factor.output() output.should.have.property('secret') diff --git a/mfkdf2-web/test/setup/factors/hotp.test.ts b/mfkdf2-web/test/setup/factors/hotp.test.ts index 6d6f6469..e34b784d 100644 --- a/mfkdf2-web/test/setup/factors/hotp.test.ts +++ b/mfkdf2-web/test/setup/factors/hotp.test.ts @@ -54,7 +54,7 @@ suite('setup/factors/hotp', () => { issuer: 'TestCorp', label: 'test@example.com' }) - factor.id.should.equal('myhotp') + factor.id?.should.equal('myhotp') factor.type.should.equal('hotp') const output = await factor.output() output.should.have.property('issuer', 'TestCorp') diff --git a/mfkdf2-web/test/setup/factors/ooba.test.ts b/mfkdf2-web/test/setup/factors/ooba.test.ts index 4acef39d..4d46ca28 100644 --- a/mfkdf2-web/test/setup/factors/ooba.test.ts +++ b/mfkdf2-web/test/setup/factors/ooba.test.ts @@ -50,7 +50,7 @@ suite('setup/factors/ooba', () => { test('valid - with defaults', async () => { const factor = await mfkdf.setup.factors.ooba({ key: keyPair.publicKey }) factor.type.should.equal('ooba') - factor.id.should.equal('ooba') + factor.id?.should.equal('ooba') factor.data.should.have.length(32) const params = await factor.params() params.should.have.property('length', 6) @@ -75,7 +75,7 @@ suite('setup/factors/ooba', () => { length: 8, params: customParams }) - factor.id.should.equal('myooba') + factor.id?.should.equal('myooba') factor.type.should.equal('ooba') }) }) diff --git a/mfkdf2-web/test/setup/factors/passkey.test.ts b/mfkdf2-web/test/setup/factors/passkey.test.ts index 332c4d1c..bf4c5ad9 100644 --- a/mfkdf2-web/test/setup/factors/passkey.test.ts +++ b/mfkdf2-web/test/setup/factors/passkey.test.ts @@ -34,15 +34,15 @@ suite('setup/factors/passkey', () => { } const factor = await mfkdf.setup.factors.passkey(secret.buffer) factor.type.should.equal('passkey') - factor.id.should.equal('passkey') + factor.id?.should.equal('passkey') factor.data.should.have.length(32) - factor.entropy.should.equal(256) + factor.entropy?.should.equal(256) }) test('valid - with id', async () => { const secret = new Uint8Array(32) const factor = await mfkdf.setup.factors.passkey(secret.buffer, { id: 'mykey' }) - factor.id.should.equal('mykey') + factor.id?.should.equal('mykey') factor.type.should.equal('passkey') const params = await factor.params() params.should.deep.equal({}) diff --git a/mfkdf2-web/test/setup/factors/password.test.ts b/mfkdf2-web/test/setup/factors/password.test.ts index 3df871c7..4e9f3f45 100644 --- a/mfkdf2-web/test/setup/factors/password.test.ts +++ b/mfkdf2-web/test/setup/factors/password.test.ts @@ -69,7 +69,7 @@ suite('setup/factors/password - with key parameter', () => { const params = await factor.params(customKey.buffer); params.should.deep.equal({}); - const output = await factor.output(customKey.buffer); + const output = await factor.output(); output.should.have.property('strength'); }); @@ -83,7 +83,7 @@ suite('setup/factors/password - with key parameter', () => { paramsNoKey.should.deep.equal(paramsWithKey); const outputNoKey = await factor.output(); - const outputWithKey = await factor.output(new Uint8Array(32).buffer); + const outputWithKey = await factor.output(); // Both should have strength property (value might differ slightly but structure same) outputNoKey.should.have.property('strength'); diff --git a/mfkdf2-web/test/setup/factors/question.test.ts b/mfkdf2-web/test/setup/factors/question.test.ts index 35f98842..d0012e37 100644 --- a/mfkdf2-web/test/setup/factors/question.test.ts +++ b/mfkdf2-web/test/setup/factors/question.test.ts @@ -28,7 +28,7 @@ suite('setup/factors/question', () => { test('valid - with defaults', async () => { const factor = await mfkdf.setup.factors.question('Paris') factor.type.should.equal('question') - factor.id.should.equal('question') + factor.id?.should.equal('question') // Answer is normalized: lowercase, alphanumeric only factor.data.toString().should.equal('paris') const params = await factor.params() @@ -54,7 +54,7 @@ suite('setup/factors/question', () => { id: 'color', question: 'Favorite color?' }) - factor.id.should.equal('color') + factor.id?.should.equal('color') const output = await factor.output() output.should.have.property('strength') }) diff --git a/mfkdf2-web/test/setup/factors/stack.test.ts b/mfkdf2-web/test/setup/factors/stack.test.ts index 15255c4c..c1aad76f 100644 --- a/mfkdf2-web/test/setup/factors/stack.test.ts +++ b/mfkdf2-web/test/setup/factors/stack.test.ts @@ -30,7 +30,7 @@ suite('setup/factors/stack', () => { const factor1 = await mfkdf.setup.factors.password('password1', { id: 'pwd1' }) const stackFactor = await mfkdf.setup.factors.stack([factor1]) stackFactor.type.should.equal('stack') - stackFactor.id.should.equal('stack') + stackFactor.id?.should.equal('stack') stackFactor.should.have.property('data') const params = await stackFactor.params() params.should.have.property('threshold', 1) @@ -43,7 +43,7 @@ suite('setup/factors/stack', () => { id: 'mystack' }) stackFactor.type.should.equal('stack') - stackFactor.id.should.equal('mystack') + stackFactor.id?.should.equal('mystack') const params = await stackFactor.params() params.should.have.property('threshold', 2) params.should.have.property('factors')