diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index f4549f6d..c02c313c 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -381,7 +381,9 @@ "Public Key To TRX Style Address", "ETH / TRX Conversion", "Key To Extended Key", - "Private Extended Key To Public" + "Private Extended Key To Public", + "From Base58Check", + "To Base58Check" ] }, { diff --git a/src/core/operations/FromBase58Check.mjs b/src/core/operations/FromBase58Check.mjs new file mode 100644 index 00000000..61006c7d --- /dev/null +++ b/src/core/operations/FromBase58Check.mjs @@ -0,0 +1,71 @@ +/** + * @author dgoldenberg [dgoldenberg@mitre.org] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import { base58Decode, b58DoubleSHAChecksum} from "../lib/Bitcoin.mjs"; +import { fromArrayBuffer } from "crypto-api/src/encoder/array-buffer.mjs"; +import {toHex} from "crypto-api/src/encoder/hex.mjs"; + + +/** + * FromBase58Check operation + */ +class FromBase58Check extends Operation { + + /** + * FromBase58Check constructor + */ + constructor() { + super(); + + this.name = "From Base58Check"; + this.module = "Default"; + this.description = "Decodes Base58 check encoded data. This is a version byte, data and a 4 byte checksum at the end. Many addresses, private keys and other cryptocurrency artifacts are encoded in this format."; + this.infoURL = "https://en.bitcoin.it/Base58Check_encoding"; // 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 = "JSON"; + this.args = [ + { + name: "Version Byte Length", + type: "number", + value: 1 + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {JSON} + */ + run(input, args) { + // We check if input is blank. + // If its blank or just whitespace, we don't need to bother dealing with it. + + if (input.trim().length === 0) { + return ""; + } + input = input.trim(); + if (b58DoubleSHAChecksum(input)) { + const decoded = base58Decode(input); + const versionHex = toHex(fromArrayBuffer(decoded.slice(0, args[0]))); + const checksum = toHex(fromArrayBuffer(decoded.slice(-4,))); + const data = toHex(fromArrayBuffer(decoded.slice(args[0], -4))); + return { + "version": versionHex, + "checksum": checksum, + "data": data + }; + + } else { + throw new OperationError("Invalid Checksum."); + } + } + +} + +export default FromBase58Check; diff --git a/src/core/operations/ToBase58Check.mjs b/src/core/operations/ToBase58Check.mjs new file mode 100644 index 00000000..74709cfe --- /dev/null +++ b/src/core/operations/ToBase58Check.mjs @@ -0,0 +1,64 @@ +/** + * @author dgoldenberg [dgoldenberg@mitre.org] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +// import OperationError from "../errors/OperationError.mjs"; +import { base58Encode, doubleSHA, makeSureIsHex} from "../lib/Bitcoin.mjs"; +import { fromArrayBuffer } from "crypto-api/src/encoder/array-buffer.mjs"; +import {toHex} from "crypto-api/src/encoder/hex.mjs"; +import Utils from "../Utils.mjs"; +/** + * To Base58Check operation + */ +class ToBase58Check extends Operation { + + /** + * ToBase58Check constructor + */ + constructor() { + super(); + + this.name = "To Base58Check"; + this.module = "Default"; + this.description = "Converts passed in string to Base58 Check Encoding. Version Bytes (as hex) are pre-pended, and a checksum is created."; + this.infoURL = "https://en.bitcoin.it/Base58Check_encoding"; // 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": "Version Bytes", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex"] + }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + // We check if input is blank. + // If its blank or just whitespace, we don't need to bother dealing with it. + + if (input.trim().length === 0) { + return ""; + } + input = input.trim(); + const processedInput = makeSureIsHex(input); + const processedVersion = makeSureIsHex(args[0].string); + const extendedInput = processedVersion + processedInput; + const checksumHash = toHex(doubleSHA(fromArrayBuffer(Utils.convertToByteArray(extendedInput, "hex")))); + const finalString = extendedInput + checksumHash.slice(0, 8); + const encodedOutput = base58Encode(Utils.convertToByteArray(finalString, "hex")); + return encodedOutput; + } + +} + +export default ToBase58Check; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index d70848c4..dcbb29c9 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -206,6 +206,7 @@ import "./tests/ETHTRXConversion.mjs"; import "./tests/KeyToExtendedKey.mjs"; import "./tests/PrivateExtendedKeyToPublic.mjs"; import "./tests/SeedToAddress.mjs"; +import "./tests/Base58Check.mjs"; import "./tests/GetAllCasings.mjs"; import "./tests/SIGABA.mjs"; import "./tests/ELFInfo.mjs"; diff --git a/tests/operations/tests/Base58Check.mjs b/tests/operations/tests/Base58Check.mjs new file mode 100644 index 00000000..694d63a1 --- /dev/null +++ b/tests/operations/tests/Base58Check.mjs @@ -0,0 +1,144 @@ +/** + * Private key to secp256k1 public key tests. + * + * @author dgoldenberg [virtualcurrency@mitre.org] + * @copyright MITRE 2023 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Basic Base58 Check Decoding", + input: "1111111111111111111114oLvT2", + expectedOutput: "{\n \"version\": \"\",\n \"checksum\": \"94a00911\",\n \"data\": \"000000000000000000000000000000000000000000\"\n}", + recipeConfig: [ + { + "op": "From Base58Check", + "args": [0] + } + ], + }, + { + name: "Basic Base58 Check Decoding (Invalid Checksum)", + input: "1111111111111111111114oLvT3", + expectedOutput: "Invalid Checksum.", + recipeConfig: [ + { + "op": "From Base58Check", + "args": [0] + } + ], + }, + { + name: "Basic Base58 Check Decoding (Invalid Checksum Leading Zeros 1)", + input: "111111111111111111114oLvT2", + expectedOutput: "Invalid Checksum.", + recipeConfig: [ + { + "op": "From Base58Check", + "args": [0] + } + ], + }, + { + name: "Basic Base58 Check Decoding (Invalid Checksum Leading Zeros 2)", + input: "11111111111111111111114oLvT2", + expectedOutput: "Invalid Checksum.", + recipeConfig: [ + { + "op": "From Base58Check", + "args": [0] + } + ], + }, + { + name: "Basic Base58 Check Decoding (Invalid Checksum Leading Zeros 2 With Single Version Byte)", + input: "11111111111111111111114oLvT2", + expectedOutput: "Invalid Checksum.", + recipeConfig: [ + { + "op": "From Base58Check", + "args": [1] + } + ], + }, + { + name: "Basic Base58 Check Decoding Gibberish", + input: "A9CfWPWwyc4JC4ATfv4ajp9aGunQ375hLHQ8gcY", + expectedOutput: "Invalid Checksum.", + recipeConfig: [ + { + "op": "From Base58Check", + "args": [1] + } + ], + }, + { + name: "Basic Base58 Check Decoding (Invalid Checksum With one Byte Version)", + input: "1111111111111111111114oLvT3", + expectedOutput: "Invalid Checksum.", + recipeConfig: [ + { + "op": "From Base58Check", + "args": [1] + } + ], + }, + { + name: "Basic Base58 Check Decoding (Version Length 1)", + input: "1111111111111111111114oLvT2", + expectedOutput: "{\n \"version\": \"00\",\n \"checksum\": \"94a00911\",\n \"data\": \"0000000000000000000000000000000000000000\"\n}", + recipeConfig: [ + { + "op": "From Base58Check", + "args": [1] + } + ], + }, + { + name: "To Base58 Check Basic", + input: "0000000000000000000000000000000000000000", + expectedOutput: "1111111111111111111114oLvT2", + recipeConfig: [ + { + "op": "To Base58Check", + "args": [{ "option": "Hex", "string": "00" }] + } + ] + }, + { + name: "To and From Base58 Check", + input: "0000000000000000000000000000000000000000", + expectedOutput: "{\n \"version\": \"00\",\n \"checksum\": \"94a00911\",\n \"data\": \"0000000000000000000000000000000000000000\"\n}", + recipeConfig: [ + { + "op": "To Base58Check", + "args": [{ "option": "Hex", "string": "00" }] + }, + { + "op": "From Base58Check", + "args": [1] + } + ] + + }, + { + name: "To and From Base58 Check (Technically wrong version byte length", + input: "0000000000000000000000000000000000000000", + expectedOutput: "{\n \"version\": \"0000000000\",\n \"checksum\": \"94a00911\",\n \"data\": \"00000000000000000000000000000000\"\n}", + recipeConfig: [ + { + "op": "To Base58Check", + "args": [{ "option": "Hex", "string": "00" }] + }, + { + "op": "From Base58Check", + "args": [5] + } + ] + + } + +]);