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

Feat/HD-Wallet SLIP0010 #1190

Merged
merged 17 commits into from
Nov 21, 2023
Merged
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
5 changes: 5 additions & 0 deletions .changeset/serious-chefs-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kadena/hd-wallet': minor
---

Add functions to support SLIP-0010 key derivation

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# ADR: Use SLIP10 for Private Key Generation

**Date:** 2023-11-20

**Status:** Accepted

## Context

Kadena has already adopted the [BIP39][1] mnemonic standard and We need a
deterministic method for generating private keys from a mnemonic. this process
is commonly referred to as an HD wallet (Hierarchical Deterministic wallet).

HD wallets derive keys from a master key, typically following the [BIP32][2]
algorithm or the twisted version [SLIP10][3] (which offers a more general way
for other curve algorithms) or maybe a custom approach like Chainweaver. BIP32
enables the creation of either a chain of private keys or a chain of public
keys, each serving different purposes. For example, a blockchain using an
account model, like Kadena, primarily employs a chain of private keys, as it
doesn't require a new address for each transaction. Conversely, a blockchain
using the [UTXO][6] model, such as Bitcoin, can benefit from extended public
keys.

BIP32 accepts a derivation path that indicates the level of children keys and
also hardened or non-hardened keys. In the Bitcoin world (and other UTXO coins),
[BIP44][4] offers a 5-level path for the BIP32 algorithm. However, BIP44 is
widely adopted by account models blockchains as well, but since there is no need
for the `change` and `address` levels, they are skipped or considered as 0.

This record exclusively concentrates on private keys, as we currently have no
plans to use extended public keys in Kadena. Also, Kadena employs the ed25519
algorithm for keys, and the solution should be compatible with bip32-ed25519.

## Decision

We have chosen to implement the twisted BIP44 protocol for only 3 levels of
child keys, defining the path restriction as `m/44'/626'/${index}'`. This
decision is based on the following considerations:

- KDA (Kadena) coin-type is [626][5].
- KDA follows an account-model coin approach, and for each key, we modify the
**account** (the third level in BIP44).
- We only use this for **private key** generation.
- Extended public keys are beyond the scope of this decision.
- All private keys are **hardened** in accordance with the [ed25519][3]
algorithm.

## Consequences

- We adopt a common approach for key derivation, promoting compatibility with
other blockchains and wallets.
- Finding libraries for implementation will be more straightforward.
- We will need to manage legacy algorithms, such as Chainweaver, for backward
compatibility.
- Some wallets already use `m/44'/626'/0'/0'/${index}'` instead of the adopted
path. Therefore, we should allow users to specify a custom path as well.

## Resources

- [BIP39 Proposal][1]
- [BIP32 Proposal][2]
- [SLIP-0010: Universal Private Key Derivation from Master Private Key][3]
- [BIP44 Proposal][4]
- [SLIP-0044: Registered Coin Types for BIP-0044][5]
- [Unspent transaction output][6]

[1]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
[2]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
[6]: https://en.wikipedia.org/wiki/Unspent_transaction_output
[4]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
[5]: https://github.com/satoshilabs/slips/blob/master/slip-0044.md
[3]: https://github.com/satoshilabs/slips/blob/master/slip-0010.md
5 changes: 4 additions & 1 deletion packages/libs/hd-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@
"test": "vitest"
},
"dependencies": {
"@kadena/client": "workspace:*",
"@kadena/cryptography-utils": "workspace:*",
"debug": "~4.3.4"
"@scure/bip39": "^1.2.1",
"debug": "~4.3.4",
"ed25519-keygen": "^0.4.8"
},
"devDependencies": {
"@kadena-dev/eslint-config": "workspace:*",
Expand Down
5 changes: 5 additions & 0 deletions packages/libs/hd-wallet/src/SLIP10/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './kadenaGenKeypairFromSeed';
export * from './kadenaGetPublic';
export * from './kadenaKeyPairsFromRandom';
export * from './kadenaMnemonic';
export * from './kadenaSign';
92 changes: 92 additions & 0 deletions packages/libs/hd-wallet/src/SLIP10/kadenaGenKeypairFromSeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { EncryptedString } from '../utils/kadenaEncryption';
import { kadenaDecrypt, kadenaEncrypt } from '../utils/kadenaEncryption';
import { deriveKeyPair } from './utils/sign';

function genKeypairFromSeed(
password: string,
seedBuffer: Uint8Array,
index: number,
derivationPathTemplate: string,
): [string, EncryptedString] {
const derivationPath = derivationPathTemplate.replace(
'<index>',
index.toString(),
);

const { publicKey, privateKey } = deriveKeyPair(seedBuffer, derivationPath);

const encryptedPrivateKey = kadenaEncrypt(
password,
Buffer.from(privateKey, 'hex'),
);

return [publicKey, encryptedPrivateKey];
}

export function kadenaGenKeypairFromSeed(
password: string,
seed: EncryptedString,
index: number,
derivationPathTemplate?: string,
): [string, EncryptedString];

export function kadenaGenKeypairFromSeed(
password: string,
seed: EncryptedString,
indexRange: [number, number],
derivationPathTemplate?: string,
): Array<[string, EncryptedString]>;

/**
* Generates a key pair from a seed buffer and an index or range of indices, and optionally encrypts the private key.
* it uses bip44 m'/44'/626'/${index}' derivation path
*
* @param {Uint8Array} seedBuffer - The seed buffer to use for key generation.
* @param {number | [number, number]} indexOrRange - Either a single index or a tuple with start and end indices for key pair generation.
* @param {string} [password] - Optional password for encrypting the private key.
* @returns {([string, string] | [string, string][])} - Depending on the input, either a tuple for a single key pair or an array of tuples for a range of key pairs, with the private key encrypted if a password is provided.
* @throws {Error} Throws an error if the seed buffer is not provided, if the indices are invalid, or if encryption fails.
*/
export function kadenaGenKeypairFromSeed(
password: string,
seed: EncryptedString,
indexOrRange: number | [number, number],
derivationPathTemplate: string = `m'/44'/626'/<index>'`,
): [string, EncryptedString] | Array<[string, EncryptedString]> {
if (typeof seed !== 'string' || seed === '') {
throw new Error('No seed provided.');
}

const seedBuffer = kadenaDecrypt(password, seed);

if (typeof indexOrRange === 'number') {
return genKeypairFromSeed(
password,
seedBuffer,
indexOrRange,
derivationPathTemplate,
);
}
if (Array.isArray(indexOrRange)) {
const [startIndex, endIndex] = indexOrRange;
if (startIndex > endIndex) {
throw new Error('The start index must be less than the end index.');
}

const keyPairs: [string, EncryptedString][] = [];

for (let index = startIndex; index <= endIndex; index++) {
const [publicKey, encryptedPrivateKey] = genKeypairFromSeed(
password,
seedBuffer,
index,
derivationPathTemplate,
);

keyPairs.push([publicKey, encryptedPrivateKey]);
}

return keyPairs;
}
throw new Error('Invalid index or range.');
}
84 changes: 84 additions & 0 deletions packages/libs/hd-wallet/src/SLIP10/kadenaGetPublic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { EncryptedString } from '../utils/kadenaEncryption';
import { kadenaDecrypt } from '../utils/kadenaEncryption';
import { deriveKeyPair } from './utils/sign';

function genPublicKeyFromSeed(
seedBuffer: Uint8Array,
index: number,
derivationPathTemplate: string,
): string {
const derivationPath = derivationPathTemplate.replace(
'<index>',
index.toString(),
);

const { publicKey } = deriveKeyPair(seedBuffer, derivationPath);

return publicKey;
}

export function kadenaGetPublic(
password: string,
seed: EncryptedString,
index: number,
derivationPathTemplate?: string,
): string;

export function kadenaGetPublic(
password: string,
seed: EncryptedString,
indexRange: [number, number],
derivationPathTemplate?: string,
): string[];

/**
* Generates a key pair from a seed buffer and an index or range of indices, and optionally encrypts the private key.
* it uses bip44 m'/44'/626'/${index}' derivation path
*
* @param {Uint8Array} seedBuffer - The seed buffer to use for key generation.
* @param {number | [number, number]} indexOrRange - Either a single index or a tuple with start and end indices for key pair generation.
* @param {string} [password] - Optional password for encrypting the private key.
* @returns {([string, string] | [string, string][])} - Depending on the input, either a tuple for a single key pair or an array of tuples for a range of key pairs, with the private key encrypted if a password is provided.
* @throws {Error} Throws an error if the seed buffer is not provided, if the indices are invalid, or if encryption fails.
*/
export function kadenaGetPublic(
password: string,
seed: EncryptedString,
indexOrRange: number | [number, number],
derivationPathTemplate: string = `m'/44'/626'/<index>'`,
): string | string[] {
if (typeof seed !== 'string' || seed === '') {
throw new Error('No seed provided.');
}

const seedBuffer = kadenaDecrypt(password, seed);

if (typeof indexOrRange === 'number') {
return genPublicKeyFromSeed(
seedBuffer,
indexOrRange,
derivationPathTemplate,
);
}
if (Array.isArray(indexOrRange)) {
const [startIndex, endIndex] = indexOrRange;
if (startIndex > endIndex) {
throw new Error('The start index must be less than the end index.');
}

const keyPairs: string[] = [];

for (let index = startIndex; index <= endIndex; index++) {
const publicKey = genPublicKeyFromSeed(
seedBuffer,
index,
derivationPathTemplate,
);

keyPairs.push(publicKey);
}

return keyPairs;
}
throw new Error('Invalid index or range.');
}
25 changes: 25 additions & 0 deletions packages/libs/hd-wallet/src/SLIP10/kadenaKeyPairsFromRandom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { randomBytes } from 'crypto';
import { deriveKeyPair } from './utils/sign';
/**
* Generates random key pairs without updating the internal state.
*
* @param {number} [count=1] - The number of key pairs to generate.
* @returns {{ publicKey: string; secretKey: string }[]} An array of generated key pairs.
*/
export function kadenaKeyPairsFromRandom(
count: number = 1,
): { publicKey: string; secretKey: string }[] {
const keyPairs: { publicKey: string; secretKey: string }[] = [];
for (let index = 0; index < count; index++) {
const randomSeedBuffer = randomBytes(32);
const derivationPath = `m'/44'/626'/${index}'`;
const pair = deriveKeyPair(randomSeedBuffer, derivationPath);

keyPairs.push({
publicKey: pair.publicKey,
secretKey: pair.privateKey,
});
}

return keyPairs;
}
Loading
Loading