diff --git a/src/core/operations/KeyToExtendedKey.mjs b/src/core/operations/KeyToExtendedKey.mjs new file mode 100644 index 00000000..b5b168b2 --- /dev/null +++ b/src/core/operations/KeyToExtendedKey.mjs @@ -0,0 +1,87 @@ +/** + * @author dcgoldenberg [dgoldenberg@mitre.org] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import { makeSureIsHex, serializeExtendedKeyFunc, getExtendedKeyVersion } from "../lib/Bitcoin.mjs"; + + +/** + * Key To Extended Key operation + */ +class KeyToExtendedKey extends Operation { + + /** + * KeyToExtendedKey constructor + */ + constructor() { + super(); + + this.name = "Key To Extended Key"; + this.module = "Default"; + this.description = "Turns a key, with chaincode and version as parameters, into an extended key. We assume the key is meant to be a master key, so depth and child number are set to 0, and fingerprint is set to 00000000."; + this.infoURL = "https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki"; // 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 = [ + { + "name": "Chaincode", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex"] + }, + { + "name": "Version Type", + "type": "option", + "value": ["xpub", "xprv", "ypub", "yprv", "zpub", "zprv", "Zpub", "Zprv", "Ypub", "Yprv", "Ltub", "Ltpv", "Mtub", "Mtpv", "ttub", "ttpv", "tpub", "tprv", "upub", "uprv", "vpub", "vprv", "Upub", "Uprv", "Vpub", "Vprv"] + } + /* Example arguments. See the project wiki for full details. + { + name: "First arg", + type: "string", + value: "Don't Panic" + }, + { + name: "Second arg", + type: "number", + value: 42 + } + */ + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + // const [firstArg, secondArg] = args; + + const inputAsHex = makeSureIsHex(input); + const isPublic = inputAsHex.length === 66 && (inputAsHex.startsWith("03") || inputAsHex.startsWith("02")); + const isPrivate = inputAsHex.length === 64; + const privateVersions = ["xprv", "yprv", "zprv", "Zprv", "Yprv", "Ltpv", "Mtpv", "ttpv", "tprv", "uprv", "vprv", "Uprv", "Vprv"]; + if (!isPublic && !isPrivate) { + throw new OperationError("Error: String " + inputAsHex + " is not a valid public or private key."); + } + if (isPublic && privateVersions.indexOf(args[1]) !== -1) { + throw new OperationError("Error: Mis-Match between version and key type. Public Key is entered, but a private version is selected."); + } + if (isPrivate && privateVersions.indexOf(args[1]) === -1) { + throw new OperationError("Error: Mis-Match between version and key type. Private Key is entered, but a public version is selected."); + } + const key = isPrivate ? "00" + inputAsHex : inputAsHex; + const newVersion = getExtendedKeyVersion(args[1]); + + const newExtendedKey = serializeExtendedKeyFunc(newVersion, 0, "00000000", 0, args[0].string, key); + return newExtendedKey; + + } + +} + +export default KeyToExtendedKey; diff --git a/tests/operations/tests/KeyToExtendedKey.mjs b/tests/operations/tests/KeyToExtendedKey.mjs new file mode 100644 index 00000000..cfb532b6 --- /dev/null +++ b/tests/operations/tests/KeyToExtendedKey.mjs @@ -0,0 +1,82 @@ +/** + * Key To Extended Key Tests. + * + * @author dgoldenberg [virtualcurrency@mitre.org] + * @copyright MITRE 2023 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + + +TestRegister.addTests([ + { + name: "Key To Extended Key (Basic, XPRV)", + input: "f55acd736ff0f80f2cdc56ab5ac1e4d4ce92e1f46d137a282a61e706681df9c5", + expectedOutput: "xprv9s21ZrQH143K4bXtdSLbsWGSfAok775A1bF3YwPeHe8ePa7QwD5V4kK1RmdZV2M6TKVfszKt19UaHfyqdQ2MZ8dv2t7G2Tvtxnef7Pxu8Qu", + recipeConfig: [ + { + "op": "Key To Extended Key", + "args": [ + { + "option": "Hex", + "string": "fedf6c5ebcc2fc7b66291e55501a005886128bf97aeced3a91478a0c44f54dbe" + }, + "xprv" + ] + } + ], + }, + { + name: "Key To Extended Key (YPRV Same Data)", + input: "f55acd736ff0f80f2cdc56ab5ac1e4d4ce92e1f46d137a282a61e706681df9c5", + expectedOutput: "yprvABrGsX5C9janutj1To8E5bMwq8xC3j4evhmGLLHXfeWXSfveBsF3goy9Syb9Uw11rxcUdTvSToq8AxbQM6SNMNKWuDogcNkPEWiJVuszXFR", + recipeConfig: [ + { + "op": "Key To Extended Key", + "args": [ + { + "option": "Hex", + "string": "fedf6c5ebcc2fc7b66291e55501a005886128bf97aeced3a91478a0c44f54dbe" + }, + "yprv" + ] + } + ], + }, + { + name: "Key To Extended Key (Public Key With Private Version), Should Throw Error.", + input: "02233f618dad285dc8b03eb7bf9a59dec039b7c1b2433dabdca636e17e10890e85", + expectedOutput: "Error: Mis-Match between version and key type. Public Key is entered, but a private version is selected.", + recipeConfig: [ + { + "op": "Key To Extended Key", + "args": [ + { + "option": "Hex", + "string": "cda6685894f356bdf1bd6af6cb7e3961e550fb9c9b2c82d9ebfd89f48c2afea6" + }, + "xprv" + ] + } + ], + }, + { + name: "Key To Extended Key (Private Key With Public Version), Should Throw Error.", + input: "f55acd736ff0f80f2cdc56ab5ac1e4d4ce92e1f46d137a282a61e706681df9c5", + expectedOutput: "Error: Mis-Match between version and key type. Private Key is entered, but a public version is selected.", + recipeConfig: [ + { + "op": "Key To Extended Key", + "args": [ + { + "option": "Hex", + "string": "fedf6c5ebcc2fc7b66291e55501a005886128bf97aeced3a91478a0c44f54dbe" + }, + "xpub" + ] + } + ], + }, + +]);