From 2b1eb7292f28807b95666b5351d50a9ee46ad5f2 Mon Sep 17 00:00:00 2001 From: morizon Date: Wed, 25 Dec 2024 18:05:51 +0800 Subject: [PATCH] fix: secret unit tests --- .../__snapshots__/secret.test.ts.snap.web | 49 + .../core/src/secret/__tests__/secret.test.ts | 1311 +++++++++++++++++ packages/core/src/secret/index.ts | 17 +- 3 files changed, 1373 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/secret/__tests__/__snapshots__/secret.test.ts.snap.web create mode 100644 packages/core/src/secret/__tests__/secret.test.ts diff --git a/packages/core/src/secret/__tests__/__snapshots__/secret.test.ts.snap.web b/packages/core/src/secret/__tests__/__snapshots__/secret.test.ts.snap.web new file mode 100644 index 00000000000..d342ca78c8d --- /dev/null +++ b/packages/core/src/secret/__tests__/__snapshots__/secret.test.ts.snap.web @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Secret Module Tests CKDPub should match snapshot for public key derivation 1`] = ` +{ + "chainCode": "ebe55dbe2a6484cbe2eafe4de05680329ca866715195dceefbc932cc7e4e7d2a", + "key": "0312d9f38f8b50099f57432befc4bb4ea304f3ce8af076558397d750fc0625d56d", +} +`; + +exports[`Secret Module Tests N should match snapshot 1`] = ` +{ + "chainCode": "0123456789abcdef0123456789abcdef", + "key": "0358e7e5314d5ac609228be043bd0af8fa463fd38b2398a71867c0d9f2e186a339", +} +`; + +exports[`Secret Module Tests compressPublicKey should match snapshot for compressed public key 1`] = `"03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7"`; + +exports[`Secret Module Tests decryptImportedCredential should match snapshot for decrypted credential 1`] = ` +{ + "privateKey": "0123456789abcdef", +} +`; + +exports[`Secret Module Tests decryptRevealableSeed should match snapshot for decrypted seed 1`] = ` +{ + "entropyWithLangPrefixed": "00112233445566778899aabbccddeeff", + "seed": "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", +} +`; + +exports[`Secret Module Tests decryptVerifyString should match snapshot for decrypted string 1`] = `"OneKey"`; + +exports[`Secret Module Tests encryptImportedCredential should match snapshot 1`] = `"|PK|94b51c8f77aa44bdf1a6071872cd89aae44fba848cf8a50c28280a9b79a56b24d3ebac3b568ef4e5369441a40eee4a246339119b757a9388f2d5cf0f71868a94659122fa0911b8078ee92275efb0956b63f2e954c77dab9413e439124c91183e"`; + +exports[`Secret Module Tests encryptRevealableSeed should match snapshot 1`] = `"|RP|94b51c8f77aa44bdf1a6071872cd89aae44fba848cf8a50c28280a9b79a56b24d3ebac3b568ef4e5369441a40eee4a24efed4e80676c0b6431053a43ce9e9190c827886c3faff68acbb63cda6e554395eeef26421bf0c40bec7c5555c402a277f9756c4bbefc940e0fee40d93c06040aaf75820eb42c40c838350b80e2b415eec8a9ae39ed7a7ac17a610f647f8afd676f7421fa403dc0dfe1978ff45ba6aa54"`; + +exports[`Secret Module Tests encryptVerifyString should match snapshot 1`] = `"|VS|94b51c8f77aa44bdf1a6071872cd89aae44fba848cf8a50c28280a9b79a56b24d3ebac3b568ef4e5369441a40eee4a244caaaee548b7845b6fe0896c5453d993"`; + +exports[`Secret Module Tests fixV4VerifyStringToV5 should match snapshot 1`] = `"|VS|test123"`; + +exports[`Secret Module Tests generateMasterKeyFromSeed should match snapshot 1`] = ` +{ + "chainCode": "15aa79fafb75cffcfda898a6b92f6e13d3693ddf269a0cf482cbe10c744f712c", + "key": "94b51c8f77aa44bdf1a6071872cd89aae44fba848cf8a50c28280a9b79a56b24d3ebac3b568ef4e5369441a40eee4a2465a263f7abc8dd3c3722cae010557e2ab290ffc250591032ae9578819c4305b4f66dd012a39a69d2397693a6acd65c4a", +} +`; + +exports[`Secret Module Tests mnemonicFromEntropyAsync should match snapshot 1`] = `"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote"`; diff --git a/packages/core/src/secret/__tests__/secret.test.ts b/packages/core/src/secret/__tests__/secret.test.ts new file mode 100644 index 00000000000..5a4a43d9dc0 --- /dev/null +++ b/packages/core/src/secret/__tests__/secret.test.ts @@ -0,0 +1,1311 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { Buffer } from 'buffer'; + +import { DEFAULT_VERIFY_STRING } from '@onekeyhq/shared/src/consts/dbConsts'; +import { IncorrectPassword } from '@onekeyhq/shared/src/errors'; + +import { + CKDPriv, + CKDPub, + N, + batchGetPrivateKeys, + batchGetPublicKeys, + batchGetPublicKeysAsync, + compressPublicKey, + decrypt, + decryptImportedCredential, + decryptRevealableSeed, + decryptVerifyString, + encrypt, + encryptImportedCredential, + encryptRevealableSeed, + encryptVerifyString, + fixV4VerifyStringToV5, + generateMasterKeyFromSeed, + generateRootFingerprintHexAsync, + mnemonicFromEntropyAsync, + mnemonicToRevealableSeed, + mnemonicToSeedAsync, + publicFromPrivate, +} from '..'; + +import type { ICoreImportedCredential, ICurveName } from '../../types'; +import type { IBip32ExtendedKey } from '../bip32'; +import type { IBip39RevealableSeed } from '../bip39'; + +/* +yarn test packages/core/src/secret/__tests__/secret.test.ts +*/ + +// Mock crypto for deterministic encryption outputs +jest.mock('crypto', () => ({ + ...jest.requireActual('crypto'), + randomBytes: jest.fn().mockImplementation((size: number) => { + // Return specific bytes for deterministic encryption outputs + if (size === 32) { + return Buffer.from( + '94b51c8f77aa44bdf1a6071872cd89aae44fba848cf8a50c28280a9b79a56b24', + 'hex', + ); + } + if (size === 16) { + return Buffer.from('d3ebac3b568ef4e5369441a40eee4a24', 'hex'); + } + if (size === 4) { + return Buffer.from('0efcb8ef', 'hex'); + } + return Buffer.alloc(size, 0xde); + }), +})); + +describe('Secret Module Tests', () => { + const TEST_PASSWORD = 'password123'; + const TEST_MNEMONIC = + 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote'; + + describe('CKDPriv', () => { + const testPassword = 'password123'; + const testSeed = Buffer.from('000102030405060708090a0b0c0d0e0f', 'hex'); + + // Test vectors based on BIP32 test vectors + const testMasterKey = { + key: Buffer.from( + 'e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35', + 'hex', + ), + chainCode: Buffer.from( + '873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508', + 'hex', + ), + }; + + it('should derive normal child key for secp256k1', () => { + const encryptedParent = { + key: encrypt(testPassword, testMasterKey.key), + chainCode: testMasterKey.chainCode, + }; + + const childKey = CKDPriv('secp256k1', encryptedParent, 0, testPassword); + + // Decrypt and verify the derived key + const decryptedKey = decrypt(testPassword, childKey.key); + expect(decryptedKey.length).toBe(32); + expect(childKey.chainCode.length).toBe(32); + + // Verify we can generate valid public key from it + const publicKey = publicFromPrivate( + 'secp256k1', + childKey.key, + testPassword, + ); + expect(publicKey.length).toBeGreaterThan(0); + }); + + it('should derive hardened child key for secp256k1', () => { + const encryptedParent = { + key: encrypt(testPassword, testMasterKey.key), + chainCode: testMasterKey.chainCode, + }; + + const hardenedIndex = 0x80_00_00_00; // 2^31 + const childKey = CKDPriv( + 'secp256k1', + encryptedParent, + hardenedIndex, + testPassword, + ); + + const decryptedKey = decrypt(testPassword, childKey.key); + expect(decryptedKey.length).toBe(32); + expect(childKey.chainCode.length).toBe(32); + }); + + it('should only support hardened derivation for ed25519', () => { + const encryptedParent = { + key: encrypt(testPassword, testMasterKey.key), + chainCode: testMasterKey.chainCode, + }; + + // Normal index should throw + expect(() => { + CKDPriv('ed25519', encryptedParent, 0, testPassword); + }).toThrow('Only hardened CKDPriv is supported for ed25519'); + + // Hardened index should work + const hardenedIndex = 0x80_00_00_00; + const childKey = CKDPriv( + 'ed25519', + encryptedParent, + hardenedIndex, + testPassword, + ); + expect(childKey.key.length).toBe(96); + expect(childKey.chainCode.length).toBe(32); + }); + + it('should throw error for invalid index', () => { + const encryptedParent = { + key: encrypt(testPassword, testMasterKey.key), + chainCode: testMasterKey.chainCode, + }; + + expect(() => { + CKDPriv('secp256k1', encryptedParent, -1, testPassword); + }).toThrow('Overflowed.'); + + expect(() => { + CKDPriv('secp256k1', encryptedParent, 2 ** 32, testPassword); + }).toThrow('Overflowed.'); + + expect(() => { + CKDPriv('secp256k1', encryptedParent, 1.5, testPassword); + }).toThrow('Invalid index'); + }); + + it('should derive child key for nistp256', () => { + const encryptedParent = { + key: encrypt(testPassword, testMasterKey.key), + chainCode: testMasterKey.chainCode, + }; + + const childKey = CKDPriv('nistp256', encryptedParent, 0, testPassword); + + const decryptedKey = decrypt(testPassword, childKey.key); + expect(decryptedKey.length).toBe(32); + expect(childKey.chainCode.length).toBe(32); + + // Verify chain code is different from parent + expect(childKey.chainCode).not.toEqual(testMasterKey.chainCode); + + // Verify we can derive multiple children + const secondChild = CKDPriv('nistp256', childKey, 1, testPassword); + expect(secondChild.key.length).toBeGreaterThan(0); + expect(secondChild.chainCode.length).toBe(32); + }); + + it('should derive child private keys correctly using CKDPriv', () => { + const testMnemonic = + 'test test test test test test test test test test test junk'; + const rs = mnemonicToRevealableSeed(testMnemonic); + const hdCredential = encryptRevealableSeed({ + rs, + password: testPassword, + }); + + // Get seed from hdCredential + const { seed } = decryptRevealableSeed({ + rs: hdCredential, + password: testPassword, + }); + const seedBuffer = Buffer.from(seed, 'hex'); + + // Create revealable seed and encrypt it + const revealableSeed = { + entropyWithLangPrefixed: seedBuffer.toString('hex'), + seed, + }; + const encryptedSeed = encryptRevealableSeed({ + rs: revealableSeed, + password: testPassword, + }); + + // Generate master key from seed + const encryptedMasterKey = generateMasterKeyFromSeed( + 'secp256k1', + encryptedSeed, + testPassword, + ); + + // Decrypt the master key for CKDPriv + const masterKey = { + key: decrypt(testPassword, encryptedMasterKey.key), + chainCode: encryptedMasterKey.chainCode, + }; + + // Verify key lengths + expect(masterKey.key.length).toBe(32); + expect(masterKey.chainCode.length).toBe(32); + + const childKey = CKDPriv( + 'secp256k1', + encryptedMasterKey, + 0, + testPassword, + ); + expect(childKey).toBeDefined(); + expect(childKey.key).toBeInstanceOf(Buffer); + expect(childKey.chainCode).toBeInstanceOf(Buffer); + + // Test hardened index derivation + const hardenedIndex = 2_147_483_648; // 2^31, first hardened index + const hardenedChild = CKDPriv( + 'secp256k1', + encryptedMasterKey, + hardenedIndex, + testPassword, + ); + expect(hardenedChild).toBeDefined(); + expect(hardenedChild.key).toBeInstanceOf(Buffer); + expect(hardenedChild.chainCode).toBeInstanceOf(Buffer); + + // Test with different curves + const nistMasterKey = { + key: encrypt( + testPassword, + Buffer.from( + '612091aaa12e22dd2abef664f8a01a82cae99ad7441b7ef8110424915c268bc2', + 'hex', + ), + ), + chainCode: Buffer.from( + 'beeb672fe4621673f722f38529c07392fecaa61015c80c34f29ce8b41b3cb6ea', + 'hex', + ), + }; + const nistChild = CKDPriv('nistp256', nistMasterKey, 0, testPassword); + expect(nistChild).toBeDefined(); + + const edMasterKey = { + key: encrypt( + testPassword, + Buffer.from('0123456789abcdef0123456789abcdef', 'hex'), + ), + chainCode: Buffer.from('0123456789abcdef0123456789abcdef', 'hex'), + }; + // ed25519 only supports hardened derivation + const edChild = CKDPriv( + 'ed25519', + edMasterKey, + hardenedIndex, + testPassword, + ); + expect(edChild).toBeDefined(); + + // Test error cases + expect(() => CKDPriv('ed25519', edMasterKey, 0, testPassword)).toThrow(); + expect(() => + CKDPriv('invalid-curve' as any, encryptedMasterKey, 0, testPassword), + ).toThrow(); + expect(() => + CKDPriv('secp256k1', encryptedMasterKey, -1, testPassword), + ).toThrow(); + }); + + it('should handle async mnemonic and seed operations', async () => { + const password = 'password123'; + const entropy = Buffer.from('000102030405060708090a0b0c0d0e0f', 'hex'); + + // Create encrypted revealable seed from mnemonic + const testMnemonic = + 'test test test test test test test test test test test junk'; + const rs = mnemonicToRevealableSeed(testMnemonic, 'optional passphrase'); + const hdCredential = encryptRevealableSeed({ + rs, + password, + }); + + // Test mnemonicFromEntropyAsync + const mnemonic = await mnemonicFromEntropyAsync({ + hdCredential, + password, + }); + expect(typeof mnemonic).toBe('string'); + expect(mnemonic.split(' ').length).toBe(12); // 12 words for 128-bit entropy + + // Test mnemonicToSeedAsync + const seedBuffer = await mnemonicToSeedAsync({ + mnemonic, + passphrase: 'optional passphrase', + }); + expect(seedBuffer).toBeInstanceOf(Buffer); + expect(seedBuffer.length).toBe(64); // 512 bits + + // Test generateRootFingerprintHexAsync + const fingerprint = await generateRootFingerprintHexAsync({ + curveName: 'secp256k1', + hdCredential, + password, + }); + expect(typeof fingerprint).toBe('string'); + expect(fingerprint).toMatch(/^[0-9a-f]{8}$/); // 4 bytes hex + + // Test error cases + await expect( + mnemonicFromEntropyAsync({ + hdCredential: 'invalid', + password: 'wrong', + }), + ).rejects.toThrow(); + + try { + await mnemonicToSeedAsync({ + mnemonic: 'invalid mnemonic', + }); + throw new Error('Should have thrown'); + } catch (error) { + expect(error).toBeDefined(); + } + + await expect( + generateRootFingerprintHexAsync({ + curveName: 'secp256k1' as ICurveName, + hdCredential: 'invalid', + password: 'wrong', + }), + ).rejects.toThrow(); + }); + + it('should throw error for invalid curve', () => { + const encryptedParent = { + key: encrypt(testPassword, testMasterKey.key), + chainCode: testMasterKey.chainCode, + }; + + expect(() => { + CKDPriv('invalid-curve' as any, encryptedParent, 0, testPassword); + }).toThrow('Key derivation is not supported for curve invalid-curve.'); + }); + }); + + // Test CKDPub function + describe('CKDPub', () => { + it('should derive child public keys correctly', () => { + const parentKey = { + key: Buffer.from( + '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', + 'hex', + ), + chainCode: Buffer.from('0123456789abcdef0123456789abcdef', 'hex'), + }; + + const testChildKey = CKDPub('secp256k1', parentKey, 0); + expect(testChildKey).toBeDefined(); + expect(testChildKey.key).toBeInstanceOf(Buffer); + expect(testChildKey.chainCode).toBeInstanceOf(Buffer); + + // Test with different curves + const nistParentKey = { + key: Buffer.from( + '03b5d465bc991d8f0f7fa68dafa4cce5e3c57e3d0d70b3c1b6f9e4e57aed0b1a87', + 'hex', + ), + chainCode: Buffer.from('0123456789abcdef0123456789abcdef', 'hex'), + }; + const nistChildKey = CKDPub('nistp256', nistParentKey, 0); + expect(nistChildKey).toBeDefined(); + expect(nistChildKey.key).toBeInstanceOf(Buffer); + expect(nistChildKey.chainCode).toBeInstanceOf(Buffer); + + // Test error cases + expect(() => CKDPub('invalid-curve' as any, parentKey, 0)).toThrow(); + expect(() => CKDPub('secp256k1', parentKey, -1)).toThrow(); + expect(() => CKDPub('secp256k1', parentKey, 2_147_483_648)).toThrow(); // Hardened index not allowed + }); + + it('should match snapshot for public key derivation', () => { + const parentKey = { + key: Buffer.from( + '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', + 'hex', + ), + chainCode: Buffer.from('0123456789abcdef0123456789abcdef', 'hex'), + }; + const extendedKey = CKDPub('secp256k1', parentKey, 0); + expect({ + key: extendedKey.key.toString('hex'), + chainCode: extendedKey.chainCode.toString('hex'), + }).toMatchSnapshot(); + }); + }); + + describe('batchGetPrivateKeys', () => { + const testPassword = 'password123'; + const testSeed: IBip39RevealableSeed = { + entropyWithLangPrefixed: '00112233445566778899aabbccddeeff', + seed: '00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff', + }; + + const encryptedSeed = encryptRevealableSeed({ + rs: testSeed, + password: testPassword, + }); + + it('should derive private keys for valid paths', () => { + const curveName: ICurveName = 'secp256k1'; + const prefix = 'm'; + const relPaths = ['0/0', '0/1', "44'/0'/0'/0/0"]; + + const privateKeys = batchGetPrivateKeys( + curveName, + encryptedSeed, + testPassword, + prefix, + relPaths, + ); + + expect(privateKeys).toHaveLength(3); + privateKeys.forEach((key, index) => { + expect(key).toHaveProperty('path'); + expect(key).toHaveProperty('parentFingerPrint'); + expect(key).toHaveProperty('extendedKey'); + expect(key.extendedKey).toHaveProperty('key'); + expect(key.extendedKey).toHaveProperty('chainCode'); + expect(Buffer.isBuffer(key.parentFingerPrint)).toBe(true); + expect(Buffer.isBuffer(key.extendedKey.key)).toBe(true); + expect(Buffer.isBuffer(key.extendedKey.chainCode)).toBe(true); + }); + }); + + it('should throw error for invalid curve name', () => { + const curveName = 'invalid-curve' as ICurveName; + const prefix = 'm'; + const relPaths = ['0/0']; + + expect(() => + batchGetPrivateKeys( + curveName, + encryptedSeed, + testPassword, + prefix, + relPaths, + ), + ).toThrow('Key derivation is not supported for curve invalid-curve.'); + }); + + it('should throw error for invalid password', () => { + const curveName: ICurveName = 'secp256k1'; + const prefix = 'm'; + const relPaths = ['0/0']; + + expect(() => + batchGetPrivateKeys( + curveName, + encryptedSeed, + 'wrong-password', + prefix, + relPaths, + ), + ).toThrow(); + }); + + it('should handle hardened and non-hardened derivation paths', () => { + const curveName: ICurveName = 'secp256k1'; + const prefix = 'm'; + const relPaths = ["44'/0'", '0/0', "1'/0/0"]; + + const privateKeys = batchGetPrivateKeys( + curveName, + encryptedSeed, + testPassword, + prefix, + relPaths, + ); + + expect(privateKeys).toHaveLength(3); + expect(privateKeys[0].path).toBe("m/44'/0'"); + expect(privateKeys[1].path).toBe('m/0/0'); + expect(privateKeys[2].path).toBe("m/1'/0/0"); + }); + }); + + describe('batchGetPublicKeys', () => { + const testPassword = 'password123'; + const testSeed: IBip39RevealableSeed = { + entropyWithLangPrefixed: '00112233445566778899aabbccddeeff', + seed: '00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff', + }; + + const encryptedSeed = encryptRevealableSeed({ + rs: testSeed, + password: testPassword, + }); + + it('should generate public keys matching private keys', () => { + const curveName: ICurveName = 'secp256k1'; + const prefix = 'm'; + const relPaths = ['0/0', '0/1', "44'/0'/0'/0/0"]; + + const privateKeys = batchGetPrivateKeys( + curveName, + encryptedSeed, + testPassword, + prefix, + relPaths, + ); + + const publicKeys = batchGetPublicKeys( + curveName, + encryptedSeed, + testPassword, + prefix, + relPaths, + ); + + expect(publicKeys).toHaveLength(privateKeys.length); + publicKeys.forEach((pubKey, index) => { + expect(pubKey.path).toBe(privateKeys[index].path); + expect(pubKey.parentFingerPrint).toEqual( + privateKeys[index].parentFingerPrint, + ); + expect(Buffer.isBuffer(pubKey.extendedKey.key)).toBe(true); + expect(Buffer.isBuffer(pubKey.extendedKey.chainCode)).toBe(true); + // Public key should be different from private key + expect(pubKey.extendedKey.key).not.toEqual( + privateKeys[index].extendedKey.key, + ); + }); + }); + + it('should throw error for invalid curve name', () => { + const curveName = 'invalid-curve' as ICurveName; + const prefix = 'm'; + const relPaths = ['0/0']; + + expect(() => + batchGetPublicKeys( + curveName, + encryptedSeed, + testPassword, + prefix, + relPaths, + ), + ).toThrow('Key derivation is not supported for curve invalid-curve.'); + }); + + it('should throw error for invalid password', () => { + const curveName: ICurveName = 'secp256k1'; + const prefix = 'm'; + const relPaths = ['0/0']; + + expect(() => + batchGetPublicKeys( + curveName, + encryptedSeed, + 'wrong-password', + prefix, + relPaths, + ), + ).toThrow(); + }); + + it('should handle hardened and non-hardened derivation paths', () => { + const curveName: ICurveName = 'secp256k1'; + const prefix = 'm'; + const relPaths = ["44'/0'", '0/0', "1'/0/0"]; + + const publicKeys = batchGetPublicKeys( + curveName, + encryptedSeed, + testPassword, + prefix, + relPaths, + ); + + expect(publicKeys).toHaveLength(3); + expect(publicKeys[0].path).toBe("m/44'/0'"); + expect(publicKeys[1].path).toBe('m/0/0'); + expect(publicKeys[2].path).toBe("m/1'/0/0"); + }); + }); + + describe('batchGetPublicKeysAsync', () => { + const testPassword = 'password123'; + const testSeed: IBip39RevealableSeed = { + entropyWithLangPrefixed: '00112233445566778899aabbccddeeff', + seed: '00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff', + }; + + const encryptedSeed = encryptRevealableSeed({ + rs: testSeed, + password: testPassword, + }); + + beforeEach(() => { + // do nothing + }); + + it('should return same results as batchGetPublicKeys in non-native environment', async () => { + const curveName: ICurveName = 'secp256k1'; + const prefix = 'm'; + const relPaths = ['0/0', '0/1', "44'/0'/0'/0/0"]; + + const syncResult = batchGetPublicKeys( + curveName, + encryptedSeed, + testPassword, + prefix, + relPaths, + ); + + const asyncResult = await batchGetPublicKeysAsync({ + curveName, + hdCredential: encryptedSeed, + password: testPassword, + prefix, + relPaths, + }); + + expect(asyncResult).toEqual(syncResult); + }); + + it('should handle native environment correctly', async () => { + const result = await batchGetPublicKeysAsync({ + curveName: 'secp256k1', + hdCredential: encryptedSeed, + password: testPassword, + prefix: 'm', + relPaths: ['0/0'], + }); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('m/0/0'); + expect(Buffer.isBuffer(result[0].parentFingerPrint)).toBe(true); + expect(result[0].parentFingerPrint.toString('hex')).toBe('0efcb8ef'); + expect(Buffer.isBuffer(result[0].extendedKey.key)).toBe(true); + expect(result[0].extendedKey.key.toString('hex')).toBe( + '034b009b02f0db41298e367d4aa2b1d8b4512d16a014d3da5cc9d8854987e3cb67', + ); + expect(Buffer.isBuffer(result[0].extendedKey.chainCode)).toBe(true); + expect(result[0].extendedKey.chainCode.toString('hex')).toBe( + '2b30a28ef711c984c636a28d41821bc927332cbcd1e0f7220cd9ebc9ebb8aa0a', + ); + }); + + afterEach(() => { + // do nothing + }); + }); + + describe('compressPublicKey', () => { + it('should compress public keys correctly', () => { + // Test with uncompressed secp256k1 public key + const uncompressedKey = Buffer.from( + '04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7', + 'hex', + ); + const compressedKey = compressPublicKey('secp256k1', uncompressedKey); + expect(compressedKey).toBeInstanceOf(Buffer); + expect(compressedKey.length).toBe(33); // Compressed public key length + + // Test with already compressed key + const alreadyCompressed = Buffer.from( + '02a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7', + 'hex', + ); + const recompressed = compressPublicKey('secp256k1', alreadyCompressed); + expect(recompressed).toEqual(alreadyCompressed); + + // Test with different curves + const nistUncompressed = Buffer.from( + '04b5d465bc991d8f0f7fa68dafa4cce5e3c57e3d0d70b3c1b6f9e4e57aed0b1a87d2390d1ca0323c898db9f3e51c4a7ead23108dd9c41d4d99f4ce0a9307048d54', + 'hex', + ); + const nistCompressed = compressPublicKey('nistp256', nistUncompressed); + expect(nistCompressed.length).toBe(33); + + // Test error cases + expect(() => + compressPublicKey('invalid-curve' as any, uncompressedKey), + ).toThrow(); + expect(() => + compressPublicKey('secp256k1', Buffer.from('invalid')), + ).toThrow(); + }); + + it('should match snapshot for compressed public key', () => { + const uncompressedKey = Buffer.from( + '04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7', + 'hex', + ); + const compressedKey = compressPublicKey('secp256k1', uncompressedKey); + expect(compressedKey.toString('hex')).toMatchSnapshot(); + }); + }); + + describe('decryptImportedCredential', () => { + const testPassword = 'test123'; + const testCredential: ICoreImportedCredential = { + privateKey: '0123456789abcdef', + }; + + it('should decrypt imported credential correctly', () => { + // First encrypt the credential + const encryptedCredential = encryptImportedCredential({ + credential: testCredential, + password: testPassword, + }); + + // Then decrypt and verify + const decryptedCredential = decryptImportedCredential({ + credential: encryptedCredential, + password: testPassword, + }); + + expect(decryptedCredential).toEqual(testCredential); + }); + + it('should handle credential with prefix correctly', () => { + const encryptedCredential = encryptImportedCredential({ + credential: testCredential, + password: testPassword, + }); + + expect(encryptedCredential.startsWith('|PK|')).toBe(true); + + const decryptedCredential = decryptImportedCredential({ + credential: encryptedCredential, + password: testPassword, + }); + + expect(decryptedCredential).toEqual(testCredential); + }); + + it('should throw error for invalid password', () => { + const encryptedCredential = encryptImportedCredential({ + credential: testCredential, + password: testPassword, + }); + + expect(() => + decryptImportedCredential({ + credential: encryptedCredential, + password: 'wrong-password', + }), + ).toThrow(); + }); + + it('should throw error for invalid credential format', () => { + expect(() => + decryptImportedCredential({ + credential: '|PK|invalid-data', + password: testPassword, + }), + ).toThrow(); + }); + + it('should match snapshot for decrypted credential', () => { + const encryptedCredential = encryptImportedCredential({ + credential: testCredential, + password: testPassword, + }); + + expect( + decryptImportedCredential({ + credential: encryptedCredential, + password: testPassword, + }), + ).toMatchSnapshot(); + }); + }); + + describe('decryptRevealableSeed', () => { + const testPassword = 'test123'; + const testSeed: IBip39RevealableSeed = { + entropyWithLangPrefixed: '00112233445566778899aabbccddeeff', + seed: '00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff', + }; + + it('should decrypt revealable seed correctly', () => { + // First encrypt the seed + const encryptedSeed = encryptRevealableSeed({ + rs: testSeed, + password: testPassword, + }); + + // Then decrypt and verify + const decryptedSeed = decryptRevealableSeed({ + rs: encryptedSeed, + password: testPassword, + }); + + expect(decryptedSeed).toEqual(testSeed); + }); + + it('should throw error for invalid password', () => { + const encryptedSeed = encryptRevealableSeed({ + rs: testSeed, + password: testPassword, + }); + + expect(() => + decryptRevealableSeed({ + rs: encryptedSeed, + password: 'wrong-password', + }), + ).toThrow(); + }); + + it('should throw error for invalid seed format', () => { + expect(() => + decryptRevealableSeed({ + rs: 'invalid-seed-data', + password: testPassword, + }), + ).toThrow(); + }); + + it('should match snapshot for decrypted seed', () => { + const encryptedSeed = encryptRevealableSeed({ + rs: testSeed, + password: testPassword, + }); + + expect( + decryptRevealableSeed({ + rs: encryptedSeed, + password: testPassword, + }), + ).toMatchSnapshot(); + }); + }); + + describe('decryptVerifyString', () => { + const testPassword = 'test123'; + + it('should decrypt verify string correctly', () => { + // First encrypt the string + const encryptedString = encryptVerifyString({ + password: testPassword, + }); + + // Then decrypt and verify + const decryptedString = decryptVerifyString({ + verifyString: encryptedString, + password: testPassword, + }); + + expect(decryptedString).toBe(DEFAULT_VERIFY_STRING); + }); + + it('should handle string with prefix correctly', () => { + const encryptedString = encryptVerifyString({ + password: testPassword, + addPrefixString: true, + }); + + expect(encryptedString.startsWith('|VS|')).toBe(true); + + const decryptedString = decryptVerifyString({ + verifyString: encryptedString, + password: testPassword, + }); + + expect(decryptedString).toBe(DEFAULT_VERIFY_STRING); + }); + + it('should throw error for invalid password', () => { + const encryptedString = encryptVerifyString({ + password: testPassword, + }); + + expect(() => + decryptVerifyString({ + verifyString: encryptedString, + password: 'wrong-password', + }), + ).toThrow(); + }); + + it('should throw error for invalid string format', () => { + expect(() => + decryptVerifyString({ + verifyString: '|VS|invalid-data', + password: testPassword, + }), + ).toThrow(); + }); + + it('should match snapshot for decrypted string', () => { + const encryptedString = encryptVerifyString({ + password: testPassword, + }); + + expect( + decryptVerifyString({ + verifyString: encryptedString, + password: testPassword, + }), + ).toMatchSnapshot(); + }); + }); + + describe('encryptImportedCredential', () => { + const testPassword = 'test123'; + const testCredential: ICoreImportedCredential = { + privateKey: '0123456789abcdef', + }; + + it('should encrypt credential correctly', () => { + const encryptedCredential = encryptImportedCredential({ + credential: testCredential, + password: testPassword, + }); + + expect(encryptedCredential.startsWith('|PK|')).toBe(true); + + // Verify we can decrypt it back + const decryptedCredential = decryptImportedCredential({ + credential: encryptedCredential, + password: testPassword, + }); + + expect(decryptedCredential).toEqual(testCredential); + }); + + it('should handle different private key formats', () => { + const longKeyCredential: ICoreImportedCredential = { + privateKey: + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + }; + + const encryptedCredential = encryptImportedCredential({ + credential: longKeyCredential, + password: testPassword, + }); + + const decryptedCredential = decryptImportedCredential({ + credential: encryptedCredential, + password: testPassword, + }); + + expect(decryptedCredential).toEqual(longKeyCredential); + }); + + it('should throw error for empty private key', () => { + const invalidCredential = { + privateKey: '', + }; + + expect(() => + encryptImportedCredential({ + credential: invalidCredential as ICoreImportedCredential, + password: testPassword, + }), + ).toThrow(); + }); + + it('should match snapshot', () => { + const encryptedCredential = encryptImportedCredential({ + credential: testCredential, + password: testPassword, + }); + + expect(encryptedCredential).toMatchSnapshot(); + }); + }); + + describe('encryptRevealableSeed', () => { + const testPassword = 'test123'; + const testSeed: IBip39RevealableSeed = { + entropyWithLangPrefixed: '0123456789abcdef0123456789abcdef', + seed: 'deadbeefdeadbeefdeadbeefdeadbeef', + }; + + it('should encrypt seed correctly', () => { + const encryptedSeed = encryptRevealableSeed({ + rs: testSeed, + password: testPassword, + }); + + // Verify we can decrypt it back + const decryptedSeed = decryptRevealableSeed({ + rs: encryptedSeed, + password: testPassword, + }); + + expect(decryptedSeed).toEqual(testSeed); + }); + + it('should handle different seed lengths', () => { + const longSeed: IBip39RevealableSeed = { + entropyWithLangPrefixed: + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + seed: 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', + }; + + const encryptedSeed = encryptRevealableSeed({ + rs: longSeed, + password: testPassword, + }); + + const decryptedSeed = decryptRevealableSeed({ + rs: encryptedSeed, + password: testPassword, + }); + + expect(decryptedSeed).toEqual(longSeed); + }); + + it('should throw error for invalid seed object', () => { + const invalidSeed = { + entropyWithLangPrefixed: '', + seed: '', + }; + + expect(() => + encryptRevealableSeed({ + rs: invalidSeed as IBip39RevealableSeed, + password: testPassword, + }), + ).toThrow(); + }); + + it('should match snapshot', () => { + const encryptedSeed = encryptRevealableSeed({ + rs: testSeed, + password: testPassword, + }); + + expect(encryptedSeed).toMatchSnapshot(); + }); + }); + + describe('encryptVerifyString', () => { + const testPassword = 'test123'; + + it('should encrypt string correctly', () => { + const encryptedString = encryptVerifyString({ + password: testPassword, + }); + + // Verify we can decrypt it back + const decryptedString = decryptVerifyString({ + verifyString: encryptedString, + password: testPassword, + }); + + expect(decryptedString).toBe('OneKey'); + }); + + it('should handle prefix option', () => { + const withPrefix = encryptVerifyString({ + password: testPassword, + addPrefixString: true, + }); + + expect(withPrefix.startsWith('|VS|')).toBe(true); + + const withoutPrefix = encryptVerifyString({ + password: testPassword, + addPrefixString: false, + }); + + expect(withoutPrefix.startsWith('|VS|')).toBe(false); + + // Both should decrypt correctly + expect( + decryptVerifyString({ + verifyString: withPrefix, + password: testPassword, + }), + ).toBe('OneKey'); + + expect( + decryptVerifyString({ + verifyString: withoutPrefix, + password: testPassword, + }), + ).toBe('OneKey'); + }); + + it('should throw error for empty password', () => { + expect(() => + encryptVerifyString({ + password: '', + }), + ).toThrow(); + }); + + it('should match snapshot', () => { + const encryptedString = encryptVerifyString({ + password: testPassword, + }); + + expect(encryptedString).toMatchSnapshot(); + }); + }); + + describe('fixV4VerifyStringToV5', () => { + const defaultVerifyString = 'OneKey'; + + it('should not modify DEFAULT_VERIFY_STRING', () => { + const result = fixV4VerifyStringToV5({ + verifyString: defaultVerifyString, + }); + expect(result).toBe(defaultVerifyString); + }); + + it('should add prefix if missing', () => { + const testString = 'abc123'; + const result = fixV4VerifyStringToV5({ + verifyString: testString, + }); + expect(result).toBe('|VS|abc123'); + }); + + it('should not duplicate prefix if already present', () => { + const testString = '|VS|abc123'; + const result = fixV4VerifyStringToV5({ + verifyString: testString, + }); + expect(result).toBe('|VS|abc123'); + }); + + it('should match snapshot', () => { + const result = fixV4VerifyStringToV5({ + verifyString: 'test123', + }); + expect(result).toMatchSnapshot(); + }); + }); + + describe('generateMasterKeyFromSeed', () => { + const testRevealableSeed = mnemonicToRevealableSeed(TEST_MNEMONIC); + + const encryptedSeed = encryptRevealableSeed({ + rs: testRevealableSeed, + password: TEST_PASSWORD, + }); + + it('should generate master key for secp256k1', () => { + const masterKey = generateMasterKeyFromSeed( + 'secp256k1', + encryptedSeed, + TEST_PASSWORD, + ); + expect(masterKey.key).toBeInstanceOf(Buffer); + expect(masterKey.chainCode).toBeInstanceOf(Buffer); + expect(masterKey.key.length).toBe(96); + expect(masterKey.chainCode.length).toBe(32); + }); + + it('should generate master key for nistp256', () => { + const masterKey = generateMasterKeyFromSeed( + 'nistp256', + encryptedSeed, + TEST_PASSWORD, + ); + expect(masterKey.key).toBeInstanceOf(Buffer); + expect(masterKey.chainCode).toBeInstanceOf(Buffer); + expect(masterKey.key.length).toBe(96); + expect(masterKey.chainCode.length).toBe(32); + }); + + it('should generate master key for ed25519', () => { + const masterKey = generateMasterKeyFromSeed( + 'ed25519', + encryptedSeed, + TEST_PASSWORD, + ); + expect(masterKey.key).toBeInstanceOf(Buffer); + expect(masterKey.chainCode).toBeInstanceOf(Buffer); + expect(masterKey.key.length).toBe(96); + expect(masterKey.chainCode.length).toBe(32); + }); + + it('should throw error for invalid curve', () => { + expect(() => { + generateMasterKeyFromSeed( + 'invalid-curve' as any, + encryptedSeed, + TEST_PASSWORD, + ); + }).toThrow('Key derivation is not supported for curve invalid-curve.'); + }); + + it('should throw error for invalid password', () => { + expect(() => { + generateMasterKeyFromSeed('secp256k1', encryptedSeed, 'wrong-password'); + }).toThrow('IncorrectPassword'); + }); + + it('should match snapshot', () => { + const masterKey = generateMasterKeyFromSeed( + 'secp256k1', + encryptedSeed, + TEST_PASSWORD, + ); + expect({ + key: masterKey.key.toString('hex'), + chainCode: masterKey.chainCode.toString('hex'), + }).toMatchSnapshot(); + }); + }); + + describe('mnemonicFromEntropyAsync', () => { + const testRevealableSeed = mnemonicToRevealableSeed(TEST_MNEMONIC); + + const encryptedSeed = encryptRevealableSeed({ + rs: testRevealableSeed, + password: TEST_PASSWORD, + }); + + it('should generate mnemonic from entropy', async () => { + const mnemonic = await mnemonicFromEntropyAsync({ + hdCredential: encryptedSeed, + password: TEST_PASSWORD, + }); + expect(typeof mnemonic).toBe('string'); + expect(mnemonic.split(' ').length).toBe(24); // 24 words for 256-bit entropy + }); + + it('should throw error for invalid password', async () => { + await expect( + mnemonicFromEntropyAsync({ + hdCredential: encryptedSeed, + password: 'wrong-password', + }), + ).rejects.toThrow(IncorrectPassword); + }); + + it('should match snapshot', async () => { + const mnemonic = await mnemonicFromEntropyAsync({ + hdCredential: encryptedSeed, + password: TEST_PASSWORD, + }); + expect(mnemonic).toMatchSnapshot(); + }); + }); + + describe('N', () => { + const testPassword = 'test123'; + const testMasterKey: IBip32ExtendedKey = { + key: encrypt( + testPassword, + Buffer.from('0123456789abcdef0123456789abcdef', 'hex'), + ), + chainCode: Buffer.from('0123456789abcdef0123456789abcdef', 'hex'), + }; + + it('should derive public key from private key', () => { + const publicKey = N('secp256k1', testMasterKey, testPassword); + expect(publicKey).toBeDefined(); + expect(publicKey.key).toBeInstanceOf(Buffer); + expect(publicKey.chainCode).toEqual(testMasterKey.chainCode); + }); + + it('should work with different curves', () => { + const curves: ICurveName[] = ['secp256k1', 'nistp256', 'ed25519']; + curves.forEach((curve) => { + const publicKey = N(curve, testMasterKey, testPassword); + expect(publicKey).toBeDefined(); + expect(publicKey.key).toBeInstanceOf(Buffer); + expect(publicKey.chainCode).toEqual(testMasterKey.chainCode); + }); + }); + + it('should throw error for invalid curve', () => { + expect(() => + N('invalid-curve' as ICurveName, testMasterKey, testPassword), + ).toThrow(); + }); + + it('should match snapshot', () => { + const publicKey = N('secp256k1', testMasterKey, testPassword); + expect({ + key: publicKey.key.toString('hex'), + chainCode: publicKey.chainCode.toString('hex'), + }).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/core/src/secret/index.ts b/packages/core/src/secret/index.ts index 82c8b441fd8..a27bf1ed18f 100644 --- a/packages/core/src/secret/index.ts +++ b/packages/core/src/secret/index.ts @@ -207,6 +207,9 @@ function encryptRevealableSeed({ rs: IBip39RevealableSeed; password: string; }): IBip39RevealableSeedEncryptHex { + if (!rs || !rs.entropyWithLangPrefixed || !rs.seed) { + throw new Error('Invalid seed object'); + } return ( EncryptPrefixHdCredential + bufferUtils.bytesToHex( @@ -239,6 +242,9 @@ function encryptImportedCredential({ credential: ICoreImportedCredential; password: string; }): ICoreImportedCredentialEncryptHex { + if (!credential || !credential.privateKey) { + throw new Error('Invalid credential object'); + } return ( EncryptPrefixImportedCredential + encryptString({ @@ -521,7 +527,7 @@ export type IMnemonicFromEntropyAsyncParams = { hdCredential: IBip39RevealableSeedEncryptHex; password: string; }; -export function mnemonicFromEntropyAsync( +async function mnemonicFromEntropyAsync( params: IMnemonicFromEntropyAsyncParams, ): Promise { if (platformEnv.isNative) { @@ -536,7 +542,7 @@ export type IMnemonicToSeedAsyncParams = { mnemonic: string; passphrase?: string; }; -export async function mnemonicToSeedAsync( +async function mnemonicToSeedAsync( params: IMnemonicToSeedAsyncParams, ): Promise { if (platformEnv.isNative) { @@ -555,7 +561,7 @@ export type IGenerateRootFingerprintHexAsyncParams = { hdCredential: IBip39RevealableSeedEncryptHex; password: string; }; -export async function generateRootFingerprintHexAsync( +async function generateRootFingerprintHexAsync( params: IGenerateRootFingerprintHexAsyncParams, ): Promise { if (platformEnv.isNative) { @@ -610,6 +616,9 @@ export { batchGetPublicKeysAsync, CKDPriv, CKDPub, + mnemonicToSeedAsync, + generateRootFingerprintHexAsync, + mnemonicFromEntropyAsync, compressPublicKey, decryptImportedCredential, decryptRevealableSeed, @@ -624,8 +633,8 @@ export { publicFromPrivate, revealableSeedFromMnemonic, revealableSeedFromTonMnemonic, - tonMnemonicFromEntropy, sign, + tonMnemonicFromEntropy, uncompressPublicKey, verify, };