From b049c5b07eb4074c6ed17285b46909a6ff88a058 Mon Sep 17 00:00:00 2001 From: pascal-triangle Date: Sat, 29 Jun 2019 14:49:02 -0700 Subject: [PATCH] Add support for bip-39 style keys --- packages/crypto/index.js | 1 + packages/crypto/package.json | 1 + packages/crypto/src/Keys.js | 7 +- packages/crypto/src/Mnemonic.js | 173 ++++++++++++++++++ packages/crypto/test/Mnemonic.spec.js | 78 ++++++++ .../test/fixtures/mnemonic/curve_714.json | 58 ++++++ .../test/fixtures/mnemonic/curve_715.json | 58 ++++++ .../test/fixtures/mnemonic/curve_716.json | 58 ++++++ .../test/fixtures/mnemonic/curve_729.json | 58 ++++++ 9 files changed, 489 insertions(+), 3 deletions(-) create mode 100644 packages/crypto/src/Mnemonic.js create mode 100644 packages/crypto/test/Mnemonic.spec.js create mode 100644 packages/crypto/test/fixtures/mnemonic/curve_714.json create mode 100644 packages/crypto/test/fixtures/mnemonic/curve_715.json create mode 100644 packages/crypto/test/fixtures/mnemonic/curve_716.json create mode 100644 packages/crypto/test/fixtures/mnemonic/curve_729.json diff --git a/packages/crypto/index.js b/packages/crypto/index.js index 47f5a7d..a66e397 100644 --- a/packages/crypto/index.js +++ b/packages/crypto/index.js @@ -14,6 +14,7 @@ module.exports = { } }, Keys: require('./src/Keys'), + Mnemonic: require('./src/Mnemonic'), mipher: { AES_CBC_ZeroPadding: require('./src/mipher/AES_CBC_ZeroPadding') } diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 9e51a5e..ee5ff4e 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@pascalcoin-sbx/common": "^0.0.18-alpha.0", + "bip39": "^3.0.2", "crypto-js": "^3.1.9-1", "elliptic": "^6.4.1", "mipher": "^1.1.5" diff --git a/packages/crypto/src/Keys.js b/packages/crypto/src/Keys.js index 055a37c..7bb31e1 100644 --- a/packages/crypto/src/Keys.js +++ b/packages/crypto/src/Keys.js @@ -23,9 +23,10 @@ class Keys { * Generates a new keypair from the given curve. * * @param {Curve} curve + * @param {Object} options * @returns {KeyPair} */ - static generate(curve) { + static generate(curve, options = {}) { if (curve === undefined) { // eslint-disable-next-line no-param-reassign curve = Curve.getDefaultCurve(); @@ -38,9 +39,9 @@ class Keys { throw new Error('Unsupported curve: ' + curve.name); } - // TODO: entropy? + // Entropy can be passed with options.entropy and options.entropyEnc // eslint-disable-next-line new-cap - const kp = new elliptic(curve.name).genKeyPair(); + const kp = new elliptic(curve.name).genKeyPair(options); return new KeyPair( new PrivateKey( diff --git a/packages/crypto/src/Mnemonic.js b/packages/crypto/src/Mnemonic.js new file mode 100644 index 0000000..d537cf5 --- /dev/null +++ b/packages/crypto/src/Mnemonic.js @@ -0,0 +1,173 @@ +/** + * Copyright (c) Benjamin Ansbach - all rights reserved. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +'use strict'; + +const rand = require('brorand'); +const bip39 = require('bip39'); +const utils = require('minimalistic-crypto-utils'); + +const Curve = require('@pascalcoin-sbx/common').Types.Keys.Curve; +const Keys = require('./Keys'); + +/** + * Mnemonic seed constants + * + * VERSION_MASK 11000000 + * CURVE_MASK 00110000 + * CURVE_MASKS + * secp256k1 00000000 + * p384 00010000 + * sect283k1 00100000 + * p521 00110000 + */ + +const VERSION_MASK = 192; +const CURVE_MASK = 48; +const CURVE_MASKS = { + secp256k1: 0, + p384: 16, + sect283k1: 32, + p521: 48 +}; + +/** + * Handles mnemonic seeds. + */ +class Mnemonic { + /** + * Gets the default language. + * + * @returns {string} + */ + static getDefaultLanguage() { + return 'english'; + } + + /** + * Gets the default language. + * + * @returns {array} + */ + static getLanguages() { + return Object.keys(bip39.wordlists); + } + + /** + * Generates a new keypair from the given curve. + * + * @param {Curve} curve + * @param {string} language + * @returns {Object} + */ + static generate(curve, lang) { + if (curve === undefined) { + // eslint-disable-next-line no-param-reassign + curve = Curve.getDefaultCurve(); + } else if (!(curve instanceof Curve)) { + // eslint-disable-next-line no-param-reassign + curve = new Curve(curve); + } + + if (curve.supported === false) { + throw new Error('Unsupported curve: ' + curve.name); + } + + if (lang === undefined) { + // eslint-disable-next-line no-param-reassign + lang = this.getDefaultLanguage(); + } + + if (!this.getLanguages().includes(lang)) { + throw new Error('Language not supported'); + } + + // Generate 192 bits of entropy. 24 bytes == 192 bits + const entropy = rand(24); + + // Modify the first byte to indicate which curve we use. This removes 4 bits of entropy + entropy[0] = (entropy[0] & ((VERSION_MASK | CURVE_MASK) ^ 255)) | CURVE_MASKS[curve.name]; + + // Generate the mnemonic from the entropy + const mnemonic = bip39.entropyToMnemonic(entropy, bip39.wordlists[lang]); + + const kp = Keys.generate(curve, { + entropy: utils.toHex(entropy), + entropyEnc: 'hex' + }); + + return { mnemonic, kp }; + + } + + /** + * Restores a keypair from the given mnemonic words. + * + * @param {string} mnemonic + * @returns {Object} + */ + static restore(mnemonic) { + + // Validate input is string + if (!(typeof mnemonic === 'string' || mnemonic instanceof String)) { + throw new Error('Argument must be a string'); + } + + // Validate correct number of words + if (mnemonic.trim().split(/\s+/g).length !== 18) { + throw new Error('Invalid word length'); + } + + // Convert mnemonic to entropy + let entropy = null; + + for (let lang in this.getLanguages()) { + try { + entropy = utils.toArray(bip39.mnemonicToEntropy(mnemonic, bip39.wordlists[lang]), 'hex'); + break; + } catch (e) { + } + } + + if (entropy === null) { + throw new Error('Invalid mnemonic'); + } + + // Version must be zero + if ((entropy[0] & VERSION_MASK) !== 0) { + throw new Error('Invalid mnemonic version'); + } + + // Bitwise and to get the curve id + const curve_id = entropy[0] & CURVE_MASK; + let curve = null; + + // Loop to find the curve name + for (let curve_name in CURVE_MASKS) { + if (CURVE_MASKS.hasOwnProperty(curve_name) && CURVE_MASKS[curve_name] === curve_id) { + curve = curve_name; + break; + } + } + + // This should never be null, since there are two bits for curve id and four curves + if (curve === null) { + throw new Error('Invalid curve'); + } + + const kp = Keys.generate(curve, { + entropy: utils.toHex(entropy), + entropyEnc: 'hex' + }); + + return { curve, kp }; + + } + +} + +module.exports = Mnemonic; diff --git a/packages/crypto/test/Mnemonic.spec.js b/packages/crypto/test/Mnemonic.spec.js new file mode 100644 index 0000000..be07417 --- /dev/null +++ b/packages/crypto/test/Mnemonic.spec.js @@ -0,0 +1,78 @@ +const fs = require('fs'); +const path = require('path'); +const Mnemonic = require('@pascalcoin-sbx/crypto').Mnemonic; +const BC = require('@pascalcoin-sbx/common').BC; +const Curve = require('@pascalcoin-sbx/common').Types.Keys.Curve; +const KeyPair = require('@pascalcoin-sbx/common').Types.Keys.KeyPair; +const PrivateKeyCoder = require('@pascalcoin-sbx/common').Coding.Pascal.Keys.PrivateKey; + +const chai = require('chai'); + +chai.expect(); +const expect = chai.expect; + +const LANGS = ['english', 'chinese_simplified', 'chinese_traditional', 'french', 'italian', 'japanese', 'korean', 'spanish']; + +const CURVE_INSTANCES = [new Curve(Curve.CN_SECP256K1), new Curve(Curve.CN_P384), new Curve(Curve.CN_P521), , new Curve(Curve.CI_SECT283K1), new Curve(0)]; + +const CURVES = [ + Curve.CI_SECP256K1, + Curve.CI_SECT283K1, + Curve.CI_P521, + Curve.CI_P384 +]; + + +describe('Crypto.Mnemonic', () => { + it('can generate mnemonic', function (done) { + this.timeout(0); + CURVE_INSTANCES.forEach((curve) => { + if (curve.supported) { + LANGS.forEach((lang) => { + const {mnemonic, kp} = Mnemonic.generate(curve.name, lang); + + expect(mnemonic).to.be.a('string'); + expect(kp).to.be.instanceof(KeyPair); + expect(kp.curve.id).to.be.equal(curve.id); + }); + } + }); + done(); + }); + + it('cannot generate unsupported curves', () => { + CURVE_INSTANCES.forEach((curve) => { + if (!curve.supported) { + LANGS.forEach((lang) => { + expect(() => Mnemonic.generate(curve.name, lang)).to.throw(); + }); + } + }); + }); + + it('cannot generate unsupported language', () => { + CURVE_INSTANCES.forEach((curve) => { + expect(() => Mnemonic.generate(curve.name, 'pig_latin')).to.throw(); + }); + }); + + it('can retrieve a keypair from a mnemonic', () => { + CURVES.forEach((c) => { + const keys = JSON.parse(fs.readFileSync(path.join(__dirname, '/fixtures/mnemonic/curve_' + c + '.json'))); + + keys.forEach((keyInfo) => { + if (new Curve(c).supported) { + const {curve, kp} = Mnemonic.restore(keyInfo.mnemonic); + + expect(curve).to.be.equal(keyInfo.curve); + expect(kp.curve.id).to.be.equal(c); + expect(new PrivateKeyCoder().encodeToBytes(kp.privateKey).toHex()).to.be.equal(keyInfo.enc_privkey); + } else { + expect(() => Mnemonic.restore(keyInfo.mnemonic)).to.throw(); + } + }); + }); + + }); + +}); diff --git a/packages/crypto/test/fixtures/mnemonic/curve_714.json b/packages/crypto/test/fixtures/mnemonic/curve_714.json new file mode 100644 index 0000000..8330e9c --- /dev/null +++ b/packages/crypto/test/fixtures/mnemonic/curve_714.json @@ -0,0 +1,58 @@ +[ + { + "ec_nid": 714, + "curve": "secp256k1", + "lang": "english", + "mnemonic": "assault valid bar lesson pear false thrive spike check gold please clinic cheese milk nothing sock equip cruise", + "enc_privkey": "CA022000B0453D198B7D0D2A2043FD22D2A71B3E7AF2B02CDB798414EB606E5693BBC03A" + }, + { + "ec_nid": 714, + "curve": "secp256k1", + "lang": "chinese_simplified", + "mnemonic": "audit walk walnut sample violin genius palm eternal census upon able toilet spot trash media gather phrase across", + "enc_privkey": "CA021F007BD84BEAFA83BAFC0618994592BA855CB96DAED145C241DEA5BD7976A8D7D0" + }, + { + "ec_nid": 714, + "curve": "secp256k1", + "lang": "chinese_traditional", + "mnemonic": "add joke elite half stay sponsor purpose universe salad off vendor cradle must figure letter twelve creek uniform", + "enc_privkey": "CA02200003223B98F5A4EB8C44C8F3C8D39E3C309AFBA2732E7D77CD60C74BA6CB853CAB" + }, + { + "ec_nid": 714, + "curve": "secp256k1", + "lang": "french", + "mnemonic": "artefact initial mass lumber cruel ketchup disagree soldier remain raise slim shield disorder main party play vehicle misery", + "enc_privkey": "CA0220003167D1226A1C041C64B09FC37AB57A74A3736E0AABA49EC258B30B1026485F0E" + }, + { + "ec_nid": 714, + "curve": "secp256k1", + "lang": "italian", + "mnemonic": "account gym foil antenna trouble curtain treat fetch course more punch icon element control donkey risk push impact", + "enc_privkey": "CA02200050EF655CA3193F5D5F6346FE759A4D2A31D65E1CA3E4CD674687A1358076EBE2" + }, + { + "ec_nid": 714, + "curve": "secp256k1", + "lang": "japanese", + "mnemonic": "atom shallow exercise believe farm eager target trial build bullet ten try fringe lawsuit illegal before virus convince", + "enc_privkey": "CA02200087F28268E9BFB2059F5ED335AF1D71ECA835AA90E1A312A6CB0639F166B0C341" + }, + { + "ec_nid": 714, + "curve": "secp256k1", + "lang": "korean", + "mnemonic": "addict total junior mass run grass violin float buddy treat mesh enter horse summer clutch school kick bar", + "enc_privkey": "CA0220004BD8CC482A099CD6E1112F1711438DD327D5CD735BC87B8713249315370F7C02" + }, + { + "ec_nid": 714, + "curve": "secp256k1", + "lang": "spanish", + "mnemonic": "acoustic limit symptom alpha love march impulse reason bubble dream reduce fatal vast accident alpha iron expose cloud", + "enc_privkey": "CA02200011A277A75BC4A3CC862E998E12E260BE375BB0A5F9340CB2202CD8CDF90E1DF3" + } +] \ No newline at end of file diff --git a/packages/crypto/test/fixtures/mnemonic/curve_715.json b/packages/crypto/test/fixtures/mnemonic/curve_715.json new file mode 100644 index 0000000..9fd284c --- /dev/null +++ b/packages/crypto/test/fixtures/mnemonic/curve_715.json @@ -0,0 +1,58 @@ +[ + { + "ec_nid": 715, + "curve": "p384", + "lang": "english", + "mnemonic": "bicycle ketchup shoulder cluster before arm basic require cream elevator angry hamster strike refuse long arch tomato top", + "enc_privkey": "CB02300017099715CAF378FB3691DDD6F750CE6BA7C2F4FE9F3CE523DA3659377172CEFEE35A21626C098A5AAF08085C5734F9F6" + }, + { + "ec_nid": 715, + "curve": "p384", + "lang": "chinese_simplified", + "mnemonic": "bottom scorpion bright ride cool tooth shell calm feel curious manual eagle round poet unhappy bird rough birth", + "enc_privkey": "CB023000511774FE614CD8A127484B4C6C4F82168636C51038A18B9FD475528B2586506708C4CB202913B9F7257EAE37C5CA49E9" + }, + { + "ec_nid": 715, + "curve": "p384", + "lang": "chinese_traditional", + "mnemonic": "banner spare predict enact vanish scan viable match nothing address token muffin ahead horror absorb arrive dash bracket", + "enc_privkey": "CB023000FD82B2EF6178DDE8A2B0507E4C8110FC6CFA07B0B9901372413D02EB4208FCAF409DA90AC6CE4B010B2A5A452BACAB48" + }, + { + "ec_nid": 715, + "curve": "p384", + "lang": "french", + "mnemonic": "boy type divide burst bicycle mom shed payment mirror error dish firm hard margin bulk hungry control dial", + "enc_privkey": "CB02300064D90C805238D88DD31BB3A42787445BA858DB0B1B84E8D6DA2D01B0F79FF2C120235A3097FE707F840698FDB821F2F8" + }, + { + "ec_nid": 715, + "curve": "p384", + "lang": "italian", + "mnemonic": "away unlock rhythm sword purchase phone that nurse siren lumber when talent awake picture picnic ghost weekend jaguar", + "enc_privkey": "CB023000043F067E936BA0A6B24AD128FC0543A056FD51057F889A52E0B7A00CD383775D222D4098EEB4DDFC2E8D24400AC39E04" + }, + { + "ec_nid": 715, + "curve": "p384", + "lang": "japanese", + "mnemonic": "biology planet water unhappy convince tired select agent shoulder draw manual credit sleep often scare gap room chief", + "enc_privkey": "CB023000044A1C7E3FDF8DEB7EFBC98D07C6AFA65F5EF739879AFB8EF71C65C1CC92DF8BD2A700BFF37BD7B89DD8BD158D505020" + }, + { + "ec_nid": 715, + "curve": "p384", + "lang": "korean", + "mnemonic": "ball giggle region pepper wire want weasel core lend sleep diesel invite thunder economy ski outside equal access", + "enc_privkey": "CB0230004C6EA95761F59FEA81CF4CB68C73C186F2A2C721E2D0379FDFE2D8B7DD39440D0EA415A024F2C5BDA812DCF60BF39A7F" + }, + { + "ec_nid": 715, + "curve": "p384", + "lang": "spanish", + "mnemonic": "better bean main wedding dove forum impose erosion foster cram junior dance rival arrange crop pond shaft gallery", + "enc_privkey": "CB0230007567AE401ECC91FA5E5390F80C1B0A4467B313913D9935D44063B1CF4FFD972ED2678999F9ED66084A6968C02CD5CC2F" + } +] \ No newline at end of file diff --git a/packages/crypto/test/fixtures/mnemonic/curve_716.json b/packages/crypto/test/fixtures/mnemonic/curve_716.json new file mode 100644 index 0000000..d1a1b9f --- /dev/null +++ b/packages/crypto/test/fixtures/mnemonic/curve_716.json @@ -0,0 +1,58 @@ +[ + { + "ec_nid": 716, + "curve": "p521", + "lang": "english", + "mnemonic": "dignity mushroom green hedgehog immense become soccer convince biology list tide fork exclude heart click student patient script", + "enc_privkey": "CC024200014C42D26FEF7629E9C8E4D7E482EB95481C2F1E0FA66F8D19914C8C2109B532A7BDFA233C133ACBA157B38966FDFF5F786386004023180FF6BDCF2B65FA0F7F7C26" + }, + { + "ec_nid": 716, + "curve": "p521", + "lang": "chinese_simplified", + "mnemonic": "cotton squeeze amateur edit leg quiz toss fee volcano stomach easy draft pepper undo baby steel prison music", + "enc_privkey": "CC0241006ACF15459FA106C44274802C8CA8523E4797F3B05850E03FF9865A96BE3B8AB8ACF2884420FD8CCFA05744E711A9EAB3FB14F433508A54ED8603E1EE9F368CA4FA" + }, + { + "ec_nid": 716, + "curve": "p521", + "lang": "chinese_traditional", + "mnemonic": "crash tray youth cement soap cram tag name bean focus bless gaze holiday frequent piano endless learn service", + "enc_privkey": "CC02420001910CABF54EDD1115AD64488C854B6895B7EF7DA5D65EA14B9FEBD2DED4A8C9DDF9D87042E3D58258EF9D1A3F9ACAD0543420CCD83B98D6138CED41DDF1A916B2DE" + }, + { + "ec_nid": 716, + "curve": "p521", + "lang": "french", + "mnemonic": "cruise sign interest cute unfold stamp index family idea electric zebra scissors call paddle injury common fiber maid", + "enc_privkey": "CC0241007EBBEE59E959A1DC138D8367565BA07F6AB4E42ABF18603797F0853E122592C7080CD2268502396325F355C5E4344CBC166E8A9821D8D9F3C8C06E62703BBFA433" + }, + { + "ec_nid": 716, + "curve": "p521", + "lang": "italian", + "mnemonic": "defense duck pass kitchen goddess draw benefit blush cup script history fix advance dance school join until ethics", + "enc_privkey": "CC0241000C40AE177252287C46D47FDB1C82AD2A830C010CEA190D3292888EFFFDD0A95D802ED22DEA6425004C8806A1D53EA25DF0B1E67470039C1A27F84E731B0B32DE7B" + }, + { + "ec_nid": 716, + "curve": "p521", + "lang": "japanese", + "mnemonic": "creek velvet world peanut glare stool advance outside wealth kiss material win dove title lazy agent equal reject", + "enc_privkey": "CC024100A87C60D5C03B66EF893DE5226F0482BC18B696193F91377A566C1760F7A17CD7D2E1BFB368D4E286D5110CF8CDE60C8EA1063645AEFB92D6839EADD8DC4190E54C" + }, + { + "ec_nid": 716, + "curve": "p521", + "lang": "korean", + "mnemonic": "credit error index math carbon intact blossom lucky write spider destroy palace hour doctor subject author account clay", + "enc_privkey": "CC024200011D76B46D33E8DD6F114028F2C2E2B1B57C5770E2B11B7ACD8C82CFBEC598BCE84077920B7572104AB7D8B0D9A8BFADF9A086653BF1BA2B918A9476ECC3D203167C" + }, + { + "ec_nid": 716, + "curve": "p521", + "lang": "spanish", + "mnemonic": "dinosaur evil tiger festival ensure earth they ghost suspect coil globe piece document sugar box miss tray resemble", + "enc_privkey": "CC024100A6EB164C46D5996E8E5216F8B595C89B8CBD506AAF0D06CAC97841F5943CD6CBA883464C36E30A031D425408387847B060D41D96D07BD3E8CC620326932BCC1AC6" + } +] \ No newline at end of file diff --git a/packages/crypto/test/fixtures/mnemonic/curve_729.json b/packages/crypto/test/fixtures/mnemonic/curve_729.json new file mode 100644 index 0000000..d150085 --- /dev/null +++ b/packages/crypto/test/fixtures/mnemonic/curve_729.json @@ -0,0 +1,58 @@ +[ + { + "ec_nid": 729, + "curve": "sect283k1", + "lang": "english", + "mnemonic": "cloud run desk odor all concert forum demand breeze clog sugar silly rebuild antique apology ride angle convince", + "enc_privkey": null + }, + { + "ec_nid": 729, + "curve": "sect283k1", + "lang": "chinese_simplified", + "mnemonic": "combine clump upon crystal soldier mind cream letter key improve attend enforce wealth guide lion evil soft collect", + "enc_privkey": null + }, + { + "ec_nid": 729, + "curve": "sect283k1", + "lang": "chinese_traditional", + "mnemonic": "common spot goat drip warrior entry rural toilet cream twin tell antenna cotton hood siren someone note eyebrow", + "enc_privkey": null + }, + { + "ec_nid": 729, + "curve": "sect283k1", + "lang": "french", + "mnemonic": "census arch economy eager ladder despair rough kite balance life stairs punch away bulb exile spy bone wage", + "enc_privkey": null + }, + { + "ec_nid": 729, + "curve": "sect283k1", + "lang": "italian", + "mnemonic": "canoe glow devote forum kind cook guitar add leave utility horn sphere practice card hedgehog sugar rent cigar", + "enc_privkey": null + }, + { + "ec_nid": 729, + "curve": "sect283k1", + "lang": "japanese", + "mnemonic": "carpet skull gap essence gorilla infant jazz noise food increase student giant comic spider tobacco trash fall crisp", + "enc_privkey": null + }, + { + "ec_nid": 729, + "curve": "sect283k1", + "lang": "korean", + "mnemonic": "car photo worry super motion length screen burden bacon hand neck sea roof vacant input abandon retreat key", + "enc_privkey": null + }, + { + "ec_nid": 729, + "curve": "sect283k1", + "lang": "spanish", + "mnemonic": "cause balcony health series protect license physical coach bomb crop sock demise excuse oblige trash use recipe tiny", + "enc_privkey": null + } +] \ No newline at end of file