From d47c7a633a852fceea7139080b52703b778b9862 Mon Sep 17 00:00:00 2001 From: David C Goldenberg Date: Fri, 20 Jun 2025 15:02:12 -0400 Subject: [PATCH] Added an operation to turn an extended private key to its public key version. We deserialize, turn the private key into the appropriate public key, grab the appropriate public key version then reserialize. --- src/core/config/Categories.json | 3 +- src/core/lib/Bitcoin.mjs | 26 ++++++++ .../operations/PrivateExtendedKeyToPublic.mjs | 65 +++++++++++++++++++ tests/operations/index.mjs | 1 + .../tests/PrivateExtendedKeyToPublic.mjs | 58 +++++++++++++++++ 5 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 src/core/operations/PrivateExtendedKeyToPublic.mjs create mode 100644 tests/operations/tests/PrivateExtendedKeyToPublic.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 56972bc7..d46e2ff9 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -358,7 +358,8 @@ "Public Key To ETH Style Address", "Public Key To TRX Style Address", "ETH / TRX Conversion", - "Key To Extended Key" + "Key To Extended Key", + "Private Extended Key To Public" ] }, { diff --git a/src/core/lib/Bitcoin.mjs b/src/core/lib/Bitcoin.mjs index 89e229e8..be790f58 100644 --- a/src/core/lib/Bitcoin.mjs +++ b/src/core/lib/Bitcoin.mjs @@ -448,6 +448,32 @@ const versionBytes = { "ttpv": "0436ef7d" }; +// Lookup matching private extended key versions to public +const privateToPublicExtendedKeys = { + "xprv": "xpub", + "yprv": "ypub", + "zprv": "zpub", + "Zprv": "Zpub", + "Yprv": "Ypub", + "Ltpv": "Ltub", + "Mtpv": "Mtub", + "ttpv": "ttub", + "tprv": "tpub", + "uprv": "upub", + "vprv": "vprv", + "Uprv": "Upub", + "Vprv": "Vpub" +}; + +/** + * For a given private extended key version string, returns the appropriate public version string. + * @param {*} input + * @returns + */ +export function privateVersionToPublicVersion(input) { + return privateToPublicExtendedKeys[input]; +} + /** * We return the correct version bytes from the versionBytes map, given input string. * @param {*} input diff --git a/src/core/operations/PrivateExtendedKeyToPublic.mjs b/src/core/operations/PrivateExtendedKeyToPublic.mjs new file mode 100644 index 00000000..fdb8d707 --- /dev/null +++ b/src/core/operations/PrivateExtendedKeyToPublic.mjs @@ -0,0 +1,65 @@ +/** + * @author dgoldenberg [virtualcurrency@mitre.org] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import ec from "elliptic"; +import { deserializeExtendedKeyFunc, serializeExtendedKeyFunc, getExtendedKeyVersion, privateVersionToPublicVersion, getExtendedKeyString +} from "../lib/Bitcoin.mjs"; + +/** + * PrivateExtendedKeyToPublic operation + */ +class PrivateExtendedKeyToPublic extends Operation { + + /** + * PrivateExtendedKeyToPublic constructor + */ + constructor() { + super(); + + this.name = "Private Extended Key To Public"; + this.module = "Default"; + this.description = "Convert a private extended key to its associated public key. This may be useful post a BIP32Derivation operation if you need the extended public key for a different toolset."; + this.infoURL = "https://en.bitcoin.it/wiki/BIP_0032"; // Usually a Wikipedia link. Remember to remove localisation (i.e. https://wikipedia.org/etc rather than https://en.wikipedia.org/etc) + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + // const [firstArg, secondArg] = args; + // const [firstArg, secondArg] = args; + if (input.trim().length === 0) { + return ""; + } + input = input.trim(); + + const result = deserializeExtendedKeyFunc(input); + const versionString = getExtendedKeyString(result.version); + const privateVersions = ["xprv", "yprv", "zprv", "Zprv", "Yprv", "Ltpv", "Mtpv", "ttpv", "tprv", "uprv", "vprv", "Uprv", "Vprv"]; + if (privateVersions.indexOf(versionString) === -1) { + throw new OperationError("Not an extended private key."); + } + const newVersion = privateVersionToPublicVersion(versionString); + + const processedInput = result.masterkey.slice(2,); + const ecContext = ec.ec("secp256k1"); + const key = ecContext.keyFromPrivate(processedInput); + const pubkey = key.getPublic(true, "hex"); + + return serializeExtendedKeyFunc(getExtendedKeyVersion(newVersion), result.level, result.fingerprint, result.i, result.chaincode, pubkey); + } + +} + +export default PrivateExtendedKeyToPublic; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 5b4e85d3..edaa2513 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -186,6 +186,7 @@ import "./tests/PublicKeyToETHStyleAddress.mjs"; import "./tests/PublicKeyToTRXStyleAddress.mjs"; import "./tests/ETHTRXConversion.mjs"; import "./tests/KeyToExtendedKey.mjs"; +import "./tests/PrivateExtendedKeyToPublic.mjs"; import "./tests/SeedToAddress.mjs"; import "./tests/GetAllCasings.mjs"; import "./tests/SIGABA.mjs"; diff --git a/tests/operations/tests/PrivateExtendedKeyToPublic.mjs b/tests/operations/tests/PrivateExtendedKeyToPublic.mjs new file mode 100644 index 00000000..671d5bce --- /dev/null +++ b/tests/operations/tests/PrivateExtendedKeyToPublic.mjs @@ -0,0 +1,58 @@ +/** + * Private Extended Key to Public Version Extended Key Tests. + * + * @author dgoldenberg [virtualcurrency@mitre.org] + * @copyright MITRE 2023 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + + +TestRegister.addTests([ + { + name: "Private Extended Key To Public (xprv)", + input: "xprv9yoUuSwPzquxGY9ZsqizskpbKBbthhTpj5w4k2o8cxoFKigDAhUV3GCHtKL4CFuUCHvwGt9bo1DRnavubpiiS2wJ9AN9bbnipT95qKyda6r", + expectedOutput: "xpub6CnqJxUHqDUFV2E2ysG1EtmKsDSP7ABg6JrfYRCkBJLECX1MiEnjb4WmjYnUvYjzvnrygmcVMH2vSuZMNUWozhrDpqr8fpEZfxAune2FYPX", + recipeConfig: [ + { + "op": "Private Extended Key To Public", + "args": [] + } + ] + }, + { + name: "Private Extended Key To Public (yprv)", + input: "yprvAJEHhwPWiVEbr18CDkhzPRm3ywbJCCuVV7Q8UzLtE4t75hajmN7DvjeS3BWbQGPdAANzyPmhUZitHEqyLX8KoBBnRqitj2Kvisw6dgmVwBH", + expectedOutput: "ypub6XDe7SvQYrnu4VCfKnEzkZhnXyRnbfdLrLKjHNkVnQR5xVutJuRUUXxutSgRgjRaJKHKHW1WXLqCmbgQgapdwsx98NnRDXwhZFkavU6ojct", + recipeConfig: [ + { + "op": "Private Extended Key To Public", + "args": [] + } + ] + }, + { + name: "Private Extended Key To Public (zprv)", + input: "zprvAdjAKuQajRu4ptst29FzkcB18fyZuHCKJhe51m38ejMcjW5P4frUffXMYfwLiTs4UrFD5m41So3VhAWGyrb4YV4gFRwYuLom8psLoW252Xd", + expectedOutput: "zpub6riWjQwUZoTN3NxM8Ao17k7jghp4JjvAfvZfp9SkD4tbcJQXcDAjDTqqPxywFFvD1BHiMMKvW3EjwMvuc7k2YcQbF1v2R42gxac4k3qq3sc", + recipeConfig: [ + { + "op": "Private Extended Key To Public", + "args": [] + } + ] + }, + { + name: "Private Extended Key To Public (blank)", + input: "", + expectedOutput: "", + recipeConfig: [ + { + "op": "Private Extended Key To Public", + "args": [] + } + ] + } + +]);