From 42a60f5c92c331628ad92afc201a63c649283956 Mon Sep 17 00:00:00 2001 From: Marco Polci Date: Wed, 1 Mar 2017 18:58:27 +0100 Subject: [PATCH 1/3] Fix test --- test/keystore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/keystore.js b/test/keystore.js index 4163460b..8c77a4af 100644 --- a/test/keystore.js +++ b/test/keystore.js @@ -72,7 +72,7 @@ describe("Keystore", function() { // No values are set expect(ks.encSeed).to.equal(undefined) - expect(ks.ksData[ks.defaultHdPathString].encHdRootPrivkey).to.equal(undefined) + expect(ks.ksData[ks.defaultHdPathString].encHdRootPriv).to.equal(undefined) expect(ks.ksData[ks.defaultHdPathString].encPrivKeys).to.deep.equal({}) expect(ks.ksData[ks.defaultHdPathString].addresses).to.deep.equal([]) done(); From 279bb8f09e558755e1ccfb2f63b82d8a3a5ac3e6 Mon Sep 17 00:00:00 2001 From: Marco Polci Date: Wed, 1 Mar 2017 17:43:10 +0100 Subject: [PATCH 2/3] Add support for non english BIP39 dictionaries languages --- lib/keystore.js | 50 +++++++++++++++++++++++++++++++++++++++--------- test/keystore.js | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/lib/keystore.js b/lib/keystore.js index a8a813f6..a277e3b6 100644 --- a/lib/keystore.js +++ b/lib/keystore.js @@ -90,8 +90,8 @@ KeyStore.prototype.init = function(mnemonic, pwDerivedKey, hdPathString, salt) { pathKsData.addresses = []; if ( (typeof pwDerivedKey !== 'undefined') && (typeof mnemonic !== 'undefined') ){ - var words = mnemonic.split(' '); - if (!Mnemonic.isValid(mnemonic, Mnemonic.Words.ENGLISH) || words.length !== 12){ + var words = mnemonic.split(/\s/); + if (!KeyStore.isSeedValid(mnemonic) || words.length !== 12){ throw new Error('KeyStore: Invalid mnemonic'); } @@ -116,6 +116,17 @@ KeyStore.prototype.init = function(mnemonic, pwDerivedKey, hdPathString, salt) { } } +/** + * + * @param opts + * @param {string=} opts.hdPathString BIP32 default hd path + * @param {string=} opts.seedPhrase BIP39 mnemonic phrase + * @param {string=} opts.seedLanguage BIP39 dictionary language for automatically generated seed. + * See https://github.com/bitpay/bitcore-mnemonic for supported values + * @param {string} opts.password + * @param {string|Buffer=} opts.salt + * @param cb + */ KeyStore.createVault = function(opts, cb) { var _this = this; @@ -126,7 +137,7 @@ KeyStore.createVault = function(opts, cb) { // Default seed phrase if not specified if (!('seedPhrase' in opts)) { - opts.seedPhrase = this.generateRandomSeed(); + opts.seedPhrase = this.generateRandomSeed(null, opts.seedLanguage || 'ENGLISH'); } if (!('salt' in opts)) { @@ -341,6 +352,18 @@ KeyStore._concatAndSha256 = function(entropyBuf0, entropyBuf1) { return hashedEnt; } +var _getDictionary = function (lang) { + var dictionary; + if (!lang) { + dictionary = Mnemonic.Words.ENGLISH + } else if (lang in Mnemonic.Words) { + dictionary = Mnemonic.Words[lang] + } else { + throw new Error('Unsupported dictionary language') + } + return dictionary +} + // External static functions @@ -353,17 +376,18 @@ KeyStore._concatAndSha256 = function(entropyBuf0, entropyBuf1) { // If extraEntropy is not set, the random number generator // is used directly. -KeyStore.generateRandomSeed = function(extraEntropy) { +KeyStore.generateRandomSeed = function(extraEntropy, seedLanguage) { var seed = ''; - if (extraEntropy === undefined) { - seed = new Mnemonic(Mnemonic.Words.ENGLISH); + var dictionary = _getDictionary(seedLanguage) + if (extraEntropy == undefined) { + seed = new Mnemonic(dictionary); } else if (typeof extraEntropy === 'string') { var entBuf = new Buffer(extraEntropy); var randBuf = Random.getRandomBuffer(256 / 8); var hashedEnt = this._concatAndSha256(randBuf, entBuf).slice(0, 128 / 8); - seed = new Mnemonic(hashedEnt, Mnemonic.Words.ENGLISH); + seed = new Mnemonic(hashedEnt, dictionary); } else { throw new Error('generateRandomSeed: extraEntropy is set but not a string.') @@ -372,8 +396,16 @@ KeyStore.generateRandomSeed = function(extraEntropy) { return seed.toString(); }; -KeyStore.isSeedValid = function(seed) { - return Mnemonic.isValid(seed, Mnemonic.Words.ENGLISH) +/** + * + * @param {string} seed + * @param {string=} seedLanguage BIP39 dictionary language, if undefined the dictionary + * will be detected from the seed. See https://github.com/bitpay/bitcore-mnemonic for supported values + * @return {boolean} + */ +KeyStore.isSeedValid = function(seed, seedLanguage) { + var wordList = seedLanguage ? _getDictionary(seedLanguage) : undefined + return Mnemonic.isValid(seed, wordList) }; // Takes keystore serialized as string and returns an instance of KeyStore diff --git a/test/keystore.js b/test/keystore.js index 8c77a4af..4b212e45 100644 --- a/test/keystore.js +++ b/test/keystore.js @@ -3,6 +3,7 @@ var keyStore = require('../lib/keystore') var upgrade = require('../lib/upgrade') var fixtures = require('./fixtures/keystore') var Promise = require('bluebird') +var Mnemonic = require('bitcore-mnemonic'); var defaultHdPathString = "m/0'/0'/0'"; @@ -33,6 +34,28 @@ describe("Keystore", function() { }); }); + Object.keys(Mnemonic.Words).forEach(function (lang) { + it('should generete random seed of language ' + lang, function (done) { + var fixture = fixtures.valid[0]; + + keyStore.createVault({ + password: fixture.password, + seedLanguage: lang, + salt: fixture.salt, + }, function (err, ks) { + if (err) return done(err) + expect(ks.encSeed).to.not.equal(undefined); + var decryptedPaddedSeed = keyStore._decryptString(ks.encSeed, Uint8Array.from(fixtures.valid[0].pwDerivedKey)); + // Check padding + var words = decryptedPaddedSeed.trim().split(/\s/); + words.forEach(function (w) { + expect(Mnemonic.Words[lang].indexOf(w)).to.not.equal(-1, 'word ' + w + ' is not in dictionary'); + }) + done(); + }); + }); + }) + it('generates a random salt for key generation', function(done) { this.timeout(10000); var fixture = fixtures.valid[0]; @@ -281,6 +304,16 @@ describe("Keystore", function() { }); describe("Seed functions", function() { + Object.keys(Mnemonic.Words).forEach(function (lang) { + it('should generate a random phrase of language ' + lang, function() { + var seed = keyStore.generateRandomSeed(null, lang); + var words = seed.split(/\s/); + words.forEach(function (w) { + expect(Mnemonic.Words[lang].indexOf(w)).to.not.equal(-1, 'word ' + w + ' is not in dictionary'); + }) + }); + }); + it('returns the unencrypted seed', function(done) { var ks = new keyStore(fixtures.valid[0].mnSeed, Uint8Array.from(fixtures.valid[0].pwDerivedKey)) expect(ks.getSeed(Uint8Array.from(fixtures.valid[0].pwDerivedKey))).to.equal(fixtures.valid[0].mnSeed) From 4c039b84fa69524e6e40e598f11255ea77ee4c24 Mon Sep 17 00:00:00 2001 From: Marco Polci Date: Wed, 1 Mar 2017 19:42:32 +0100 Subject: [PATCH 3/3] Add tests for generated hd priv keys --- test/fixtures/keystore.json | 65 +++++++++++++++++++++++++++++++++++++ test/keystore.js | 54 +++++++++++++++++++----------- 2 files changed, 100 insertions(+), 19 deletions(-) diff --git a/test/fixtures/keystore.json b/test/fixtures/keystore.json index 6576451b..bb926aa1 100644 --- a/test/fixtures/keystore.json +++ b/test/fixtures/keystore.json @@ -149,5 +149,70 @@ "ent1" : "c", "targetHash" : "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" } + ], + "mnemonicSeedTests": [ + { + "password" : "password", + "salt": "lightwalletSalt", + "pwDerivedKey" : [12, 37, 184, 176, 141, 255, 15, 70, 189, 195, 206, 218, 109, 172, 141, 233, 117, 114, 2, 10, 156, 57, 255, 102, 37, 66, 2, 177, 82, 70, 1, 246], + "mnSeed": "jelly better achieve collect unaware mountain thought cargo oxygen act hood bridge", + "hdRootPriv": "xprv9s21ZrQH143K3vCy7FkijuhZgUbbTSGwrWmvwyLsW7bHDwtE3x5fXLfMTX4N3sHGfCRwRr9n7R5fGg5y6sRhbQN7J58mu1gbMputHnrZYAw" + }, + { + "password" : "asdflknwqeroilasdflkjnzvmnwmet", + "salt": "lightwalletSalt", + "pwDerivedKey" : [24,100,147,66,23,117,206,186,27,54,254,137,187,174,103,5,38,18,139,128,139,74,217,188,166,34,0,30,54,133,158,208], + "mnSeed": "afford alter spike radar gate glance object seek swamp infant panel yellow", + "hdRootPriv": "xprv9s21ZrQH143K4F3jaSGsPkj54F6VXF3DZxjb69DH4AVmoYanpgwLA8RqQMXV1nbXyQrsXzwiY15sxt3sgh4rD61S6aXd5DmVSvbonWem6HR" + }, + { + "password" : "!@&#*#()", + "salt": "lightwalletSalt", + "pwDerivedKey" : [51,238,218,224,61,216,29,19,249,245,254,125,54,245,160,253,1,73,137,234,137,138,109,226,61,11,128,41,76,115,80,43], + "mnSeed": "turtle front uncle idea crush write shrug there lottery flower risk shell", + "hdRootPriv": "xprv9s21ZrQH143K3ENdSTFWNa61Dpo7TyVwXdNGnr9LX4UGqEJgCxGhnDmAeVZTPTddYgpDPTQqyruHCyuRHx5yAbnLSzce4GhGHWmXLyi3ujA" + }, + { + "password" : "PassHello", + "salt": "lightwalletSalt", + "pwDerivedKey" : [113,91,117,133,102,129,128,16,251,116,91,122,215,255,183,202,72,108,222,129,238,46,143,236,132,13,127,227,110,202,254,112], + "mnSeed": "board flee heavy tunnel powder denial science ski answer betray cargo cat", + "hdRootPriv": "xprv9s21ZrQH143K33giBRhHYG2HGme23pHx8zPWqM7H6jVuj5Uz47GefcuhFf4U8AtCRQVpGZRrSfgz8A4v7v9rFUG1eyYvL9Z1GhDEjCRVWdd" + }, + { + "password" : "password", + "salt": "strangeSalt", + "pwDerivedKey": [205, 127, 41, 55, 40, 243, 95, 138, 187, 239, 244, 242, 33, 239, 174, 4, 146, 184, 75, 185, 221, 160, 223, 207, 124, 49, 16, 208, 237, 141, 15, 120], + "mnSeed": "そいとげる かぶか しちりん かわく がいらい けしごむ ときおり くさい そっこう さめる たいまつばな うすめる", + "hdRootPriv": "xprv9s21ZrQH143K4ZC7EcgBoLbGp4BRVNNGhQZ1KCtMxYNkKXELKKe97BkPu9xj2M7NhksgyApceqk8wJANuNwhHR4DzsakecLhR28aCwk5XHd" + }, + { + "password" : "password", + "salt": "strangeSalt", + "pwDerivedKey": [205, 127, 41, 55, 40, 243, 95, 138, 187, 239, 244, 242, 33, 239, 174, 4, 146, 184, 75, 185, 221, 160, 223, 207, 124, 49, 16, 208, 237, 141, 15, 120], + "mnSeed": "conocer cero abeja falso crónica norma clínica vivaz pañuelo tubería innato género", + "hdRootPriv": "xprv9s21ZrQH143K2ZciTt8VLGEYMuqt9jV6LyHWES2TipbgiRjEGE547Y3SfJZcDphfT2ysr48e7kzScN2xJJ4r8yR2RnFLurvDo7WA2xRqfnu" + }, + { + "password" : "password", + "salt": "strangeSalt", + "pwDerivedKey": [205, 127, 41, 55, 40, 243, 95, 138, 187, 239, 244, 242, 33, 239, 174, 4, 146, 184, 75, 185, 221, 160, 223, 207, 124, 49, 16, 208, 237, 141, 15, 120], + "mnSeed": "标 叫 是 控 专 网 约 堵 怪 胺 居 医", + "hdRootPriv": "xprv9s21ZrQH143K2uixweugxzUnaxgxrWyy6oPuf3JAvFNLQTYc5a7PJRxK99bzomR2RPLve6z68zVdHTsbheYYcUBsbFeQiFt5o8Yk3XqnsCu" + }, + { + "password" : "password", + "salt": "strangeSalt", + "pwDerivedKey": [205, 127, 41, 55, 40, 243, 95, 138, 187, 239, 244, 242, 33, 239, 174, 4, 146, 184, 75, 185, 221, 160, 223, 207, 124, 49, 16, 208, 237, 141, 15, 120], + "mnSeed": "rouge racine arriver sucre chute phrase moqueur désigner arriver décembre nuancer chien", + "hdRootPriv": "xprv9s21ZrQH143K2Mg2MDBrYxpmZ7MUarcQnF9G1jzKU4MSZoNfjepSwZTZF83JZB42pdwydCtgLp2PrKbPLXSSEQXwaeSitQ7qzSNzVNUPDY8" + }, + { + "password" : "password", + "salt": "strangeSalt", + "pwDerivedKey": [205, 127, 41, 55, 40, 243, 95, 138, 187, 239, 244, 242, 33, 239, 174, 4, 146, 184, 75, 185, 221, 160, 223, 207, 124, 49, 16, 208, 237, 141, 15, 120], + "mnSeed": "smeraldo scodella arso tecnico continuo rischio peso eletto arso domato prefisso comodo", + "hdRootPriv": "xprv9s21ZrQH143K4AucDHZwwJKWYXT3X5FNAK4cfWGHR8dMrdvuJk2jv5SoiBD21hPtB4VTNLeWh7sVyaiVHjV5oEvzQuDyyyviZQPxjRxoaUd" + } ] } diff --git a/test/keystore.js b/test/keystore.js index 4b212e45..10c1c281 100644 --- a/test/keystore.js +++ b/test/keystore.js @@ -17,23 +17,6 @@ var Transaction = require('ethereumjs-tx'); describe("Keystore", function() { describe("createVault constructor", function() { - it('accepts a variety of options', function(done) { - var fixture = fixtures.valid[0]; - - keyStore.createVault({ - password: fixture.password, - seedPhrase: fixture.mnSeed, - salt: fixture.salt, - }, function(err, ks) { - expect(ks.encSeed).to.not.equal(undefined); - var decryptedPaddedSeed = keyStore._decryptString(ks.encSeed, Uint8Array.from(fixtures.valid[0].pwDerivedKey)); - // Check padding - expect(decryptedPaddedSeed.length).to.equal(120); - expect(decryptedPaddedSeed.trim()).to.equal(fixtures.valid[0].mnSeed); - done(); - }); - }); - Object.keys(Mnemonic.Words).forEach(function (lang) { it('should generete random seed of language ' + lang, function (done) { var fixture = fixtures.valid[0]; @@ -45,8 +28,7 @@ describe("Keystore", function() { }, function (err, ks) { if (err) return done(err) expect(ks.encSeed).to.not.equal(undefined); - var decryptedPaddedSeed = keyStore._decryptString(ks.encSeed, Uint8Array.from(fixtures.valid[0].pwDerivedKey)); - // Check padding + var decryptedPaddedSeed = keyStore._decryptString(ks.encSeed, Uint8Array.from(fixture.pwDerivedKey)); var words = decryptedPaddedSeed.trim().split(/\s/); words.forEach(function (w) { expect(Mnemonic.Words[lang].indexOf(w)).to.not.equal(-1, 'word ' + w + ' is not in dictionary'); @@ -56,6 +38,40 @@ describe("Keystore", function() { }); }) + fixtures.mnemonicSeedTests.forEach(function (fixture) { + it('should store mnemonic seed "' + fixture.mnSeed + '"', function (done) { + keyStore.createVault({ + password: fixture.password, + seedPhrase: fixture.mnSeed, + salt: fixture.salt, + }, function(err, ks) { + if (err) return done(err) + expect(ks.encSeed).to.not.equal(undefined); + var decryptedPaddedSeed = keyStore._decryptString(ks.encSeed, Uint8Array.from(fixture.pwDerivedKey)); + // Check padding + expect(decryptedPaddedSeed.length).to.equal(120); + expect(decryptedPaddedSeed.trim()).to.equal(fixture.mnSeed); + done(); + }); + }) + }); + + fixtures.mnemonicSeedTests.forEach(function (fixture) { + it('should generate HD root key for seed "' + fixture.mnSeed + '"', function (done) { + keyStore.createVault({ + password: fixture.password, + seedPhrase: fixture.mnSeed, + salt: fixture.salt, + }, function (err, ks) { + if (err) return done(err) + expect(ks.encSeed).to.not.equal(undefined); + var decryptedHdRootPriv = keyStore._decryptString(ks.encHdRootPriv, Uint8Array.from(fixture.pwDerivedKey)); + expect(decryptedHdRootPriv.trim()).to.equal(fixture.hdRootPriv); + done(); + }); + }); + }); + it('generates a random salt for key generation', function(done) { this.timeout(10000); var fixture = fixtures.valid[0];