From e22c5f661aaddc02dc363d911623c15d1b856512 Mon Sep 17 00:00:00 2001 From: Javad Khalilian Date: Mon, 13 Nov 2023 13:59:41 +0100 Subject: [PATCH] test(hd-wallet): update unit tests --- packages/libs/hd-wallet/src/bip44/index.ts | 4 +- .../src/bip44/kadenaGenKeypairFromSeed.ts | 21 +- .../hd-wallet/src/bip44/kadenaGetPublic.ts | 9 +- .../bip44/{mnemonic.ts => kadenaMnemonic.ts} | 5 +- .../src/bip44/{sign.ts => kadenaSign.ts} | 17 +- .../tests/kadenaGenKeypairFromSeed.test.ts | 78 +++ .../src/bip44/tests/kadenaGetPublic.test.ts | 57 ++ .../src/bip44/tests/kadenaMnemonic.test.ts | 45 ++ .../bip44/tests/kadenaSignWithKeyPair.test.ts | 36 ++ .../bip44/tests/kadenaSignWithSeed.test.ts | 30 + .../libs/hd-wallet/src/tests/index.test.ts | 562 ------------------ .../libs/hd-wallet/src/tests/keys.test.ts | 25 - .../hd-wallet/src/utils/kadenaEncryption.ts | 49 +- .../src/utils/tests/kadenaEncryption.test.ts | 161 +++++ 14 files changed, 471 insertions(+), 628 deletions(-) rename packages/libs/hd-wallet/src/bip44/{mnemonic.ts => kadenaMnemonic.ts} (89%) rename packages/libs/hd-wallet/src/bip44/{sign.ts => kadenaSign.ts} (82%) create mode 100644 packages/libs/hd-wallet/src/bip44/tests/kadenaGenKeypairFromSeed.test.ts create mode 100644 packages/libs/hd-wallet/src/bip44/tests/kadenaGetPublic.test.ts create mode 100644 packages/libs/hd-wallet/src/bip44/tests/kadenaMnemonic.test.ts create mode 100644 packages/libs/hd-wallet/src/bip44/tests/kadenaSignWithKeyPair.test.ts create mode 100644 packages/libs/hd-wallet/src/bip44/tests/kadenaSignWithSeed.test.ts delete mode 100644 packages/libs/hd-wallet/src/tests/index.test.ts delete mode 100644 packages/libs/hd-wallet/src/tests/keys.test.ts create mode 100644 packages/libs/hd-wallet/src/utils/tests/kadenaEncryption.test.ts diff --git a/packages/libs/hd-wallet/src/bip44/index.ts b/packages/libs/hd-wallet/src/bip44/index.ts index 7f3567be326..e5f99e3e15c 100644 --- a/packages/libs/hd-wallet/src/bip44/index.ts +++ b/packages/libs/hd-wallet/src/bip44/index.ts @@ -1,5 +1,5 @@ export * from './kadenaGenKeypairFromSeed'; export * from './kadenaGetPublic'; export * from './kadenaKeyPairsFromRandom'; -export * from './mnemonic'; -export * from './sign'; +export * from './kadenaMnemonic'; +export * from './kadenaSign'; diff --git a/packages/libs/hd-wallet/src/bip44/kadenaGenKeypairFromSeed.ts b/packages/libs/hd-wallet/src/bip44/kadenaGenKeypairFromSeed.ts index e1c4ffe7d88..de24504d0bb 100644 --- a/packages/libs/hd-wallet/src/bip44/kadenaGenKeypairFromSeed.ts +++ b/packages/libs/hd-wallet/src/bip44/kadenaGenKeypairFromSeed.ts @@ -1,3 +1,4 @@ +import type { EncryptedString } from '../utils/kadenaEncryption'; import { kadenaDecrypt, kadenaEncrypt } from '../utils/kadenaEncryption'; import { deriveKeyPair } from './utils/sign'; @@ -6,7 +7,7 @@ function genKeypairFromSeed( seedBuffer: Uint8Array, index: number, derivationPathTemplate: string, -): [string, string] { +): [string, EncryptedString] { const derivationPath = derivationPathTemplate.replace( '', index.toString(), @@ -15,8 +16,8 @@ function genKeypairFromSeed( const { publicKey, privateKey } = deriveKeyPair(seedBuffer, derivationPath); const encryptedPrivateKey = kadenaEncrypt( - Buffer.from(privateKey, 'hex'), password, + Buffer.from(privateKey, 'hex'), ); return [publicKey, encryptedPrivateKey]; @@ -24,17 +25,17 @@ function genKeypairFromSeed( export function kadenaGenKeypairFromSeed( password: string, - seed: string, + seed: EncryptedString, index: number, derivationPathTemplate?: string, -): [string, string]; +): [string, EncryptedString]; export function kadenaGenKeypairFromSeed( password: string, - seed: string, + seed: EncryptedString, indexRange: [number, number], derivationPathTemplate?: string, -): Array<[string, 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. @@ -48,15 +49,15 @@ export function kadenaGenKeypairFromSeed( */ export function kadenaGenKeypairFromSeed( password: string, - seed: string, + seed: EncryptedString, indexOrRange: number | [number, number], derivationPathTemplate: string = `m'/44'/626'/'/0'/0'`, -): [string, string] | Array<[string, string]> { +): [string, EncryptedString] | Array<[string, EncryptedString]> { if (typeof seed !== 'string' || seed === '') { throw new Error('No seed provided.'); } - const seedBuffer = kadenaDecrypt(seed, password); + const seedBuffer = kadenaDecrypt(password, seed); if (typeof indexOrRange === 'number') { return genKeypairFromSeed( @@ -72,7 +73,7 @@ export function kadenaGenKeypairFromSeed( throw new Error('The start index must be less than the end index.'); } - const keyPairs: [string, string][] = []; + const keyPairs: [string, EncryptedString][] = []; for (let index = startIndex; index <= endIndex; index++) { const [publicKey, encryptedPrivateKey] = genKeypairFromSeed( diff --git a/packages/libs/hd-wallet/src/bip44/kadenaGetPublic.ts b/packages/libs/hd-wallet/src/bip44/kadenaGetPublic.ts index 73e4df9ef65..19c5fcac25d 100644 --- a/packages/libs/hd-wallet/src/bip44/kadenaGetPublic.ts +++ b/packages/libs/hd-wallet/src/bip44/kadenaGetPublic.ts @@ -1,3 +1,4 @@ +import type { EncryptedString } from '../utils/kadenaEncryption'; import { kadenaDecrypt } from '../utils/kadenaEncryption'; import { deriveKeyPair } from './utils/sign'; @@ -18,14 +19,14 @@ function genPublicKeyFromSeed( export function kadenaGetPublic( password: string, - seed: string, + seed: EncryptedString, index: number, derivationPathTemplate?: string, ): string; export function kadenaGetPublic( password: string, - seed: string, + seed: EncryptedString, indexRange: [number, number], derivationPathTemplate?: string, ): string[]; @@ -42,7 +43,7 @@ export function kadenaGetPublic( */ export function kadenaGetPublic( password: string, - seed: string, + seed: EncryptedString, indexOrRange: number | [number, number], derivationPathTemplate: string = `m'/44'/626'/'/0'/0'`, ): string | string[] { @@ -50,7 +51,7 @@ export function kadenaGetPublic( throw new Error('No seed provided.'); } - const seedBuffer = kadenaDecrypt(seed, password); + const seedBuffer = kadenaDecrypt(password, seed); if (typeof indexOrRange === 'number') { return genPublicKeyFromSeed( diff --git a/packages/libs/hd-wallet/src/bip44/mnemonic.ts b/packages/libs/hd-wallet/src/bip44/kadenaMnemonic.ts similarity index 89% rename from packages/libs/hd-wallet/src/bip44/mnemonic.ts rename to packages/libs/hd-wallet/src/bip44/kadenaMnemonic.ts index 30bd4a7974a..eff4dde5755 100644 --- a/packages/libs/hd-wallet/src/bip44/mnemonic.ts +++ b/packages/libs/hd-wallet/src/bip44/kadenaMnemonic.ts @@ -1,5 +1,6 @@ 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. @@ -23,12 +24,12 @@ export async function kadenaMnemonicToSeed( password: string, mnemonic: string, // wordList: string[] = wordlist, -): Promise { +): Promise { if (bip39.validateMnemonic(mnemonic, wordlist) === false) { throw Error('Invalid mnemonic.'); } const seedBuffer = await bip39.mnemonicToSeed(mnemonic); - return kadenaEncrypt(seedBuffer, password); + return kadenaEncrypt(password, seedBuffer); } diff --git a/packages/libs/hd-wallet/src/bip44/sign.ts b/packages/libs/hd-wallet/src/bip44/kadenaSign.ts similarity index 82% rename from packages/libs/hd-wallet/src/bip44/sign.ts rename to packages/libs/hd-wallet/src/bip44/kadenaSign.ts index a9b92667f8b..1a2f1702511 100644 --- a/packages/libs/hd-wallet/src/bip44/sign.ts +++ b/packages/libs/hd-wallet/src/bip44/kadenaSign.ts @@ -1,19 +1,25 @@ 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} privateKey - The private 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, - privateKey: string, + encryptedPrivateKey: EncryptedString, ): (tx: IUnsignedCommand) => { sigs: { sig: string }[] } { - return signWithKeyPair(publicKey, privateKey); + return signWithKeyPair( + publicKey, + Buffer.from(kadenaDecrypt(password, encryptedPrivateKey)).toString('hex'), + ); } /** @@ -24,12 +30,13 @@ export function kadenaSignWithKeyPair( * @returns {Function} A function that takes an unsigned command (`IUnsignedCommand`) and returns an object with an array of signatures. */ export function kadenaSignWithSeed( - seed: Uint8Array, + password: string, + seed: EncryptedString, index: number, derivationPathTemplate: string = `m'/44'/626'/'/0'/0'`, ): (tx: IUnsignedCommand) => { sigs: { sig: string }[] } { return signWithSeed( - seed, + kadenaDecrypt(password, seed), derivationPathTemplate.replace('', index.toString()), ); } diff --git a/packages/libs/hd-wallet/src/bip44/tests/kadenaGenKeypairFromSeed.test.ts b/packages/libs/hd-wallet/src/bip44/tests/kadenaGenKeypairFromSeed.test.ts new file mode 100644 index 00000000000..70395436eb4 --- /dev/null +++ b/packages/libs/hd-wallet/src/bip44/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/bip44/tests/kadenaGetPublic.test.ts b/packages/libs/hd-wallet/src/bip44/tests/kadenaGetPublic.test.ts new file mode 100644 index 00000000000..ea878b7380c --- /dev/null +++ b/packages/libs/hd-wallet/src/bip44/tests/kadenaGetPublic.test.ts @@ -0,0 +1,57 @@ +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); + expect(publicKey).toBe( + '43726c4a2e7b03fa5d23635307e5b130baf8b261e1081c099a2b43db1d4554cc', + ); + publicKey = kadenaGetPublic(password, seed, 1); + expect(publicKey).toBe( + '3f53dfad097fdf8501c32b275e109980ed7121866a63ca34bb035c4a2e41a265', + ); + publicKey = kadenaGetPublic(password, seed, 2); + expect(publicKey).toBe( + '3021bcfa703cc4fac007ab4c5050df5c0b8ca7d655ea80c84af9ea5e43ecf0ff', + ); + }); +}); diff --git a/packages/libs/hd-wallet/src/bip44/tests/kadenaMnemonic.test.ts b/packages/libs/hd-wallet/src/bip44/tests/kadenaMnemonic.test.ts new file mode 100644 index 00000000000..fa61a8e0b51 --- /dev/null +++ b/packages/libs/hd-wallet/src/bip44/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/bip44/tests/kadenaSignWithKeyPair.test.ts b/packages/libs/hd-wallet/src/bip44/tests/kadenaSignWithKeyPair.test.ts new file mode 100644 index 00000000000..0e838b30b91 --- /dev/null +++ b/packages/libs/hd-wallet/src/bip44/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.only('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/bip44/tests/kadenaSignWithSeed.test.ts b/packages/libs/hd-wallet/src/bip44/tests/kadenaSignWithSeed.test.ts new file mode 100644 index 00000000000..9d4fbe33b1c --- /dev/null +++ b/packages/libs/hd-wallet/src/bip44/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/tests/index.test.ts b/packages/libs/hd-wallet/src/tests/index.test.ts deleted file mode 100644 index 2a601d37082..00000000000 --- a/packages/libs/hd-wallet/src/tests/index.test.ts +++ /dev/null @@ -1,562 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - kadenaChangePassword, - kadenaDecryptPrivateKey, - kadenaGenKeypair, - kadenaGenMnemonic, - kadenaGenSeedFromMnemonic, - kadenaGetPublic, - kadenaRestoreSeedBufferFromSeed, - kadenaSignWithKeyPair, - kadenaSignWithSeed, -} from '..'; - -import type { IUnsignedCommand } from '@kadena/client'; - -import { kadenaDecrypt, kadenaEncrypt } from '../utils/kadenaEncryption'; -import { deriveKeyPair, uint8ArrayToHex } from '../utils/sign'; - -const toHexStr = (bytes: Uint8Array) => Buffer.from(bytes).toString('hex'); - -describe('kadenaGenMnemonic', () => { - it('should generate a valid mnemonic', () => { - const mnemonic = kadenaGenMnemonic(); - expect(mnemonic.split(' ')).toHaveLength(12); - }); -}); - -describe('kadenaGenSeedFromMnemonic', () => { - it('should convert mnemonic to seed buffer and encrypt seed with a password', async () => { - const mnemonic = kadenaGenMnemonic(); - const password = 'password'; - const { seedBuffer, seed } = await kadenaGenSeedFromMnemonic( - mnemonic, - password, - ); - expect(seedBuffer).toBeInstanceOf(Uint8Array); - - expect(typeof seed).toBe('string'); // Check if the seed is a string, indicating it has been encrypted - expect(seed).not.toBe(toHexStr(seedBuffer)); // Additional checks to ensure seed is not the same as seedBuffer - }); - - it('should convert mnemonic to seed buffer and encode seed without a password', async () => { - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer, seed } = await kadenaGenSeedFromMnemonic(mnemonic); - expect(seedBuffer).toBeInstanceOf(Uint8Array); - - expect(typeof seed).toBe('string'); // Check if the seed is a string, indicating it has been encoded - expect(seed).toBe(Buffer.from(seedBuffer).toString('base64')); // Check that the encoded seed is the base64 version of the seedBuffer - }); - - it('should throw an error for an invalid mnemonic', async () => { - const invalidMnemonic = 'this is not a valid mnemonic'; - - await expect( - kadenaGenSeedFromMnemonic(invalidMnemonic), - ).rejects.toThrowError('Invalid mnemonic.'); - }); - - it('should throw an error when mnemonic is empty', async () => { - const emptyMnemonic = ''; - - await expect(kadenaGenSeedFromMnemonic(emptyMnemonic)).rejects.toThrowError( - 'Invalid mnemonic.', - ); - }); -}); - -describe('kadenaGenKeypair', () => { - it('should generate an encrypted keypair from the seedBuffer when a password is provided', async () => { - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - const password = 'correcthorsebatterystaple'; - const [publicKey, encryptedPrivateKey] = kadenaGenKeypair( - seedBuffer, - 0, - password, - ); - - expect(publicKey).toHaveLength(64); - expect(typeof encryptedPrivateKey).toBe('string'); // Checks if privateKey is a string, thus encrypted - }); - - it('should generate a non-encrypted keypair from the seedBuffer when no password is provided', async () => { - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - const [publicKey, privateKey] = kadenaGenKeypair(seedBuffer, 0); - - expect(publicKey).toHaveLength(64); - expect(typeof privateKey).toBe('string'); // The privateKey should be a hex string if not encrypted - expect(privateKey).toMatch(/^[a-fA-F0-9]+$/); // Reg-ex for hex string - }); - - it('should generate a keypair from the seedBuffer', async () => { - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - const [publicKey, privateKey] = kadenaGenKeypair(seedBuffer, 0); - expect(publicKey).toHaveLength(64); - expect(privateKey).toHaveLength(64); - }); - - it('should generate a range of keypairs from the seedBuffer', async () => { - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - const keyPairs = kadenaGenKeypair(seedBuffer, [0, 3]); - expect(keyPairs).toHaveLength(4); - keyPairs.forEach(([publicKey, privateKey]) => { - expect(publicKey).toHaveLength(64); - expect(privateKey).toHaveLength(64); - }); - }); - - it('should throw an error for out-of-bounds index values', async () => { - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - const outOfBoundsIndex = -1; - - expect(() => { - kadenaGenKeypair(seedBuffer, outOfBoundsIndex); - }).toThrowError('Invalid child index: -1'); - }); - - it('should throw an error for out-of-bounds index values when a password is provided', async () => { - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - const outOfBoundsIndex = -1; - const password = 'peanutbutterjellytime'; - - expect(() => { - kadenaGenKeypair(seedBuffer, outOfBoundsIndex, password); - }).toThrowError('Invalid child index: -1'); - }); - - it('should throw an error when the seed buffer is empty', async () => { - const emptySeedBuffer = new Uint8Array([]); - - expect(() => { - kadenaGenKeypair(emptySeedBuffer, 0); - }).toThrowError( - 'HDKey: wrong seed length=0. Should be between 128 and 512 bits; 256 bits is advised)', - ); - }); - - it('should handle the highest non-hardened index without throwing errors', async () => { - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(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(() => { - kadenaGenKeypair(seedBuffer, highestNonHardenedIndex); - }).not.toThrow(); - }); -}); - -describe('kadenaGetPublic', () => { - it('should retrieve the public key from seedBuffer and default to index 0 when no index is given', async () => { - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - - const publicKeyDefault = kadenaGetPublic(seedBuffer); - const publicKeyIndex0 = kadenaGetPublic(seedBuffer, 0); - - expect(publicKeyDefault).toHaveLength(64); - expect(publicKeyIndex0).toHaveLength(64); - - // Check that the public key obtained without specifying an index - // is the same as the one obtained when index 0 is explicitly provided - expect(publicKeyDefault).toBe(publicKeyIndex0); - }); - - it('should retrieve the public key from seedBuffer and index', async () => { - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - const publicKey = kadenaGetPublic(seedBuffer, 0); - expect(publicKey).toHaveLength(64); - }); - - it('should retrieve distinct public keys from seedBuffer for different indexes', async () => { - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - - const indexes = [0, 1, 2, 3, 4]; - const publicKeys = indexes.map((index) => - kadenaGetPublic(seedBuffer, 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); - }); -}); - -describe('kadenaRestoreSeedBufferFromSeed', () => { - it('should restore the seed buffer from an encrypted/encoded seed', async () => { - const mnemonic = kadenaGenMnemonic(); - const password = 'password'; - const { seed } = await kadenaGenSeedFromMnemonic(mnemonic, password); - const seedBuffer = kadenaRestoreSeedBufferFromSeed(seed, password); - expect(seedBuffer).toBeInstanceOf(Uint8Array); - }); - - it('should restore the seed buffer from an encrypted seed using a password', async () => { - const mnemonic = kadenaGenMnemonic(); - const password = 'password'; - - const { seedBuffer: originalSeedBuffer, seed: encryptedSeed } = - await kadenaGenSeedFromMnemonic(mnemonic, password); - - const restoredSeedBuffer = kadenaRestoreSeedBufferFromSeed( - encryptedSeed, - password, - ); - expect(restoredSeedBuffer).toBeInstanceOf(Uint8Array); - - expect(Buffer.from(restoredSeedBuffer).toString('hex')).toBe( - Buffer.from(originalSeedBuffer).toString('hex'), - ); // Check if the restored seed buffer matches the original seed buffer - }); - - it('should restore the seed buffer from an encoded seed without a password', async () => { - const mnemonic = kadenaGenMnemonic(); - - const { seedBuffer: originalSeedBuffer, seed: encodedSeed } = - await kadenaGenSeedFromMnemonic(mnemonic); - - const restoredSeedBuffer = kadenaRestoreSeedBufferFromSeed(encodedSeed); - expect(restoredSeedBuffer).toBeInstanceOf(Uint8Array); - - expect(Buffer.from(restoredSeedBuffer).toString('hex')).toBe( - Buffer.from(originalSeedBuffer).toString('hex'), - ); // Check if the restored seed buffer matches the original seed buffer - }); - - it('should fail to restore the seed buffer with an incorrect password', async () => { - const mnemonic = kadenaGenMnemonic(); - const correctPassword = 'password'; - const wrongPassword = 'wrongPassword'; - const { seed } = await kadenaGenSeedFromMnemonic(mnemonic, correctPassword); - - expect(() => { - kadenaRestoreSeedBufferFromSeed(seed, wrongPassword); - }).toThrowError('Failed to decrypt seed.'); - }); - - it('should throw an error when no seed is provided', () => { - expect(() => { - kadenaRestoreSeedBufferFromSeed(''); - }).toThrowError('No seed provided.'); - }); -}); - -describe('kadenaDecryptPrivateKey', () => { - it('should correctly decrypt a previously encrypted private key', async () => { - const password = 'test-password'; - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - - const [, encryptedPrivateKey] = kadenaGenKeypair( - seedBuffer, - 0, - password, - ) as [string, string]; - - const decryptedKey = kadenaDecryptPrivateKey(encryptedPrivateKey, password); - - const originalPrivateKey = Buffer.from( - deriveKeyPair(seedBuffer, 0).privateKey, - 'hex', - ); - - expect(decryptedKey).toEqual(new Uint8Array(originalPrivateKey)); - }); - - it('should throw an error when the incorrect password is provided', async () => { - const correctPassword = 'correct-password'; - const wrongPassword = 'wrong-password'; - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - - // Generate and encrypt the key pair - const [, encryptedPrivateKey] = kadenaGenKeypair( - seedBuffer, - 0, - correctPassword, - ) as [string, string]; - - // Attempt to decrypt with the wrong password should fail - expect(() => { - kadenaDecryptPrivateKey(encryptedPrivateKey, wrongPassword); - }).toThrow('Decryption failed'); - }); - - it('should throw an error if the encrypted key is corrupted', () => { - const password = 'test-password'; - const corruptedEncryptedPrivateKey = 'corrupted-data'; - - expect(() => { - kadenaDecryptPrivateKey(corruptedEncryptedPrivateKey, password); - }).toThrow('Decryption failed'); - }); - - it('should throw an error if the encrypted private key or password is empty', () => { - const password = 'test-password'; - const emptyEncryptedPrivateKey = ''; - - expect(() => { - kadenaDecryptPrivateKey(emptyEncryptedPrivateKey, password); - }).toThrow('Decryption failed'); - - expect(() => { - kadenaDecryptPrivateKey(emptyEncryptedPrivateKey, ''); - }).toThrow('Decryption failed'); - }); - - it('should handle passwords with special characters', async () => { - const specialCharPassword = 'p@ssw0rd!#%&'; - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - const [, encryptedPrivateKey] = kadenaGenKeypair( - seedBuffer, - 0, - specialCharPassword, - ) as [string, string]; - - const decryptedKey = kadenaDecryptPrivateKey( - encryptedPrivateKey, - specialCharPassword, - ); - expect(decryptedKey).toBeInstanceOf(Uint8Array); - }); - - it('should handle extremely long passwords', async () => { - const longPassword = 'p'.repeat(1000); - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - - const keyPair = kadenaGenKeypair(seedBuffer, 0, longPassword) as [ - string, - string, - ]; // When using an index, kadenaGenKeypair returns a single key pair, not an array - - const [, encryptedPrivateKey] = keyPair; - - const decryptedKey = kadenaDecryptPrivateKey( - encryptedPrivateKey, - longPassword, - ); - expect(decryptedKey).toBeInstanceOf(Uint8Array); - }); - - it('should handle unicode characters in passwords', async () => { - const unicodePassword = '密码'; // 'password' in Chinese - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - const [, encryptedPrivateKey] = kadenaGenKeypair( - seedBuffer, - 0, - unicodePassword, - ) as [string, string]; - - const decryptedKey = kadenaDecryptPrivateKey( - encryptedPrivateKey, - unicodePassword, - ); - expect(decryptedKey).toBeInstanceOf(Uint8Array); - }); -}); - -describe('kadenaChangePassword', () => { - const privateKey = Uint8Array.from({ length: 32 }, () => - Math.floor(Math.random() * 256), - ); - const oldPassword = 'oldPassword123'; - const newPassword = 'newPassword123'; - - // Helper function to simulate encrypting a private key - const encryptAndChangePassword = ( - privateKey: Uint8Array, - oldPassword: string, - newPassword: string, - ) => { - const encryptedPrivateKey = kadenaEncrypt(privateKey, oldPassword); - return kadenaChangePassword(encryptedPrivateKey, oldPassword, newPassword); - }; - - it('changes the password successfully and allows decryption with the new password', () => { - const newEncryptedPrivateKey = encryptAndChangePassword( - privateKey, - oldPassword, - newPassword, - ); - const decryptedWithNewPassword = kadenaDecryptPrivateKey( - newEncryptedPrivateKey, - newPassword, - ); - expect(decryptedWithNewPassword).toEqual(privateKey); - }); - - it('throws an error when the old password is incorrect', () => { - const encryptedPrivateKey = kadenaEncrypt(privateKey, oldPassword); - const incorrectOldPassword = 'wrongPassword'; - const changePasswordAttempt = () => - kadenaChangePassword( - encryptedPrivateKey, - incorrectOldPassword, - 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 newEncryptedPrivateKey = encryptAndChangePassword( - privateKey, - oldPassword, - newPassword, - ); - const decryptWithOldPasswordAttempt = () => - kadenaDecrypt(newEncryptedPrivateKey, oldPassword); - expect(decryptWithOldPasswordAttempt).toThrow(); - }); - - it('fails when the new password is an empty string', () => { - const encryptedPrivateKey = kadenaEncrypt(privateKey, oldPassword); - const newPassword = ''; - const changePasswordAttempt = () => - kadenaChangePassword(encryptedPrivateKey, oldPassword, newPassword); - - expect(changePasswordAttempt).toThrow(); - }); - - it('throws an error when the encrypted private key is in an incorrect format', () => { - const incorrectFormatPrivateKey = 'not a valid encrypted key'; - const changePasswordAttempt = () => - kadenaChangePassword(incorrectFormatPrivateKey, oldPassword, newPassword); - - expect(changePasswordAttempt).toThrow(); - }); - - describe('kadenaChangePassword with kadenaGenKeypair', () => { - const seedBuffer = Uint8Array.from({ length: 32 }, () => - Math.floor(Math.random() * 256), - ); // Simulated seed buffer - const index = 0; // The index to generate the key pair - const oldPassword = 'oldPassword123'; - const newPassword = 'newPassword123'; - - it('generates a key pair, changes the password, and allows decryption with the new password', () => { - const [, encryptedPrivateKey] = kadenaGenKeypair( - seedBuffer, - index, - oldPassword, - ) as string[]; // Generate the key pair with the old password - - const newEncryptedPrivateKey = kadenaChangePassword( - encryptedPrivateKey, - oldPassword, - newPassword, - ); // Change the password of the encrypted private key - - const decryptedWithNewPassword = kadenaDecrypt( - newEncryptedPrivateKey, - newPassword, - ); // Decrypt the new encrypted private key with the new password - - const decryptedPrivateKeyHex = uint8ArrayToHex( - new Uint8Array(decryptedWithNewPassword), - ); // Convert the decrypted Uint8Array back to a hex string to compare with the original - - const { privateKey: originalPrivateKeyHex } = deriveKeyPair( - seedBuffer, - index, - ); - - expect(decryptedPrivateKeyHex).toEqual(originalPrivateKeyHex); // Compare the decrypted private key with the original - }); - }); - - const testUndefined = undefined as unknown as string; - const testNull = null as unknown as string; - const testUnint8Array = Uint8Array.from({ length: 32 }) as unknown as string; - - it('handles non-string inputs for private keys and passwords', () => { - expect(() => - kadenaChangePassword(testUndefined, oldPassword, newPassword), - ).toThrow(); - expect(() => - kadenaChangePassword(testUnint8Array, testUndefined, newPassword), - ).toThrow(); - expect(() => - kadenaChangePassword(testUnint8Array, oldPassword, testUndefined), - ).toThrow(); - expect(() => - kadenaChangePassword(testNull, oldPassword, newPassword), - ).toThrow(); - expect(() => - kadenaChangePassword(testUnint8Array, testNull, newPassword), - ).toThrow(); - expect(() => - kadenaChangePassword(testUnint8Array, oldPassword, testNull), - ).toThrow(); - }); -}); - -describe.only('kadenaSignWithKeyPair', async () => { - const mnemonic = kadenaGenMnemonic(); - const { seedBuffer } = await kadenaGenSeedFromMnemonic(mnemonic); - - const [publicKey, privateKey] = kadenaGenKeypair(seedBuffer, 0) as [ - string, - string, - ]; - - const mockUnsignedCommand: IUnsignedCommand = { - cmd: '{"command":"value"}', - hash: 'kadena-hash', - sigs: [], - }; - it('should sign a transaction with a public and private key pair', () => { - const signer = kadenaSignWithKeyPair(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(); - }); -}); - -describe('kadenaSignWithSeed', () => { - const mockSeed = new Uint8Array(32); - const mockIndex = 0; - const mockUnsignedCommand: IUnsignedCommand = { - cmd: '{"commands":"value"}', - hash: 'kadena-hash', - sigs: [], - }; - - it('should sign a transaction with a seed and index', () => { - const signer = kadenaSignWithSeed(mockSeed, mockIndex); - 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/tests/keys.test.ts b/packages/libs/hd-wallet/src/tests/keys.test.ts deleted file mode 100644 index 0246b252a5c..00000000000 --- a/packages/libs/hd-wallet/src/tests/keys.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { kadenaGetPublic, kadenaMnemonicToSeed } from '..'; - -describe('kadenaGetPublic', async () => { - it('should get the public key', async () => { - const password = 'pass'; - const seed = await kadenaMnemonicToSeed( - password, - 'coyote utility final warfare thumb symbol mule scale final nominee behave crumble', - ); - console.log(seed); - let publicKey = kadenaGetPublic(password, seed, 0); - expect(publicKey).toBe( - '43726c4a2e7b03fa5d23635307e5b130baf8b261e1081c099a2b43db1d4554cc', - ); - publicKey = kadenaGetPublic(password, seed, 1); - expect(publicKey).toBe( - '3f53dfad097fdf8501c32b275e109980ed7121866a63ca34bb035c4a2e41a265', - ); - publicKey = kadenaGetPublic(password, seed, 2); - expect(publicKey).toBe( - '3021bcfa703cc4fac007ab4c5050df5c0b8ca7d655ea80c84af9ea5e43ecf0ff', - ); - }); -}); diff --git a/packages/libs/hd-wallet/src/utils/kadenaEncryption.ts b/packages/libs/hd-wallet/src/utils/kadenaEncryption.ts index 1e8cc00ad4a..df2e2b377f3 100644 --- a/packages/libs/hd-wallet/src/utils/kadenaEncryption.ts +++ b/packages/libs/hd-wallet/src/utils/kadenaEncryption.ts @@ -1,19 +1,25 @@ 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 + * @returns {string} The encrypted string */ - -export function kadenaEncrypt(message: Uint8Array, password: string): 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 [salt, iv, tag, cipherText].map((x) => x.toString('base64')).join('.'); + return Buffer.from( + [salt, iv, tag, cipherText].map((x) => x.toString('base64')).join('.'), + ).toString('base64') as EncryptedString; } /** @@ -27,12 +33,19 @@ export function kadenaEncrypt(message: Uint8Array, password: string): string { * @throws {Error} Throws an error if decryption fails. */ export function kadenaDecrypt( - encryptedData: string, password: string, + encryptedData: EncryptedString, ): Uint8Array { - const [saltBase64, ivBase64, tagBase64, encryptedBase64] = - encryptedData.split('.'); - console.log({ saltBase64, ivBase64, tagBase64, encryptedBase64 }); + // 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'); @@ -45,33 +58,33 @@ export function kadenaDecrypt( if (!decrypted) { throw new Error('Decryption failed'); } - return new Uint8Array(decrypted); + return decrypted; } /** * Changes the password of an encrypted data. * * @param {string} privateKey - The encrypted private key as a Base64 encoded string. - * @param {string} oldPassword - The current password used to encrypt the private key. + * @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( - encryptedData: string, - oldPassword: string, + password: string, + encryptedData: EncryptedString, newPassword: string, -): string { - if (typeof oldPassword !== 'string' || typeof newPassword !== 'string') { +): EncryptedString { + if (typeof password !== 'string' || typeof newPassword !== 'string') { throw new Error('The old and new passwords must be strings.'); } - if (oldPassword === '') { + if (password === '') { throw new Error('The old password cannot be empty.'); } if (newPassword === '') { throw new Error('The new password cannot be empty.'); } - if (oldPassword === newPassword) { + if (password === newPassword) { throw new Error( 'The new password must be different from the old password.', ); @@ -79,7 +92,7 @@ export function kadenaChangePassword( let decryptedPrivateKey: Uint8Array; try { - decryptedPrivateKey = kadenaDecrypt(encryptedData, oldPassword); + decryptedPrivateKey = kadenaDecrypt(password, encryptedData); } catch (error) { throw new Error( `Failed to decrypt the private key with the old password: ${error.message}`, @@ -87,7 +100,7 @@ export function kadenaChangePassword( } try { - return kadenaEncrypt(decryptedPrivateKey, newPassword); + 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 00000000000..6110214beb3 --- /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(); + }); +});