@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/README.md +17 -17
- package/dist/sss-key-manager.cjs.development.js +126 -36
- package/dist/sss-key-manager.cjs.development.js.map +2 -2
- package/dist/sss-key-manager.cjs.production.min.js +6 -6
- package/dist/sss-key-manager.cjs.production.min.js.map +3 -3
- package/dist/sss-key-manager.esm.js +126 -36
- package/dist/sss-key-manager.esm.js.map +2 -2
- package/package.json +62 -52
- package/src/api-client.ts +257 -0
- package/src/atomic-operations.test.ts +327 -0
- package/src/atomic-operations.ts +275 -0
- package/src/auth-coordinator.test.ts +13 -0
- package/src/auth-coordinator.ts +12 -0
- package/src/critical-paths.test.ts +380 -0
- package/src/crypto.test.ts +214 -0
- package/src/crypto.ts +203 -0
- package/src/index.ts +146 -0
- package/src/key-manager.test.ts +330 -0
- package/src/key-manager.ts +323 -0
- package/src/passkey.test.ts +59 -0
- package/src/passkey.ts +222 -0
- package/src/qr-crypto.test.ts +122 -0
- package/src/qr-crypto.ts +206 -0
- package/src/qr-login-notify.test.ts +95 -0
- package/src/qr-login.test.ts +548 -0
- package/src/qr-login.ts +339 -0
- package/src/recovery-phrase.test.ts +287 -0
- package/src/recovery-phrase.ts +131 -0
- package/src/sss-strategy.test.ts +1956 -0
- package/src/sss-strategy.ts +1119 -0
- package/src/sss.test.ts +242 -0
- package/src/sss.ts +49 -0
- package/src/storage.test.ts +530 -0
- package/src/storage.ts +467 -0
- package/src/types.ts +200 -0
- package/LICENSE +0 -21
package/src/sss.test.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
hexToBytes,
|
|
4
|
+
bytesToHex,
|
|
5
|
+
generateRandomBytes,
|
|
6
|
+
bufferToBase64,
|
|
7
|
+
base64ToBuffer,
|
|
8
|
+
DEFAULT_KDF_PARAMS,
|
|
9
|
+
} from './crypto';
|
|
10
|
+
|
|
11
|
+
describe('Crypto utilities', () => {
|
|
12
|
+
describe('hexToBytes / bytesToHex', () => {
|
|
13
|
+
it('should convert even-length hex to bytes and back', () => {
|
|
14
|
+
const originalHex = 'deadbeef01234567890abcde';
|
|
15
|
+
const bytes = hexToBytes(originalHex);
|
|
16
|
+
const backToHex = bytesToHex(bytes);
|
|
17
|
+
expect(backToHex).toBe(originalHex);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should handle empty hex string', () => {
|
|
21
|
+
const bytes = hexToBytes('');
|
|
22
|
+
expect(bytes.length).toBe(0);
|
|
23
|
+
expect(bytesToHex(bytes)).toBe('');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should convert 32-byte private key hex correctly', () => {
|
|
27
|
+
const privateKeyHex = 'a'.repeat(64);
|
|
28
|
+
const bytes = hexToBytes(privateKeyHex);
|
|
29
|
+
expect(bytes.length).toBe(32);
|
|
30
|
+
expect(bytesToHex(bytes)).toBe(privateKeyHex);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should handle mixed case hex', () => {
|
|
34
|
+
const hex = 'DeAdBeEf';
|
|
35
|
+
const bytes = hexToBytes(hex);
|
|
36
|
+
expect(bytesToHex(bytes)).toBe('deadbeef');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should pad single digit hex values with leading zero', () => {
|
|
40
|
+
const bytes = new Uint8Array([0, 1, 15, 255]);
|
|
41
|
+
expect(bytesToHex(bytes)).toBe('00010fff');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('base64 encoding', () => {
|
|
46
|
+
it('should encode and decode buffer to base64', () => {
|
|
47
|
+
const original = new Uint8Array([1, 2, 3, 4, 5]);
|
|
48
|
+
const b64 = bufferToBase64(original.buffer);
|
|
49
|
+
const decoded = base64ToBuffer(b64);
|
|
50
|
+
expect(Array.from(decoded)).toEqual(Array.from(original));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should handle empty buffer', () => {
|
|
54
|
+
const empty = new Uint8Array([]);
|
|
55
|
+
const b64 = bufferToBase64(empty.buffer);
|
|
56
|
+
expect(b64).toBe('');
|
|
57
|
+
expect(base64ToBuffer(b64).length).toBe(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should handle large buffers', () => {
|
|
61
|
+
const large = new Uint8Array(1024);
|
|
62
|
+
for (let i = 0; i < large.length; i++) {
|
|
63
|
+
large[i] = i % 256;
|
|
64
|
+
}
|
|
65
|
+
const b64 = bufferToBase64(large.buffer);
|
|
66
|
+
const decoded = base64ToBuffer(b64);
|
|
67
|
+
expect(decoded.length).toBe(1024);
|
|
68
|
+
expect(Array.from(decoded)).toEqual(Array.from(large));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('generateRandomBytes', () => {
|
|
73
|
+
it('should generate bytes of specified length', () => {
|
|
74
|
+
expect(generateRandomBytes(16).length).toBe(16);
|
|
75
|
+
expect(generateRandomBytes(32).length).toBe(32);
|
|
76
|
+
expect(generateRandomBytes(64).length).toBe(64);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should generate different values each time', () => {
|
|
80
|
+
const a = generateRandomBytes(32);
|
|
81
|
+
const b = generateRandomBytes(32);
|
|
82
|
+
expect(bytesToHex(a)).not.toBe(bytesToHex(b));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should handle zero length', () => {
|
|
86
|
+
const bytes = generateRandomBytes(0);
|
|
87
|
+
expect(bytes.length).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('KDF params', () => {
|
|
92
|
+
it('should have secure default parameters', () => {
|
|
93
|
+
expect(DEFAULT_KDF_PARAMS.algorithm).toBe('argon2id');
|
|
94
|
+
expect(DEFAULT_KDF_PARAMS.timeCost).toBeGreaterThanOrEqual(3);
|
|
95
|
+
expect(DEFAULT_KDF_PARAMS.memoryCost).toBeGreaterThanOrEqual(65536);
|
|
96
|
+
expect(DEFAULT_KDF_PARAMS.parallelism).toBeGreaterThanOrEqual(1);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('SSS operations', () => {
|
|
102
|
+
it('should export splitPrivateKey function', async () => {
|
|
103
|
+
const sss = await import('./sss');
|
|
104
|
+
expect(typeof sss.splitPrivateKey).toBe('function');
|
|
105
|
+
expect(typeof sss.reconstructPrivateKey).toBe('function');
|
|
106
|
+
expect(typeof sss.reconstructFromShares).toBe('function');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should have correct threshold constants', async () => {
|
|
110
|
+
const sss = await import('./sss');
|
|
111
|
+
expect(sss.SSS_TOTAL_SHARES).toBe(4);
|
|
112
|
+
expect(sss.SSS_THRESHOLD).toBe(2);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should split and reconstruct a private key', async () => {
|
|
116
|
+
const { splitPrivateKey, reconstructFromShares } = await import('./sss');
|
|
117
|
+
|
|
118
|
+
const privateKeyHex = 'abcd1234'.repeat(8);
|
|
119
|
+
const shares = await splitPrivateKey(privateKeyHex);
|
|
120
|
+
|
|
121
|
+
expect(shares.deviceShare).toBeDefined();
|
|
122
|
+
expect(shares.authShare).toBeDefined();
|
|
123
|
+
expect(shares.recoveryShare).toBeDefined();
|
|
124
|
+
expect(shares.emailShare).toBeDefined();
|
|
125
|
+
|
|
126
|
+
expect(shares.deviceShare).not.toBe(shares.authShare);
|
|
127
|
+
expect(shares.authShare).not.toBe(shares.recoveryShare);
|
|
128
|
+
expect(shares.recoveryShare).not.toBe(shares.emailShare);
|
|
129
|
+
|
|
130
|
+
// All 6 combinations of 4 shares (threshold 2) should reconstruct
|
|
131
|
+
const reconstructed = await reconstructFromShares([
|
|
132
|
+
shares.deviceShare,
|
|
133
|
+
shares.authShare,
|
|
134
|
+
]);
|
|
135
|
+
expect(reconstructed).toBe(privateKeyHex);
|
|
136
|
+
|
|
137
|
+
const reconstructed2 = await reconstructFromShares([
|
|
138
|
+
shares.deviceShare,
|
|
139
|
+
shares.recoveryShare,
|
|
140
|
+
]);
|
|
141
|
+
expect(reconstructed2).toBe(privateKeyHex);
|
|
142
|
+
|
|
143
|
+
const reconstructed3 = await reconstructFromShares([
|
|
144
|
+
shares.authShare,
|
|
145
|
+
shares.recoveryShare,
|
|
146
|
+
]);
|
|
147
|
+
expect(reconstructed3).toBe(privateKeyHex);
|
|
148
|
+
|
|
149
|
+
const reconstructed4 = await reconstructFromShares([
|
|
150
|
+
shares.deviceShare,
|
|
151
|
+
shares.emailShare,
|
|
152
|
+
]);
|
|
153
|
+
expect(reconstructed4).toBe(privateKeyHex);
|
|
154
|
+
|
|
155
|
+
const reconstructed5 = await reconstructFromShares([
|
|
156
|
+
shares.authShare,
|
|
157
|
+
shares.emailShare,
|
|
158
|
+
]);
|
|
159
|
+
expect(reconstructed5).toBe(privateKeyHex);
|
|
160
|
+
|
|
161
|
+
const reconstructed6 = await reconstructFromShares([
|
|
162
|
+
shares.recoveryShare,
|
|
163
|
+
shares.emailShare,
|
|
164
|
+
]);
|
|
165
|
+
expect(reconstructed6).toBe(privateKeyHex);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('Storage operations', () => {
|
|
170
|
+
it('should export storage functions', async () => {
|
|
171
|
+
const storage = await import('./storage');
|
|
172
|
+
expect(typeof storage.storeDeviceShare).toBe('function');
|
|
173
|
+
expect(typeof storage.getDeviceShare).toBe('function');
|
|
174
|
+
expect(typeof storage.hasDeviceShare).toBe('function');
|
|
175
|
+
expect(typeof storage.deleteDeviceShare).toBe('function');
|
|
176
|
+
expect(typeof storage.clearAllShares).toBe('function');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('Key Manager', () => {
|
|
181
|
+
const mockAuthProvider = {
|
|
182
|
+
getIdToken: async () => 'mock-token',
|
|
183
|
+
getCurrentUser: async () => ({
|
|
184
|
+
id: 'user-123',
|
|
185
|
+
email: 'test@example.com',
|
|
186
|
+
providerType: 'firebase' as const,
|
|
187
|
+
}),
|
|
188
|
+
getProviderType: () => 'firebase' as const,
|
|
189
|
+
signOut: async () => {},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
it('should create key manager instance', async () => {
|
|
193
|
+
const { createSSSKeyManager } = await import('./key-manager');
|
|
194
|
+
|
|
195
|
+
const keyManager = createSSSKeyManager({
|
|
196
|
+
serverUrl: 'http://localhost:3000',
|
|
197
|
+
authProvider: mockAuthProvider,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(keyManager).toBeDefined();
|
|
201
|
+
expect(keyManager.name).toBe('sss');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should expose required methods', async () => {
|
|
205
|
+
const { createSSSKeyManager } = await import('./key-manager');
|
|
206
|
+
|
|
207
|
+
const keyManager = createSSSKeyManager({
|
|
208
|
+
serverUrl: 'http://localhost:3000',
|
|
209
|
+
authProvider: mockAuthProvider,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(typeof keyManager.connect).toBe('function');
|
|
213
|
+
expect(typeof keyManager.setupNewKey).toBe('function');
|
|
214
|
+
expect(typeof keyManager.setupWithKey).toBe('function');
|
|
215
|
+
expect(typeof keyManager.migrate).toBe('function');
|
|
216
|
+
expect(typeof keyManager.addRecoveryMethod).toBe('function');
|
|
217
|
+
expect(typeof keyManager.recover).toBe('function');
|
|
218
|
+
expect(typeof keyManager.exportBackup).toBe('function');
|
|
219
|
+
expect(typeof keyManager.clearLocalData).toBe('function');
|
|
220
|
+
expect(typeof keyManager.isInitialized).toBe('function');
|
|
221
|
+
expect(typeof keyManager.hasLocalKey).toBe('function');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should report not initialized before connect', async () => {
|
|
225
|
+
const { createSSSKeyManager } = await import('./key-manager');
|
|
226
|
+
|
|
227
|
+
const keyManager = createSSSKeyManager({
|
|
228
|
+
serverUrl: 'http://localhost:3000',
|
|
229
|
+
authProvider: mockAuthProvider,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(keyManager.isInitialized()).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('Types', () => {
|
|
237
|
+
it('should export all required types', async () => {
|
|
238
|
+
const types = await import('./types');
|
|
239
|
+
|
|
240
|
+
expect(types).toHaveProperty('SecurityLevels');
|
|
241
|
+
});
|
|
242
|
+
});
|
package/src/sss.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shamir Secret Sharing operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { split, combine } from 'shamir-secret-sharing';
|
|
6
|
+
import { hexToBytes, bytesToHex } from './crypto';
|
|
7
|
+
|
|
8
|
+
export const SSS_TOTAL_SHARES = 4;
|
|
9
|
+
export const SSS_THRESHOLD = 2;
|
|
10
|
+
|
|
11
|
+
export interface SSSShares {
|
|
12
|
+
deviceShare: string;
|
|
13
|
+
authShare: string;
|
|
14
|
+
recoveryShare: string;
|
|
15
|
+
emailShare: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function splitPrivateKey(privateKeyHex: string): Promise<SSSShares> {
|
|
19
|
+
const privateKeyBytes = hexToBytes(privateKeyHex);
|
|
20
|
+
|
|
21
|
+
const shares = await split(privateKeyBytes, SSS_TOTAL_SHARES, SSS_THRESHOLD);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
deviceShare: bytesToHex(shares[0]),
|
|
25
|
+
authShare: bytesToHex(shares[1]),
|
|
26
|
+
recoveryShare: bytesToHex(shares[2]),
|
|
27
|
+
emailShare: bytesToHex(shares[3]),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function reconstructPrivateKey(share1Hex: string, share2Hex: string): Promise<string> {
|
|
32
|
+
const share1 = hexToBytes(share1Hex);
|
|
33
|
+
const share2 = hexToBytes(share2Hex);
|
|
34
|
+
|
|
35
|
+
const reconstructed = await combine([share1, share2]);
|
|
36
|
+
|
|
37
|
+
return bytesToHex(reconstructed);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function reconstructFromShares(shares: string[]): Promise<string> {
|
|
41
|
+
if (shares.length < SSS_THRESHOLD) {
|
|
42
|
+
throw new Error(`Need at least ${SSS_THRESHOLD} shares to reconstruct key`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const shareBytes = shares.slice(0, SSS_THRESHOLD).map(hexToBytes);
|
|
46
|
+
const reconstructed = await combine(shareBytes);
|
|
47
|
+
|
|
48
|
+
return bytesToHex(reconstructed);
|
|
49
|
+
}
|