feat(shared): lift recovery-code primitives into shared crypto module
This commit is contained in:
@@ -40,7 +40,9 @@
|
||||
"react-i18next": { "optional": false }
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/libsodium-wrappers": "^0.7.14",
|
||||
"@types/react": "^18.3.12",
|
||||
"libsodium-wrappers-sumo": "0.7.15",
|
||||
"react": "^18.3.1",
|
||||
"react-i18next": "^15.1.1"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './backend';
|
||||
export * from './box';
|
||||
export * from './keys';
|
||||
export * from './recoveryCode';
|
||||
export * from './sessionKeys';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it, beforeAll } from 'vitest';
|
||||
|
||||
import { setCryptoBackend } from './backend';
|
||||
import { makeWasmTestBackend } from './testBackend';
|
||||
import { generateRecoveryCode, normalizeRecoveryCode, RECOVERY_CODE_LEN } from './recoveryCode';
|
||||
|
||||
beforeAll(async () => { setCryptoBackend(await makeWasmTestBackend()); });
|
||||
|
||||
describe('recoveryCode', () => {
|
||||
it('generates a 24-char alphabet-restricted code grouped 4×6 with dashes', async () => {
|
||||
const code = await generateRecoveryCode();
|
||||
expect(code).toHaveLength(27);
|
||||
const groups = code.split('-');
|
||||
expect(groups).toHaveLength(4);
|
||||
for (const g of groups) expect(g).toMatch(/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/);
|
||||
});
|
||||
|
||||
it('normalizeRecoveryCode strips dashes/spaces and uppercases', () => {
|
||||
expect(normalizeRecoveryCode(' abcdef-ghjklm-npqrst-uvwxyz ')).toBe('ABCDEFGHJKLMNPQRSTUVWXYZ');
|
||||
});
|
||||
|
||||
it('normalizeRecoveryCode drops characters outside the alphabet', () => {
|
||||
expect(normalizeRecoveryCode('AB1OD-EF0H1J')).toBe('ABDEFHJ');
|
||||
});
|
||||
|
||||
it('RECOVERY_CODE_LEN matches alphabet length', () => {
|
||||
expect(RECOVERY_CODE_LEN).toBe(24);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { getCryptoBackend } from './backend';
|
||||
|
||||
export const RECOVERY_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
export const RECOVERY_CODE_LEN = 24;
|
||||
|
||||
export async function generateRecoveryCode(): Promise<string> {
|
||||
const raw = getCryptoBackend().randomBytes(RECOVERY_CODE_LEN);
|
||||
let out = '';
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
out += RECOVERY_ALPHABET[raw[i]! % RECOVERY_ALPHABET.length];
|
||||
if ((i + 1) % 6 === 0 && i !== raw.length - 1) out += '-';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function normalizeRecoveryCode(input: string): string {
|
||||
return input
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.filter((c) => RECOVERY_ALPHABET.includes(c))
|
||||
.join('');
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
|
||||
import type { CryptoBackend } from './backend';
|
||||
|
||||
// Real libsodium WASM backend — used in unit tests so cryptographic invariants
|
||||
// (length, KDF determinism) hold. Not registered for production.
|
||||
export async function makeWasmTestBackend(): Promise<CryptoBackend> {
|
||||
await sodium.ready;
|
||||
return {
|
||||
name: 'libsodium-wasm-test',
|
||||
nonceLength: sodium.crypto_box_NONCEBYTES,
|
||||
publicKeyLength: sodium.crypto_box_PUBLICKEYBYTES,
|
||||
privateKeyLength: sodium.crypto_box_SECRETKEYBYTES,
|
||||
secretboxKeyLength: sodium.crypto_secretbox_KEYBYTES,
|
||||
secretboxNonceLength: sodium.crypto_secretbox_NONCEBYTES,
|
||||
randomBytes: (n) => sodium.randombytes_buf(n),
|
||||
generateKeyPair: () => {
|
||||
const kp = sodium.crypto_box_keypair();
|
||||
return { publicKey: kp.publicKey, privateKey: kp.privateKey };
|
||||
},
|
||||
box: (m, n, pk, sk) => sodium.crypto_box_easy(m, n, pk, sk),
|
||||
boxOpen: (c, n, pk, sk) => sodium.crypto_box_open_easy(c, n, pk, sk),
|
||||
secretbox: (m, n, k) => sodium.crypto_secretbox_easy(m, n, k),
|
||||
secretboxOpen: (c, n, k) => sodium.crypto_secretbox_open_easy(c, n, k),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Shim sumo types to the non-sumo types package. sumo is an API superset of
|
||||
// libsodium-wrappers; its runtime exports match the standard wrappers module
|
||||
// and additionally include `crypto_pwhash` (Argon2id). Reuse the existing
|
||||
// `@types/libsodium-wrappers` definitions rather than duplicating them.
|
||||
declare module 'libsodium-wrappers-sumo' {
|
||||
import sodium from 'libsodium-wrappers';
|
||||
export default sodium;
|
||||
export * from 'libsodium-wrappers';
|
||||
}
|
||||
Generated
+7
-1
@@ -253,9 +253,15 @@ importers:
|
||||
specifier: ^23.16.4
|
||||
version: 23.16.8
|
||||
devDependencies:
|
||||
'@types/libsodium-wrappers':
|
||||
specifier: ^0.7.14
|
||||
version: 0.7.14
|
||||
'@types/react':
|
||||
specifier: ^18.3.12
|
||||
version: 18.3.28
|
||||
libsodium-wrappers-sumo:
|
||||
specifier: 0.7.15
|
||||
version: 0.7.15
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
@@ -1238,7 +1244,7 @@ packages:
|
||||
|
||||
'@expo/bunyan@4.0.1':
|
||||
resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
engines: {'0': node >=0.10.0}
|
||||
|
||||
'@expo/cli@0.22.28':
|
||||
resolution: {integrity: sha512-lvt72KNitGuixYD2l3SZmRKVu2G4zJpmg5V7WfUBNpmUU5oODBw/6qmiJ6kSLAlfDozscUk+BBGknBBzxUrwrA==}
|
||||
|
||||
Reference in New Issue
Block a user