Skip to content
This repository has been archived by the owner on Oct 7, 2024. It is now read-only.

Commit

Permalink
integrate @metamask/scure-bip39
Browse files Browse the repository at this point in the history
  • Loading branch information
adonesky1 committed Sep 13, 2022
1 parent 52391a2 commit 8b21927
Show file tree
Hide file tree
Showing 5 changed files with 993 additions and 1,188 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ module.exports = {
{
files: ['test/**/*.js'],
extends: ['@metamask/eslint-config-jest'],
rules: {
'node/no-unpublished-require': 0,
},
},
],

Expand Down
69 changes: 51 additions & 18 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { hdkey } = require('ethereumjs-wallet');
const SimpleKeyring = require('eth-simple-keyring');
const bip39 = require('@metamask/bip39');
const bip39 = require('@metamask/scure-bip39');
const { wordlist } = require('@metamask/scure-bip39/dist/wordlists/english');
const { normalize } = require('@metamask/eth-sig-util');

// Options:
Expand All @@ -16,17 +17,54 @@ class HdKeyring extends SimpleKeyring {
}

generateRandomMnemonic() {
this._initFromMnemonic(bip39.generateMnemonic());
this._initFromMnemonic(bip39.generateMnemonic(wordlist));
}

serialize() {
const mnemonicAsBuffer =
typeof this.mnemonic === 'string'
? Buffer.from(this.mnemonic, 'utf8')
: this.mnemonic;
uint8ArrayToString(mnemonic) {
const recoveredIndices = Array.from(
new Uint16Array(new Uint8Array(mnemonic).buffer),
);
return recoveredIndices.map((i) => wordlist[i]).join(' ');
}

stringToUint8Array(mnemonic) {
const indices = mnemonic.split(' ').map((word) => wordlist.indexOf(word));
return new Uint8Array(new Uint16Array(indices).buffer);
}

mnemonicToUint8Array(mnemonic) {
let mnemonicData = mnemonic;
// when encrypted/decrypted, buffers get cast into js object with a property type set to buffer
if (mnemonic && mnemonic.type && mnemonic.type === 'Buffer') {
mnemonicData = mnemonic.data;
}

if (
// this block is for backwards compatibility with vaults that were previously stored as buffers, number arrays or plain text strings
typeof mnemonicData === 'string' ||
Buffer.isBuffer(mnemonicData) ||
Array.isArray(mnemonicData)
) {
let mnemonicAsString = mnemonicData;
if (Array.isArray(mnemonicData)) {
mnemonicAsString = Buffer.from(mnemonicData).toString();
} else if (Buffer.isBuffer(mnemonicData)) {
mnemonicAsString = mnemonicData.toString();
}
return this.stringToUint8Array(mnemonicAsString);
} else if (
mnemonicData instanceof Object &&
!(mnemonicData instanceof Uint8Array)
) {
// when encrypted/decrypted the Uint8Array becomes a js object we need to cast back to a Uint8Array
return Uint8Array.from(Object.values(mnemonicData));
}
return mnemonicData;
}

serialize() {
return Promise.resolve({
mnemonic: Array.from(mnemonicAsBuffer.values()),
mnemonic: this.mnemonicToUint8Array(this.mnemonic),
numberOfAccounts: this.wallets.length,
hdPath: this.hdPath,
});
Expand Down Expand Up @@ -104,24 +142,19 @@ class HdKeyring extends SimpleKeyring {
'Eth-Hd-Keyring: Secret recovery phrase already provided',
);
}

this.mnemonic = this.mnemonicToUint8Array(mnemonic);

// validate before initializing
const isValid = bip39.validateMnemonic(mnemonic);
const isValid = bip39.validateMnemonic(this.mnemonic, wordlist);
if (!isValid) {
throw new Error(
'Eth-Hd-Keyring: Invalid secret recovery phrase provided',
);
}

if (typeof mnemonic === 'string') {
this.mnemonic = Buffer.from(mnemonic, 'utf8');
} else if (Array.isArray(mnemonic)) {
this.mnemonic = Buffer.from(mnemonic);
} else {
this.mnemonic = mnemonic;
}

// eslint-disable-next-line node/no-sync
const seed = bip39.mnemonicToSeedSync(this.mnemonic);
const seed = bip39.mnemonicToSeedSync(this.mnemonic, wordlist);
this.hdWallet = hdkey.fromMasterSeed(seed);
this.root = this.hdWallet.derivePath(this.hdPath);
}
Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,24 @@
"author": "Dan Finlay",
"main": "index.js",
"scripts": {
"setup": "yarn install && yarn allow-scripts",
"lint:eslint": "eslint . --cache --ext js,ts",
"lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' '**/*.yml' --ignore-path .gitignore",
"lint": "yarn lint:eslint && yarn lint:misc --check",
"lint:eslint": "eslint . --cache --ext js,ts",
"lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write",
"lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' '**/*.yml' --ignore-path .gitignore",
"setup": "yarn install && yarn allow-scripts",
"test": "jest"
},
"dependencies": {
"@metamask/bip39": "^4.0.0",
"@metamask/eth-sig-util": "^4.0.0",
"@metamask/scure-bip39": "^2.0.3",
"eth-simple-keyring": "^4.2.0",
"ethereumjs-util": "^7.0.9",
"ethereumjs-wallet": "^1.0.1"
},
"devDependencies": {
"@ethereumjs/util": "^8.0.0-beta.3",
"@lavamoat/allow-scripts": "^1.0.6",
"@metamask/auto-changelog": "^2.5.0",
"@metamask/bip39": "^4.0.0",
"@metamask/eslint-config": "^8.0.0",
"@metamask/eslint-config-jest": "^9.0.0",
"@metamask/eslint-config-nodejs": "^8.0.0",
Expand Down
56 changes: 40 additions & 16 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ const {
signTypedData,
SignTypedDataVersion,
} = require('@metamask/eth-sig-util');
const ethUtil = require('ethereumjs-util');
const { wordlist } = require('@metamask/scure-bip39/dist/wordlists/english');
const oldMMForkBIP39 = require('@metamask/bip39');
const { isValidAddress } = require('@ethereumjs/util');
const HdKeyring = require('..');

// Sample account:
Expand Down Expand Up @@ -36,9 +38,9 @@ describe('hd-keyring', () => {
expect(accounts[1]).toStrictEqual(secondAcct);
});

it('constructs with a typeof array mnemonic', async () => {
it('constructs with a typeof buffer mnemonic', async () => {
keyring = new HdKeyring({
mnemonic: Array.from(Buffer.from(sampleMnemonic, 'utf8').values()),
mnemonic: Buffer.from(sampleMnemonic, 'utf8'),
numberOfAccounts: 2,
});

Expand All @@ -47,9 +49,15 @@ describe('hd-keyring', () => {
expect(accounts[1]).toStrictEqual(secondAcct);
});

it('constructs with a typeof buffer mnemonic', async () => {
it('constructs with a typeof Uint8Array mnemonic', async () => {
const indices = sampleMnemonic
.split(' ')
.map((word) => wordlist.indexOf(word));
const uInt8ArrayOfMnemonic = new Uint8Array(
new Uint16Array(indices).buffer,
);
keyring = new HdKeyring({
mnemonic: Buffer.from(sampleMnemonic, 'utf8'),
mnemonic: uInt8ArrayOfMnemonic,
numberOfAccounts: 2,
});

Expand Down Expand Up @@ -132,18 +140,34 @@ describe('hd-keyring', () => {
});

describe('#serialize mnemonic.', () => {
it('serializes mnemonic stored as a buffer in a class variable into a buffer array and does not add accounts', async () => {
keyring.generateRandomMnemonic();
it('serializes mnemonic stored as a buffer to a Uint8Array', async () => {
keyring.mnemonic = oldMMForkBIP39.generateMnemonic();
const mnemonicAsUint8Array = keyring.stringToUint8Array(
keyring.mnemonic.toString(),
);
const output = await keyring.serialize();
expect(output.numberOfAccounts).toBe(0);
expect(Array.isArray(output.mnemonic)).toBe(true);
expect(output.mnemonic).toStrictEqual(mnemonicAsUint8Array);
});

it('serializes keyring data with mnemonic stored as a Uint8Array', async () => {
keyring.generateRandomMnemonic();
const { mnemonic } = keyring;
const hdpath = keyring.hdPath;
keyring.addAccounts(1);
const output = await keyring.serialize();
expect(output.numberOfAccounts).toBe(1);
expect(output.hdPath).toStrictEqual(hdpath);
expect(output.mnemonic).toStrictEqual(mnemonic);
});

it('serializes mnemonic stored as a string in a class variable into a buffer array and does not add accounts', async () => {
it('serializes mnemonic stored as a string', async () => {
keyring.mnemonic = sampleMnemonic;
const output = await keyring.serialize();
expect(output.numberOfAccounts).toBe(0);
expect(Array.isArray(output.mnemonic)).toBe(true);
expect(output.mnemonic).toStrictEqual(
keyring.stringToUint8Array(sampleMnemonic),
);
});
});

Expand All @@ -160,7 +184,7 @@ describe('hd-keyring', () => {
expect(accounts[1]).toStrictEqual(secondAcct);
expect(accounts).toHaveLength(2);
const serialized = await keyring.serialize();
expect(Buffer.from(serialized.mnemonic).toString()).toStrictEqual(
expect(keyring.uint8ArrayToString(serialized.mnemonic)).toStrictEqual(
sampleMnemonic,
);
});
Expand Down Expand Up @@ -437,7 +461,7 @@ describe('hd-keyring', () => {
);

expect(address).not.toBe(appKeyAddress);
expect(ethUtil.isValidAddress(appKeyAddress)).toBe(true);
expect(isValidAddress(appKeyAddress)).toBe(true);

const accounts = await keyring.getAccounts();
expect(accounts[0]).toStrictEqual(firstAcct);
Expand All @@ -456,14 +480,14 @@ describe('hd-keyring', () => {
'someapp.origin.io',
);

expect(ethUtil.isValidAddress(appKeyAddress1)).toBe(true);
expect(isValidAddress(appKeyAddress1)).toBe(true);

const appKeyAddress2 = await keyring.getAppKeyAddress(
address,
'anotherapp.origin.io',
);

expect(ethUtil.isValidAddress(appKeyAddress2)).toBe(true);
expect(isValidAddress(appKeyAddress2)).toBe(true);

expect(appKeyAddress1).not.toBe(appKeyAddress2);
});
Expand All @@ -481,14 +505,14 @@ describe('hd-keyring', () => {
'someapp.origin.io',
);

expect(ethUtil.isValidAddress(appKeyAddress1)).toBe(true);
expect(isValidAddress(appKeyAddress1)).toBe(true);

const appKeyAddress2 = await keyring.getAppKeyAddress(
address,
'someapp.origin.io',
);

expect(ethUtil.isValidAddress(appKeyAddress2)).toBe(true);
expect(isValidAddress(appKeyAddress2)).toBe(true);

expect(appKeyAddress1).toStrictEqual(appKeyAddress2);
});
Expand Down
Loading

0 comments on commit 8b21927

Please sign in to comment.