Skip to content

Commit

Permalink
fix: offload bip39 from wallet-sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
ahsan-javaid authored and janniks committed May 4, 2022
1 parent 591b0fb commit 701416a
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 23 deletions.
2 changes: 1 addition & 1 deletion packages/wallet-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"@stacks/profile": "^4.0.0",
"@stacks/storage": "^4.0.0",
"@stacks/transactions": "^4.0.0",
"bip39": "^3.0.2",
"@scure/bip39": "^1.0.0",
"bitcoinjs-lib": "^5.2.0",
"bn.js": "^5.2.0",
"c32check": "^1.1.3",
Expand Down
14 changes: 11 additions & 3 deletions packages/wallet-sdk/src/generate.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { generateMnemonic, mnemonicToSeed } from 'bip39';
// https://github.com/paulmillr/scure-bip39
// Secure, audited & minimal implementation of BIP39 mnemonic phrases.
import { generateMnemonic, mnemonicToSeed } from '@scure/bip39';
// Word lists not imported by default as that would increase bundle sizes too much as in case of bitcoinjs/bip39
// Use default english world list similiar to bitcoinjs/bip39
// Backward compatible with bitcoinjs/bip39 dependency
// Very small in size as compared to bitcoinjs/bip39 wordlist
// Reference: https://github.com/paulmillr/scure-bip39
import { wordlist } from '@scure/bip39/wordlists/english';

// https://github.com/paulmillr/scure-bip32
// Secure, audited & minimal implementation of BIP32 hierarchical deterministic (HD) wallets.
import { HDKey } from '@scure/bip32';
import { randomBytes } from '@stacks/encryption';
import { Wallet, getRootNode } from './models/common';
import { encrypt } from './encryption';
import { deriveAccount, deriveWalletKeys } from './derive';
Expand All @@ -11,7 +19,7 @@ import { DerivationType } from '.';
export type AllowedKeyEntropyBits = 128 | 256;

export const generateSecretKey = (entropy: AllowedKeyEntropyBits = 256) => {
const secretKey = generateMnemonic(entropy, randomBytes);
const secretKey = generateMnemonic(wordlist, entropy);
return secretKey;
};

Expand Down
4 changes: 3 additions & 1 deletion packages/wallet-sdk/tests/derive-keychain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
selectStxDerivation,
fetchUsernameForAccountByDerivationType,
} from '../src';
import { mnemonicToSeed } from 'bip39';
// https://github.com/paulmillr/scure-bip39
// Secure, audited & minimal implementation of BIP39 mnemonic phrases.
import { mnemonicToSeed } from '@scure/bip39';
import { BIP32Interface, fromBase58 } from 'bip32';
import { HDKey } from '@scure/bip32';
import { TransactionVersion, bytesToHex } from '@stacks/transactions';
Expand Down
32 changes: 17 additions & 15 deletions packages/wallet-sdk/tests/derive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
selectStxDerivation,
fetchUsernameForAccountByDerivationType,
} from '../src';
import { mnemonicToSeed } from 'bip39';
// https://github.com/paulmillr/scure-bip39
// Secure, audited & minimal implementation of BIP39 mnemonic phrases.
import { mnemonicToSeed } from '@scure/bip39';
import { fromBase58, fromSeed } from 'bip32';
import { TransactionVersion } from '@stacks/transactions';
import { StacksMainnet } from '@stacks/network';
Expand All @@ -22,7 +24,7 @@ const DATA_ADDRESS = 'SP30RZ44NTH2D95M1HSWVMM8VVHSAFY71VF3XQZ0K';

test('keys are serialized, and can be deserialized properly using wallet private key for stx', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode1 = fromSeed(rootPrivateKey);
const rootNode1 = fromSeed(Buffer.from(rootPrivateKey));
const derived = await deriveWalletKeys(rootNode1);
const rootNode = fromBase58(derived.rootKey);
const account = deriveAccount({
Expand All @@ -38,7 +40,7 @@ test('keys are serialized, and can be deserialized properly using wallet private

test('keys are serialized, and can be deserialized properly using data private key for stx', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode1 = fromSeed(rootPrivateKey);
const rootNode1 = fromSeed(Buffer.from(rootPrivateKey));
const derived = await deriveWalletKeys(rootNode1);
const rootNode = fromBase58(derived.rootKey);
const account = deriveAccount({
Expand All @@ -54,14 +56,14 @@ test('keys are serialized, and can be deserialized properly using data private k

test('backwards compatible legacy config private key derivation', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode = fromSeed(rootPrivateKey);
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
const legacyKey = deriveLegacyConfigPrivateKey(rootNode);
expect(legacyKey).toEqual('767b51d866d068b02ce126afe3737896f4d0c486263d9b932f2822109565a3c6');
});

test('derive derivation path without username', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode = fromSeed(rootPrivateKey);
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
const network = new StacksMainnet();
const { username, stxDerivationType } = await selectStxDerivation({
username: undefined,
Expand All @@ -75,7 +77,7 @@ test('derive derivation path without username', async () => {

test('derive derivation path with username owned by address of stx derivation path', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode = fromSeed(rootPrivateKey);
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
const network = new StacksMainnet();

fetchMock.once(JSON.stringify({ address: DATA_ADDRESS }));
Expand All @@ -92,7 +94,7 @@ test('derive derivation path with username owned by address of stx derivation pa

test('derive derivation path with username owned by address of unknown derivation path', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode = fromSeed(rootPrivateKey);
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
const network = new StacksMainnet();

fetchMock.once(JSON.stringify({ address: 'SP000000000000000000002Q6VF78' }));
Expand All @@ -109,7 +111,7 @@ test('derive derivation path with username owned by address of unknown derivatio

test('derive derivation path with username owned by address of data derivation path', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode = fromSeed(rootPrivateKey);
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
const network = new StacksMainnet();

fetchMock.once(JSON.stringify({ address: 'SP30RZ44NTH2D95M1HSWVMM8VVHSAFY71VF3XQZ0K' }));
Expand All @@ -126,7 +128,7 @@ test('derive derivation path with username owned by address of data derivation p

test('derive derivation path with new username owned by address of stx derivation path', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode = fromSeed(rootPrivateKey);
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
const network = new StacksMainnet();

fetchMock.once(JSON.stringify({ names: ['public_profile_for_testing.id.blockstack'] }));
Expand All @@ -146,7 +148,7 @@ test('derive derivation path with new username owned by address of stx derivatio

test('derive derivation path with new username owned by address of data derivation path', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode = fromSeed(rootPrivateKey);
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
const network = new StacksMainnet();

fetchMock
Expand All @@ -171,7 +173,7 @@ test('derive derivation path with new username owned by address of data derivati

test('derive derivation path with username and without network', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode = fromSeed(rootPrivateKey);
const rootNode = fromSeed(Buffer.from(rootPrivateKey));

const { username, stxDerivationType } = await selectStxDerivation({
username: 'public_profile_for_testing.id.blockstack',
Expand All @@ -184,7 +186,7 @@ test('derive derivation path with username and without network', async () => {

test('derive derivation path without username and without network', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode = fromSeed(rootPrivateKey);
const rootNode = fromSeed(Buffer.from(rootPrivateKey));

const { username, stxDerivationType } = await selectStxDerivation({
username: undefined,
Expand All @@ -197,7 +199,7 @@ test('derive derivation path without username and without network', async () =>

test('fetch username owned by derivation type', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode = fromSeed(rootPrivateKey);
const rootNode = fromSeed(Buffer.from(rootPrivateKey));

fetchMock.once(JSON.stringify({ names: ['public_profile_for_testing.id.blockstack'] }));

Expand All @@ -212,7 +214,7 @@ test('fetch username owned by derivation type', async () => {

test('fetch username owned by different derivation type', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode = fromSeed(rootPrivateKey);
const rootNode = fromSeed(Buffer.from(rootPrivateKey));

fetchMock.once(JSON.stringify({ names: [] }));

Expand All @@ -227,7 +229,7 @@ test('fetch username owned by different derivation type', async () => {

test('fetch username defaults to mainnet', async () => {
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
const rootNode = fromSeed(rootPrivateKey);
const rootNode = fromSeed(Buffer.from(rootPrivateKey));

fetchMock.once(JSON.stringify({ names: ['public_profile_for_testing.id.blockstack'] }));

Expand Down
75 changes: 72 additions & 3 deletions packages/wallet-sdk/tests/generate.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { validateMnemonic } from 'bip39';
// https://github.com/paulmillr/scure-bip39
// Secure, audited & minimal implementation of BIP39 mnemonic phrases.
import {entropyToMnemonic, mnemonicToSeed, mnemonicToEntropy, validateMnemonic} from '@scure/bip39';
// Word lists not imported by default as that would increase bundle sizes too much as in case of bitcoinjs/bip39
// Use default english world list similiar to bitcoinjs/bip39
// Backward compatible with bitcoinjs/bip39 dependency
// Very small in size as compared to bitcoinjs/bip39 wordlist
// Reference: https://github.com/paulmillr/scure-bip39
import { wordlist } from '@scure/bip39/wordlists/english';
import {
generateSecretKey,
generateWallet,
Expand All @@ -21,8 +29,8 @@ describe(generateSecretKey, () => {
});

test('generates a valid mnemonic', () => {
expect(validateMnemonic(generateSecretKey())).toBeTruthy();
expect(validateMnemonic(generateSecretKey(128))).toBeTruthy();
expect(validateMnemonic(generateSecretKey(), wordlist)).toBeTruthy();
expect(validateMnemonic(generateSecretKey(128), wordlist)).toBeTruthy();
});
});

Expand Down Expand Up @@ -70,3 +78,64 @@ describe(generateWallet, () => {
);
});
});

describe('Compatibility verification @scure/bip39 vs bitcoinjs/bip39', () => {
test('Verify compatibility @scure/bip39 <=> bitcoinjs/bip39', () => {
// Consider an entropy
const entropy = '00000000000000000000000000000000';
// Consider same entropy in array format
const entropyUint8Array = new Uint8Array(entropy.split('').map(Number));

// Use vectors to verify result with bitcoinjs/bip39 instead of importing bitcoinjs/bip39
const bitcoinjsBip39 = { // Consider it equivalent to bitcoinjs/bip39 (offloaded now)
// Using this map of required functions from bitcoinjs/bip39 and mocking the output for considered entropy
entropyToMnemonicBip39: (_: string) => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
validateMnemonicBip39: (_: string) => true,
mnemonicToEntropyBip39: (_: string) => '00000000000000000000000000000000',
};

// entropyToMnemonicBip39 imported from bitcoinjs/bip39
const bip39Mnemonic = bitcoinjsBip39.entropyToMnemonicBip39(entropy);
// entropyToMnemonic imported from @scure/bip39
const mnemonic = entropyToMnemonic(entropyUint8Array, wordlist);

//Phase 1: Cross verify mnemonic validity: @scure/bip39 <=> bitcoinjs/bip39

// validateMnemonic imported from @scure/bip39
expect(validateMnemonic(bip39Mnemonic, wordlist)).toEqual(true);
// validateMnemonicBip39 imported from bitcoinjs/bip39
expect(bitcoinjsBip39.validateMnemonicBip39(mnemonic)).toEqual(true);

// validateMnemonic imported from @scure/bip39
expect(validateMnemonic(mnemonic, wordlist)).toEqual(true);
// validateMnemonicBip39 imported from bitcoinjs/bip39
expect(bitcoinjsBip39.validateMnemonicBip39(bip39Mnemonic)).toEqual(true);

//Phase 2: Get back entropy from mnemonic and verify @scure/bip39 <=> bitcoinjs/bip39

// mnemonicToEntropy imported from @scure/bip39
expect(mnemonicToEntropy(mnemonic, wordlist)).toEqual(entropyUint8Array);
// mnemonicToEntropyBip39 imported from bitcoinjs/bip39
expect(bitcoinjsBip39.mnemonicToEntropyBip39(bip39Mnemonic)).toEqual(entropy);
// mnemonicToEntropy imported from @scure/bip39
expect(Buffer.from(mnemonicToEntropy(bip39Mnemonic, wordlist)).toString('hex')).toEqual(entropy);
// mnemonicToEntropyBip39 imported from bitcoinjs/bip39
const entropyString = bitcoinjsBip39.mnemonicToEntropyBip39(mnemonic);
// Convert entropy to bytes
const entropyInBytes = new Uint8Array(entropyString.split('').map(Number))
// entropy should match with entropyUint8Array
expect(entropyInBytes).toEqual(entropyUint8Array);
});

test('Seed verification @scure/bip39 <=> bitcoinjs/bip39', async () => {
// Consider an entropy as actually generated by calling generateMnemonic(wordlist)
const mnemonic = 'limb basket cactus metal come display chicken brief execute version attract journey';
const seed = await mnemonicToSeed(mnemonic);
// Use vectors to verify result with bitcoinjs/bip39 instead of importing bitcoinjs/bip39
const bitcoinjsBip39 = { // Consider it equivalent to bitcoinjs/bip39 (offloaded now)
// Using this map of required functions from bitcoinjs/bip39 and mocking the output for considered entropy
mnemonicToSeedBip39: (_: string) => '8f157914f06a56abf3a188c9a96faa74100e34d30aff7a6bafe8af33d5c398ef703759e30654f536a2241dc88a5fd3d963b743153b450c91dcfc0ab9f3d90256'
};
expect(Buffer.from(seed).toString('hex')).toEqual(bitcoinjsBip39.mnemonicToSeedBip39(mnemonic));
});
});

1 comment on commit 701416a

@vercel
Copy link

@vercel vercel bot commented on 701416a May 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.