diff --git a/src/core/account.ts b/src/core/account.ts index 837a09a03..64d38d0fa 100644 --- a/src/core/account.ts +++ b/src/core/account.ts @@ -220,13 +220,13 @@ export class Account { legacy?: boolean; }): Account { const { path, mnemonic, scheme, legacy } = args; - let privateKey; + let privateKey: PrivateKey; switch (scheme) { case SigningSchemeInput.Secp256k1Ecdsa: - privateKey = new Secp256k1PrivateKey(Secp256k1PrivateKey.fromDerivationPath(path, mnemonic)); + privateKey = Secp256k1PrivateKey.fromDerivationPath(path, mnemonic); break; case SigningSchemeInput.Ed25519: - privateKey = new Ed25519PrivateKey(Ed25519PrivateKey.fromDerivationPath(path, mnemonic)); + privateKey = Ed25519PrivateKey.fromDerivationPath(path, mnemonic); break; default: throw new Error(`Unsupported scheme ${scheme}`); diff --git a/src/core/crypto/ed25519.ts b/src/core/crypto/ed25519.ts index 2bc4ee59d..ce2648236 100644 --- a/src/core/crypto/ed25519.ts +++ b/src/core/crypto/ed25519.ts @@ -7,7 +7,7 @@ import { Deserializer } from "../../bcs/deserializer"; import { Serializer } from "../../bcs/serializer"; import { Hex } from "../hex"; import { HexInput } from "../../types"; -import { CKDPriv, deriveKey, HARDENED_OFFSET, isValidHardenedPath, KeyType, mnemonicToSeed, splitPath } from "./hdKey"; +import { CKDPriv, deriveKey, HARDENED_OFFSET, isValidHardenedPath, mnemonicToSeed, splitPath } from "./hdKey"; /** * Represents the public key of an Ed25519 key pair. @@ -99,6 +99,12 @@ export class Ed25519PrivateKey extends PrivateKey { */ static readonly LENGTH: number = 32; + /** + * The Ed25519 key seed to use for BIP-32 compatibility + * See more {@link https://github.com/satoshilabs/slips/blob/master/slip-0010.md} + */ + static readonly SLIP_0010_SEED = "ed25519 seed"; + /** * The Ed25519 signing key * @private @@ -191,13 +197,25 @@ export class Ed25519PrivateKey extends PrivateKey { * * @param path the BIP44 path * @param mnemonics the mnemonic seed phrase - * @param offset the offset used for key derivation, defaults to 0x80000000 */ - static fromDerivationPath(path: string, mnemonics: string, offset = HARDENED_OFFSET): Uint8Array { + static fromDerivationPath(path: string, mnemonics: string): Ed25519PrivateKey { if (!isValidHardenedPath(path)) { throw new Error(`Invalid derivation path ${path}`); } - const { key, chainCode } = deriveKey(KeyType.ED25519, mnemonicToSeed(mnemonics)); + return Ed25519PrivateKey.fromDerivationPathInner(path, mnemonicToSeed(mnemonics)); + } + + /** + * A private inner function so we can separate from the main fromDerivationPath() method + * to add tests to verify we create the keys correctly. + * + * @param path the BIP44 path + * @param seed the seed phrase created by the mnemonics + * @param offset the offset used for key derivation, defaults to 0x80000000 + * @returns + */ + private static fromDerivationPathInner(path: string, seed: Uint8Array, offset = HARDENED_OFFSET): Ed25519PrivateKey { + const { key, chainCode } = deriveKey(Ed25519PrivateKey.SLIP_0010_SEED, seed); const segments = splitPath(path).map((el) => parseInt(el, 10)); @@ -206,7 +224,7 @@ export class Ed25519PrivateKey extends PrivateKey { key, chainCode, }); - return privateKey; + return new Ed25519PrivateKey(privateKey); } } diff --git a/src/core/crypto/secp256k1.ts b/src/core/crypto/secp256k1.ts index 02126d1e1..c0e1e17c7 100644 --- a/src/core/crypto/secp256k1.ts +++ b/src/core/crypto/secp256k1.ts @@ -181,19 +181,34 @@ export class Secp256k1PrivateKey extends PrivateKey { * * @param path the BIP44 path * @param mnemonics the mnemonic seed phrase + * + * @returns The generated key */ - static fromDerivationPath(path: string, mnemonics: string): Uint8Array { + static fromDerivationPath(path: string, mnemonics: string): Secp256k1PrivateKey { if (!isValidBIP44Path(path)) { throw new Error(`Invalid derivation path ${path}`); } - const { privateKey } = HDKey.fromMasterSeed(mnemonicToSeed(mnemonics)).derive(path); + return Secp256k1PrivateKey.fromDerivationPathInner(path, mnemonicToSeed(mnemonics)); + } + + /** + * A private inner function so we can separate from the main fromDerivationPath() method + * to add tests to verify we create the keys correctly. + * + * @param path the BIP44 path + * @param seed the seed phrase created by the mnemonics + * + * @returns The generated key + */ + private static fromDerivationPathInner(path: string, seed: Uint8Array): Secp256k1PrivateKey { + const { privateKey } = HDKey.fromMasterSeed(seed).derive(path); // library returns privateKey as Uint8Array | null if (privateKey === null) { throw new Error("Invalid key"); } - return privateKey; + return new Secp256k1PrivateKey(privateKey); } } diff --git a/tests/unit/account.test.ts b/tests/unit/account.test.ts index 121dd3101..3cc7a1b7a 100644 --- a/tests/unit/account.test.ts +++ b/tests/unit/account.test.ts @@ -138,14 +138,6 @@ describe("Account", () => { }); expect(newAccount.accountAddress.toString()).toEqual(address); }); - - it("should prevent an invalid bip44 path ", () => { - const { mnemonic } = wallet; - const path = "1234"; - expect(() => Account.fromDerivationPath({ path, mnemonic, scheme: SigningSchemeInput.Ed25519 })).toThrow( - "Invalid derivation path", - ); - }); }); describe("sign and verify", () => { diff --git a/tests/unit/ed25519.test.ts b/tests/unit/ed25519.test.ts index b6eddecd5..b52387784 100644 --- a/tests/unit/ed25519.test.ts +++ b/tests/unit/ed25519.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Deserializer, Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature, Hex, Serializer } from "../../src"; -import { ed25519 } from "./helper"; +import { ed25519, wallet } from "./helper"; describe("Ed25519PublicKey", () => { it("should create the instance correctly without error", () => { @@ -166,6 +166,19 @@ describe("PrivateKey", () => { expect(publicKey).toBeInstanceOf(Ed25519PublicKey); expect(publicKey.toString()).toEqual(ed25519.publicKey); }); + + it("should prevent an invalid bip44 path ", () => { + const { mnemonic } = wallet; + const path = "1234"; + expect(() => Ed25519PrivateKey.fromDerivationPath(path, mnemonic)).toThrow("Invalid derivation path"); + }); + + it("should derive from path and mnemonic", () => { + const { mnemonic, path, privateKey } = wallet; + const key = Ed25519PrivateKey.fromDerivationPath(path, mnemonic); + expect(key).toBeInstanceOf(Ed25519PrivateKey); + expect(privateKey).toEqual(key.toString()); + }); }); describe("Signature", () => { diff --git a/tests/unit/hdKey.test.ts b/tests/unit/hdKey.test.ts index 55df16572..a09c691b3 100644 --- a/tests/unit/hdKey.test.ts +++ b/tests/unit/hdKey.test.ts @@ -1,5 +1,5 @@ import { secp256k1WalletTestObject, wallet } from "./helper"; -import { isValidBIP44Path, isValidHardenedPath } from "../../src"; +import { Ed25519PrivateKey, Hex, isValidBIP44Path, isValidHardenedPath, Secp256k1PrivateKey } from "../../src"; describe("Hierarchical Deterministic Key (hdkey)", () => { describe("hardened path", () => { @@ -61,4 +61,106 @@ describe("Hierarchical Deterministic Key (hdkey)", () => { expect(isValidBIP44Path("m/44'/637'/0/0/0/")).toBe(false); }); }); + + // testing against https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-1-for-ed25519 + describe("Ed25519", () => { + const ed25519 = [ + { + seed: Hex.fromHexInput("000102030405060708090a0b0c0d0e0f"), + vectors: [ + { + chain: "m", + private: "2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7", + public: "00a4b2856bfec510abab89753fac1ac0e1112364e7d250545963f135f2a33188ed", + }, + { + chain: "m/0'", + private: "68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3", + public: "008c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c", + }, + { + chain: "m/0'/1'", + private: "b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2", + public: "001932a5270f335bed617d5b935c80aedb1a35bd9fc1e31acafd5372c30f5c1187", + }, + { + chain: "m/0'/1'/2'", + private: "92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9", + public: "00ae98736566d30ed0e9d2f4486a64bc95740d89c7db33f52121f8ea8f76ff0fc1", + }, + { + chain: "m/0'/1'/2'/2'", + private: "30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662", + public: "008abae2d66361c879b900d204ad2cc4984fa2aa344dd7ddc46007329ac76c429c", + }, + { + chain: "m/0'/1'/2'/2'/1000000000'", + private: "8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793", + public: "003c24da049451555d51a7014a37337aa4e12d41e485abccfa46b47dfb2af54b7a", + }, + ], + }, + ]; + + ed25519.forEach(({ seed, vectors }) => { + vectors.forEach(({ chain, private: privateKey }) => { + it(`should generate correct key pair for ${chain}`, () => { + // eslint-disable-next-line @typescript-eslint/dot-notation + const key = Ed25519PrivateKey["fromDerivationPathInner"](chain, seed.toUint8Array()); + expect(key.toString()).toBe(`0x${privateKey}`); + }); + }); + }); + }); + + // testing against https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-1-for-secp256k1 + describe("secp256k1", () => { + const secp256k1 = [ + { + seed: Hex.fromHexInput("000102030405060708090a0b0c0d0e0f"), + vectors: [ + { + chain: "m", + private: "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35", + public: "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2", + }, + { + chain: "m/0'", + private: "edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea", + public: "035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56", + }, + { + chain: "m/0'/1", + private: "3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368", + public: "03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c", + }, + { + chain: "m/0'/1/2'", + private: "cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca", + public: "0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2", + }, + { + chain: "m/0'/1/2'/2", + private: "0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4", + public: "02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29", + }, + { + chain: "m/0'/1/2'/2/1000000000", + private: "471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8", + public: "022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011", + }, + ], + }, + ]; + + secp256k1.forEach(({ seed, vectors }) => { + vectors.forEach(({ chain, private: privateKey }) => { + it(`should generate correct key pair for ${chain}`, () => { + // eslint-disable-next-line @typescript-eslint/dot-notation + const key = Secp256k1PrivateKey["fromDerivationPathInner"](chain, seed.toUint8Array()); + expect(key.toString()).toBe(`0x${privateKey}`); + }); + }); + }); + }); }); diff --git a/tests/unit/secp256k1.test.ts b/tests/unit/secp256k1.test.ts index 0c4acca2e..df836ccc2 100644 --- a/tests/unit/secp256k1.test.ts +++ b/tests/unit/secp256k1.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { secp256k1 } from "@noble/curves/secp256k1"; -import { secp256k1TestObject } from "./helper"; +import { secp256k1TestObject, secp256k1WalletTestObject } from "./helper"; import { Deserializer, Hex, Secp256k1PrivateKey, Secp256k1PublicKey, Secp256k1Signature, Serializer } from "../../src"; /* eslint-disable max-len */ @@ -127,6 +127,19 @@ describe("Secp256k1PrivateKey", () => { expect(deserializedPrivateKey.toString()).toEqual(privateKey.toString()); }); + + it("should prevent an invalid bip44 path ", () => { + const { mnemonic } = secp256k1WalletTestObject; + const path = "1234"; + expect(() => Secp256k1PrivateKey.fromDerivationPath(path, mnemonic)).toThrow("Invalid derivation path"); + }); + + it("should derive from path and mnemonic", () => { + const { mnemonic, path, privateKey } = secp256k1WalletTestObject; + const key = Secp256k1PrivateKey.fromDerivationPath(path, mnemonic); + expect(key).toBeInstanceOf(Secp256k1PrivateKey); + expect(privateKey).toEqual(key.toString()); + }); }); describe("Secp256k1Signature", () => {