-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathslip39.js
216 lines (187 loc) · 7.8 KB
/
slip39.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// slip39.js
const bip39 = require("bip39");
const crypto = require("crypto");
const sss = require("shamirs-secret-sharing");
/**
* Converts a BIP-39 mnemonic phrase to entropy.
* @param {string} bip39Mnemonic - A valid BIP-39 mnemonic phrase.
* @returns {Buffer} - Entropy derived from the BIP-39 mnemonic.
* @throws Will throw an error if the mnemonic is invalid.
*/
function bip39ToEntropy(bip39Mnemonic) {
if (!bip39.validateMnemonic(bip39Mnemonic)) {
throw new Error("Invalid BIP-39 mnemonic");
}
const entropyHex = bip39.mnemonicToEntropy(bip39Mnemonic);
return Buffer.from(entropyHex, "hex");
}
/**
* Converts entropy to a BIP-39 mnemonic phrase.
* @param {Buffer} entropy - Entropy to convert.
* @returns {string} - BIP-39 mnemonic.
*/
function entropyToBip39(entropy) {
return bip39.entropyToMnemonic(entropy.toString("hex"));
}
/**
* Splits the secret entropy into shares using Shamir's Secret Sharing.
* @param {Buffer} secret - The secret entropy to split.
* @param {number} totalShares - The total number of shares to generate.
* @param {number} threshold - The minimum number of shares needed to reconstruct the secret.
* @returns {Array<Buffer>} - Array of binary shares.
*/
function splitSecret(secret, totalShares, threshold) {
if (threshold > totalShares) {
throw new Error("Threshold cannot be greater than total shares");
}
// Convert secret to a Uint8Array as required by shamirs-secret-sharing
const shares = sss.split(secret, { shares: totalShares, threshold: threshold });
return shares;
}
/**
* Converts a binary share into a Base64-encoded string.
* @param {Buffer} share - A single SSS share.
* @returns {string} - Base64-encoded string representing the share.
*/
function shareToBase64(share) {
return share.toString("base64");
}
/**
* Converts a Base64-encoded share back to a Buffer.
* @param {string} base64Share - The Base64-encoded share.
* @returns {Buffer} - The original binary share.
* @throws Will throw an error if the Base64 string is invalid.
*/
function base64ToShare(base64Share) {
return Buffer.from(base64Share, "base64");
}
/**
* Encrypts a Base64-encoded share with a password using AES-256-GCM.
* @param {string} base64Share - The Base64-encoded share to encrypt.
* @param {string} password - The password to use for encryption.
* @returns {string} - Encrypted share in Base64 format, including salt, IV, and auth tag.
*/
function encryptShare(base64Share, password) {
// Derive a key using PBKDF2 with a random salt
const salt = crypto.randomBytes(16);
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
// Encrypt using AES-256-GCM
const iv = crypto.randomBytes(12); // 96-bit nonce for GCM
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
const encrypted = Buffer.concat([cipher.update(base64Share, "utf8"), cipher.final()]);
const authTag = cipher.getAuthTag();
// Combine salt, iv, authTag, and encrypted data
const encryptedData = Buffer.concat([salt, iv, authTag, encrypted]);
return encryptedData.toString("base64");
}
/**
* Decrypts an encrypted share with a password.
* @param {string} encryptedShare - The encrypted share in Base64 format.
* @param {string} password - The password to decrypt with.
* @returns {string} - Decrypted Base64-encoded share.
* @throws Will throw an error if decryption fails.
*/
function decryptShare(encryptedShare, password) {
const encryptedBuffer = Buffer.from(encryptedShare, "base64");
// Extract salt, iv, authTag, and encrypted data
const salt = encryptedBuffer.slice(0, 16);
const iv = encryptedBuffer.slice(16, 28);
const authTag = encryptedBuffer.slice(28, 44);
const encryptedData = encryptedBuffer.slice(44);
// Derive the key using PBKDF2 with the extracted salt
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
// Decrypt using AES-256-GCM
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(authTag);
let decrypted;
try {
decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
} catch (err) {
throw new Error("Failed to decrypt share. Possible incorrect password or corrupted data.");
}
return decrypted.toString("utf8"); // This is the Base64-encoded share
}
/**
* Converts multiple binary shares to Base64-encoded strings.
* @param {Array<Buffer>} shares - Array of binary shares.
* @returns {Array<string>} - Array of Base64-encoded strings.
*/
function sharesToBase64(shares) {
return shares.map((share) => shareToBase64(share));
}
/**
* Converts multiple Base64-encoded shares back to binary shares.
* @param {Array<string>} base64Shares - Array of Base64-encoded shares.
* @returns {Array<Buffer>} - Array of binary shares.
*/
function base64ToShares(base64Shares) {
return base64Shares.map((base64Share) => base64ToShare(base64Share));
}
/**
* Encrypts multiple Base64-encoded shares with their respective passwords.
* @param {Array<string>} base64Shares - Array of Base64-encoded shares.
* @param {Array<string>} passwords - Array of passwords for each share.
* @returns {Array<string>} - Array of encrypted shares in Base64 format.
*/
function encryptShares(base64Shares, passwords) {
return base64Shares.map((base64Share, index) => encryptShare(base64Share, passwords[index]));
}
/**
* Decrypts multiple encrypted shares with their respective passwords.
* @param {Array<string>} encryptedShares - Array of encrypted shares in Base64 format.
* @param {Array<string>} passwords - Array of passwords for each share.
* @returns {Array<string>} - Array of decrypted Base64-encoded shares.
*/
function decryptShares(encryptedShares, passwords) {
return encryptedShares.map((encryptedShare, index) => decryptShare(encryptedShare, passwords[index]));
}
/**
* Converts a BIP-39 mnemonic to SLIP-39 shares with password encryption.
* @param {string} bip39Mnemonic - The BIP-39 mnemonic to convert.
* @param {number} totalShares - Total number of shares to generate.
* @param {number} threshold - Minimum number of shares required to reconstruct.
* @param {Array<string>} passwords - Array of passwords for each share.
* @returns {Array<string>} - Array of encrypted shares in Base64 format.
*/
function bip39ToSlip39(bip39Mnemonic, totalShares, threshold, passwords) {
if (passwords.length !== totalShares) {
throw new Error("Number of passwords must match the total number of shares");
}
const entropy = bip39ToEntropy(bip39Mnemonic);
// Split the entropy into shares
const shares = splitSecret(entropy, totalShares, threshold);
// Convert shares to Base64
const base64Shares = sharesToBase64(shares);
// Encrypt each share with its corresponding password
const encryptedShares = encryptShares(base64Shares, passwords);
return encryptedShares;
}
/**
* Reconstructs the BIP-39 mnemonic from decrypted SLIP-39 shares.
* @param {Array<string>} decryptedShares - Array of decrypted Base64-encoded shares.
* @returns {string} - The original BIP-39 mnemonic.
*/
function reconstructBip39Mnemonic(decryptedShares) {
if (decryptedShares.length === 0) {
throw new Error("No shares provided for reconstruction.");
}
// Convert Base64 shares back to binary
const shares = base64ToShares(decryptedShares);
// Use Shamir's Secret Sharing to combine the shares
let reconstructedEntropy;
try {
reconstructedEntropy = sss.combine(shares);
} catch (err) {
throw new Error("Failed to reconstruct entropy from shares. Ensure you have enough valid shares.");
}
// Convert entropy back to BIP-39 mnemonic
return entropyToBip39(reconstructedEntropy);
}
module.exports = {
bip39ToSlip39,
decryptShare, // For decrypting individual shares if needed
decryptShares, // For decrypting multiple shares
reconstructBip39Mnemonic,
encryptShare, // Exported for potential separate use
encryptShares, // Exported for encrypting multiple shares
};