Skip to content

Commit 7faaab4

Browse files
committed
feat: add support for injecting crypto functions for Bip32Account
our libsodium-based key derivation performs poorly when running in mobile BREAKING CHANGE: add a new 'dependencies' parameter in Bip32Account ctor
1 parent 35de279 commit 7faaab4

File tree

19 files changed

+124
-71
lines changed

19 files changed

+124
-71
lines changed

packages/crypto/src/Bip32/Bip32PublicKey.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import * as Bip32KeyDerivation from './Bip32KeyDerivation';
2-
import { BIP32_PUBLIC_KEY_HASH_LENGTH, Bip32PublicKeyHashHex, Bip32PublicKeyHex } from '../hexTypes';
3-
import { ED25519_PUBLIC_KEY_LENGTH, Ed25519PublicKey } from '../Ed25519e';
2+
import {
3+
BIP32_PUBLIC_KEY_HASH_LENGTH,
4+
Bip32PublicKeyHashHex,
5+
Bip32PublicKeyHex,
6+
ED25519_PUBLIC_KEY_LENGTH
7+
} from '../hexTypes';
8+
import { Ed25519PublicKey } from '../Ed25519e';
49
import { InvalidArgumentError } from '@cardano-sdk/util';
510
import sodium from 'libsodium-wrappers-sumo';
611

packages/crypto/src/Ed25519e/Ed25519PublicKey.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { ED25519_PUBLIC_KEY_HASH_LENGTH, Ed25519KeyHash } from './Ed25519KeyHash';
2-
import { Ed25519PublicKeyHex } from '../hexTypes';
2+
import { ED25519_PUBLIC_KEY_LENGTH, Ed25519PublicKeyHex } from '../hexTypes';
33
import { Ed25519Signature } from './Ed25519Signature';
44
import { HexBlob, InvalidArgumentError } from '@cardano-sdk/util';
55
import sodium from 'libsodium-wrappers-sumo';
66

7-
export const ED25519_PUBLIC_KEY_LENGTH = 32;
8-
97
/**
108
* Ed25519 raw public key. This key can be used for cryptographically verifying messages
119
* previously signed with the matching Ed25519 raw private key.

packages/crypto/src/hexTypes.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { HexBlob, OpaqueString, castHexBlob, typedHex } from '@cardano-sdk/util';
22

33
export const BIP32_PUBLIC_KEY_HASH_LENGTH = 28;
4+
export const ED25519_PUBLIC_KEY_LENGTH = 32;
45

56
/** 28 byte hash as hex string */
67
export type Hash28ByteBase16 = OpaqueString<'Hash28ByteBase16'> & HexBlob;
@@ -33,7 +34,10 @@ export const Bip32PrivateKeyHex = (key: string): Bip32PrivateKeyHex => typedHex(
3334

3435
/** Ed25519 public key as hex string */
3536
export type Ed25519PublicKeyHex = OpaqueString<'Ed25519PublicKeyHex'> & HexBlob;
36-
export const Ed25519PublicKeyHex = (value: string): Ed25519PublicKeyHex => typedHex(value, 64);
37+
export const Ed25519PublicKeyHex = (value: string): Ed25519PublicKeyHex =>
38+
typedHex(value, ED25519_PUBLIC_KEY_LENGTH * HexBlob.CHARS_PER_BYTE);
39+
Ed25519PublicKeyHex.fromBip32PublicKey = (bip32PublicKey: Bip32PublicKeyHex): Ed25519PublicKeyHex =>
40+
bip32PublicKey.slice(0, ED25519_PUBLIC_KEY_LENGTH * HexBlob.CHARS_PER_BYTE) as Ed25519PublicKeyHex;
3741

3842
/** Ed25519 private extended key as hex string */
3943
export type Ed25519PrivateExtendedKeyHex = OpaqueString<'Ed25519PrivateKeyHex'> & HexBlob;
@@ -50,4 +54,4 @@ export const Ed25519KeyHashHex = (value: string): Ed25519KeyHashHex => typedHex(
5054
/** 28 byte BIP32 public key hash as hex string */
5155
export type Bip32PublicKeyHashHex = OpaqueString<'Bip32PublicKeyHashHex'> & HexBlob;
5256
export const Bip32PublicKeyHashHex = (value: string): Bip32PublicKeyHashHex =>
53-
typedHex(value, BIP32_PUBLIC_KEY_HASH_LENGTH * 2);
57+
typedHex(value, BIP32_PUBLIC_KEY_HASH_LENGTH * HexBlob.CHARS_PER_BYTE);

packages/e2e/src/factories.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { Logger } from 'ts-log';
6262
import { NoCache, NodeTxSubmitProvider } from '@cardano-sdk/cardano-services';
6363
import { OgmiosObservableCardanoNode } from '@cardano-sdk/ogmios';
6464
import { TrezorKeyAgent } from '@cardano-sdk/hardware-trezor';
65+
import { blake2b } from '@cardano-sdk/crypto';
6566
import { createStubHandleProvider, createStubStakePoolProvider } from '@cardano-sdk/util-dev';
6667
import { filter, firstValueFrom, of } from 'rxjs';
6768
import { getEnv, walletVariables } from './environment';
@@ -619,7 +620,7 @@ export const getWallet = async (props: GetWalletProps) => {
619620
bip32Ed25519,
620621
logger
621622
}));
622-
const bip32Account = await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent);
623+
const bip32Account = await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent, { bip32Ed25519, blake2b });
623624
if (!polling?.interval && env.NETWORK_SPEED === 'fast') {
624625
polling = { ...polling, interval: 50 };
625626
}

packages/e2e/test/load-test-custom/wallet-init/wallet-init.test.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Logger } from 'ts-log';
1010
import { bufferCount, bufferTime, from, mergeAll, tap } from 'rxjs';
1111
import { logger } from '@cardano-sdk/util-dev';
1212

13-
import { Bip32Account, util } from '@cardano-sdk/key-management';
13+
import { Bip32Account, KeyAgentDependencies, util } from '@cardano-sdk/key-management';
1414
import {
1515
MeasurementUtil,
1616
assetProviderFactory,
@@ -29,6 +29,7 @@ import {
2929
waitForWalletStateSettle,
3030
walletVariables
3131
} from '../../../src';
32+
import { blake2b } from '@cardano-sdk/crypto';
3233

3334
// Example call that creates 5000 wallets in 10 minutes:
3435
// VIRTUAL_USERS_GENERATE_DURATION=600 VIRTUAL_USERS_COUNT=5000 yarn load-test-custom:wallet-init
@@ -94,37 +95,39 @@ const getProviders = async () => ({
9495
)
9596
});
9697

97-
const getKeyAgent = async (accountIndex: number) => {
98+
const getKeyAgent = async (accountIndex: number, dependencies: KeyAgentDependencies) => {
9899
const createKeyAgent = await keyManagementFactory.create(
99100
env.KEY_MANAGEMENT_PROVIDER,
100101
{ ...env.KEY_MANAGEMENT_PARAMS, accountIndex },
101102
logger
102103
);
103-
const bip32Ed25519 = await bip32Ed25519Factory.create(env.KEY_MANAGEMENT_PARAMS.bip32Ed25519, null, logger);
104-
const keyAgent = await createKeyAgent({ bip32Ed25519, logger });
104+
105+
const keyAgent = await createKeyAgent(dependencies);
105106
return { keyAgent };
106107
};
107108

108-
const createWallet = async (accountIndex: number): Promise<BaseWallet> => {
109+
const createWallet = async (accountIndex: number, dependencies: KeyAgentDependencies): Promise<BaseWallet> => {
109110
measurementUtil.addStartMarker(MeasureTarget.keyAgent, accountIndex);
110111
const providers = await getProviders();
111-
const { keyAgent } = await getKeyAgent(accountIndex);
112+
const { keyAgent } = await getKeyAgent(accountIndex, dependencies);
112113
measurementUtil.addMeasureMarker(MeasureTarget.keyAgent, accountIndex);
113114

114115
measurementUtil.addStartMarker(MeasureTarget.wallet, accountIndex);
115116
return createPersonalWallet(
116117
{ name: `Wallet ${accountIndex}` },
117118
{
118119
...providers,
119-
bip32Account: await Bip32Account.fromAsyncKeyAgent(keyAgent),
120+
bip32Account: await Bip32Account.fromAsyncKeyAgent(keyAgent, { ...dependencies, blake2b }),
120121
logger,
121122
witnesser: util.createBip32Ed25519Witnesser(keyAgent)
122123
}
123124
);
124125
};
125126

126127
const initWallet = async (idx: number) => {
127-
const wallet = await createWallet(idx);
128+
const bip32Ed25519 = await bip32Ed25519Factory.create(env.KEY_MANAGEMENT_PARAMS.bip32Ed25519, null, logger);
129+
const dependencies = { bip32Ed25519, logger };
130+
const wallet = await createWallet(idx, dependencies);
128131
await waitForWalletStateSettle(wallet);
129132
measurementUtil.addMeasureMarker(MeasureTarget.wallet, idx);
130133
return wallet;

packages/e2e/test/local-network/register-pool.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ describe('local-network/register-pool', () => {
8787
role: KeyRole.External
8888
});
8989

90-
const poolKeyHash = await bip32Ed25519.getPubKeyHash(poolPubKey.hex());
90+
const poolKeyHash = bip32Ed25519.getPubKeyHash(poolPubKey);
9191
const poolId = Cardano.PoolId.fromKeyHash(poolKeyHash);
9292
const poolRewardAccount = (
9393
await wallet1.bip32Account.deriveAddress(
@@ -174,7 +174,7 @@ describe('local-network/register-pool', () => {
174174
role: KeyRole.External
175175
});
176176

177-
const poolKeyHash = await bip32Ed25519.getPubKeyHash(poolPubKey.hex());
177+
const poolKeyHash = bip32Ed25519.getPubKeyHash(poolPubKey);
178178
const poolId = Cardano.PoolId.fromKeyHash(poolKeyHash);
179179
const poolRewardAccount = (
180180
await wallet2.bip32Account.deriveAddress(

packages/e2e/test/long-running/cache-invalidation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ describe('cache invalidation', () => {
112112
role: KeyRole.External
113113
});
114114

115-
const poolKeyHash = await bip32Ed25519.getPubKeyHash(poolPubKey.hex());
115+
const poolKeyHash = bip32Ed25519.getPubKeyHash(poolPubKey);
116116
const poolId = Cardano.PoolId.fromKeyHash(poolKeyHash);
117117
const poolRewardAccount = (
118118
await wallet1.bip32Account.deriveAddress(

packages/e2e/test/wallet_epoch_0/PersonalWallet/cip30WalletApi.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ describe('PersonalWallet/cip30WalletApi', () => {
6969

7070
it('can signData with bech32 base address', async () => {
7171
const [{ address, index }] = await firstValueFrom(wallet.addresses$);
72-
const paymentKeyHex = (await bip32Account.derivePublicKey({ index, role: KeyRole.External })).hex();
72+
const paymentKeyHex = bip32Account.derivePublicKey({ index, role: KeyRole.External });
7373

7474
const signature = await walletApi.signData({ sender: '' } as unknown as SenderContext, address, HexBlob('abc123'));
7575

@@ -79,7 +79,7 @@ describe('PersonalWallet/cip30WalletApi', () => {
7979
it('can signData with hex-encoded base address', async () => {
8080
const [{ address, index }] = await firstValueFrom(wallet.addresses$);
8181
const addressHex = Cardano.Address.fromBech32(address).toBytes();
82-
const paymentKeyHex = (await bip32Account.derivePublicKey({ index, role: KeyRole.External })).hex();
82+
const paymentKeyHex = bip32Account.derivePublicKey({ index, role: KeyRole.External });
8383

8484
const signature = await walletApi.signData(
8585
{ sender: '' } as unknown as SenderContext,
@@ -92,7 +92,7 @@ describe('PersonalWallet/cip30WalletApi', () => {
9292

9393
it('can signData with bech32 base address', async () => {
9494
const [{ rewardAccount, index }] = await firstValueFrom(wallet.addresses$);
95-
const stakeKeyHex = (await bip32Account.derivePublicKey({ index, role: KeyRole.Stake })).hex();
95+
const stakeKeyHex = bip32Account.derivePublicKey({ index, role: KeyRole.Stake });
9696

9797
const signature = await walletApi.signData(
9898
{ sender: '' } as unknown as SenderContext,
@@ -106,7 +106,7 @@ describe('PersonalWallet/cip30WalletApi', () => {
106106
it('can signData with hex-encoded reward account', async () => {
107107
const [{ rewardAccount, index }] = await firstValueFrom(wallet.addresses$);
108108
const rewardAccountHex = Cardano.Address.fromBech32(rewardAccount).toBytes();
109-
const stakeKeyHex = (await bip32Account.derivePublicKey({ index, role: KeyRole.Stake })).hex();
109+
const stakeKeyHex = bip32Account.derivePublicKey({ index, role: KeyRole.Stake });
110110

111111
const signature = await walletApi.signData(
112112
{ sender: '' } as unknown as SenderContext,

packages/key-management/src/Bip32Account.ts

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,53 @@ import {
66
GroupedAddress,
77
KeyRole
88
} from './types';
9+
import {
10+
BIP32_PUBLIC_KEY_HASH_LENGTH,
11+
Bip32Ed25519,
12+
Blake2b,
13+
Ed25519PublicKeyHex,
14+
SodiumBip32Ed25519,
15+
blake2b
16+
} from '@cardano-sdk/crypto';
917
import { Cardano } from '@cardano-sdk/core';
10-
import { Hash28ByteBase16 } from '@cardano-sdk/crypto';
1118

1219
type Bip32AccountProps = {
1320
extendedAccountPublicKey: Crypto.Bip32PublicKeyHex;
1421
accountIndex: number;
1522
chainId: Cardano.ChainId;
1623
};
1724

25+
type Bip32AccountDependencies = {
26+
bip32Ed25519: Pick<Bip32Ed25519, 'derivePublicKey'>;
27+
blake2b: Blake2b;
28+
};
29+
1830
/** Derives public keys and addresses from a BIP32-ED25519 public key */
1931
export class Bip32Account {
20-
readonly extendedAccountPublicKey: Crypto.Bip32PublicKey;
32+
readonly extendedAccountPublicKeyHex: Crypto.Bip32PublicKeyHex;
2133
readonly chainId: Cardano.ChainId;
2234
readonly accountIndex: number;
35+
readonly #bip32Ed25519: Bip32AccountDependencies['bip32Ed25519'];
36+
readonly #blake2b: Blake2b;
2337

2438
/** Initializes a new instance of the Bip32Ed25519AddressManager class. */
25-
constructor({ extendedAccountPublicKey, chainId, accountIndex }: Bip32AccountProps) {
26-
this.extendedAccountPublicKey = Crypto.Bip32PublicKey.fromHex(extendedAccountPublicKey);
39+
constructor(
40+
{ extendedAccountPublicKey, chainId, accountIndex }: Bip32AccountProps,
41+
dependencies: Bip32AccountDependencies
42+
) {
43+
this.extendedAccountPublicKeyHex = extendedAccountPublicKey;
44+
this.#bip32Ed25519 = dependencies.bip32Ed25519;
45+
this.#blake2b = dependencies.blake2b;
2746
this.chainId = chainId;
2847
this.accountIndex = accountIndex;
2948
}
3049

3150
async derivePublicKey(derivationPath: AccountKeyDerivationPath) {
32-
const key = await this.extendedAccountPublicKey.derive([derivationPath.role, derivationPath.index]);
33-
return key.toRawKey();
51+
const extendedKey = this.#bip32Ed25519.derivePublicKey(this.extendedAccountPublicKeyHex, [
52+
derivationPath.role,
53+
derivationPath.index
54+
]);
55+
return Ed25519PublicKeyHex.fromBip32PublicKey(extendedKey);
3456
}
3557

3658
async deriveAddress(
@@ -47,16 +69,16 @@ export class Bip32Account {
4769
role: Number(paymentKeyDerivationPath.type)
4870
});
4971

50-
const derivedPublicPaymentKeyHash = await derivedPublicPaymentKey.hash();
72+
const derivedPublicPaymentKeyHash = this.#blake2b.hash(derivedPublicPaymentKey, BIP32_PUBLIC_KEY_HASH_LENGTH);
5173

5274
const publicStakeKey = await this.derivePublicKey(stakeKeyDerivationPath);
53-
const publicStakeKeyHash = await publicStakeKey.hash();
75+
const publicStakeKeyHash = this.#blake2b.hash(publicStakeKey, BIP32_PUBLIC_KEY_HASH_LENGTH);
5476

55-
const stakeCredential = { hash: Hash28ByteBase16(publicStakeKeyHash.hex()), type: Cardano.CredentialType.KeyHash };
77+
const stakeCredential = { hash: publicStakeKeyHash, type: Cardano.CredentialType.KeyHash };
5678

5779
const address = Cardano.BaseAddress.fromCredentials(
5880
this.chainId.networkId,
59-
{ hash: Hash28ByteBase16(derivedPublicPaymentKeyHash.hex()), type: Cardano.CredentialType.KeyHash },
81+
{ hash: derivedPublicPaymentKeyHash, type: Cardano.CredentialType.KeyHash },
6082
stakeCredential
6183
).toAddress();
6284

@@ -72,16 +94,30 @@ export class Bip32Account {
7294
};
7395
}
7496

97+
static async createDefaultDependencies(): Promise<Bip32AccountDependencies> {
98+
return {
99+
bip32Ed25519: await SodiumBip32Ed25519.create(),
100+
blake2b
101+
};
102+
}
103+
75104
/**
76105
* Creates a new instance of the Bip32Ed25519AddressManager class.
77106
*
78107
* @param keyAgent The key agent that will be used to derive addresses.
79108
*/
80-
static async fromAsyncKeyAgent(keyAgent: AsyncKeyAgent): Promise<Bip32Account> {
81-
return new Bip32Account({
82-
accountIndex: await keyAgent.getAccountIndex(),
83-
chainId: await keyAgent.getChainId(),
84-
extendedAccountPublicKey: await keyAgent.getExtendedAccountPublicKey()
85-
});
109+
static async fromAsyncKeyAgent(
110+
keyAgent: AsyncKeyAgent,
111+
dependencies?: Bip32AccountDependencies
112+
): Promise<Bip32Account> {
113+
dependencies ||= await Bip32Account.createDefaultDependencies();
114+
return new Bip32Account(
115+
{
116+
accountIndex: await keyAgent.getAccountIndex(),
117+
chainId: await keyAgent.getChainId(),
118+
extendedAccountPublicKey: await keyAgent.getExtendedAccountPublicKey()
119+
},
120+
dependencies
121+
);
86122
}
87123
}

packages/key-management/src/KeyAgentBase.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Cardano, Serialization } from '@cardano-sdk/core';
1616
import { Cip30DataSignature } from '@cardano-sdk/dapp-connector';
1717
import { Cip8SignDataContext } from './cip8';
1818
import { HexBlob } from '@cardano-sdk/util';
19+
import { blake2b } from '@cardano-sdk/crypto';
1920

2021
export abstract class KeyAgentBase implements KeyAgent {
2122
readonly #serializableData: SerializableKeyAgentData;
@@ -54,7 +55,7 @@ export abstract class KeyAgentBase implements KeyAgent {
5455
constructor(serializableData: SerializableKeyAgentData, { bip32Ed25519 }: KeyAgentDependencies) {
5556
this.#serializableData = serializableData;
5657
this.#bip32Ed25519 = bip32Ed25519;
57-
this.#account = new Bip32Account(serializableData);
58+
this.#account = new Bip32Account(serializableData, { bip32Ed25519, blake2b });
5859
}
5960

6061
/** See https://github.com/cardano-foundation/CIPs/tree/master/CIP-1852#specification */
@@ -66,6 +67,6 @@ export abstract class KeyAgentBase implements KeyAgent {
6667
}
6768

6869
async derivePublicKey(derivationPath: AccountKeyDerivationPath): Promise<Crypto.Ed25519PublicKeyHex> {
69-
return (await this.#account.derivePublicKey(derivationPath)).hex();
70+
return this.#account.derivePublicKey(derivationPath);
7071
}
7172
}

0 commit comments

Comments
 (0)