Added BIP32Derive, which uses bip32 from bitcoinjs. Modified a few more operations. Code compiles with 20.3.0

This commit is contained in:
David C Goldenberg 2024-08-19 20:37:12 -04:00
parent 810c94803d
commit f474ffaf72
17 changed files with 764 additions and 170 deletions

View file

@ -320,7 +320,9 @@
"Seedphrase To Seed",
"Change Extended Key Version",
"Seed To Master Key",
"Decrypt Keystore File"
"Decrypt Keystore File",
"BIP32Derive",
"Public Key To ETH Style Address"
]
},
{

View file

@ -13,9 +13,9 @@
* Javascript code below taken from:
* https://github.com/geco/bech32-js/blob/master/bech32-js.js
* Implements various segwit encoding / decoding functions.
*
*
* MIT License
*
*
* Copyright (c) 2019 geco
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@ -23,10 +23,10 @@
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

View file

@ -12,6 +12,126 @@ import {toHex} from "crypto-api/src/encoder/hex.mjs";
import Utils from "../Utils.mjs";
import OperationError from "../errors/OperationError.mjs";
/**
* Validates the length of the passed in input as one of the allowable lengths.
* @param {*} input
* @param {*} allowableLengths
* @returns
*/
function validateLengths(input, allowableLengths) {
return allowableLengths.includes(input.length);
}
/**
* Returns true if input is a valid hex string, false otherwise.
* @param {*} input
*/
function isHex(input) {
const re = /^[0-9A-Fa-f]{2,}$/g;
return re.test(input);
}
/**
* Returns true if input could be interpreted as a byte string, false otherwise.
*/
function isValidBytes(input) {
for (let i=0; i < input.length; i ++) {
if (input.charCodeAt(i) > 255) {
return false;
}
}
return true;
}
/**
* We validate a passed in input to see if it could be a valid private key.
* A valid private key is string of length 64 that is valid hex, or of length 32 that could be valid bytes.
* @param {*} input
*/
export function validatePrivateKey(input) {
const curInput = input.trim();
if (!validateLengths(curInput, [32, 64])) {
return "Invalid length. We want either 32 or 64 but we got: " + curInput.length;
}
if (curInput.length === 64 && !isHex(curInput)) {
return "We have a string of length 64, but not valid hex. Cannot be interpreted as a private key.";
}
if (curInput.length === 32 && !isValidBytes(curInput)) {
return "We have a string of length 32 but cannot cannot be interpreted as valid bytes.";
}
return "";
}
/**
* We validate a passed in input to see if it could be a valid public key.
* A valid public key (in bytes) is either:
* 65 bytes beginning with 04
* 33 bytes beginning with 02 or 03
* @param {*} input
*/
export function validatePublicKey(input) {
const curInput = input.trim();
if (!validateLengths(curInput, [33, 65, 66, 130])) {
return "Invalid length. We want either 33, 65 (if bytes) or 66, 130 (if hex) but we got: " + curInput.length;
}
if (isHex(curInput)) {
if (!validateLengths(curInput, [66, 130])) {
return "We have a hex string, but its length is wrong. We want 66, 130 but we got: " + curInput.length;
}
if (curInput.length === 66 && (curInput.slice(0, 2) !== "02" && curInput.slice(0, 2) !== "03")) {
return "We have a valid hex string, of reasonable length, (66) but doesn't start with the right value. Correct values are 02, or 03 but we have: " + curInput.slice(0, 2);
}
if (curInput.length === 130 && curInput.slice(0, 2) !== "04") {
return "We have a valid hex string of reasonable length, (130) but doesn't start with the right value. Correct values are 04 but we have: " + curInput.slice(0, 2);
}
return "";
}
if (isValidBytes(curInput)) {
if (!validateLengths(curInput, [33, 65])) {
return "We have a byte string, but its length is wrong. We want 33 or 65 but we got: " + curInput.length;
}
if (curInput.length === 33 && toHex(curInput[0]) !== "02" && toHex(curInput[0]) !== "03") {
return "We have a valid byte string, of reasonable length, (33) but doesn't start with the right value. Correct values are 02, or 03 but we have: " + toHex(curInput[0]) ;
}
if (curInput.length === 65 && toHex(curInput[0]) !== "04") {
return "We have a valid byte string, of reasonable length, (65) but doesn't start with the right value. Correct value is 04 but we have: " + toHex(curInput[0]);
}
return "";
}
}
/**
* We make sure the input is a valid hex string, regardless of if its hex or bytes.
* If not valid bytes or hex, we throw TypeError.
* @param {*} input
* @returns
*/
export function makeSureIsHex(input) {
if (!(isValidBytes(input)) && !(isHex(input))) {
throw TypeError("Input: " + input + " is not valid bytes or hex.");
}
if (isValidBytes(input) && !isHex(input)) {
return toHex(input);
}
return input;
}
/**
* We make sure the input is valid bytes, regardless of if its hex or bytes.
* If not valid bytes or hex, we throw TypeError.
* @param {*} input
*/
export function makeSureIsBytes(input) {
if (!(isValidBytes(input)) && !(isHex(input))) {
throw TypeError("Input: " + input + " is not valid bytes or hex.");
}
if (isHex(input)) {
return fromArrayBuffer(Utils.convertToByteArray(input, "hex"));
}
return input;
}
// ################################################ BEGIN HELPER HASH FUNCTIONS #################################################
// SHA256(SHA256(input))
@ -212,6 +332,35 @@ export function deserializeExtendedKeyFunc (input) {
}
}
// Reverse lookup for version bytes
const versionString = {
"043587cf": "tpub",
"04358394": "tprv",
"044a5262": "upub",
"044a4e28": "uprv",
"045f1cf6": "vpub",
"045f18bc": "vprv",
"024289ef": "Upub",
"024285b5": "Uprv",
"02575483": "Vpub",
"02575048": "Vprv",
"0488b21e": "xpub",
"0488ade4": "xprv",
"049d7cb2": "ypub",
"049d7878": "yprv",
"04b24746": "zpub",
"04b2430c": "zprv",
"02aa7ed3": "Zpub",
"02aa7a99": "Zprv",
"0295b43f": "Ypub",
"0295b005": "Yprv",
"019da462": "Ltub",
"019d9cfe": "Ltpv",
"01b26ef6": "Mtub",
"01b26792": "Mtpv",
"0436f6e1": "ttub",
"0436ef7d": "ttpv"
};
// Version byte dictionary.
const versionBytes = {
@ -253,6 +402,16 @@ export function getExtendedKeyVersion(input) {
}
/**
* Reverse lookup for version string. We take in bytes, output string.
* @param {*} input
* @returns
*/
export function getExtendedKeyString(input) {
return versionString[input];
}
/**
* We serialize the extended key based off of the passed in data.
* We assume that the i value should be interpreted as a Uint32 LE.
@ -296,6 +455,12 @@ const versionByteInfo = {
"P2SH": "C4",
"WIF": "EF",
"hrp": "tb"
},
"LTC": {
"hrp": "ltc",
"P2PKH": "30",
"P2SH": "32",
"WIF": "B0"
}
};

View file

@ -0,0 +1,93 @@
/**
* @author dgoldenberg [virtualcurrency@mitre.org]
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
// import OperationError from "../errors/OperationError.mjs";
import { b58DoubleSHAChecksum} from "../lib/Bitcoin.mjs";
import { BIP32Factory} from "bip32";
import ecc from "@bitcoinerlab/secp256k1";
/**
* Sanity checks a derivation path.
* @param {*} input
*/
function verifyDerivationPath(input) {
const splitResults = input.split("/");
let startIndex = 0;
// We skip the first index if its m, as that's common.
if (splitResults[0] === "m") {
startIndex = 1;
}
for (let i =startIndex; i < splitResults.length; i++) {
const re = /^[0-9]{1,}[']{0,1}$/g;
if (!re.test(splitResults[i])) {
return false;
}
}
return true;
}
/**
* BIP32Derive operation
*/
class BIP32Derive extends Operation {
/**
* BIP32Derive constructor
*/
constructor() {
super();
this.name = "BIP32Derive";
this.module = "Default";
this.description = "Takes in an extended key, performs BIP32 key derivation on the extended key, and returns the result as an extended key.";
this.infoURL = "https://en.bitcoin.it/wiki/BIP_0032";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Derivation Path",
"type": "string",
"value": ""
},
];
this.checks = [
{
"pattern": "^(X|x|Y|y|Z|z|L|l|T|t)[pub|prv|tbv|tub][A-HJ-NP-Za-km-z1-9]{2,}$",
"flags": "",
"args": []
}
];
}
/**
* @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();
if (!verifyDerivationPath(args[0])) {
return "Invalid derivation path: " + args[0] + "\n";
}
const xkeyRe = /^(X|x|Y|y|Z|z|L|l|T|t)[pub|prv|tbv|tub][A-HJ-NP-Za-km-z1-9]{2,}$/g;
if (!b58DoubleSHAChecksum(input) || !xkeyRe.test(input)) {
return "Possibly invalid Extended Key: " + input + "\n";
}
const bip32 = BIP32Factory(ecc);
const node = bip32.fromBase58(input);
const child = node.derivePath(args[0]);
return child.toBase58();
}
}
export default BIP32Derive;

View file

@ -7,9 +7,11 @@
*/
import Operation from "../Operation.mjs";
import {toHex} from "../lib/Hex.mjs";
import ec from "elliptic";
import { validatePrivateKey, makeSureIsHex} from "../lib/Bitcoin.mjs";
// import { toHex } from "crypto-api/src/encoder/hex.mjs";
// const curves = ["secp256k1", "ed25519", "curve25519", "p521", "p384", "p256", "p224", "p192"];
/**
* Class that takes in a private key, and returns the public key, either in compressed or uncompressed form(s).
*/
@ -56,24 +58,15 @@ class PrivateECKeyToPublic extends Operation {
return "";
}
input = input.trim();
const re = /^[0-9A-Fa-f]{2,}$/g;
if (!(input.length === 64 && re.test(input)) && !(input.length === 32)) {
return "Must pass a hex string of length 64, or a byte string of length 32. Got length " + input.length;
}
// If we have bytes, we need to turn the bytes to hex.
if (input.length !== undefined && input.length === 32) {
const buf = new Uint8Array(new ArrayBuffer(32));
for (let i= 0; i < 32; i ++) {
if (input.charCodeAt(i) > 255) {
return "Cannot interpret this 32 character string as bytes.";
}
buf[i] = input.charCodeAt(i);
}
input = toHex(buf, "", 2, "", 0);
const privKeyCheck = validatePrivateKey(input);
if (privKeyCheck.trim().length !== 0) {
return "Error with the input as private key. Error is:\n\t" + privKeyCheck;
}
const processedInput = makeSureIsHex(input);
const ecContext = ec.ec("secp256k1");
const key = ecContext.keyFromPrivate(input);
const key = ecContext.keyFromPrivate(processedInput);
const pubkey = key.getPublic(args[0], "hex");
return pubkey;

View file

@ -7,10 +7,10 @@
*/
import Operation from "../Operation.mjs";
import { base58Encode, getWIFVersionByte, doubleSHA} from "../lib/Bitcoin.mjs";
import { base58Encode, getWIFVersionByte, doubleSHA, validatePrivateKey, 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 {toHex as toHexOther} from "../lib/Hex.mjs";
// import {toHex as toHexOther} from "../lib/Hex.mjs";
import Utils from "../Utils.mjs";
@ -65,26 +65,13 @@ class PrivateKeyToWIF extends Operation {
return "";
}
input = input.trim();
// We check to see if the input is hex or not.
// If it is not, we convert it back to hex
const re = /[0-9A-Fa-f]{2,}/g;
if (!(input.length === 64 && re.test(input)) && !(input.length === 32)) {
return "Must pass a hex string of length 64, or a byte string of length 32. Got length: " + input.length;
const privateKeyCheck = validatePrivateKey(input);
if (privateKeyCheck.trim().length !== 0) {
return "Error parsing private key. Error is:\n\t" + privateKeyCheck;
}
if (input.length === 32) {
const buf = new Uint8Array(new ArrayBuffer(32));
for (let i= 0; i < 32; i ++) {
if (input.charCodeAt(i) > 255) {
return "Cannot interpret this 32 character string as bytes.";
}
buf[i] = input.charCodeAt(i);
}
input = toHexOther(buf, "", 2, "", 0);
}
const processedKey = makeSureIsHex(input);
const versionByte = getWIFVersionByte(args[0]);
let extendedPrivateKey = versionByte + input;
let extendedPrivateKey = versionByte + processedKey;
if (args[1]) {
extendedPrivateKey += "01";
}

View file

@ -0,0 +1,87 @@
/**
* @author dgoldenberg [virtualcurrency@mitre.org]
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import { makeSureIsBytes, validatePublicKey} from "../lib/Bitcoin.mjs";
import JSSHA3 from "js-sha3";
import Utils from "../Utils.mjs";
import ec from "elliptic";
/**
* Turns a public key into an ETH address.
* @param {*} input Input, a public key in hex or bytes.
*/
function pubKeyToETHAddress(input) {
// Ethereum addresses require uncompressed public keys.
// We convert if the public key is compressed.
let curKey = makeSureIsBytes(input);
if (curKey[0] !== 0x04 || curKey.length !== 65) {
const ecContext = ec.ec("secp256k1");
const thisKey = ecContext.keyFromPublic(curKey);
curKey = thisKey.getPublic(false, "hex");
}
const algo = JSSHA3.keccak256;
// We need to redo the hex-> bytes transformation here because Javascript is silly.
// sometimes what is desired is an array of ints.
// Other times a string
// Here, the Keccak algorithm seems to want an array of ints. (sigh)
const result = algo(Utils.convertToByteArray(curKey, "hex").slice(1,));
return "0x" + result.slice(-40);
}
/**
* PublicKeyToETHStyleAddress operation
*/
class PublicKeyToETHStyleAddress extends Operation {
/**
* PublicKeyToETHStyleAddress constructor
*/
constructor() {
super();
this.name = "Public Key To ETH Style Address";
this.module = "Default";
this.description = "Converts a public key (compressed or uncompressed) to an Ethereum style address.";
this.infoURL = "https://www.freecodecamp.org/news/how-to-create-an-ethereum-wallet-address-from-a-private-key-ae72b0eee27b/";
this.inputType = "string";
this.outputType = "string";
this.args = [];
this.checks = [
{
pattern: "^0[3|2][a-fA-F0-9]{64}$",
flags: "",
args: []
},
{
pattern: "^04[a-fA-F0-9]{128}$",
flags: "",
args: []
}
];
}
/**
* @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 "";
}
if (validatePublicKey(input) !== "") {
return validatePublicKey(input);
}
return pubKeyToETHAddress(input);
}
}
export default PublicKeyToETHStyleAddress;

View file

@ -9,11 +9,11 @@
import Operation from "../Operation.mjs";
import { fromArrayBuffer } from "crypto-api/src/encoder/array-buffer.mjs";
import {toHex} from "crypto-api/src/encoder/hex.mjs";
import { base58Encode, getP2PKHVersionByte, getP2SHVersionByte, hash160Func, doubleSHA, getHumanReadablePart} from "../lib/Bitcoin.mjs";
import { base58Encode, getP2PKHVersionByte, getP2SHVersionByte, hash160Func, doubleSHA, getHumanReadablePart, makeSureIsBytes, validatePublicKey} from "../lib/Bitcoin.mjs";
import {encodeProgramToSegwit} from "../lib/Bech32.mjs";
import JSSHA3 from "js-sha3";
import Utils from "../Utils.mjs";
/**
* Converts a Public Key to a P2PKH Address of the given type.
*/
@ -27,19 +27,19 @@ class PublicKeyToP2PKHAddress extends Operation {
this.name = "Public Key To Cryptocurrency Address";
this.module = "Default";
this.description = "Turns a public key into a cryptocurrency address.";
this.description = "Turns a public key into a cryptocurrency address. Can select P2PKH, P2SH-P2WPKH and P2WPKH addresses for Bitcoin and Testnet.";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Currency Type",
"type": "option",
"value": ["BTC", "Testnet", "Ethereum"]
"value": ["BTC", "Testnet", "LTC"]
},
{
"name": "Address Type",
"type": "option",
"value": ["P2PKH (V1 BTC Addresses)", "P2SH-P2PWPKH (Segwit Compatible)", "Segwit (P2WPKH)"]
"value": ["P2PKH (V1 BTC Addresses)", "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)", "Segwit (P2WPKH bc1 Addresses)"]
}
];
this.checks = [
@ -48,11 +48,6 @@ class PublicKeyToP2PKHAddress extends Operation {
flags: "",
args: ["BTC", "P2PKH (V1 BTC Addresses)"]
},
{
pattern: "^04[a-fA-F0-9]{128}$",
flags: "",
args: ["Ethereum", "P2PKH (V1 BTC Addresses)"]
}
];
}
@ -68,75 +63,39 @@ class PublicKeyToP2PKHAddress extends Operation {
if (input.trim().length === 0) {
return "";
}
// We check to see if the input is hex or not.
// If it is, we convert back to bytes.
const re = /([0-9A-Fa-f]{2,})/g;
let inputIsHex = false;
let curInput = input;
if (re.test(input)) {
inputIsHex = true;
}
if (inputIsHex) {
curInput = fromArrayBuffer(Utils.convertToByteArray(input, "hex"));
if (validatePublicKey(input) !== "") {
return validatePublicKey(input);
}
// We sanity check the input
const startByte = toHex(curInput[0]);
if (curInput.length !== 33 && curInput.length !== 65) {
return "Input is wrong length. Should be either 33 or 65 bytes, but is: " + curInput.length;
}
if (curInput.length === 33 && startByte !== "03" && startByte !== "02") {
return "Input is 33 bytes, but begins with invalid byte: " + startByte;
}
if (curInput.length === 65 && startByte !== "04") {
return "Input is 65 bytes, but begins with invalid byte: " + startByte;
}
if (args[0] === "Ethereum") {
// Ethereum addresses require uncompressed public keys.
if (startByte !== "04" || curInput.length !== 65) {
return "Ethereum addresses require uncompressed public keys.";
}
const algo = JSSHA3.keccak256;
// We need to redo the hex-> bytes transformation here because Javascript is silly.
// sometimes what is desired is an array of ints.
// Other times a string
// Here, the Keccak algorithm seems to want an array of ints. (sigh)
let result;
if (inputIsHex) {
result = algo(Utils.convertToByteArray(input, "hex").slice(1,));
} else {
result = algo(Utils.convertToByteArray(toHex(input), "hex").slice(1,));
}
return "0x" + result.slice(-40);
} else {
// We hash the input
const hash160 = toHex(hash160Func(curInput));
// We do segwit addresses first.
if (args[1] === "Segwit (P2WPKH)") {
const redeemScript = hash160;
const hrp = getHumanReadablePart(args[0]);
// We hash the input
const curInput = makeSureIsBytes(input);
const hash160 = toHex(hash160Func(curInput));
// We do segwit addresses first.
if (args[1] === "Segwit (P2WPKH bc1 Addresses)") {
const redeemScript = hash160;
const hrp = getHumanReadablePart(args[0]);
if (hrp !== "") {
return encodeProgramToSegwit(hrp, 0, Utils.convertToByteArray(redeemScript, "hex"));
}
// It its not segwit, we create the redeemScript either for P2PKH or P2SH-P2WPKH addresses.
const versionByte = "P2PKH (V1 BTC Addresses)" === args[1] ? getP2PKHVersionByte(args[0]) : getP2SHVersionByte(args[0]);
// If its a P2SH-P2WPKH address, we have to prepend some extra bytes and hash again. Either way we prepend the version byte.
let hashRedeemedScript;
if (args[1] === "P2SH-P2PWPKH (Segwit Compatible)") {
const redeemScript = "0014" + hash160;
hashRedeemedScript = versionByte + toHex(hash160Func(fromArrayBuffer(Utils.convertToByteArray(redeemScript, "hex"))));
} else {
hashRedeemedScript = versionByte + hash160;
return args[0] + " does not support Segwit Addresses.";
}
// We calculate the checksum, convert to Base58 and then we're done!
const checksumHash = toHex(doubleSHA(fromArrayBuffer(Utils.convertToByteArray(hashRedeemedScript, "hex"))));
const finalString = hashRedeemedScript + checksumHash.slice(0, 8);
const address = base58Encode(Utils.convertToByteArray(finalString, "hex"));
return address;
}
// It its not segwit, we create the redeemScript either for P2PKH or P2SH-P2WPKH addresses.
const versionByte = "P2PKH (V1 BTC Addresses)" === args[1] ? getP2PKHVersionByte(args[0]) : getP2SHVersionByte(args[0]);
// If its a P2SH-P2WPKH address, we have to prepend some extra bytes and hash again. Either way we prepend the version byte.
let hashRedeemedScript;
if (args[1] === "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)") {
const redeemScript = "0014" + hash160;
hashRedeemedScript = versionByte + toHex(hash160Func(fromArrayBuffer(Utils.convertToByteArray(redeemScript, "hex"))));
} else {
hashRedeemedScript = versionByte + hash160;
}
// We calculate the checksum, convert to Base58 and then we're done!
const checksumHash = toHex(doubleSHA(fromArrayBuffer(Utils.convertToByteArray(hashRedeemedScript, "hex"))));
const finalString = hashRedeemedScript + checksumHash.slice(0, 8);
const address = base58Encode(Utils.convertToByteArray(finalString, "hex"));
return address;
}