@learncard/sss-key-manager 0.1.14 → 0.1.15
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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BIP39 Recovery Phrase utilities for SSS recovery
|
|
3
|
+
* The recovery phrase directly encodes a share (not encryption)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { bytesToHex, hexToBytes, bufferToBase64, base64ToBuffer } from './crypto';
|
|
7
|
+
|
|
8
|
+
export interface RecoveryPhraseData {
|
|
9
|
+
phrase: string;
|
|
10
|
+
shareHex: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const WORDLIST_MODULE = '@scure/bip39/wordlists/english';
|
|
14
|
+
|
|
15
|
+
let wordlistPromise: Promise<string[]> | null = null;
|
|
16
|
+
|
|
17
|
+
async function getWordlist(): Promise<string[]> {
|
|
18
|
+
if (!wordlistPromise) {
|
|
19
|
+
wordlistPromise = import('@scure/bip39/wordlists/english').then(m => m.wordlist);
|
|
20
|
+
}
|
|
21
|
+
return wordlistPromise;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function bytesToBits(bytes: Uint8Array): string {
|
|
25
|
+
return Array.from(bytes)
|
|
26
|
+
.map(b => b.toString(2).padStart(8, '0'))
|
|
27
|
+
.join('');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function bitsToBytes(bits: string): Uint8Array {
|
|
31
|
+
const bytes = new Uint8Array(Math.ceil(bits.length / 8));
|
|
32
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
33
|
+
bytes[i] = parseInt(bits.slice(i * 8, (i + 1) * 8).padEnd(8, '0'), 2);
|
|
34
|
+
}
|
|
35
|
+
return bytes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function computeChecksum(data: Uint8Array): Promise<string> {
|
|
39
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
40
|
+
const hashBits = bytesToBits(new Uint8Array(hash));
|
|
41
|
+
const checksumLength = Math.floor(data.length / 4);
|
|
42
|
+
return hashBits.slice(0, checksumLength);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function shareToRecoveryPhrase(shareHex: string): Promise<string> {
|
|
46
|
+
const wordlist = await getWordlist();
|
|
47
|
+
const shareBytes = hexToBytes(shareHex);
|
|
48
|
+
|
|
49
|
+
const checksum = await computeChecksum(shareBytes);
|
|
50
|
+
const allBits = bytesToBits(shareBytes) + checksum;
|
|
51
|
+
|
|
52
|
+
const words: string[] = [];
|
|
53
|
+
for (let i = 0; i < allBits.length; i += 11) {
|
|
54
|
+
const index = parseInt(allBits.slice(i, i + 11).padEnd(11, '0'), 2);
|
|
55
|
+
words.push(wordlist[index]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return words.join(' ');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function recoveryPhraseToShare(phrase: string): Promise<string> {
|
|
62
|
+
const wordlist = await getWordlist();
|
|
63
|
+
const words = phrase.trim().toLowerCase().split(/\s+/);
|
|
64
|
+
|
|
65
|
+
// SSS shares include an index byte, so they produce 25 words instead of standard BIP39 24
|
|
66
|
+
if (words.length < 12 || words.length > 27) {
|
|
67
|
+
throw new Error('Recovery phrase must be 12-27 words');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let bits = '';
|
|
71
|
+
for (const word of words) {
|
|
72
|
+
const index = wordlist.indexOf(word);
|
|
73
|
+
if (index === -1) {
|
|
74
|
+
throw new Error(`Invalid word in recovery phrase: ${word}`);
|
|
75
|
+
}
|
|
76
|
+
bits += index.toString(2).padStart(11, '0');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Calculate the correct data byte count
|
|
80
|
+
// The encoding formula: totalBits = dataBytes * 8 + floor(dataBytes / 4)
|
|
81
|
+
// We need to find the LARGEST dataBytes such that ceil(totalBits / 11) == words.length
|
|
82
|
+
// Multiple byte counts can produce the same word count, so we want the largest (most likely)
|
|
83
|
+
let dataByteCount = 0;
|
|
84
|
+
for (let bytes = 1; bytes <= 64; bytes++) {
|
|
85
|
+
const checksumBits = Math.floor(bytes / 4);
|
|
86
|
+
const totalBits = bytes * 8 + checksumBits;
|
|
87
|
+
const wordsNeeded = Math.ceil(totalBits / 11);
|
|
88
|
+
if (wordsNeeded === words.length) {
|
|
89
|
+
dataByteCount = bytes; // Keep updating to get the largest match
|
|
90
|
+
} else if (wordsNeeded > words.length) {
|
|
91
|
+
break; // Gone past, stop
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (dataByteCount === 0) {
|
|
96
|
+
throw new Error('Could not determine data size from word count');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const checksumLength = Math.floor(dataByteCount / 4);
|
|
100
|
+
const dataBitCount = dataByteCount * 8;
|
|
101
|
+
|
|
102
|
+
const dataBits = bits.slice(0, dataBitCount);
|
|
103
|
+
const checksumBits = bits.slice(dataBitCount, dataBitCount + checksumLength);
|
|
104
|
+
|
|
105
|
+
const dataBytes = bitsToBytes(dataBits);
|
|
106
|
+
|
|
107
|
+
const expectedChecksum = await computeChecksum(dataBytes);
|
|
108
|
+
if (checksumBits !== expectedChecksum.slice(0, checksumLength)) {
|
|
109
|
+
throw new Error('Invalid recovery phrase checksum');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return bytesToHex(dataBytes);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function generateRecoveryPhrase(shareHex: string): Promise<RecoveryPhraseData> {
|
|
116
|
+
const phrase = await shareToRecoveryPhrase(shareHex);
|
|
117
|
+
return { phrase, shareHex };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function validateRecoveryPhrase(phrase: string): Promise<boolean> {
|
|
121
|
+
try {
|
|
122
|
+
await recoveryPhraseToShare(phrase);
|
|
123
|
+
return true;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function countWords(phrase: string): number {
|
|
130
|
+
return phrase.trim().split(/\s+/).filter(w => w.length > 0).length;
|
|
131
|
+
}
|