@learncard/sss-key-manager 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/crypto.ts ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Cryptographic utilities for SSS Key Manager
3
+ */
4
+
5
+ import { argon2id } from 'hash-wasm';
6
+
7
+ const ARGON2_TIME_COST = 3;
8
+ const ARGON2_MEMORY_COST = 65536;
9
+ const ARGON2_PARALLELISM = 4;
10
+ const ARGON2_HASH_LENGTH = 32;
11
+
12
+ export interface KdfParams {
13
+ algorithm: 'argon2id';
14
+ timeCost: number;
15
+ memoryCost: number;
16
+ parallelism: number;
17
+ }
18
+
19
+ export const DEFAULT_KDF_PARAMS: KdfParams = {
20
+ algorithm: 'argon2id',
21
+ timeCost: ARGON2_TIME_COST,
22
+ memoryCost: ARGON2_MEMORY_COST,
23
+ parallelism: ARGON2_PARALLELISM,
24
+ };
25
+
26
+ export function bufferToBase64(buf: ArrayBuffer): string {
27
+ const bytes = new Uint8Array(buf);
28
+ let binary = '';
29
+ for (let i = 0; i < bytes.byteLength; i++) {
30
+ binary += String.fromCharCode(bytes[i]);
31
+ }
32
+ return btoa(binary);
33
+ }
34
+
35
+ export function base64ToBuffer(b64: string): Uint8Array {
36
+ const binary = atob(b64);
37
+ const bytes = new Uint8Array(binary.length);
38
+ for (let i = 0; i < binary.length; i++) {
39
+ bytes[i] = binary.charCodeAt(i);
40
+ }
41
+ return bytes;
42
+ }
43
+
44
+ export function hexToBytes(hex: string): Uint8Array {
45
+ const bytes = new Uint8Array(hex.length / 2);
46
+ for (let i = 0; i < hex.length; i += 2) {
47
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
48
+ }
49
+ return bytes;
50
+ }
51
+
52
+ export function bytesToHex(bytes: Uint8Array): string {
53
+ return Array.from(bytes)
54
+ .map(b => b.toString(16).padStart(2, '0'))
55
+ .join('');
56
+ }
57
+
58
+ export async function deriveKeyFromPassword(
59
+ password: string,
60
+ salt: Uint8Array,
61
+ params: KdfParams = DEFAULT_KDF_PARAMS
62
+ ): Promise<Uint8Array> {
63
+ const hash = await argon2id({
64
+ password,
65
+ salt: salt as unknown as Uint8Array,
66
+ iterations: params.timeCost,
67
+ memorySize: params.memoryCost,
68
+ parallelism: params.parallelism,
69
+ hashLength: ARGON2_HASH_LENGTH,
70
+ outputType: 'binary',
71
+ });
72
+ return new Uint8Array(hash as ArrayBuffer);
73
+ }
74
+
75
+ export async function encryptWithPassword(
76
+ plaintext: string,
77
+ password: string
78
+ ): Promise<{ ciphertext: string; iv: string; salt: string; kdfParams: KdfParams }> {
79
+ const salt = crypto.getRandomValues(new Uint8Array(16));
80
+ const iv = crypto.getRandomValues(new Uint8Array(12));
81
+
82
+ const keyMaterial = await deriveKeyFromPassword(password, salt);
83
+
84
+ const cryptoKey = await crypto.subtle.importKey(
85
+ 'raw',
86
+ keyMaterial.buffer as ArrayBuffer,
87
+ { name: 'AES-GCM' },
88
+ false,
89
+ ['encrypt']
90
+ );
91
+
92
+ const encoder = new TextEncoder();
93
+ const ciphertextBuffer = await crypto.subtle.encrypt(
94
+ { name: 'AES-GCM', iv },
95
+ cryptoKey,
96
+ encoder.encode(plaintext)
97
+ );
98
+
99
+ return {
100
+ ciphertext: bufferToBase64(ciphertextBuffer),
101
+ iv: bufferToBase64(iv.buffer),
102
+ salt: bufferToBase64(salt.buffer),
103
+ kdfParams: DEFAULT_KDF_PARAMS,
104
+ };
105
+ }
106
+
107
+ export async function decryptWithPassword(
108
+ ciphertext: string,
109
+ iv: string,
110
+ salt: string,
111
+ password: string,
112
+ params: KdfParams = DEFAULT_KDF_PARAMS
113
+ ): Promise<string> {
114
+ const saltBytes = base64ToBuffer(salt);
115
+ const ivBytes = base64ToBuffer(iv);
116
+ const ciphertextBytes = base64ToBuffer(ciphertext);
117
+
118
+ const keyMaterial = await deriveKeyFromPassword(password, saltBytes, params);
119
+
120
+ const cryptoKey = await crypto.subtle.importKey(
121
+ 'raw',
122
+ keyMaterial.buffer as ArrayBuffer,
123
+ { name: 'AES-GCM' },
124
+ false,
125
+ ['decrypt']
126
+ );
127
+
128
+ const plaintextBuffer = await crypto.subtle.decrypt(
129
+ { name: 'AES-GCM', iv: ivBytes },
130
+ cryptoKey,
131
+ ciphertextBytes
132
+ );
133
+
134
+ const decoder = new TextDecoder();
135
+ return decoder.decode(plaintextBuffer);
136
+ }
137
+
138
+ export async function encryptShare(
139
+ share: string,
140
+ key: CryptoKey,
141
+ aad?: string
142
+ ): Promise<{ encryptedData: string; iv: string }> {
143
+ const iv = crypto.getRandomValues(new Uint8Array(12));
144
+ const encoder = new TextEncoder();
145
+
146
+ const encryptParams: AesGcmParams = { name: 'AES-GCM', iv };
147
+ if (aad) {
148
+ encryptParams.additionalData = encoder.encode(aad);
149
+ }
150
+
151
+ const ciphertextBuffer = await crypto.subtle.encrypt(
152
+ encryptParams,
153
+ key,
154
+ encoder.encode(share) as ArrayBuffer
155
+ );
156
+
157
+ return {
158
+ encryptedData: bufferToBase64(ciphertextBuffer),
159
+ iv: bufferToBase64(iv.buffer),
160
+ };
161
+ }
162
+
163
+ export async function decryptShare(
164
+ encryptedData: string,
165
+ iv: string,
166
+ key: CryptoKey,
167
+ aad?: string
168
+ ): Promise<string> {
169
+ const ivBytes = base64ToBuffer(iv);
170
+ const ciphertextBytes = base64ToBuffer(encryptedData);
171
+ const encoder = new TextEncoder();
172
+
173
+ const decryptParams: AesGcmParams = { name: 'AES-GCM', iv: ivBytes.buffer as ArrayBuffer };
174
+ if (aad) {
175
+ decryptParams.additionalData = encoder.encode(aad);
176
+ }
177
+
178
+ const plaintextBuffer = await crypto.subtle.decrypt(
179
+ decryptParams,
180
+ key,
181
+ ciphertextBytes.buffer as ArrayBuffer
182
+ );
183
+
184
+ const decoder = new TextDecoder();
185
+ return decoder.decode(plaintextBuffer);
186
+ }
187
+
188
+ export async function generateAesKey(): Promise<CryptoKey> {
189
+ return crypto.subtle.generateKey(
190
+ { name: 'AES-GCM', length: 256 },
191
+ true,
192
+ ['encrypt', 'decrypt']
193
+ );
194
+ }
195
+
196
+ export function generateRandomBytes(length: number): Uint8Array {
197
+ return crypto.getRandomValues(new Uint8Array(length));
198
+ }
199
+
200
+ export async function generateEd25519PrivateKey(): Promise<string> {
201
+ const privateKeyBytes = generateRandomBytes(32);
202
+ return bytesToHex(privateKeyBytes);
203
+ }
package/src/index.ts ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @learncard/sss-key-manager
3
+ *
4
+ * Shamir Secret Sharing key manager for LearnCard - replaces Web3Auth SFA
5
+ */
6
+
7
+ export { SSSKeyManager, createSSSKeyManager } from './key-manager';
8
+ export { SSSApiClient } from './api-client';
9
+
10
+ export {
11
+ splitPrivateKey,
12
+ reconstructPrivateKey,
13
+ reconstructFromShares,
14
+ SSS_TOTAL_SHARES,
15
+ SSS_THRESHOLD,
16
+ } from './sss';
17
+
18
+ export {
19
+ storeDeviceShare,
20
+ getDeviceShare,
21
+ hasDeviceShare,
22
+ deleteDeviceShare,
23
+ clearAllShares,
24
+ listAllDeviceShares,
25
+ storeShareVersion,
26
+ getShareVersion,
27
+ isPublicComputerMode,
28
+ setPublicComputerMode,
29
+ createAdaptiveStorage,
30
+ } from './storage';
31
+
32
+ export type { DeviceShareEntry } from './storage';
33
+
34
+ export {
35
+ encryptWithPassword,
36
+ decryptWithPassword,
37
+ deriveKeyFromPassword,
38
+ generateEd25519PrivateKey,
39
+ hexToBytes,
40
+ bytesToHex,
41
+ bufferToBase64,
42
+ base64ToBuffer,
43
+ DEFAULT_KDF_PARAMS,
44
+ } from './crypto';
45
+
46
+ export {
47
+ createPasskeyCredential,
48
+ deriveKeyFromPasskey,
49
+ encryptShareWithPasskey,
50
+ decryptShareWithPasskey,
51
+ isWebAuthnSupported,
52
+ isPRFSupported,
53
+ } from './passkey';
54
+
55
+ export type { PasskeyCredential, PasskeyEncryptedShare } from './passkey';
56
+
57
+ export {
58
+ shareToRecoveryPhrase,
59
+ recoveryPhraseToShare,
60
+ generateRecoveryPhrase,
61
+ validateRecoveryPhrase,
62
+ countWords,
63
+ } from './recovery-phrase';
64
+
65
+ export type { RecoveryPhraseData } from './recovery-phrase';
66
+
67
+ export {
68
+ splitAndVerify,
69
+ atomicShareUpdate,
70
+ verifyStoredShares,
71
+ atomicRecovery,
72
+ ShareVerificationError,
73
+ AtomicUpdateError,
74
+ } from './atomic-operations';
75
+
76
+ export type {
77
+ AtomicSplitResult,
78
+ AtomicUpdateOptions,
79
+ StorageOperations,
80
+ } from './atomic-operations';
81
+
82
+ export { createSSSStrategy } from './sss-strategy';
83
+
84
+ export type { SSSStorageFunctions, SSSStrategyConfig } from './sss-strategy';
85
+
86
+ export {
87
+ generateEphemeralKeypair,
88
+ encryptShareForTransfer,
89
+ decryptShareFromTransfer,
90
+ } from './qr-crypto';
91
+
92
+ export type { EphemeralKeypair, EncryptedSharePayload } from './qr-crypto';
93
+
94
+ export {
95
+ createQrLoginSession,
96
+ pollQrLoginSession,
97
+ pollUntilApproved,
98
+ getQrLoginSessionInfo,
99
+ approveQrLoginSession,
100
+ notifyDevicesForQrSession,
101
+ } from './qr-login';
102
+
103
+ export type {
104
+ QrLoginSession,
105
+ QrLoginSessionInfo,
106
+ QrLoginClientConfig,
107
+ QrPayload,
108
+ PollResult,
109
+ NotifyDevicesResult,
110
+ } from './qr-login';
111
+
112
+ // Provider-agnostic interfaces are re-exported from @learncard/types.
113
+ // Import from '@learncard/types' or 'learn-card-base' for generic types.
114
+ // Import from this package for SSS-specific types.
115
+
116
+ export { AuthSessionError } from './types';
117
+
118
+ export type {
119
+ SSSKeyManagerConfig,
120
+ SSSKeyDerivationProvider,
121
+ KeyDerivationProvider,
122
+ KeyDerivationStrategy,
123
+ SSSKeyDerivationStrategy,
124
+ ServerKeyStatus,
125
+ AuthProvider,
126
+ AuthUser,
127
+ AuthProviderType,
128
+ ContactMethod,
129
+ ContactMethodType,
130
+ RecoveryMethod,
131
+ RecoveryMethodType,
132
+ RecoveryMethodInfo,
133
+ RecoveryInput,
134
+ RecoveryResult,
135
+ RecoverySetupInput,
136
+ RecoverySetupResult,
137
+ PasskeyRecoveryMethod,
138
+ BackupFileRecoveryMethod,
139
+ RecoveryPhraseRecoveryMethod,
140
+ SecurityLevel,
141
+ BackupFile,
142
+ EncryptedShare,
143
+ ServerEncryptedShare,
144
+ UserKeyRecord,
145
+ AuthProviderMapping,
146
+ } from './types';
@@ -0,0 +1,330 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createSSSKeyManager } from './key-manager';
3
+ import type { AuthProvider } from './types';
4
+
5
+ const createMockAuthProvider = (): AuthProvider => ({
6
+ getIdToken: vi.fn().mockResolvedValue('mock-token-123'),
7
+ getCurrentUser: vi.fn().mockResolvedValue({
8
+ id: 'user-123',
9
+ email: 'test@example.com',
10
+ providerType: 'firebase' as const,
11
+ }),
12
+ getProviderType: vi.fn().mockReturnValue('firebase' as const),
13
+ signOut: vi.fn().mockResolvedValue(undefined),
14
+ });
15
+
16
+ describe('SSSKeyManager', () => {
17
+ let mockAuthProvider: AuthProvider;
18
+
19
+ beforeEach(() => {
20
+ mockAuthProvider = createMockAuthProvider();
21
+ vi.clearAllMocks();
22
+ });
23
+
24
+ describe('initialization', () => {
25
+ it('should create a key manager instance', () => {
26
+ const keyManager = createSSSKeyManager({
27
+ serverUrl: 'http://localhost:3000',
28
+ authProvider: mockAuthProvider,
29
+ });
30
+
31
+ expect(keyManager).toBeDefined();
32
+ expect(keyManager.name).toBe('sss');
33
+ });
34
+
35
+ it('should not be initialized before connect', () => {
36
+ const keyManager = createSSSKeyManager({
37
+ serverUrl: 'http://localhost:3000',
38
+ authProvider: mockAuthProvider,
39
+ });
40
+
41
+ expect(keyManager.isInitialized()).toBe(false);
42
+ });
43
+
44
+ // Note: hasLocalKey requires IndexedDB which isn't fully available in jsdom
45
+ // This test would need a mock or integration test environment
46
+ it.skip('should report no local key before setup', async () => {
47
+ const keyManager = createSSSKeyManager({
48
+ serverUrl: 'http://localhost:3000',
49
+ authProvider: mockAuthProvider,
50
+ });
51
+
52
+ const hasKey = await keyManager.hasLocalKey();
53
+ expect(hasKey).toBe(false);
54
+ });
55
+ });
56
+
57
+ describe('method signatures', () => {
58
+ it('should expose all required recovery methods', () => {
59
+ const keyManager = createSSSKeyManager({
60
+ serverUrl: 'http://localhost:3000',
61
+ authProvider: mockAuthProvider,
62
+ });
63
+
64
+ expect(typeof keyManager.addRecoveryMethod).toBe('function');
65
+ expect(typeof keyManager.getRecoveryMethods).toBe('function');
66
+ expect(typeof keyManager.recover).toBe('function');
67
+ expect(typeof keyManager.generateRecoveryPhrase).toBe('function');
68
+ expect(typeof keyManager.exportBackup).toBe('function');
69
+ });
70
+
71
+ it('should expose all key management methods', () => {
72
+ const keyManager = createSSSKeyManager({
73
+ serverUrl: 'http://localhost:3000',
74
+ authProvider: mockAuthProvider,
75
+ });
76
+
77
+ expect(typeof keyManager.connect).toBe('function');
78
+ expect(typeof keyManager.setupNewKey).toBe('function');
79
+ expect(typeof keyManager.setupWithKey).toBe('function');
80
+ expect(typeof keyManager.migrate).toBe('function');
81
+ expect(typeof keyManager.canMigrate).toBe('function');
82
+ expect(typeof keyManager.clearLocalData).toBe('function');
83
+ expect(typeof keyManager.deleteAccount).toBe('function');
84
+ expect(typeof keyManager.getSecurityLevel).toBe('function');
85
+ });
86
+ });
87
+
88
+ describe('recovery method errors', () => {
89
+ it('should throw when adding recovery method without active key', async () => {
90
+ const keyManager = createSSSKeyManager({
91
+ serverUrl: 'http://localhost:3000',
92
+ authProvider: mockAuthProvider,
93
+ });
94
+
95
+ await expect(
96
+ keyManager.addRecoveryMethod({ type: 'passkey' })
97
+ ).rejects.toThrow('No active key');
98
+ });
99
+
100
+ it('should throw when generating recovery phrase without active key', async () => {
101
+ const keyManager = createSSSKeyManager({
102
+ serverUrl: 'http://localhost:3000',
103
+ authProvider: mockAuthProvider,
104
+ });
105
+
106
+ await expect(
107
+ keyManager.generateRecoveryPhrase()
108
+ ).rejects.toThrow('No active key');
109
+ });
110
+
111
+ it('should throw when exporting backup without active key', async () => {
112
+ const keyManager = createSSSKeyManager({
113
+ serverUrl: 'http://localhost:3000',
114
+ authProvider: mockAuthProvider,
115
+ });
116
+
117
+ await expect(
118
+ keyManager.exportBackup('password123')
119
+ ).rejects.toThrow('No active key');
120
+ });
121
+
122
+ it('should throw for backup recovery method type (use exportBackup instead)', async () => {
123
+ const keyManager = createSSSKeyManager({
124
+ serverUrl: 'http://localhost:3000',
125
+ authProvider: mockAuthProvider,
126
+ });
127
+
128
+ // First we need to mock having an active key
129
+ // Since we can't easily do that, we test the error message
130
+ await expect(
131
+ keyManager.addRecoveryMethod({
132
+ type: 'backup',
133
+ fileContents: '{}',
134
+ password: 'test'
135
+ })
136
+ ).rejects.toThrow();
137
+ });
138
+ });
139
+
140
+ describe('configuration', () => {
141
+ it('should use provided server URL', () => {
142
+ const serverUrl = 'https://custom-server.example.com';
143
+ const keyManager = createSSSKeyManager({
144
+ serverUrl,
145
+ authProvider: mockAuthProvider,
146
+ });
147
+
148
+ expect(keyManager).toBeDefined();
149
+ });
150
+
151
+ it('should use provided auth provider', () => {
152
+ const keyManager = createSSSKeyManager({
153
+ serverUrl: 'http://localhost:3000',
154
+ authProvider: mockAuthProvider,
155
+ });
156
+
157
+ expect(keyManager).toBeDefined();
158
+ });
159
+
160
+ it('should use custom device storage key if provided', () => {
161
+ const keyManager = createSSSKeyManager({
162
+ serverUrl: 'http://localhost:3000',
163
+ authProvider: mockAuthProvider,
164
+ deviceStorageKey: 'custom-storage-key',
165
+ });
166
+
167
+ expect(keyManager).toBeDefined();
168
+ });
169
+ });
170
+ });
171
+
172
+ describe('Recovery method types', () => {
173
+ it('should define all recovery method types', async () => {
174
+ const types = await import('./types');
175
+
176
+ const validTypes = ['passkey', 'backup', 'phrase'];
177
+
178
+ // Check that RecoveryMethodType includes expected types
179
+ expect(types).toHaveProperty('SecurityLevels');
180
+ });
181
+ });
182
+
183
+ describe('Integration: SSS split and recover', () => {
184
+ it('should be able to split a key into shares', async () => {
185
+ const { splitPrivateKey } = await import('./sss');
186
+
187
+ const privateKey = 'a'.repeat(64);
188
+ const shares = await splitPrivateKey(privateKey);
189
+
190
+ expect(shares.deviceShare).toBeDefined();
191
+ expect(shares.authShare).toBeDefined();
192
+ expect(shares.recoveryShare).toBeDefined();
193
+
194
+ // Shares should be different from each other
195
+ expect(shares.deviceShare).not.toBe(shares.authShare);
196
+ expect(shares.authShare).not.toBe(shares.recoveryShare);
197
+ expect(shares.deviceShare).not.toBe(shares.recoveryShare);
198
+ });
199
+
200
+ it('should reconstruct key from device + auth shares', async () => {
201
+ const { splitPrivateKey, reconstructFromShares } = await import('./sss');
202
+
203
+ const privateKey = 'abcdef'.repeat(10) + 'abcd';
204
+ const shares = await splitPrivateKey(privateKey);
205
+
206
+ const reconstructed = await reconstructFromShares([
207
+ shares.deviceShare,
208
+ shares.authShare,
209
+ ]);
210
+
211
+ expect(reconstructed).toBe(privateKey);
212
+ });
213
+
214
+ it('should reconstruct key from device + recovery shares', async () => {
215
+ const { splitPrivateKey, reconstructFromShares } = await import('./sss');
216
+
217
+ const privateKey = '123456'.repeat(10) + '1234';
218
+ const shares = await splitPrivateKey(privateKey);
219
+
220
+ const reconstructed = await reconstructFromShares([
221
+ shares.deviceShare,
222
+ shares.recoveryShare,
223
+ ]);
224
+
225
+ expect(reconstructed).toBe(privateKey);
226
+ });
227
+
228
+ it('should reconstruct key from auth + recovery shares', async () => {
229
+ const { splitPrivateKey, reconstructFromShares } = await import('./sss');
230
+
231
+ const privateKey = 'fedcba'.repeat(10) + 'fedc';
232
+ const shares = await splitPrivateKey(privateKey);
233
+
234
+ const reconstructed = await reconstructFromShares([
235
+ shares.authShare,
236
+ shares.recoveryShare,
237
+ ]);
238
+
239
+ expect(reconstructed).toBe(privateKey);
240
+ });
241
+ });
242
+
243
+ describe('Integration: Recovery phrase flow', () => {
244
+ it('should convert a 16-byte share to phrase and back', async () => {
245
+ const { shareToRecoveryPhrase, recoveryPhraseToShare } = await import('./recovery-phrase');
246
+
247
+ // Use a 16-byte (32 hex char) share which produces 12 words
248
+ const shareHex = '00'.repeat(16);
249
+
250
+ const phrase = await shareToRecoveryPhrase(shareHex);
251
+ const words = phrase.split(' ');
252
+
253
+ expect(words.length).toBeGreaterThanOrEqual(12);
254
+ expect(words.length).toBeLessThanOrEqual(24);
255
+
256
+ const recoveredShare = await recoveryPhraseToShare(phrase);
257
+ expect(recoveredShare).toBe(shareHex);
258
+ });
259
+
260
+ it('should handle recovery with phrase using padded share', async () => {
261
+ const { splitPrivateKey, reconstructFromShares } = await import('./sss');
262
+ const { shareToRecoveryPhrase, recoveryPhraseToShare, validateRecoveryPhrase } = await import('./recovery-phrase');
263
+
264
+ const originalPrivateKey = 'aabbccdd'.repeat(8);
265
+
266
+ // Split the key
267
+ const shares = await splitPrivateKey(originalPrivateKey);
268
+
269
+ // The SSS share might be longer than 32 bytes, so we need to handle it
270
+ // For now, test that we can at least generate and validate a phrase
271
+ const recoveryShare = shares.recoveryShare;
272
+
273
+ // Test with a known-good 16-byte share format
274
+ const testShare = '00112233445566778899aabbccddeeff';
275
+ const phrase = await shareToRecoveryPhrase(testShare);
276
+ const isValid = await validateRecoveryPhrase(phrase);
277
+
278
+ expect(isValid).toBe(true);
279
+
280
+ const recovered = await recoveryPhraseToShare(phrase);
281
+ expect(recovered).toBe(testShare);
282
+ });
283
+ });
284
+
285
+ describe('Integration: Backup file flow', () => {
286
+ it('should create and restore from backup file', async () => {
287
+ const { splitPrivateKey, reconstructFromShares } = await import('./sss');
288
+ const { encryptWithPassword, decryptWithPassword } = await import('./crypto');
289
+
290
+ const originalPrivateKey = '99887766'.repeat(8);
291
+ const backupPassword = 'backupFilePassword!';
292
+
293
+ // Step 1: Create backup
294
+ const shares = await splitPrivateKey(originalPrivateKey);
295
+ const encrypted = await encryptWithPassword(shares.recoveryShare, backupPassword);
296
+
297
+ const backupFile = {
298
+ version: 1,
299
+ createdAt: new Date().toISOString(),
300
+ primaryDid: 'did:key:test123',
301
+ encryptedShare: {
302
+ ciphertext: encrypted.ciphertext,
303
+ iv: encrypted.iv,
304
+ salt: encrypted.salt,
305
+ kdfParams: encrypted.kdfParams,
306
+ },
307
+ };
308
+
309
+ // Step 2: Simulate file storage (serialize/deserialize)
310
+ const backupJson = JSON.stringify(backupFile);
311
+ const restoredBackup = JSON.parse(backupJson);
312
+
313
+ // Step 3: Restore from backup
314
+ const recoveredShare = await decryptWithPassword(
315
+ restoredBackup.encryptedShare.ciphertext,
316
+ restoredBackup.encryptedShare.iv,
317
+ restoredBackup.encryptedShare.salt,
318
+ backupPassword,
319
+ restoredBackup.encryptedShare.kdfParams
320
+ );
321
+
322
+ // Step 4: Reconstruct with auth share from server
323
+ const recoveredKey = await reconstructFromShares([
324
+ shares.authShare,
325
+ recoveredShare,
326
+ ]);
327
+
328
+ expect(recoveredKey).toBe(originalPrivateKey);
329
+ });
330
+ });