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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,622 changes: 1,384 additions & 238 deletions mfkdf2-web/src/api.ts

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions mfkdf2-web/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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,
};
2 changes: 0 additions & 2 deletions mfkdf2-web/test/features/reconstitution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,5 @@ suite('features/reconstitution', () => {

await setup.reconstitute([], [], 4).should.be.rejectedWith(Mfkdf2Error.InvalidThreshold)
})

// TODO: type error tests are not added
})
});
3 changes: 0 additions & 3 deletions mfkdf2-web/test/mfkdf2/hints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
8 changes: 0 additions & 8 deletions mfkdf2-web/test/mfkdf2/security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions mfkdf2-web/test/setup/factors/hmacsha1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion mfkdf2-web/test/setup/factors/hotp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
4 changes: 2 additions & 2 deletions mfkdf2-web/test/setup/factors/ooba.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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')
})
})
Expand Down
6 changes: 3 additions & 3 deletions mfkdf2-web/test/setup/factors/passkey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({})
Expand Down
4 changes: 2 additions & 2 deletions mfkdf2-web/test/setup/factors/password.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand All @@ -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');
Expand Down
4 changes: 2 additions & 2 deletions mfkdf2-web/test/setup/factors/question.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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')
})
Expand Down
4 changes: 2 additions & 2 deletions mfkdf2-web/test/setup/factors/stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion mfkdf2-web/test/setup/factors/totp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
4 changes: 4 additions & 0 deletions mfkdf2/src/policy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ impl Policy {
}
}

#[cfg(feature = "bindings")]
#[cfg_attr(feature = "bindings", uniffi::export(name = "policy_ids"))]
fn policy_ids(policy: &Policy) -> Vec<String> { policy.ids() }

#[cfg(feature = "bindings")]
#[cfg_attr(feature = "bindings", uniffi::export(name = "policy_validate"))]
fn validate(policy: &Policy) -> bool { policy.validate() }