diff --git a/.changeset/serious-chefs-vanish.md b/.changeset/serious-chefs-vanish.md new file mode 100644 index 0000000000..2e6cd19593 --- /dev/null +++ b/.changeset/serious-chefs-vanish.md @@ -0,0 +1,5 @@ +--- +'@kadena/hd-wallet': minor +--- + +Add functions to support SLIP-0010 key derivation diff --git a/packages/libs/hd-wallet/docs/decisions/0001-use-bip44-for-private-key-generation.md b/packages/libs/hd-wallet/docs/decisions/0001-use-bip44-for-private-key-generation.md deleted file mode 100644 index 481a5fd9ca..0000000000 --- a/packages/libs/hd-wallet/docs/decisions/0001-use-bip44-for-private-key-generation.md +++ /dev/null @@ -1,68 +0,0 @@ -# ADR: Use BIP44 for Private Key Generation - -**Date:** 2023-10-31 - -**Status:** Proposal - -## Context - -We require a deterministic method for generating private keys from a mnemonic, -as Kadena has already adopted the [BIP39][1] mnemonic standard. In the realm of -cryptocurrencies, this process is commonly referred to as creating an HD-wallet -(Hierarchical Deterministic wallet). - -HD wallets derive keys from a master key, typically following the [BIP32][2] -algorithm, which permits various derivation paths for both hardened and -non-hardened keys to create a chain of keys. 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][3] model, such -as Bitcoin, can benefit from extended public keys. - -This record exclusively concentrates on private keys, as we currently have no -plans to use extended public keys in Kadena. Kadena employs the ed25519 -algorithm for keys, and the solution should be compatible with bip32-ed25519. - -## Decision - -We have chosen to implement the [BIP44][4] protocol, defining the path -restriction for BIP32 as `m/44'/626'/${index}'/0'/0'`. 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** (third level in BIP44). -- We exclusively use BIP44 for **private key** generation. -- Extended public keys are beyond the scope of this decision. -- All private keys are **hardened** in accordance with the [ed25519][6] - algorithm. - -## Consequences - -- We adopt a standard 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 starting with - `m/44'/626'` as well. - -## Resources - -- [BIP39 Proposal][1] -- [BIP32 Proposal][2] -- [Unspent transaction output][3] -- [BIP44 Proposal][4] -- [SLIP-0044: Registered Coin Types for BIP-0044][5] -- [SLIP-0010: Universal Private Key Derivation from Master Private Key][6] - -[1]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki -[2]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki -[3]: 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 -[6]: https://github.com/satoshilabs/slips/blob/master/slip-0010.md diff --git a/packages/libs/hd-wallet/docs/decisions/0001-use-slip10-for-private-key-generation.md b/packages/libs/hd-wallet/docs/decisions/0001-use-slip10-for-private-key-generation.md new file mode 100644 index 0000000000..8fed89a383 --- /dev/null +++ b/packages/libs/hd-wallet/docs/decisions/0001-use-slip10-for-private-key-generation.md @@ -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 diff --git a/packages/libs/hd-wallet/package.json b/packages/libs/hd-wallet/package.json index 78866abc49..5c6505fac4 100644 --- a/packages/libs/hd-wallet/package.json +++ b/packages/libs/hd-wallet/package.json @@ -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:*", diff --git a/packages/libs/hd-wallet/src/SLIP10/index.ts b/packages/libs/hd-wallet/src/SLIP10/index.ts new file mode 100644 index 0000000000..e5f99e3e15 --- /dev/null +++ b/packages/libs/hd-wallet/src/SLIP10/index.ts @@ -0,0 +1,5 @@ +export * from './kadenaGenKeypairFromSeed'; +export * from './kadenaGetPublic'; +export * from './kadenaKeyPairsFromRandom'; +export * from './kadenaMnemonic'; +export * from './kadenaSign'; diff --git a/packages/libs/hd-wallet/src/SLIP10/kadenaGenKeypairFromSeed.ts b/packages/libs/hd-wallet/src/SLIP10/kadenaGenKeypairFromSeed.ts new file mode 100644 index 0000000000..9be24a27ce --- /dev/null +++ b/packages/libs/hd-wallet/src/SLIP10/kadenaGenKeypairFromSeed.ts @@ -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.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'/'`, +): [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.'); +} diff --git a/packages/libs/hd-wallet/src/SLIP10/kadenaGetPublic.ts b/packages/libs/hd-wallet/src/SLIP10/kadenaGetPublic.ts new file mode 100644 index 0000000000..3cc006e150 --- /dev/null +++ b/packages/libs/hd-wallet/src/SLIP10/kadenaGetPublic.ts @@ -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.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'/'`, +): 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.'); +} diff --git a/packages/libs/hd-wallet/src/SLIP10/kadenaKeyPairsFromRandom.ts b/packages/libs/hd-wallet/src/SLIP10/kadenaKeyPairsFromRandom.ts new file mode 100644 index 0000000000..0d8a8bb61f --- /dev/null +++ b/packages/libs/hd-wallet/src/SLIP10/kadenaKeyPairsFromRandom.ts @@ -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; +} diff --git a/packages/libs/hd-wallet/src/SLIP10/kadenaMnemonic.ts b/packages/libs/hd-wallet/src/SLIP10/kadenaMnemonic.ts new file mode 100644 index 0000000000..eff4dde575 --- /dev/null +++ b/packages/libs/hd-wallet/src/SLIP10/kadenaMnemonic.ts @@ -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 { + if (bip39.validateMnemonic(mnemonic, wordlist) === false) { + throw Error('Invalid mnemonic.'); + } + + const seedBuffer = await bip39.mnemonicToSeed(mnemonic); + + return kadenaEncrypt(password, seedBuffer); +} diff --git a/packages/libs/hd-wallet/src/SLIP10/kadenaSign.ts b/packages/libs/hd-wallet/src/SLIP10/kadenaSign.ts new file mode 100644 index 0000000000..45616a7716 --- /dev/null +++ b/packages/libs/hd-wallet/src/SLIP10/kadenaSign.ts @@ -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'/'`, +): (tx: IUnsignedCommand) => { sigs: { sig: string }[] } { + return signWithSeed( + kadenaDecrypt(password, seed), + derivationPathTemplate.replace('', 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); +} diff --git a/packages/libs/hd-wallet/src/SLIP10/tests/kadenaGenKeypairFromSeed.test.ts b/packages/libs/hd-wallet/src/SLIP10/tests/kadenaGenKeypairFromSeed.test.ts new file mode 100644 index 0000000000..b4d9d2c8d7 --- /dev/null +++ b/packages/libs/hd-wallet/src/SLIP10/tests/kadenaGenKeypairFromSeed.test.ts @@ -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(); + // }); +}); diff --git a/packages/libs/hd-wallet/src/SLIP10/tests/kadenaGetPublic.test.ts b/packages/libs/hd-wallet/src/SLIP10/tests/kadenaGetPublic.test.ts new file mode 100644 index 0000000000..3c6b208835 --- /dev/null +++ b/packages/libs/hd-wallet/src/SLIP10/tests/kadenaGetPublic.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest'; + +import { kadenaGenMnemonic, kadenaGetPublic, kadenaMnemonicToSeed } from '..'; + +describe('kadenaGetPublic', () => { + it('should retrieve the public key from seed and the given index', async () => { + const mnemonic = kadenaGenMnemonic(); + const password = 'password'; + const seed = await kadenaMnemonicToSeed(password, mnemonic); + + const publicKeyIndex0 = kadenaGetPublic(password, seed, 0); + const publicKeyIndex1 = kadenaGetPublic(password, seed, 1); + + expect(publicKeyIndex0).toHaveLength(64); + expect(publicKeyIndex1).toHaveLength(64); + expect(publicKeyIndex1).not.toBe(publicKeyIndex0); + }); + + it('should retrieve distinct public keys from seedBuffer for different indexes', async () => { + const mnemonic = kadenaGenMnemonic(); + const password = 'password'; + const seed = await kadenaMnemonicToSeed(password, mnemonic); + + const indexes = [0, 1, 2, 3, 4]; + const publicKeys = indexes.map((index) => + kadenaGetPublic(password, seed, index), + ); + + publicKeys.forEach((publicKey) => { + expect(publicKey).toHaveLength(64); + }); + + const uniquePublicKeys = new Set(publicKeys); // Check that all public keys are unique + expect(uniquePublicKeys.size).toBe(indexes.length); + }); + + it('should get the similar public keys as Enkrypt for the same path', async () => { + const password = 'pass'; + const seed = await kadenaMnemonicToSeed( + password, + // this mnemonic is generated by Enkrypt wallet + 'coyote utility final warfare thumb symbol mule scale final nominee behave crumble', + ); + let publicKey = kadenaGetPublic( + password, + seed, + 0, + "m'/44'/626'//0'/0'", + ); + expect(publicKey).toBe( + '43726c4a2e7b03fa5d23635307e5b130baf8b261e1081c099a2b43db1d4554cc', + ); + publicKey = kadenaGetPublic(password, seed, 1, "m'/44'/626'//0'/0'"); + expect(publicKey).toBe( + '3f53dfad097fdf8501c32b275e109980ed7121866a63ca34bb035c4a2e41a265', + ); + publicKey = kadenaGetPublic(password, seed, 2, "m'/44'/626'//0'/0'"); + expect(publicKey).toBe( + '3021bcfa703cc4fac007ab4c5050df5c0b8ca7d655ea80c84af9ea5e43ecf0ff', + ); + }); +}); diff --git a/packages/libs/hd-wallet/src/SLIP10/tests/kadenaMnemonic.test.ts b/packages/libs/hd-wallet/src/SLIP10/tests/kadenaMnemonic.test.ts new file mode 100644 index 0000000000..e3c10d5dbd --- /dev/null +++ b/packages/libs/hd-wallet/src/SLIP10/tests/kadenaMnemonic.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; + +import { kadenaGenMnemonic, kadenaMnemonicToSeed } from '..'; + +import { kadenaDecrypt } from '../../utils/kadenaEncryption'; + +describe('kadenaGenMnemonic', () => { + it('should generate a valid mnemonic', () => { + const mnemonic = kadenaGenMnemonic(); + expect(mnemonic.split(' ')).toHaveLength(12); + }); +}); + +describe('kadenaMnemonicToSeed', () => { + it('should convert mnemonic to encrypt seed with a password', async () => { + const mnemonic = kadenaGenMnemonic(); + const password = 'password'; + const seed = await kadenaMnemonicToSeed(password, mnemonic); + expect(typeof seed).toBe('string'); // Check if the seed is a string, indicating it has been encrypted + }); + + it('returns encrypted seed that can be decrypted with the password', async () => { + const mnemonic = kadenaGenMnemonic(); + const password = 'password'; + const seed = await kadenaMnemonicToSeed(password, mnemonic); + const decryptedSeed = kadenaDecrypt(password, seed); + expect(decryptedSeed).toBeTruthy(); + }); + + it('should throw an error for an invalid mnemonic', async () => { + const invalidMnemonic = 'this is not a valid mnemonic'; + + await expect( + kadenaMnemonicToSeed('password', invalidMnemonic), + ).rejects.toThrowError('Invalid mnemonic.'); + }); + + it('should throw an error when mnemonic is empty', async () => { + const emptyMnemonic = ''; + + await expect( + kadenaMnemonicToSeed('password', emptyMnemonic), + ).rejects.toThrowError('Invalid mnemonic.'); + }); +}); diff --git a/packages/libs/hd-wallet/src/SLIP10/tests/kadenaSignWithKeyPair.test.ts b/packages/libs/hd-wallet/src/SLIP10/tests/kadenaSignWithKeyPair.test.ts new file mode 100644 index 0000000000..002c992945 --- /dev/null +++ b/packages/libs/hd-wallet/src/SLIP10/tests/kadenaSignWithKeyPair.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; + +import { + kadenaGenKeypairFromSeed, + kadenaGenMnemonic, + kadenaMnemonicToSeed, + kadenaSignWithKeyPair, +} from '..'; + +import type { IUnsignedCommand } from '@kadena/client'; + +describe('kadenaSignWithKeyPair', async () => { + const password = 'password'; + const mnemonic = kadenaGenMnemonic(); + const seed = await kadenaMnemonicToSeed(password, mnemonic); + + const [publicKey, privateKey] = kadenaGenKeypairFromSeed(password, seed, 0); + + const mockUnsignedCommand: IUnsignedCommand = { + cmd: '{"command":"value"}', + hash: 'kadena-hash', + sigs: [], + }; + + it('should sign a transaction with a public and private key ans password', () => { + const signer = kadenaSignWithKeyPair(password, publicKey, privateKey); + + const signedTx = signer(mockUnsignedCommand); + + expect(signedTx).toHaveProperty('sigs'); + expect(signedTx.sigs).toBeInstanceOf(Array); + expect(signedTx.sigs.length).toBeGreaterThan(0); + expect(signedTx.sigs[0]).toHaveProperty('sig'); + expect(signedTx.sigs[0].sig).toBeTruthy(); + }); +}); diff --git a/packages/libs/hd-wallet/src/SLIP10/tests/kadenaSignWithSeed.test.ts b/packages/libs/hd-wallet/src/SLIP10/tests/kadenaSignWithSeed.test.ts new file mode 100644 index 0000000000..9d4fbe33b1 --- /dev/null +++ b/packages/libs/hd-wallet/src/SLIP10/tests/kadenaSignWithSeed.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; + +import { + kadenaGenMnemonic, + kadenaMnemonicToSeed, + kadenaSignWithSeed, +} from '..'; + +import type { IUnsignedCommand } from '@kadena/client'; + +describe('kadenaSignWithSeed', async () => { + const password = 'password'; + const mnemonic = kadenaGenMnemonic(); + const seed = await kadenaMnemonicToSeed(password, mnemonic); + const index = 0; + const mockUnsignedCommand: IUnsignedCommand = { + cmd: '{"commands":"value"}', + hash: 'kadena-hash', + sigs: [], + }; + + it('should sign a transaction with a seed and index', () => { + const signer = kadenaSignWithSeed(password, seed, index); + const signedTx = signer(mockUnsignedCommand); + expect(signedTx).toHaveProperty('sigs'); + expect(signedTx.sigs).toBeInstanceOf(Array); + expect(signedTx.sigs[0]).toHaveProperty('sig'); + expect(signedTx.sigs[0].sig).toBeTruthy(); + }); +}); diff --git a/packages/libs/hd-wallet/src/SLIP10/utils/sign.ts b/packages/libs/hd-wallet/src/SLIP10/utils/sign.ts new file mode 100644 index 0000000000..c087d540a6 --- /dev/null +++ b/packages/libs/hd-wallet/src/SLIP10/utils/sign.ts @@ -0,0 +1,67 @@ +import type { IUnsignedCommand } from '@kadena/client'; +import { sign } from '@kadena/cryptography-utils'; + +import { HDKey } from 'ed25519-keygen/hdkey'; +import { uint8ArrayToHex } from '../../utils/buffer-helpers'; + +/** + * Derive a key pair using a seed and an index. + * @param {Uint8Array} seed - The seed for key derivation. + * @param {number} index - The index for key derivation. + * @returns {{ privateKey: string; publicKey: string }} - Returns the derived private and public keys. + */ +export const deriveKeyPair = ( + seed: Uint8Array, + derivationPath: string, +): { privateKey: string; publicKey: string } => { + const key = HDKey.fromMasterSeed(seed).derive(derivationPath, true); + + return { + privateKey: uint8ArrayToHex(key.privateKey), + publicKey: uint8ArrayToHex(key.publicKey), + }; +}; + +/** + * Creates a signer function for a given public and secret key pair. + * + * @function + * @param {string} publicKey - The public key for signing. + * @param {string} [secretKey] - The optional secret key for signing. + * @returns {Function} A function that takes an unsigned command and returns the command with its signature. + * + * @example + * const signer = signWithKeyPair('myPublicKey', 'mySecretKey'); + * const signedCommand = signer(myUnsignedCommand); + * + * @throws {Error} Throws an error if the signature is undefined. + */ +export const signWithKeyPair = ( + publicKey: string, + secretKey?: string, +): ((tx: IUnsignedCommand) => { sigs: { sig: string }[] }) => { + return (tx: IUnsignedCommand) => { + const { sig } = sign(tx.cmd, { publicKey, secretKey }); + if (sig === undefined) { + throw new Error('Signature is undefined'); + } + return { + ...tx, + sigs: [{ sig }], + }; + }; +}; + +/** + * Generate a signer function using a seed and an index. + * @param {Uint8Array} seed - The seed for key derivation. + * @param {number} index - The index for key derivation. + * @returns {(tx: IUnsignedCommand) => { sigs: { sig: string }[] }} - Returns a function that can sign a transaction. + */ +export const signWithSeed = ( + seed: Uint8Array, + derivationPath: string, +): ((tx: IUnsignedCommand) => { sigs: { sig: string }[] }) => { + const { publicKey, privateKey } = deriveKeyPair(seed, derivationPath); + return signWithKeyPair(publicKey, privateKey); +}; diff --git a/packages/libs/hd-wallet/src/chainweaver/compatibility/kadenaGenKeypair.ts b/packages/libs/hd-wallet/src/chainweaver/compatibility/kadenaGenKeypair.ts index 3475d51566..01e02b290b 100644 --- a/packages/libs/hd-wallet/src/chainweaver/compatibility/kadenaGenKeypair.ts +++ b/packages/libs/hd-wallet/src/chainweaver/compatibility/kadenaGenKeypair.ts @@ -1,4 +1,4 @@ -import { HARDENED_OFFSET, harden } from '../../utils'; +import { HARDENED_OFFSET, harden } from '../../utils/crypto'; import { kadenaGenKeypair as kadenaGenKeypairOriginal } from '../vendor/kadena-crypto'; function kadenaGenOneKeypair( diff --git a/packages/libs/hd-wallet/src/index.ts b/packages/libs/hd-wallet/src/index.ts index 04bca77e0d..2820e78722 100644 --- a/packages/libs/hd-wallet/src/index.ts +++ b/packages/libs/hd-wallet/src/index.ts @@ -1 +1,2 @@ -export * from './utils'; +export * from './SLIP10'; +export * from './utils/kadenaEncryption'; diff --git a/packages/libs/hd-wallet/src/utils.ts b/packages/libs/hd-wallet/src/utils.ts deleted file mode 100644 index 50e4419e30..0000000000 --- a/packages/libs/hd-wallet/src/utils.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const HARDENED_OFFSET = 0x80000000; -export const harden = (n: number) => HARDENED_OFFSET + n; diff --git a/packages/libs/hd-wallet/src/utils/buffer-helpers.ts b/packages/libs/hd-wallet/src/utils/buffer-helpers.ts new file mode 100644 index 0000000000..1692d04323 --- /dev/null +++ b/packages/libs/hd-wallet/src/utils/buffer-helpers.ts @@ -0,0 +1,29 @@ +/** + * Convert a Buffer to a Base64 encoded string. + * @param {Buffer} buffer - Buffer to convert. + * @returns {string} - Returns the Base64 encoded string. + */ +export function bufferToBase64(buffer: Buffer): string { + return buffer.toString('base64'); +} + +/** + * Convert a Base64 encoded string to a Buffer. + * @param {string} base64 - Base64 encoded string to convert. + * @returns {Buffer} - Returns the resulting Buffer. + */ +export function base64ToBuffer(base64: string): Buffer { + return Buffer.from(base64, 'base64'); +} + +/** + * Convert a Uint8Array to a hexadecimal string. + * @param {Uint8Array} uint8Array - The array to convert. + * @returns {string} - Returns the hexadecimal representation of the input. + */ +export const uint8ArrayToHex = (uint8Array: Uint8Array): string => { + if (uint8Array.length === 33 && uint8Array.at(0) === 0) { + uint8Array = uint8Array.slice(1); + } + return [...uint8Array].map((x) => x.toString(16).padStart(2, '0')).join(''); +}; diff --git a/packages/libs/hd-wallet/src/utils/crypto.ts b/packages/libs/hd-wallet/src/utils/crypto.ts new file mode 100644 index 0000000000..1952cfab2b --- /dev/null +++ b/packages/libs/hd-wallet/src/utils/crypto.ts @@ -0,0 +1,73 @@ +import { + createCipheriv, + createDecipheriv, + pbkdf2Sync, + randomBytes, +} from 'crypto'; + +type BinaryLike = string | NodeJS.ArrayBufferView; + +/** + * Derive a cryptographic key from the provided password. + * @param {string} password - User's password. + * @returns {Buffer} - Returns the derived cryptographic key. + */ +function deriveKey(password: string, salt: BinaryLike): Buffer { + return pbkdf2Sync(password, salt, 1000, 32, 'sha256'); +} + +/** + * Encrypt the provided text using AES-256-GCM algorithm. + * @param {Buffer} text - Text to encrypt. + * @param {string} password - User's password. + * @returns {{ cipherText: Buffer; iv: Buffer; tag: Buffer }} - Returns the encrypted text, initialization vector, and authentication tag. + */ +export function encrypt( + text: Buffer, + password: string, + salt: BinaryLike, +): { cipherText: Buffer; iv: Buffer; tag: Buffer } { + const key = deriveKey(password, salt); + const iv = randomBytes(12); + const cipher = createCipheriv('aes-256-gcm', key, iv); + const cipherText = Buffer.concat([cipher.update(text), cipher.final()]); + const tag = cipher.getAuthTag(); // Capture the authentication tag + return { + cipherText, + iv, + tag, + }; +} + +/** + * Decrypt the provided encrypted text using AES-256-GCM algorithm. + * @param encrypted - Encrypted text, initialization vector, and authentication tag. + * @param password - User's password. + * @returns Returns the decrypted text or undefined if decryption fails. + * @internal + */ +export function decrypt( + encrypted: { + cipherText: Buffer; + iv: Buffer; + tag: Buffer; + }, + password: string, + salt: BinaryLike, +): Buffer | undefined { + const key = deriveKey(password, salt); + const decipher = createDecipheriv('aes-256-gcm', key, encrypted.iv); + decipher.setAuthTag(encrypted.tag); // Set the authentication tag + try { + return Buffer.concat([ + decipher.update(encrypted.cipherText), + decipher.final(), + ]); + } catch (err) { + console.warn('Failed to decrypt'); + return undefined; + } +} + +export const HARDENED_OFFSET = 0x80000000; +export const harden = (n: number) => HARDENED_OFFSET + n; diff --git a/packages/libs/hd-wallet/src/utils/kadenaEncryption.ts b/packages/libs/hd-wallet/src/utils/kadenaEncryption.ts new file mode 100644 index 0000000000..df2e2b377f --- /dev/null +++ b/packages/libs/hd-wallet/src/utils/kadenaEncryption.ts @@ -0,0 +1,109 @@ +import { randomBytes } from 'crypto'; +import { decrypt, encrypt } from './crypto'; + +export type EncryptedString = string & { _brand: 'EncryptedString' }; + +/** + * Encrypts the message with a password . + * @param {Uint8Array} message - The message to be encrypted. + * @param {string} password - password used for encryption. + * @returns {string} The encrypted string + */ +export function kadenaEncrypt( + password: string, + message: Uint8Array, +): EncryptedString { + // Using randomBytes for the salt is fine here because the salt is not secret but should be unique. + const salt = randomBytes(16); + const { cipherText, iv, tag } = encrypt(Buffer.from(message), password, salt); + + return Buffer.from( + [salt, iv, tag, cipherText].map((x) => x.toString('base64')).join('.'), + ).toString('base64') as EncryptedString; +} + +/** + * Decrypts an encrypted message using the provided password. + * This function is a wrapper for the internal decryption logic, intended + * for public-facing API usage where the private key encryption follows + * + * @param {string} encryptedData - The encrypted data as a Base64 encoded string. + * @param {string} password - The password used to encrypt the private key. + * @returns {Uint8Array} The decrypted private key. + * @throws {Error} Throws an error if decryption fails. + */ +export function kadenaDecrypt( + password: string, + encryptedData: EncryptedString, +): Uint8Array { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (!encryptedData) { + throw new Error('Encrypted data is empty'); + } + const [saltBase64, ivBase64, tagBase64, encryptedBase64] = Buffer.from( + encryptedData, + 'base64', + ) + .toString() + .split('.'); + + // Convert from Base64. + const salt = Buffer.from(saltBase64, 'base64'); + const iv = Buffer.from(ivBase64, 'base64'); + const tag = Buffer.from(tagBase64, 'base64'); + const cipherText = Buffer.from(encryptedBase64, 'base64'); + + // decrypt and return the private key. + const decrypted = decrypt({ cipherText, iv, tag }, password, salt); + if (!decrypted) { + throw new Error('Decryption failed'); + } + return decrypted; +} + +/** + * Changes the password of an encrypted data. + * + * @param {string} privateKey - The encrypted private key as a Base64 encoded string. + * @param {string} password - The current password used to encrypt the private key. + * @param {string} newPassword - The new password to encrypt the private key with. + * @returns {string} - The newly encrypted private key as a Base64 encoded string. + * @throws {Error} - Throws an error if the old password is empty, new password is incorrect empty passwords are empty, or if encryption with the new password fails. + */ +export function kadenaChangePassword( + password: string, + encryptedData: EncryptedString, + newPassword: string, +): EncryptedString { + if (typeof password !== 'string' || typeof newPassword !== 'string') { + throw new Error('The old and new passwords must be strings.'); + } + if (password === '') { + throw new Error('The old password cannot be empty.'); + } + if (newPassword === '') { + throw new Error('The new password cannot be empty.'); + } + if (password === newPassword) { + throw new Error( + 'The new password must be different from the old password.', + ); + } + + let decryptedPrivateKey: Uint8Array; + try { + decryptedPrivateKey = kadenaDecrypt(password, encryptedData); + } catch (error) { + throw new Error( + `Failed to decrypt the private key with the old password: ${error.message}`, + ); + } + + try { + return kadenaEncrypt(newPassword, decryptedPrivateKey); + } catch (error) { + throw new Error( + `Failed to encrypt the private key with the new password: ${error.message}`, + ); + } +} diff --git a/packages/libs/hd-wallet/src/utils/tests/kadenaEncryption.test.ts b/packages/libs/hd-wallet/src/utils/tests/kadenaEncryption.test.ts new file mode 100644 index 0000000000..6110214beb --- /dev/null +++ b/packages/libs/hd-wallet/src/utils/tests/kadenaEncryption.test.ts @@ -0,0 +1,161 @@ +import { describe, expect, it } from 'vitest'; + +import type { EncryptedString } from '../kadenaEncryption'; +import { + kadenaChangePassword, + kadenaDecrypt, + kadenaEncrypt, +} from '../kadenaEncryption'; + +describe('kadenaDecrypt', () => { + it('should correctly decrypt a previously encrypted string', async () => { + const password = 'test-password'; + const message = Buffer.from('test-message'); + const encryptedMessage = kadenaEncrypt(password, message); + const decryptedMessage = kadenaDecrypt(password, encryptedMessage); + expect(Buffer.from(decryptedMessage).toString('utf-8')).toEqual( + message.toString('utf-8'), + ); + expect(message.toJSON()).not.toBe(encryptedMessage); + expect(encryptedMessage.toString()).not.toBe(decryptedMessage.toString()); + }); + + it('should throw an error when the incorrect password is provided', async () => { + const correctPassword = 'correct-password'; + const wrongPassword = 'wrong-password'; + const message = Buffer.from('test-message'); + const encryptedMessage = kadenaEncrypt(correctPassword, message); + expect(() => { + kadenaDecrypt(wrongPassword, encryptedMessage); + }).toThrow('Decryption failed'); + }); + + it('should throw an error if the encrypted key is corrupted', () => { + const password = 'test-password'; + const corruptedEncryptedPrivateKey = 'corrupted-data' as EncryptedString; + + expect(() => { + kadenaDecrypt(password, corruptedEncryptedPrivateKey); + }).toThrow(); + }); + + it('should throw an error if the encrypted data is empty', () => { + const password = 'test-password'; + const emptyEncryptedMessage = '' as EncryptedString; + + expect(() => { + kadenaDecrypt(password, emptyEncryptedMessage); + }).toThrow('Encrypted data is empty'); + }); + + it('should handle passwords with special characters', async () => { + const specialCharPassword = 'p@ssw0rd!#%&'; + const message = Buffer.from('test-message'); + const encryptedMessage = kadenaEncrypt(specialCharPassword, message); + expect( + kadenaDecrypt(specialCharPassword, encryptedMessage).toString(), + ).toBe(message.toString()); + }); + + it('should handle extremely long passwords', async () => { + const longPassword = 'p'.repeat(1000); + const message = Buffer.from('test-message'); + const encryptedMessage = kadenaEncrypt(longPassword, message); + expect(kadenaDecrypt(longPassword, encryptedMessage).toString()).toBe( + message.toString(), + ); + }); + + it('should handle unicode characters in passwords', async () => { + const unicodePassword = '密码'; // 'password' in Chinese + const message = Buffer.from('test-message'); + const encryptedMessage = kadenaEncrypt(unicodePassword, message); + + expect(kadenaDecrypt(unicodePassword, encryptedMessage).toString()).toBe( + message.toString(), + ); + }); +}); + +describe('kadenaChangePassword', () => { + const privateKey = Uint8Array.from({ length: 32 }, () => + Math.floor(Math.random() * 256), + ); + const password = 'currentPassword123'; + const newPassword = 'newPassword123'; + + it('changes the password successfully and allows decryption with the new password', () => { + const firstPassword = 'firstPassword123'; + const secondPassword = 'secondPassword123'; + const message = Buffer.from('test-message'); + const encryptedMessage = kadenaEncrypt(firstPassword, message); + const newEncryptedMessage = kadenaChangePassword( + firstPassword, + encryptedMessage, + secondPassword, + ); + expect(encryptedMessage.toString()).not.toBe( + newEncryptedMessage.toString(), + ); + + const decryptedMessage = Buffer.from( + kadenaDecrypt(secondPassword, newEncryptedMessage), + ); + expect(decryptedMessage.toString()).toBe(message.toString()); + }); + + it('throws an error when the password is incorrect', () => { + const encryptedPrivateKey = kadenaEncrypt(password, privateKey); + const incorrectPassword = 'wrongPassword'; + const changePasswordAttempt = () => + kadenaChangePassword(incorrectPassword, encryptedPrivateKey, newPassword); + + expect(changePasswordAttempt).toThrow( + 'Failed to decrypt the private key with the old password: Decryption failed', + ); + }); + + it('fails to decrypt with the old password after the password has been changed', () => { + const firstPassword = 'firstPassword123'; + const secondPassword = 'secondPassword123'; + const message = Buffer.from('test-message'); + const encryptedMessage = kadenaEncrypt(firstPassword, message); + const newEncryptedMessage = kadenaChangePassword( + firstPassword, + encryptedMessage, + secondPassword, + ); + expect(() => kadenaDecrypt(firstPassword, newEncryptedMessage)).toThrow(); + }); + + const testUndefined = undefined as unknown as EncryptedString; + const testNull = null as unknown as EncryptedString; + const testUnint8Array = Uint8Array.from({ + length: 32, + }) as unknown as EncryptedString; + const testMessage = 'test-message' as EncryptedString; + + it('handles non-string inputs for private keys and passwords', () => { + expect(() => + kadenaChangePassword(password, testUndefined, newPassword), + ).toThrow(); + expect(() => + kadenaChangePassword(testUndefined, testMessage, newPassword), + ).toThrow(); + expect(() => + kadenaChangePassword(password, testMessage, testUndefined), + ).toThrow(); + expect(() => + kadenaChangePassword(password, testNull, newPassword), + ).toThrow(); + expect(() => + kadenaChangePassword(testNull, testUnint8Array, newPassword), + ).toThrow(); + expect(() => + kadenaChangePassword(password, testMessage, testNull), + ).toThrow(); + expect(() => + kadenaChangePassword(password, testMessage, password), + ).toThrow(); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a6312fdc3..46ede50c7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1215,12 +1215,21 @@ importers: packages/libs/hd-wallet: dependencies: + '@kadena/client': + specifier: workspace:* + version: link:../client '@kadena/cryptography-utils': specifier: workspace:* version: link:../cryptography-utils + '@scure/bip39': + specifier: ^1.2.1 + version: 1.2.1 debug: specifier: ~4.3.4 version: 4.3.4(supports-color@5.5.0) + ed25519-keygen: + specifier: ^0.4.8 + version: 0.4.10 devDependencies: '@kadena-dev/eslint-config': specifier: workspace:* @@ -6045,6 +6054,17 @@ packages: requiresBuild: true optional: true + /@noble/curves@1.2.0: + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + dependencies: + '@noble/hashes': 1.3.2 + dev: false + + /@noble/hashes@1.3.2: + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -7615,6 +7635,17 @@ packages: string-argv: 0.3.2 dev: true + /@scure/base@1.1.3: + resolution: {integrity: sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==} + dev: false + + /@scure/bip39@1.2.1: + resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} + dependencies: + '@noble/hashes': 1.3.2 + '@scure/base': 1.1.3 + dev: false + /@sideway/address@4.1.4: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} dependencies: @@ -14194,6 +14225,15 @@ packages: safe-buffer: 5.2.1 dev: false + /ed25519-keygen@0.4.10: + resolution: {integrity: sha512-9lbxed6EL60XiL7hX6JqBOSlJuGtGTRyR+qxVURxjh2cvneofUolYB1dZ+Ek8nL6jUOR78GWJ4MJbjb6c8MNwA==} + dependencies: + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@scure/base': 1.1.3 + micro-packed: 0.3.2 + dev: false + /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: true @@ -19096,6 +19136,12 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + /micro-packed@0.3.2: + resolution: {integrity: sha512-D1Bq0/lVOzdxhnX5vylCxZpdw5LylH7Vd81py0DfRsKUP36XYpwvy8ZIsECVo3UfnoROn8pdKqkOzL7Cd82sGA==} + dependencies: + '@scure/base': 1.1.3 + dev: false + /micromark-core-commonmark@1.1.0: resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} dependencies: