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

[wip] Add support for bip-39 style keys #1

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions packages/crypto/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
}
},
Keys: require('./src/Keys'),
Mnemonic: require('./src/Mnemonic'),
mipher: {
AES_CBC_ZeroPadding: require('./src/mipher/AES_CBC_ZeroPadding')
}
Expand Down
1 change: 1 addition & 0 deletions packages/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 4 additions & 3 deletions packages/crypto/src/Keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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(
Expand Down
173 changes: 173 additions & 0 deletions packages/crypto/src/Mnemonic.js
Original file line number Diff line number Diff line change
@@ -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;
78 changes: 78 additions & 0 deletions packages/crypto/test/Mnemonic.spec.js
Original file line number Diff line number Diff line change
@@ -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();
}
});
});

});

});
58 changes: 58 additions & 0 deletions packages/crypto/test/fixtures/mnemonic/curve_714.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
Loading