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/); }); });