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 12 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
3 changes: 3 additions & 0 deletions packages/libs/hd-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
},
"dependencies": {
"@kadena/cryptography-utils": "workspace:*",
"@kadena/client": "workspace:^",
"@scure/bip39": "^1.2.1",
"ed25519-keygen": "^0.4.8",
"debug": "~4.3.4"
},
"devDependencies": {
Expand Down
5 changes: 5 additions & 0 deletions packages/libs/hd-wallet/src/bip44/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/bip44/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}'/0'/0' 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>'/0'/0'`,
): [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/bip44/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}'/0'/0' 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>'/0'/0'`,
): 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/bip44/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 i = 0; i < count; i++) {
const randomSeedBuffer = randomBytes(32);
const derivationPath = `m'/44'/626'/${i}'/0'/0'`;
const pair = deriveKeyPair(randomSeedBuffer, derivationPath);

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

return keyPairs;
}
35 changes: 35 additions & 0 deletions packages/libs/hd-wallet/src/bip44/kadenaMnemonic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as bip39 from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import type { EncryptedString } from '../utils/kadenaEncryption';
import { kadenaEncrypt } from '../utils/kadenaEncryption';
/**
* Generates a mnemonic phrase using the BIP39 protocol with a specified wordlist.
*
* @returns {string} A valid BIP39 mnemonic phrase.
* @throws {Error} If the generated mnemonic is invalid.
*/
export function kadenaGenMnemonic(): string {
return bip39.generateMnemonic(wordlist);
}

/**
* Convert a given mnemonic phrase into a seed buffer.
*
* @param {string} mnemonic - A mnemonic seed phrase to be converted into a seed buffer.
* @param {string} [password] - Optional password for encrypting the seed.
* @throws {Error} Throws an error if the provided mnemonic is not valid.
* @returns {Promise<{ seedBuffer: Uint8Array, seed: string }>} - Returns the seed buffer and processed seed.
*/
export async function kadenaMnemonicToSeed(
password: string,
mnemonic: string,
// wordList: string[] = wordlist,
): Promise<EncryptedString> {
if (bip39.validateMnemonic(mnemonic, wordlist) === false) {
throw Error('Invalid mnemonic.');
}

const seedBuffer = await bip39.mnemonicToSeed(mnemonic);

return kadenaEncrypt(password, seedBuffer);
}
63 changes: 63 additions & 0 deletions packages/libs/hd-wallet/src/bip44/kadenaSign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { IUnsignedCommand } from '@kadena/client';
import { verifySig } from '@kadena/cryptography-utils';
import type { EncryptedString } from '../utils/kadenaEncryption';
import { kadenaDecrypt } from '../utils/kadenaEncryption';
import { signWithKeyPair, signWithSeed } from './utils/sign';

/**
* Signs a Kadena transaction with a given public and private key pair.
*
* @param {string} publicKey - The public key to be used for signing the transaction.
* @param {string} encryptedPrivateKey - The private key to be used for signing the transaction.
* @returns {Function} A function that takes an unsigned command (`IUnsignedCommand`) and returns an object with an array of signatures.
*/
export function kadenaSignWithKeyPair(
password: string,
publicKey: string,
encryptedPrivateKey: EncryptedString,
): (tx: IUnsignedCommand) => { sigs: { sig: string }[] } {
return signWithKeyPair(
publicKey,
Buffer.from(kadenaDecrypt(password, encryptedPrivateKey)).toString('hex'),
);
}

/**
* Signs a Kadena transaction with a seed and index.
*
* @param {Uint8Array} seed - The seed array used to derive key pairs for signing.
* @param {number} index - The index number used to select the correct key pair from the derived set.
* @returns {Function} A function that takes an unsigned command (`IUnsignedCommand`) and returns an object with an array of signatures.
*/
export function kadenaSignWithSeed(
password: string,
seed: EncryptedString,
index: number,
derivationPathTemplate: string = `m'/44'/626'/<index>'/0'/0'`,
): (tx: IUnsignedCommand) => { sigs: { sig: string }[] } {
return signWithSeed(
kadenaDecrypt(password, seed),
derivationPathTemplate.replace('<index>', index.toString()),
);
}

/**
* Verifies the signature for a message against a given public key using the Kadena signature verification convention.
*
* @param {string} message - The message in string format to be verified.
* @param {string} publicKey - The public key in hexadecimal string format to verify the signature against.
* @param {string} signature - The signature in hexadecimal string format to be verified.
* @returns {boolean} - Returns true if verification succeeded or false if it failed.
*/
export function kadenaVerify(
message: string,
publicKey: string,
signature: string,
): boolean {
// Convert the message, public key, and signature from hex string to Uint8Array
const msgUint8Array = Uint8Array.from(Buffer.from(message, 'hex'));
const publicKeyUint8Array = Uint8Array.from(Buffer.from(publicKey, 'hex'));
const signatureUint8Array = Uint8Array.from(Buffer.from(signature, 'hex'));

return verifySig(msgUint8Array, signatureUint8Array, publicKeyUint8Array);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, expect, it } from 'vitest';

import {
kadenaGenKeypairFromSeed,
kadenaGenMnemonic,
kadenaMnemonicToSeed,
} from '../';

import { kadenaDecrypt } from '../../utils/kadenaEncryption';

describe('kadenaGenKeypairFromSeed', () => {
it('should generate an encrypted keypair from the seedBuffer when a password is provided', async () => {
const mnemonic = kadenaGenMnemonic();
const password = 'password';
const seed = await kadenaMnemonicToSeed(password, mnemonic);
const [publicKey, encryptedPrivateKey] = kadenaGenKeypairFromSeed(
password,
seed,
0,
);

expect(publicKey).toHaveLength(64);
expect(typeof encryptedPrivateKey).toBe('string'); // Checks if privateKey is a string, thus encrypted
});

it('should generate a range of keypairs from the seed', async () => {
const mnemonic = kadenaGenMnemonic();
const password = 'password';
const seed = await kadenaMnemonicToSeed(password, mnemonic);
const keyPairs = kadenaGenKeypairFromSeed(password, seed, [0, 3]);
expect(keyPairs).toHaveLength(4);
keyPairs.forEach(([publicKey, privateKey]) => {
expect(publicKey).toHaveLength(64);
expect(
Buffer.from(kadenaDecrypt(password, privateKey)).toString('hex'),
).toHaveLength(64);
});
});

it('should throw an error for out-of-bounds index values', async () => {
const mnemonic = kadenaGenMnemonic();
const password = 'password';
const seed = await kadenaMnemonicToSeed(password, mnemonic);
const outOfBoundsIndex = -1;

expect(() => {
kadenaGenKeypairFromSeed(password, seed, outOfBoundsIndex);
}).toThrowError('Invalid child index: -1');
});

it('returns an encrypted private key that can be decrypted with the password', async () => {
const mnemonic = kadenaGenMnemonic();
const password = 'password';
const seed = await kadenaMnemonicToSeed(password, mnemonic);
const [, encryptedPrivateKey] = kadenaGenKeypairFromSeed(password, seed, 0);
const decryptedPrivateKey = kadenaDecrypt(password, encryptedPrivateKey);
expect(decryptedPrivateKey).toBeTruthy();
expect(Buffer.from(decryptedPrivateKey).toString('hex')).toHaveLength(64);
});

// it('should handle the highest non-hardened index without throwing errors', async () => {
// const mnemonic = kadenaGenMnemonic();
// const { seedBuffer } = await kadenaMnemonicToSeed(mnemonic);

// /*
// * HD wallets as per BIP32 spec define two types of indices:
// * - Non-hardened (ranging from 0 to 2^31 - 1)
// * - Hardened (ranging from 2^31 to 2^32 - 1).
// * The highest non-hardened index is therefore 2^31 - 1,
// * which is the largest 32-bit integer that can be used for generating non-hardened keys.
// */

// const highestNonHardenedIndex = 2 ** 31 - 1;
// expect(() => {
// kadenaGenKeypairFromSeed(seedBuffer, highestNonHardenedIndex);
// }).not.toThrow();
// });
});
Loading
Loading