108 lines
3.9 KiB
TypeScript
108 lines
3.9 KiB
TypeScript
|
|
import { describe, test, expect } from 'bun:test';
|
||
|
|
import {
|
||
|
|
exportBackup,
|
||
|
|
importBackup,
|
||
|
|
backupToString,
|
||
|
|
backupFromString,
|
||
|
|
} from '../src/backup.js';
|
||
|
|
import { SubtleCryptoProvider, MemoryStorage } from '@shade/crypto-web';
|
||
|
|
import { ShadeSessionManager } from '@shade/core';
|
||
|
|
|
||
|
|
const crypto = new SubtleCryptoProvider();
|
||
|
|
|
||
|
|
describe('Backup export/import', () => {
|
||
|
|
test('roundtrip: export then import reproduces identity', async () => {
|
||
|
|
const sourceStorage = new MemoryStorage();
|
||
|
|
const manager = new ShadeSessionManager(crypto, sourceStorage);
|
||
|
|
await manager.initialize();
|
||
|
|
await manager.generateOneTimePreKeys(5);
|
||
|
|
|
||
|
|
const originalId = await sourceStorage.getIdentityKeyPair();
|
||
|
|
const originalRegId = await sourceStorage.getLocalRegistrationId();
|
||
|
|
|
||
|
|
// Export
|
||
|
|
const blob = await exportBackup(crypto, sourceStorage, 'correct horse battery staple');
|
||
|
|
|
||
|
|
// Import into a fresh storage
|
||
|
|
const targetStorage = new MemoryStorage();
|
||
|
|
await importBackup(crypto, targetStorage, blob, 'correct horse battery staple');
|
||
|
|
|
||
|
|
const restoredId = await targetStorage.getIdentityKeyPair();
|
||
|
|
expect(restoredId).not.toBeNull();
|
||
|
|
expect(restoredId!.signingPublicKey).toEqual(originalId!.signingPublicKey);
|
||
|
|
expect(restoredId!.dhPrivateKey).toEqual(originalId!.dhPrivateKey);
|
||
|
|
expect(await targetStorage.getLocalRegistrationId()).toBe(originalRegId);
|
||
|
|
expect(await targetStorage.getOneTimePreKeyCount()).toBe(5);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('wrong passphrase fails to import', async () => {
|
||
|
|
const storage = new MemoryStorage();
|
||
|
|
const manager = new ShadeSessionManager(crypto, storage);
|
||
|
|
await manager.initialize();
|
||
|
|
|
||
|
|
const blob = await exportBackup(crypto, storage, 'correct passphrase');
|
||
|
|
|
||
|
|
const target = new MemoryStorage();
|
||
|
|
expect(
|
||
|
|
importBackup(crypto, target, blob, 'wrong passphrase'),
|
||
|
|
).rejects.toThrow(/Wrong passphrase|corrupted/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('short passphrase is rejected', async () => {
|
||
|
|
const storage = new MemoryStorage();
|
||
|
|
const manager = new ShadeSessionManager(crypto, storage);
|
||
|
|
await manager.initialize();
|
||
|
|
expect(exportBackup(crypto, storage, 'short')).rejects.toThrow(/at least 12/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('sessions are included when addresses provided', async () => {
|
||
|
|
const aliceStorage = new MemoryStorage();
|
||
|
|
const bobStorage = new MemoryStorage();
|
||
|
|
const alice = new ShadeSessionManager(crypto, aliceStorage);
|
||
|
|
const bob = new ShadeSessionManager(crypto, bobStorage);
|
||
|
|
await alice.initialize();
|
||
|
|
await bob.initialize();
|
||
|
|
|
||
|
|
const otpks = await bob.generateOneTimePreKeys(5);
|
||
|
|
const bundle = await bob.createPreKeyBundle();
|
||
|
|
bundle.oneTimePreKey = { keyId: otpks[0].keyId, publicKey: otpks[0].keyPair.publicKey };
|
||
|
|
await alice.initSessionFromBundle('bob', bundle);
|
||
|
|
const env = await alice.encrypt('bob', 'hello');
|
||
|
|
await bob.decrypt('alice', env);
|
||
|
|
|
||
|
|
// Export with known addresses
|
||
|
|
const blob = await exportBackup(
|
||
|
|
crypto,
|
||
|
|
aliceStorage,
|
||
|
|
'twelve chars minimum',
|
||
|
|
['bob'],
|
||
|
|
);
|
||
|
|
|
||
|
|
const target = new MemoryStorage();
|
||
|
|
await importBackup(crypto, target, blob, 'twelve chars minimum');
|
||
|
|
|
||
|
|
const restoredSession = await target.getSession('bob');
|
||
|
|
expect(restoredSession).not.toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('backupToString / backupFromString roundtrip', async () => {
|
||
|
|
const storage = new MemoryStorage();
|
||
|
|
const manager = new ShadeSessionManager(crypto, storage);
|
||
|
|
await manager.initialize();
|
||
|
|
|
||
|
|
const blob = await exportBackup(crypto, storage, 'twelve chars minimum');
|
||
|
|
const str = backupToString(blob);
|
||
|
|
expect(str.startsWith('shade-backup:v1:')).toBe(true);
|
||
|
|
|
||
|
|
const parsed = backupFromString(str);
|
||
|
|
expect(parsed.version).toBe(blob.version);
|
||
|
|
expect(parsed.salt).toBe(blob.salt);
|
||
|
|
expect(parsed.nonce).toBe(blob.nonce);
|
||
|
|
expect(parsed.ciphertext).toBe(blob.ciphertext);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('invalid backup string format throws', () => {
|
||
|
|
expect(() => backupFromString('not-a-valid-backup')).toThrow(/Invalid backup/);
|
||
|
|
});
|
||
|
|
});
|