Skip to content

Commit

Permalink
fix: offload bip32 dependency from wallet-sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
ahsan-javaid authored and janniks committed Apr 11, 2022
1 parent bd8c847 commit c729006
Show file tree
Hide file tree
Showing 7 changed files with 436 additions and 42 deletions.
3 changes: 3 additions & 0 deletions packages/transactions/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import fetch from 'cross-fetch';
import { c32addressDecode } from 'c32check';
import lodashCloneDeep from 'lodash.clonedeep';
import { with0x } from '@stacks/common';
import { bytesToHex } from '@noble/hashes/utils';

/**
* Use utils.randomBytes to replace randombytes dependency
Expand All @@ -17,6 +18,8 @@ import { with0x } from '@stacks/common';
*/
export const randomBytes = (bytesLength?: number) => Buffer.from(utils.randomBytes(bytesLength));

export { bytesToHex };

export class BufferArray {
_value: Buffer[] = [];
get value() {
Expand Down
3 changes: 2 additions & 1 deletion packages/wallet-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@types/jest": "^26.0.22",
"@types/node": "^14.14.43",
"assert": "^2.0.0",
"bip32": "^2.0.6",
"crypto-browserify": "^3.12.0",
"jest": "^26.6.3",
"jest-fetch-mock": "^3.0.3",
Expand All @@ -51,7 +52,7 @@
"@stacks/profile": "^3.3.0",
"@stacks/storage": "^3.3.0",
"@stacks/transactions": "^3.3.0",
"bip32": "^2.0.6",
"@scure/bip32": "^1.0.1",
"bip39": "^3.0.2",
"bitcoinjs-lib": "^5.2.0",
"bn.js": "^5.2.0",
Expand Down
128 changes: 97 additions & 31 deletions packages/wallet-sdk/src/derive.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { BIP32Interface } from 'bip32';
// https://github.com/paulmillr/scure-bip32
// Secure, audited & minimal implementation of BIP32 hierarchical deterministic (HD) wallets.
import { HDKey } from '@scure/bip32';
import { Buffer, ChainID, TransactionVersion } from '@stacks/common';
import { HARDENED_OFFSET } from './models/common';
import { ECPair } from 'bitcoinjs-lib';
import { createSha2Hash, ecPairToHexString } from '@stacks/encryption';

import { assertIsTruthy, whenChainId } from './utils';
import { Account, WalletKeys } from './models/common';
import { Account, WalletKeys, BIP32Interface } from './models/common';
import { StacksMainnet, StacksNetwork } from '@stacks/network';
import { getAddressFromPrivateKey } from '@stacks/transactions';
import { getAddressFromPrivateKey, bytesToHex } from '@stacks/transactions';
import { fetchFirstName } from './usernames';

const DATA_DERIVATION_PATH = `m/888'/0'`;
const WALLET_CONFIG_PATH = `m/44/5757'/0'/1`;
const STX_DERIVATION_PATH = `m/44'/5757'/0'/0`;

export const deriveWalletKeys = async (rootNode: BIP32Interface): Promise<WalletKeys> => {
export const deriveWalletKeys = async (rootNode: HDKey | BIP32Interface): Promise<WalletKeys> => {
// Keep BIP32Interface for backward compatibility with bip32
assertIsTruthy(rootNode.privateKey);
const derived: WalletKeys = {
salt: await deriveSalt(rootNode),
rootKey: rootNode.toBase58(),
rootKey: rootNode instanceof HDKey ? rootNode.privateExtendedKey : rootNode.toBase58(), // Backward compatibility with bip32
configPrivateKey: deriveConfigPrivateKey(rootNode).toString('hex'),
};
return derived;
Expand All @@ -34,11 +38,21 @@ export const deriveWalletKeys = async (rootNode: BIP32Interface): Promise<Wallet
*
* @param rootNode A keychain that was created using the wallet's seed phrase
*/
export const deriveConfigPrivateKey = (rootNode: BIP32Interface): Buffer => {
const derivedConfigKey = rootNode.derivePath(WALLET_CONFIG_PATH).privateKey;
export const deriveConfigPrivateKey = (rootNode: HDKey | BIP32Interface): Buffer => {
// Keep BIP32Interface for backward compatibility with bip32
let derivedConfigKey;
if (rootNode instanceof HDKey) {
derivedConfigKey = rootNode.derive(WALLET_CONFIG_PATH).privateKey;
} else {
// Backward compatibility with bip32
derivedConfigKey = rootNode.derivePath(WALLET_CONFIG_PATH).privateKey;
}
if (!derivedConfigKey) {
throw new TypeError('Unable to derive config key for wallet identities');
}
if (derivedConfigKey instanceof Uint8Array) {
derivedConfigKey = Buffer.from(derivedConfigKey);
}
return derivedConfigKey;
};

Expand All @@ -49,22 +63,42 @@ export const deriveConfigPrivateKey = (rootNode: BIP32Interface): Buffer => {
* The path for this key is `m/45'`
* @param rootNode A keychain that was created using the wallet's seed phrase
*/
export const deriveLegacyConfigPrivateKey = (rootNode: BIP32Interface): string => {
const derivedLegacyKey = rootNode.deriveHardened(45).privateKey;
export const deriveLegacyConfigPrivateKey = (rootNode: HDKey | BIP32Interface): string => {
// Keep BIP32Interface for backward compatibility with bip32
let derivedLegacyKey;
if (rootNode instanceof HDKey) {
derivedLegacyKey = rootNode.deriveChild(45 + HARDENED_OFFSET).privateKey;
} else {
// Backward compatibility with bip32
derivedLegacyKey = rootNode.deriveHardened(45).privateKey;
}
if (!derivedLegacyKey) {
throw new TypeError('Unable to derive config key for wallet identities');
}
const configPrivateKey = derivedLegacyKey.toString('hex');
return configPrivateKey;
if (derivedLegacyKey instanceof Buffer) {
return derivedLegacyKey.toString('hex');
} else {
return bytesToHex(derivedLegacyKey);
}
};

/**
* Generate a salt, which is used for generating an app-specific private key
* @param rootNode
*/
export const deriveSalt = async (rootNode: BIP32Interface) => {
const identitiesKeychain = rootNode.derivePath(DATA_DERIVATION_PATH);
const publicKeyHex = Buffer.from(identitiesKeychain.publicKey.toString('hex'));
export const deriveSalt = async (rootNode: HDKey | BIP32Interface) => {
// Keep BIP32Interface for backward compatibility with bip32
let identitiesKeychain;
let publicKeyHex;

if (rootNode instanceof HDKey) {
identitiesKeychain = rootNode.derive(DATA_DERIVATION_PATH);
publicKeyHex = Buffer.from(bytesToHex(identitiesKeychain.publicKey as Uint8Array));
} else {
// Backward compatibility with bip32
identitiesKeychain = rootNode.derivePath(DATA_DERIVATION_PATH);
publicKeyHex = Buffer.from(identitiesKeychain.publicKey.toString('hex'));
}

const sha2Hash = await createSha2Hash();
const saltData = await sha2Hash.digest(publicKeyHex, 'sha256');
Expand Down Expand Up @@ -111,7 +145,7 @@ export const selectStxDerivation = async ({
network,
}: {
username?: string;
rootNode: BIP32Interface;
rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32
index: number;
network?: StacksNetwork;
}): Promise<{ username: string | undefined; stxDerivationType: DerivationType }> => {
Expand Down Expand Up @@ -141,7 +175,7 @@ const selectDerivationTypeForUsername = async ({
network,
}: {
username: string;
rootNode: BIP32Interface;
rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32
index: number;
network?: StacksNetwork;
}): Promise<DerivationType> => {
Expand Down Expand Up @@ -175,7 +209,7 @@ const selectUsernameForAccount = async ({
index,
network,
}: {
rootNode: BIP32Interface;
rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32
index: number;
network?: StacksNetwork;
}): Promise<{ username: string | undefined; derivationType: DerivationType }> => {
Expand Down Expand Up @@ -211,7 +245,7 @@ export const fetchUsernameForAccountByDerivationType = async ({
derivationType,
network,
}: {
rootNode: BIP32Interface;
rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32
index: number;
derivationType: DerivationType.Wallet | DerivationType.Data;
network?: StacksNetwork;
Expand All @@ -235,7 +269,7 @@ export const derivePrivateKeyByType = ({
index,
derivationType,
}: {
rootNode: BIP32Interface;
rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32
index: number;
derivationType: DerivationType;
}): string => {
Expand All @@ -248,25 +282,45 @@ export const deriveStxPrivateKey = ({
rootNode,
index,
}: {
rootNode: BIP32Interface;
rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32
index: number;
}) => {
const childKey = rootNode.derivePath(STX_DERIVATION_PATH).derive(index);
let childKey;
if (rootNode instanceof HDKey) {
childKey = rootNode.derive(STX_DERIVATION_PATH).deriveChild(index);
} else {
// Backward compatibility with bip32
childKey = rootNode.derivePath(STX_DERIVATION_PATH).derive(index);
}
assertIsTruthy(childKey.privateKey);
const ecPair = ECPair.fromPrivateKey(childKey.privateKey);
const privateKey =
childKey.privateKey instanceof Uint8Array
? Buffer.from(childKey.privateKey)
: childKey.privateKey;
const ecPair = ECPair.fromPrivateKey(privateKey);
return ecPairToHexString(ecPair);
};

export const deriveDataPrivateKey = ({
rootNode,
index,
}: {
rootNode: BIP32Interface;
rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32
index: number;
}) => {
const childKey = rootNode.derivePath(DATA_DERIVATION_PATH).deriveHardened(index);
let childKey;
if (rootNode instanceof HDKey) {
childKey = rootNode.derive(DATA_DERIVATION_PATH).deriveChild(index + HARDENED_OFFSET);
} else {
// Backward compatibility with bip32
childKey = rootNode.derivePath(DATA_DERIVATION_PATH).deriveHardened(index);
}
assertIsTruthy(childKey.privateKey);
const ecPair = ECPair.fromPrivateKey(childKey.privateKey);
const privateKey =
childKey.privateKey instanceof Uint8Array
? Buffer.from(childKey.privateKey)
: childKey.privateKey;
const ecPair = ECPair.fromPrivateKey(privateKey);
return ecPairToHexString(ecPair);
};

Expand All @@ -276,7 +330,7 @@ export const deriveAccount = ({
salt,
stxDerivationType,
}: {
rootNode: BIP32Interface;
rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32
index: number;
salt: string;
stxDerivationType: DerivationType.Wallet | DerivationType.Data;
Expand All @@ -285,13 +339,25 @@ export const deriveAccount = ({
stxDerivationType === DerivationType.Wallet
? deriveStxPrivateKey({ rootNode, index })
: deriveDataPrivateKey({ rootNode, index });
const identitiesKeychain = rootNode.derivePath(DATA_DERIVATION_PATH);
let dataPrivateKey;
let appsKey;
if (rootNode instanceof HDKey) {
const identitiesKeychain = rootNode.derive(DATA_DERIVATION_PATH);
const identityKeychain = identitiesKeychain.deriveChild(index + HARDENED_OFFSET);
if (!identityKeychain.privateKey) throw new Error('Must have private key to derive identities');
dataPrivateKey = bytesToHex(identityKeychain.privateKey);

appsKey = identityKeychain.deriveChild(0 + HARDENED_OFFSET).privateExtendedKey;
} else {
// Backward compatibility with bip32
const identitiesKeychain = rootNode.derivePath(DATA_DERIVATION_PATH);

const identityKeychain = identitiesKeychain.deriveHardened(index);
if (!identityKeychain.privateKey) throw new Error('Must have private key to derive identities');
const dataPrivateKey = identityKeychain.privateKey.toString('hex');
const identityKeychain = identitiesKeychain.deriveHardened(index);
if (!identityKeychain.privateKey) throw new Error('Must have private key to derive identities');
dataPrivateKey = identityKeychain.privateKey.toString('hex');

const appsKey = identityKeychain.deriveHardened(0).toBase58();
appsKey = identityKeychain.deriveHardened(0).toBase58();
}

return {
stxPrivateKey,
Expand Down
6 changes: 4 additions & 2 deletions packages/wallet-sdk/src/generate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { generateMnemonic, mnemonicToSeed } from 'bip39';
import { fromSeed } from 'bip32';
// 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';
Expand Down Expand Up @@ -29,7 +31,7 @@ export const generateWallet = async ({
const encryptedSecretKey = ciphertextBuffer.toString('hex');

const rootPrivateKey = await mnemonicToSeed(secretKey);
const rootNode = fromSeed(rootPrivateKey);
const rootNode = HDKey.fromMasterSeed(rootPrivateKey);
const walletKeys = await deriveWalletKeys(rootNode);

const wallet = {
Expand Down
14 changes: 8 additions & 6 deletions packages/wallet-sdk/src/models/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import {
hashSha256Sync,
} from '@stacks/encryption';
import { makeAuthResponse as _makeAuthResponse } from '@stacks/auth';
import { TransactionVersion, getAddressFromPrivateKey } from '@stacks/transactions';
import { fromBase58 } from 'bip32';
import { TransactionVersion, getAddressFromPrivateKey, bytesToHex } from '@stacks/transactions';
// https://github.com/paulmillr/scure-bip32
// Secure, audited & minimal implementation of BIP32 hierarchical deterministic (HD) wallets.
import { HDKey } from '@scure/bip32';
import {
DEFAULT_PROFILE,
fetchAccountProfileUrl,
fetchProfileFromUrl,
signAndUploadProfile,
} from './profile';
import { Account } from './common';
import { Account, HARDENED_OFFSET } from './common';
import { ECPair } from 'bitcoinjs-lib';
import { connectToGaiaHubWithConfig, getHubInfo, makeGaiaAssociationToken } from '../utils';
import { Buffer } from '@stacks/common';
Expand Down Expand Up @@ -54,10 +56,10 @@ export const getAppPrivateKey = ({
const hashBuffer = hashSha256Sync(Buffer.from(`${appDomain}${account.salt}`));
const hash = hashBuffer.toString('hex');
const appIndex = hashCode(hash);
const appsNode = fromBase58(account.appsKey);
const appKeychain = appsNode.deriveHardened(appIndex);
const appsNode = HDKey.fromExtendedKey(account.appsKey);
const appKeychain = appsNode.deriveChild(appIndex + HARDENED_OFFSET);
if (!appKeychain.privateKey) throw 'Needs private key';
return appKeychain.privateKey.toString('hex');
return bytesToHex(appKeychain.privateKey);
};

export const makeAuthResponse = async ({
Expand Down
52 changes: 50 additions & 2 deletions packages/wallet-sdk/src/models/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// @ts-ignore
import { Buffer } from '@stacks/common';
import { getPublicKeyFromPrivate, publicKeyToAddress } from '@stacks/encryption';
import { fromBase58 } from 'bip32';
// https://github.com/paulmillr/scure-bip32
// Secure, audited & minimal implementation of BIP32 hierarchical deterministic (HD) wallets.
import { HDKey } from '@scure/bip32';

export interface Account {
/** The private key used for STX payments */
Expand All @@ -18,6 +22,50 @@ export interface Account {
index: number;
}

// Reference: https://github.com/bitcoinjs/bip32/blob/79c6dedb3edfdc8505fe74d9f34c115c33e8a2da/ts-src/bip32.ts#L102
// Used to replicate deriveHardened bip32 method using deriveChild of scure-bip32 to offload old bip32 library
export const HARDENED_OFFSET = 0x80000000;

// Reference: https://github.com/bitcoinjs/bip32/blob/79c6dedb3edfdc8505fe74d9f34c115c33e8a2da/ts-src/bip32.ts#L7-L17
// Used inside BIP32Interface for backward compatibility with offloaded bip32 dependency
interface Network {
wif: number;
bip32: {
public: number;
private: number;
};
messagePrefix?: string;
bech32?: string;
pubKeyHash?: number;
scriptHash?: number;
}

// Reference: https://github.com/bitcoinjs/bip32/blob/79c6dedb3edfdc8505fe74d9f34c115c33e8a2da/ts-src/bip32.ts#L19-L41
// Using BIP32Interface for backward compatibility with offloaded bip32 dependency
export interface BIP32Interface {
chainCode: Buffer;
network: Network;
lowR: boolean;
depth: number;
index: number;
parentFingerprint: number;
publicKey: Buffer;
privateKey?: Buffer;
identifier: Buffer;
fingerprint: Buffer;
isNeutered(): boolean;
neutered(): BIP32Interface;
toBase58(): string;
toWIF(): string;
derive(index: number): BIP32Interface;
deriveHardened(index: number): BIP32Interface;
derivePath(path: string): BIP32Interface;
sign(hash: Buffer, lowR?: boolean): Buffer;
verify(hash: Buffer, signature: Buffer): boolean;
signSchnorr?(hash: Buffer): Buffer;
verifySchnorr?(hash: Buffer, signature: Buffer): boolean;
}

const PERSON_TYPE = 'Person';
const CONTEXT = 'http://schema.org';
const IMAGE_TYPE = 'ImageObject';
Expand Down Expand Up @@ -72,5 +120,5 @@ export const getGaiaAddress = (account: Account) => {
};

export const getRootNode = (wallet: Wallet) => {
return fromBase58(wallet.rootKey);
return HDKey.fromExtendedKey(wallet.rootKey);
};

1 comment on commit c729006

@vercel
Copy link

@vercel vercel bot commented on c729006 Apr 11, 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.