-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
mnemonic.ts
198 lines (171 loc) · 6.77 KB
/
mnemonic.ts
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
import { Base58 } from '@ethersproject/basex';
import type { BytesLike } from '@ethersproject/bytes';
import { hexDataSlice, concat, hexlify, arrayify } from '@ethersproject/bytes';
import { pbkdf2 } from '@ethersproject/pbkdf2';
import { computeHmac, sha256, SupportedAlgorithm } from '@ethersproject/sha2';
import { randomBytes } from '@fuel-ts/keystore';
import { english } from '@fuel-ts/wordlists';
import type { MnemonicPhrase } from './utils';
import {
entropyToMnemonicIndices,
getWords,
getPhrase,
mnemonicWordsToEntropy,
toUtf8Bytes,
} from './utils';
//
// Constants
//
// "Bitcoin seed"
const MasterSecret = toUtf8Bytes('Bitcoin seed');
// 4 byte: version bytes (mainnet: 0x0488B21E public, 0x0488ADE4 private; testnet: 0x043587CF public, 0x04358394 private)
const MainnetPRV = 0x0488ade4;
const TestnetPRV = 0x04358394;
function assertWordList(wordlist: Array<string>) {
if (wordlist.length !== 2048) {
throw new Error('Invalid word list length');
}
}
function assertEntropy(entropy: BytesLike) {
if (entropy.length % 4 !== 0 || entropy.length < 16 || entropy.length > 32) {
throw new Error('invalid entropy');
}
}
function assertMnemonic(words: Array<string>) {
if (![12, 15, 18, 21, 24].includes(words.length)) {
throw new Error('invalid mnemonic size');
}
}
class Mnemonic {
wordlist: Array<string>;
/**
*
* @param wordlist - Provide a wordlist with the list of words used to generate the mnemonic phrase. The default value is the English list.
* @returns Mnemonic instance
*/
constructor(wordlist: Array<string> = english) {
this.wordlist = wordlist;
assertWordList(this.wordlist);
}
/**
*
* @param phrase - Mnemonic phrase composed by words from the provided wordlist
* @returns Entropy hash
*/
mnemonicToEntropy(phrase: MnemonicPhrase) {
return Mnemonic.mnemonicToEntropy(phrase, this.wordlist);
}
/**
*
* @param entropy - Entropy source to the mnemonic phrase.
* @returns Mnemonic phrase
*/
entropyToMnemonic(entropy: BytesLike) {
return Mnemonic.entropyToMnemonic(entropy, this.wordlist);
}
/**
*
* @param phrase - Mnemonic phrase composed by words from the provided wordlist
* @param wordlist - Provide a wordlist with the list of words used to generate the mnemonic phrase. The default value is the English list.
* @returns Mnemonic phrase
*/
static mnemonicToEntropy(phrase: MnemonicPhrase, wordlist: Array<string> = english): string {
const words = getWords(phrase);
assertMnemonic(words);
return hexlify(mnemonicWordsToEntropy(words, wordlist));
}
/**
* @param entropy - Entropy source to the mnemonic phrase.
* @param testnet - Inform if should use testnet or mainnet prefix, default value is true (`mainnet`).
* @returns 64-byte array contains privateKey and chainCode as described on BIP39
*/
static entropyToMnemonic(entropy: BytesLike, wordlist: Array<string> = english): string {
const entropyBytes = arrayify(entropy, {
allowMissingPrefix: true,
});
assertWordList(wordlist);
assertEntropy(entropyBytes);
return entropyToMnemonicIndices(entropyBytes)
.map((i) => wordlist[i])
.join(' ');
}
/**
* @param phrase - Mnemonic phrase composed by words from the provided wordlist
* @param passphrase - Add additional security to protect the generated seed with a memorized passphrase. `Note: if the owner forgot the passphrase, all wallets and accounts derive from the phrase will be lost.`
* @returns 64-byte array contains privateKey and chainCode as described on BIP39
*/
static mnemonicToSeed(phrase: MnemonicPhrase, passphrase: BytesLike = '') {
assertMnemonic(getWords(phrase));
const phraseBytes = toUtf8Bytes(getPhrase(phrase));
const salt = toUtf8Bytes(`mnemonic${passphrase}`);
return pbkdf2(phraseBytes, salt, 2048, 64, 'sha512');
}
/**
* @param phrase - Mnemonic phrase composed by words from the provided wordlist
* @param passphrase - Add additional security to protect the generated seed with a memorized passphrase. `Note: if the owner forgot the passphrase, all wallets and accounts derive from the phrase will be lost.`
* @returns 64-byte array contains privateKey and chainCode as described on BIP39
*/
static mnemonicToMasterKeys(phrase: MnemonicPhrase, passphrase: BytesLike = '') {
const seed = Mnemonic.mnemonicToSeed(phrase, passphrase);
return Mnemonic.masterKeysFromSeed(seed);
}
/**
* @param seed - BIP39 seed
* @param testnet - Inform if should use testnet or mainnet prefix, the default value is true (`mainnet`).
* @returns 64-byte array contains privateKey and chainCode as described on BIP39
*/
static masterKeysFromSeed(seed: string): Uint8Array {
const seedArray = arrayify(seed);
if (seedArray.length < 16 || seedArray.length > 64) {
throw new Error('invalid seed');
}
return arrayify(computeHmac(SupportedAlgorithm.sha512, MasterSecret, seedArray));
}
/**
* Get the extendKey as defined on BIP-32 from the provided seed
*
* @param seed - BIP39 seed
* @param testnet - Inform if should use testnet or mainnet prefix, default value is true (`mainnet`).
* @returns BIP-32 extended private key
*/
static seedToExtendedKey(seed: string, testnet: boolean = false): string {
const masterKey = Mnemonic.masterKeysFromSeed(seed);
const prefix = arrayify(testnet ? TestnetPRV : MainnetPRV);
const depth = '0x00';
const fingerprint = '0x00000000';
const index = '0x00000000';
// last 32 bites from the key
const chainCode = masterKey.slice(32);
// first 32 bites from the key
const privateKey = masterKey.slice(0, 32);
const extendedKey = concat([
prefix,
depth,
fingerprint,
index,
chainCode,
concat(['0x00', privateKey]),
]);
const checksum = hexDataSlice(sha256(sha256(extendedKey)), 0, 4);
return Base58.encode(concat([extendedKey, checksum]));
}
/**
* Create a new mnemonic using a randomly generated number as entropy.
* As defined in BIP39, the entropy must be a multiple of 32 bits, and its size must be between 128 and 256 bits.
* Therefore, the possible values for `strength` are 128, 160, 192, 224, and 256.
* If not provided, the default entropy length will be set to 256 bits.
* The return is a list of words that encodes the generated entropy.
*
*
* @param size - Number of bytes used as an entropy
* @param extraEntropy - Optional extra entropy to increase randomness
* @returns A randomly generated mnemonic
*/
static generate(size: number = 32, extraEntropy: BytesLike = '') {
const entropy = extraEntropy
? sha256(concat([randomBytes(size), arrayify(extraEntropy)]))
: randomBytes(size);
return Mnemonic.entropyToMnemonic(entropy);
}
}
export default Mnemonic;