ADD: add passphrase support to wallets

This commit is contained in:
Ivan Vershigora 2021-05-14 19:07:33 +03:00
parent ada9e6ba0a
commit bd91e7df59
10 changed files with 174 additions and 22 deletions

View File

@ -52,7 +52,8 @@ export class AbstractHDWallet extends LegacyWallet {
*/
_getSeed() {
const mnemonic = this.secret;
return bip39.mnemonicToSeedSync(mnemonic);
const passphrase = this.passphrase;
return bip39.mnemonicToSeedSync(mnemonic, passphrase);
}
setSecret(newSecret) {
@ -61,6 +62,14 @@ export class AbstractHDWallet extends LegacyWallet {
return this;
}
setPassphrase(passphrase) {
this.passphrase = passphrase;
}
getPassphrase() {
return this.passphrase;
}
/**
* @return {Boolean} is mnemonic in `this.secret` valid
*/
@ -68,10 +77,6 @@ export class AbstractHDWallet extends LegacyWallet {
return bip39.validateMnemonic(this.secret);
}
getMnemonicToSeedHex() {
return bip39.mnemonicToSeedSync(this.secret).toString('hex');
}
/**
* Derives from hierarchy, returns next free address
* (the one that has no transactions). Looks for several,

View File

@ -5,9 +5,6 @@ const mn = require('electrum-mnemonic');
const HDNode = require('bip32');
const PREFIX = mn.PREFIXES.standard;
const MNEMONIC_TO_SEED_OPTS = {
prefix: PREFIX,
};
/**
* ElectrumSeed means that instead of BIP39 seed format it works with the format invented by Electrum wallet. Otherwise
@ -32,7 +29,9 @@ export class HDLegacyElectrumSeedP2PKHWallet extends HDLegacyP2PKHWallet {
if (this._xpub) {
return this._xpub; // cache hit
}
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, MNEMONIC_TO_SEED_OPTS));
const args = { prefix: PREFIX };
if (this.passphrase) args.passphrase = this.passphrase;
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, args));
this._xpub = root.neutered().toBase58();
return this._xpub;
}
@ -63,7 +62,9 @@ export class HDLegacyElectrumSeedP2PKHWallet extends HDLegacyP2PKHWallet {
_getWIFByIndex(internal, index) {
if (!this.secret) return false;
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, MNEMONIC_TO_SEED_OPTS));
const args = { prefix: PREFIX };
if (this.passphrase) args.passphrase = this.passphrase;
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, args));
const path = `m/${internal ? 1 : 0}/${index}`;
const child = root.derivePath(path);

View File

@ -6,9 +6,6 @@ const mn = require('electrum-mnemonic');
const HDNode = require('bip32');
const PREFIX = mn.PREFIXES.segwit;
const MNEMONIC_TO_SEED_OPTS = {
prefix: PREFIX,
};
/**
* ElectrumSeed means that instead of BIP39 seed format it works with the format invented by Electrum wallet. Otherwise
@ -33,7 +30,9 @@ export class HDSegwitElectrumSeedP2WPKHWallet extends HDSegwitBech32Wallet {
if (this._xpub) {
return this._xpub; // cache hit
}
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, MNEMONIC_TO_SEED_OPTS));
const args = { prefix: PREFIX };
if (this.passphrase) args.passphrase = this.passphrase;
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, args));
const xpub = root.derivePath("m/0'").neutered().toBase58();
// bitcoinjs does not support zpub yet, so we just convert it from xpub
@ -73,7 +72,9 @@ export class HDSegwitElectrumSeedP2WPKHWallet extends HDSegwitBech32Wallet {
_getWIFByIndex(internal, index) {
if (!this.secret) return false;
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, MNEMONIC_TO_SEED_OPTS));
const args = { prefix: PREFIX };
if (this.passphrase) args.passphrase = this.passphrase;
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, args));
const path = `m/0'/${internal ? 1 : 0}/${index}`;
const child = root.derivePath(path);

View File

@ -8,7 +8,7 @@ import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
// collection of SLIP39 functions
const SLIP39Mixin = {
_getSeed() {
const master = slip39.recoverSecret(this.secret);
const master = slip39.recoverSecret(this.secret, this.passphrase);
return Buffer.from(master);
},

View File

@ -50,4 +50,21 @@ describe('HDLegacyElectrumSeedP2PKHWallet', () => {
hd.setSecret('bs');
assert.ok(!hd.validateMnemonic());
});
it('can use mnemonic with passphrase', () => {
const mnemonic = 'receive happy wash prosper update pet neck acid try profit proud hungry ';
const passphrase = 'super secret passphrase';
const hd = new HDLegacyElectrumSeedP2PKHWallet();
hd.setSecret(mnemonic);
hd.setPassphrase(passphrase);
assert.strictEqual(
hd.getXpub(),
'xpub661MyMwAqRbcGSUBZaVtq8qEoRkJM1TZNNvUJEgQvtiZE73gS1wKWQoTj6R2E46UDYS2SBpmGGrSHGsJUNxtr1krixFuq8JA772pG43Mo6R',
);
assert.strictEqual(hd._getExternalAddressByIndex(0), '13sPvsrgRN8XibZNHtZXNqVDJPnNZLjTap');
assert.strictEqual(hd._getInternalAddressByIndex(0), '16oEuy5H7ejmapqc2AtKAYerdfkDkoyrDX');
assert.strictEqual(hd._getExternalWIFByIndex(0), 'Ky9WTDUTTZUKKYSPEE6uah2y5sJa89z6177kD23xh5cq1znX2HDj');
});
});

View File

@ -1,6 +1,7 @@
import assert from 'assert';
import * as bitcoin from 'bitcoinjs-lib';
import { HDLegacyP2PKHWallet } from '../../class';
const assert = require('assert');
const bitcoin = require('bitcoinjs-lib');
describe('Legacy HD (BIP44)', () => {
it('works', async () => {
@ -156,4 +157,22 @@ describe('Legacy HD (BIP44)', () => {
hd.setSecret(mnemonic);
assert.strictEqual(hd.getMasterFingerprintHex(), '73C5DA0A');
});
// from electrum tests https://github.com/spesmilo/electrum/blob/9c1a51547a301e765b9b0f9935c6d940bb9d658e/electrum/tests/test_wallet_vertical.py#L292
it('can use mnemonic with passphrase', () => {
const mnemonic = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial';
const UNICODE_HORROR = '₿ 😀 😈 う けたま わる w͢͢͝h͡o͢͡ ̸͢k̵͟n̴͘ǫw̸̛s͘ ̀́w͘͢ḩ̵a҉̡͢t ̧̕h́o̵r͏̵rors̡ ̶͡͠lį̶e͟͟ ̶͝in͢ ͏t̕h̷̡͟e ͟͟d̛a͜r̕͡k̢̨ ͡h̴e͏a̷̢̡rt́͏ ̴̷͠ò̵̶f̸ u̧͘ní̛͜c͢͏͏d̸͢e̡͝?͞';
const hd = new HDLegacyP2PKHWallet();
hd.setSecret(mnemonic);
hd.setPassphrase(UNICODE_HORROR);
assert.strictEqual(
hd.getXpub(),
'xpub6D85QDBajeLe2JXJrZeGyQCaw47PWKi3J9DPuHakjTkVBWCxVQQkmMVMSSfnw39tj9FntbozpRtb1AJ8ubjeVSBhyK4M5mzdvsXZzKPwodT',
);
assert.strictEqual(hd._getExternalAddressByIndex(0), '1F88g2naBMhDB7pYFttPWGQgryba3hPevM');
assert.strictEqual(hd._getInternalAddressByIndex(0), '1H4QD1rg2zQJ4UjuAVJr5eW1fEM8WMqyxh');
assert.strictEqual(hd._getExternalWIFByIndex(0), 'L3HLzdVcwo4711gFiZG4fiLzLVNJpR6nejfo6J85wuYn9YF2G5zk');
});
});

View File

@ -1,5 +1,5 @@
import assert from 'assert';
import { HDSegwitBech32Wallet } from '../../class';
const assert = require('assert');
describe('Bech32 Segwit HD (BIP84)', () => {
it('can create', async function () {
@ -163,4 +163,21 @@ describe('Bech32 Segwit HD (BIP84)', () => {
true,
);
});
it('can use mnemonic with passphrase', () => {
const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const passphrase = 'super secret passphrase';
const hd = new HDSegwitBech32Wallet();
hd.setSecret(mnemonic);
hd.setPassphrase(passphrase);
assert.strictEqual(
hd.getXpub(),
'zpub6qNvUL1qNQwaReccveprgd4urE2EUvShpcFe7WB9tzf9L4NJNcWhPzJSk4fzNXqBNZdRr6135hBKaqqp5RVvyxZ6eMbZXL6u5iK4zrfkCaQ',
);
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qgaj3satczjem43pz46ct6r3758twhnny4y720z');
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1qthe7wh5eplzxczslvthyrer36ph3kxpnfnxgjg');
assert.strictEqual(hd._getExternalWIFByIndex(0), 'L1tfV6fbRjDNwGQdJqHC9fneM9bTHigApnWgoKoU8JwgziwbbE7i');
});
});

View File

@ -48,4 +48,22 @@ describe('HDSegwitElectrumSeedP2WPKHWallet', () => {
hd.setSecret('bs');
assert.ok(!hd.validateMnemonic());
});
// from electrum tests https://github.com/spesmilo/electrum/blob/9c1a51547a301e765b9b0f9935c6d940bb9d658e/electrum/tests/test_wallet_vertical.py#L130
it('can use mnemonic with passphrase', () => {
const mnemonic = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver';
const UNICODE_HORROR = '₿ 😀 😈 う けたま わる w͢͢͝h͡o͢͡ ̸͢k̵͟n̴͘ǫw̸̛s͘ ̀́w͘͢ḩ̵a҉̡͢t ̧̕h́o̵r͏̵rors̡ ̶͡͠lį̶e͟͟ ̶͝in͢ ͏t̕h̷̡͟e ͟͟d̛a͜r̕͡k̢̨ ͡h̴e͏a̷̢̡rt́͏ ̴̷͠ò̵̶f̸ u̧͘ní̛͜c͢͏͏d̸͢e̡͝?͞';
const hd = new HDSegwitElectrumSeedP2WPKHWallet();
hd.setSecret(mnemonic);
hd.setPassphrase(UNICODE_HORROR);
assert.strictEqual(
hd.getXpub(),
'zpub6nD7dvF6ArArjskKHZLmEL9ky8FqaSti1LN5maDWGwFrqwwGTp1b6ic4EHwciFNaYDmCXcQYxXSiF9BjcLCMPcaYkVN2nQD6QjYQ8vpSR3Z',
);
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qx94dutas7ysn2my645cyttujrms5d9p57f6aam');
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1qcywwsy87sdp8vz5rfjh3sxdv6rt95kujdqq38g');
assert.strictEqual(hd._getExternalWIFByIndex(0), 'KyBagP6JHrNTGanqBSDVzKrsBTVbD9hhkTeVe1zEhewKeCU6wJb7');
});
});

View File

@ -1,5 +1,6 @@
import assert from 'assert';
import { SegwitP2SHWallet, SegwitBech32Wallet, HDSegwitP2SHWallet, HDLegacyP2PKHWallet, LegacyWallet } from '../../class';
const assert = require('assert');
describe('P2SH Segwit HD (BIP49)', () => {
it('can create a wallet', async () => {
@ -106,7 +107,7 @@ describe('P2SH Segwit HD (BIP49)', () => {
'honey risk juice trip orient galaxy win situate shoot anchor bounce remind horse traffic exotic since escape mimic ramp skin judge owner topple erode';
let hd = new HDSegwitP2SHWallet();
hd.setSecret(mnemonic);
const seed1 = hd.getMnemonicToSeedHex();
const seed1 = hd._getSeed().toString('hex');
assert.ok(hd.validateMnemonic());
mnemonic = 'hell';
@ -120,7 +121,7 @@ describe('P2SH Segwit HD (BIP49)', () => {
' honey risk juice trip orient galaxy win !situate ;; shoot ;;; anchor Bounce remind\nhorse \n traffic exotic since escape mimic ramp skin judge owner topple erode ';
hd = new HDSegwitP2SHWallet();
hd.setSecret(mnemonic);
const seed2 = hd.getMnemonicToSeedHex();
const seed2 = hd._getSeed().toString('hex');
assert.strictEqual(seed1, seed2);
assert.ok(hd.validateMnemonic());
});
@ -189,4 +190,21 @@ describe('P2SH Segwit HD (BIP49)', () => {
hd.setSecret(mnemonic);
assert.strictEqual(hd.getMasterFingerprintHex(), '73C5DA0A');
});
it('can use mnemonic with passphrase', () => {
const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const passphrase = 'super secret passphrase';
const hd = new HDSegwitP2SHWallet();
hd.setSecret(mnemonic);
hd.setPassphrase(passphrase);
assert.strictEqual(
hd.getXpub(),
'ypub6Xa3WiyXHriYt1fxZGWS8B1iduw92yxHCMWKSwJkW6w92FUCTJxwWQQHLXjRHBSsMLY6SvRu8ErqFEC3JmrkTHEm7KSUfbzhUhj7Yopo2JR',
);
assert.strictEqual(hd._getExternalAddressByIndex(0), '3BtnNenqpGTXwwjb5a1KgzzoKV4TjCuySm');
assert.strictEqual(hd._getInternalAddressByIndex(0), '3EJctafkUBvcSHYhunQRa2iYUHjrMGLXBV');
assert.strictEqual(hd._getExternalWIFByIndex(0), 'L489rJZvUMrFsNop9EyuG2XdEmyKNTbjC1DWkg9WGEc1ddK6jgDg');
});
});

View File

@ -75,4 +75,60 @@ describe('SLIP39 wallets tests', () => {
assert.strictEqual(w._getInternalAddressByIndex(0), 'bc1qgx35amln8aryyr0lw6j2729l3gemzjftp5xrne');
assert.strictEqual(w._getInternalAddressByIndex(1), 'bc1q48v0hcuz2jjsls628wj8jtn7rqp8wsyz2gxdxm');
});
// tests below are from https://github.com/spesmilo/electrum/pull/6917/files#diff-2940d8023ed102277f9c8b91135a9d6fa90fd2752b7b6147c1b5911f26db6d7fR497
it('SLIP39LegacyP2PKHWallet can use passphrase', async () => {
const w = new SLIP39LegacyP2PKHWallet();
w.setSecret(
'extra extend academic bishop cricket bundle tofu goat apart victim enlarge program behavior permit course armed jerky faint language modern\n' +
'extra extend academic acne away best indicate impact square oasis prospect painting voting guest either argue username racism enemy eclipse\n' +
'extra extend academic arcade born dive legal hush gross briefing talent drug much home firefly toxic analysis idea umbrella slice',
);
w.setPassphrase('TREZOR');
assert.strictEqual(
w.getXpub(),
'xpub6CDgeTHt97zmW24XoYdjH9PVFMUj6qcYAe9SZXMKRKJbvTNeZhutNkQqajLyZrQ9DCqdnGenKhBD6UTrT1nHnoLCfFHkdeX8hDsZx1je6b2',
);
assert.strictEqual(w._getExternalAddressByIndex(0), '1NomKAUNnbASwbPuGHmkSVmnrJS5tZeVce');
assert.strictEqual(w._getInternalAddressByIndex(0), '1Aw4wpXsAyEHSgMZqPdyewoAtJqH9Jaso3');
assert.strictEqual(w._getExternalWIFByIndex(0), 'L55ZfXJgyXNPFrFtq2eqnwAkrjrarkKdmbkTEWxYXd2srXcfj3KF');
});
it('SLIP39SegwitP2SHWallet can use passphrase', async () => {
const w = new SLIP39SegwitP2SHWallet();
w.setSecret(
'hobo romp academic axis august founder knife legal recover alien expect emphasis loan kitchen involve teacher capture rebuild trial numb spider forward ladle lying voter typical security quantity hawk legs idle leaves gasoline\n' +
'hobo romp academic agency ancestor industry argue sister scene midst graduate profile numb paid headset airport daisy flame express scene usual welcome quick silent downtown oral critical step remove says rhythm venture aunt',
);
w.setPassphrase('TREZOR');
assert.strictEqual(
w.getXpub(),
'ypub6Y6aCjkcjCP2y7jZStTevc8Tj3GjoXncqC4ReMzdVZWScB68vKZSZBZ88ENvuPUXXBBR58JXkuz1UrwLnCFvnFTUEpzu5yQabeYBRyd7Edf',
);
assert.strictEqual(w._getExternalAddressByIndex(0), '3GCgNoWWVqVdhBxWxrnWQHgwLtffGSYn7D');
assert.strictEqual(w._getInternalAddressByIndex(0), '3FVvdRhR7racZhmcvrGAqX9eJoP8Sw3ypp');
});
it('SLIP39SegwitBech32Wallet can use passphrase', async () => {
const w = new SLIP39SegwitBech32Wallet();
w.setSecret(
'eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice\n' +
'eraser senior ceramic snake clay various huge numb argue hesitate auction category timber browser greatest hanger petition script leaf pickup\n' +
'eraser senior ceramic shaft dynamic become junior wrist silver peasant force math alto coal amazing segment yelp velvet image paces\n' +
'eraser senior ceramic round column hawk trust auction smug shame alive greatest sheriff living perfect corner chest sled fumes adequate',
);
w.setPassphrase('TREZOR');
assert.strictEqual(
w.getXpub(),
'zpub6rs6bFckxdWVHBucVX129aNAYqiwPwh1HgsWt6HEQBa9F9QBKRcYzsw7WZR7rPSCWKmRVTUaEgrGrHStx2LSTpbgAEerbnrh4XxkRXbUUZF',
);
assert.strictEqual(w._getExternalAddressByIndex(0), 'bc1qaggygkqgqjjpt58zrmhvjz5m9dj8mjshw0lpgu');
assert.strictEqual(w._getInternalAddressByIndex(0), 'bc1q8l6hcvlczu4mtjcnlwhczw7vdxnvwccpjl3cwz');
});
});