Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for non english BIP39 dictionaries languages #133

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
50 changes: 41 additions & 9 deletions lib/keystore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand All @@ -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;

Expand All @@ -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)) {
Expand Down Expand Up @@ -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


Expand All @@ -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.')
Expand All @@ -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
Expand Down
65 changes: 65 additions & 0 deletions test/fixtures/keystore.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
77 changes: 63 additions & 14 deletions test/keystore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'";

Expand All @@ -16,20 +17,58 @@ 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];
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(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');
})
done();
});
});
})

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();
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();
});
});
});

Expand Down Expand Up @@ -72,7 +111,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();
Expand Down Expand Up @@ -281,6 +320,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)
Expand Down