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": [] + } + ] + } + +]);