Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion src/mnemonic/mnemonic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,41 @@ describe('mnemonic', () => {
expect((await mnemonicNew()).length).toBe(24);
});
it('should validate mnemonics', async () => {
expect(await mnemonicValidate(['a'])).toBe(false);
expect(await mnemonicValidate([
'hospital', 'stove', 'relief', 'fringe', 'tongue', 'always', 'charge', 'angry', 'urge',
'sentence', 'again', 'match', 'nerve', 'inquiry', 'senior', 'coconut', 'label', 'tumble',
'carry', 'category', 'beauty', 'bean', 'road', 'solution'])
).toBe(true);

expect(await mnemonicValidate(['a'])).toBe(false);

// 23 words
expect(await mnemonicValidate([
'hospital', 'stove', 'relief', 'fringe', 'tongue', 'always', 'charge', 'angry', 'urge',
'sentence', 'again', 'match', 'nerve', 'inquiry', 'senior', 'coconut', 'label', 'tumble',
'carry', 'category', 'beauty', 'bean', 'road'])
).toBe(false);

// 25 words
expect(await mnemonicValidate([
'hospital', 'stove', 'relief', 'fringe', 'tongue', 'always', 'charge', 'angry', 'urge',
'sentence', 'again', 'match', 'nerve', 'inquiry', 'senior', 'coconut', 'label', 'tumble',
'carry', 'category', 'beauty', 'bean', 'road', 'solution', 'road'])
).toBe(false);

// typo in word
expect(await mnemonicValidate([
'haspital', 'stove', 'relief', 'fringe', 'tongue', 'always', 'charge', 'angry', 'urge',
'sentence', 'again', 'match', 'nerve', 'inquiry', 'senior', 'coconut', 'label', 'tumble',
'carry', 'category', 'beauty', 'bean', 'road', 'solution'])
).toBe(false);

// invalid checksum
expect(await mnemonicValidate([
'hospital', 'hospital', 'relief', 'fringe', 'tongue', 'always', 'charge', 'angry', 'urge',
'sentence', 'again', 'match', 'nerve', 'inquiry', 'senior', 'coconut', 'label', 'tumble',
'carry', 'category', 'beauty', 'bean', 'road', 'solution'])
).toBe(false);
});

for (let i = 0; i < testVectors.length; i++) {
Expand All @@ -91,6 +120,16 @@ describe('mnemonic', () => {
expect(wk.secretKey.toString('hex')).toEqual(testVectors[i].key);
});
}

it('should fail if mnemonic is invalid', async () => {
// Typo in the first word: hospital -> haspital
expect(mnemonicToWalletKey([
'haspital', 'stove', 'relief', 'fringe', 'tongue', 'always', 'charge', 'angry', 'urge',
'sentence', 'again', 'match', 'nerve', 'inquiry', 'senior', 'coconut', 'label', 'tumble',
'carry', 'category', 'beauty', 'bean', 'road', 'solution'])
).rejects.toThrowError('Invalid mnemonic');
});

it('should generate same keys for mnemonicToPrivateKey and mnemonicToWalletKey', async () => {
for (let i = 0; i < 10; i++) {
let k = await mnemonicNew();
Expand Down
38 changes: 36 additions & 2 deletions src/mnemonic/mnemonic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ export async function mnemonicToSeed(mnemonicArray: string[], seed: string, pass
}

/**
* Extract private key from mnemonic
* Extract private key from mnemonic (do not check if mnemonic is valid)
* @param mnemonicArray mnemonic array
* @param password mnemonic password
* @returns Key Pair
*/
export async function mnemonicToPrivateKey(mnemonicArray: string[], password?: string | null | undefined): Promise<KeyPair> {
export async function mnemonicToPrivateKey_unsafe(mnemonicArray: string[], password?: string | null | undefined): Promise<KeyPair> {
// https://github.com/ton-blockchain/ton/blob/24dc184a2ea67f9c47042b4104bbb4d82289fac1/tonlib/tonlib/keys/Mnemonic.cpp#L64
// td::Ed25519::PrivateKey Mnemonic::to_private_key() const {
// return td::Ed25519::PrivateKey(td::SecureString(as_slice(to_seed()).substr(0, td::Ed25519::PrivateKey::LENGTH)));
Expand All @@ -89,10 +89,44 @@ export async function mnemonicToPrivateKey(mnemonicArray: string[], password?: s
};
}

/**
* Extract private key from mnemonic
* @param mnemonicArray mnemonic array
* @param password mnemonic password
* @throws Error if mnemonic is invalid
* @returns Key Pair
*/
export async function mnemonicToPrivateKey(mnemonicArray: string[], password?: string | null | undefined): Promise<KeyPair> {
mnemonicArray = normalizeMnemonic(mnemonicArray);

if (!await mnemonicValidate(mnemonicArray)) {
throw new Error('Invalid mnemonic');
}

return mnemonicToPrivateKey_unsafe(mnemonicArray, password);
}

/**
* Convert mnemonic to wallet key pair (do not check if mnemonic is valid)
* @param mnemonicArray mnemonic array
* @param password mnemonic password
* @returns Key Pair
*/
export async function mnemonicToWalletKey_unsafe(mnemonicArray: string[], password?: string | null | undefined): Promise<KeyPair> {
let seedPk = await mnemonicToPrivateKey_unsafe(mnemonicArray, password);
let seedSecret = seedPk.secretKey.slice(0, 32);
const keyPair = nacl.sign.keyPair.fromSeed(seedSecret);
return {
publicKey: Buffer.from(keyPair.publicKey),
secretKey: Buffer.from(keyPair.secretKey)
};
}

/**
* Convert mnemonic to wallet key pair
* @param mnemonicArray mnemonic array
* @param password mnemonic password
* @throws Error if mnemonic is invalid
* @returns Key Pair
*/
export async function mnemonicToWalletKey(mnemonicArray: string[], password?: string | null | undefined): Promise<KeyPair> {
Expand Down