UNPKG

@ngraveio/ur-blockchain-commons

Version:

A JS implementation of Uniform Resources(UR) Registry specification from Blockchain Commons.

498 lines (490 loc) 22.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HDKey = void 0; const bc_ur_1 = require("@ngraveio/bc-ur"); const Keypath_1 = require("./Keypath"); const CoinInfo_1 = require("./CoinInfo"); const base_1 = require("@scure/base"); const sha256_1 = require("@noble/hashes/sha256"); // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#serialization-format const BTCVersionBytes = { MAINNET_XPUB: Buffer.from('0488B21E', 'hex'), MAINNET_XPRIV: Buffer.from('0488ADE4', 'hex'), TESTNET_XPUB: Buffer.from('043587CF', 'hex'), TESTNET_XPRIV: Buffer.from('04358394', 'hex'), }; class HDKey extends (0, bc_ur_1.registryItemFactory)({ tag: 40303, URType: 'hdkey', keyMap: { isMaster: 1, isPrivateKey: 2, keyData: 3, chainCode: 4, useInfo: 5, origin: 6, children: 7, parentFingerprint: 8, name: 9, note: 10, }, CDDL: ` tagged-hdkey = #6.40303(hdkey) ; An HD key is either a master key or a derived key. hdkey = { master-key / derived-key } ; A master key is always private, has no use or derivation information, ; and always includes a chain code. master-key = ( is-master: true, key-data: key-data-bytes, chain-code: chain-code-bytes ) ; A derived key may be private or public, has an optional chain code, and ; may carry additional metadata about its use and derivation. ; To maintain isomorphism with [BIP32] and allow keys to be derived from ; this key 'chain-code', 'origin', and 'parent-fingerprint' must be present. ; If 'origin' contains only a single derivation step and also contains 'source-fingerprint', ; then 'parent-fingerprint' MUST be identical to 'source-fingerprint' or may be omitted. derived-key = ( ? is-private: bool .default false, ; true if key is private, false if public key-data: key-data-bytes, ? chain-code: chain-code-bytes ; omit if no further keys may be derived from this key ? use-info: tagged-coininfo, ; How the key is to be used ? origin: tagged-keypath, ; How the key was derived ? children: tagged-keypath, ; What children should/can be derived from this ? parent-fingerprint: uint32 .ne 0, ; The fingerprint of this key's direct ancestor, per [BIP32] ? name: text, ; A short name for this key. ? note: text ; An arbitrary amount of text describing the key. ) ; If the 'use-info' field is omitted, defaults (mainnet BTC key) are assumed. ; If 'cointype' and 'origin' are both present, then per [BIP44], the second path ; component's 'child-index' must match 'cointype'. ; The 'children' field may be used to specify what set of child keys should or can be derived from this key. ; This may include 'child-index-range' or 'child-index-wildcard' as its last component. ; Any components that specify hardened derivation will require the key be private. is-master = 1 is-private = 2 key-data = 3 chain-code = 4 use-info = 5 origin = 6 children = 7 parent-fingerprint = 8 name = 9 note = 10 uint8 = uint .size 1 key-data-bytes = bytes .size 33 chain-code-bytes = bytes .size 32 `, }) { constructor(input) { super(input); /** * @returns {boolean} True if the key is a master key, otherwise false. */ this.getIsMaster = () => this.data.isMaster || false; /** * @returns {boolean} True if the key is a private key, otherwise false. */ this.getIsPrivateKey = () => { // Master key is always private if (this.getIsMaster()) return true; return this.data.isPrivateKey || false; }; /** * @returns {Buffer} The key data. */ this.getKeyData = () => this.data.keyData; /** * @returns {Buffer | undefined} The chain code. */ this.getChainCode = () => this.data.chainCode; /** * @returns {CoinInfo | undefined} The coin information. */ this.getUseInfo = () => this.data.useInfo; /** * @returns {Keypath | undefined} The origin keypath. */ this.getOrigin = () => this.data.origin; /** * @returns {Keypath | undefined} The children keypath. */ this.getChildren = () => this.data.children; /** * @returns {number | undefined} The parent fingerprint. */ this.getParentFingerprint = () => this.data.parentFingerprint; /** * @returns {string | undefined} The name of the key. */ this.getName = () => this.data.name; /** * @returns {string | undefined} The note associated with the key. */ this.getNote = () => this.data.note; // Check if this is a master key key or a derived key if (input.isMaster) { this.data = { isMaster: true, keyData: input.keyData, chainCode: input.chainCode, }; } else { this.data = { isMaster: input?.isMaster, isPrivateKey: input.isPrivateKey, // By default it is false keyData: input.keyData, chainCode: input.chainCode, useInfo: input.useInfo, origin: input.origin, children: input.children, parentFingerprint: input.parentFingerprint, name: input.name, note: input.note, }; } } /** * Prepares the data for CBOR encoding. * @returns {any} The data prepared for CBOR encoding. * @throws {Error} If the input data is invalid. */ preCBOR() { const { valid, reasons } = this.verifyInput(this.data); if (!valid) { if (reasons && reasons.length > 0) { const reasonMessages = reasons .map(r => r.message ?? '') .filter(Boolean) .join(', '); throw new Error(`Invalid HDKey: ${reasonMessages}`); } } return super.preCBOR(); } /** * Verifies the input data. * @param {HDKeyArgs} input - The input data to verify. * @returns {{ valid: boolean; reasons?: Error[] }} The verification result. */ verifyInput(input) { const errors = []; if (input.isMaster !== undefined && typeof input.isMaster !== 'boolean') { errors.push(new Error('isMaster must be a boolean')); } if (!(input.keyData instanceof Uint8Array)) { errors.push(new Error('keyData must be a Buffer or Uint8Array')); } // If this is a master key, chainCode is required if (input.isMaster && !input.chainCode) { errors.push(new Error('chainCode is required for master key')); } if (input.chainCode && !(input.chainCode instanceof Uint8Array)) { errors.push(new Error('chainCode must be a Buffer or Uint8Array')); } if (input.isPrivateKey !== undefined && typeof input.isPrivateKey !== 'boolean') { errors.push(new Error('isPrivateKey must be a boolean')); } if (input.useInfo && !(input.useInfo instanceof CoinInfo_1.CoinInfo)) { errors.push(new Error('useInfo must be an instance of CoinInfo')); } if (input.origin && !(input.origin instanceof Keypath_1.Keypath)) { errors.push(new Error('origin must be an instance of Keypath')); } if (input.children && !(input.children instanceof Keypath_1.Keypath)) { errors.push(new Error('children must be an instance of Keypath')); } if (input.parentFingerprint !== undefined) { // It needs to be an integer and bigger than 0 and maximum 32 bit size if (typeof input.parentFingerprint !== 'number' || input.parentFingerprint < 0 || input.parentFingerprint > 0xffffffff) { errors.push(new Error('parentFingerprint must be a positive integer (uint32)')); } // Check if this is a master key if (input.isMaster) { errors.push(new Error('Master key cannot contain a parent fingerprint')); } } if (input.origin && input.origin.getComponents().length === 1 && input.origin.getSourceFingerprint() !== undefined) { if (input.parentFingerprint !== input.origin.getSourceFingerprint()) { errors.push(new Error('Parent fingerprint for single derivation path should match the source fingerprint of the origin keypath.')); } } if (input.useInfo && input.origin) { const components = input.origin.getComponents(); if (components.length < 2) { errors.push(new Error('When BIP44 is specified, the derivation path should contain at least two components.')); } else if (components.length >= 2 && components[1].getIndex() !== input.useInfo.getType()) { errors.push(new Error('When BIP44 is specified, the derivation path should contain the coin type value.')); } } if (input.children) { const components = input.children.getComponents(); if (components.some(component => component.isHardened()) && !input.isPrivateKey) { errors.push(new Error('Only a private key can have hardened children keys.')); } } if (input.name && typeof input.name !== 'string') { errors.push(new Error('name must be a string')); } if (input.note && typeof input.note !== 'string') { errors.push(new Error('note must be a string')); } return { valid: errors.length === 0, reasons: errors.length > 0 ? errors : undefined, }; } /** * Creates an HDKey instance from an extended public key (xpub). * @param {string} xpub - The extended public key. * @param {{ xpubPath?: string; isPrivate?: boolean; sourceFingerprint?: number }} [params] - Optional parameters. * @returns {HDKey} The HDKey instance. * @throws {Error} If the xpub is invalid or inconsistent with the provided path. */ static fromXpub(xpub, params) { const { version: version, depth, parentFingerprint, childNumber, chainCode, keyData, checksum } = HDKey.parseXpub(xpub); const isMaster = depth === 0; if (isMaster) { const masterKeyParams = { isMaster: true, keyData: Buffer.from(keyData), chainCode: Buffer.from(chainCode), }; // Lets Generate the master HD KEY return new HDKey(masterKeyParams); } // Otherwise its a derived key let origin; // Lets check consistency of the xpub if (params?.xpubPath) { const components = Keypath_1.Keypath.pathToComponents(params.xpubPath); // Now lets check if the xpubPath is consistent with the xpub if (components.length !== depth) { throw new Error(`Provided path is not consistent with the xpub depth. Provided path: ${params.xpubPath}, xpub depth: ${depth}`); } // Check if child number is consistent with the xpub path const lastComponent = components[components.length - 1]; const lastComponentIndex = lastComponent.getIndex() || 0; const generatedChildNumber = lastComponent.isHardened() ? lastComponentIndex + 0x80000000 : lastComponentIndex; if (generatedChildNumber !== childNumber) { throw new Error(`Provided path is not consistent with the xpub. Provided path child number: ${generatedChildNumber}, xpub child number: ${childNumber}`); } // Source fingerprint should be equal to the parent fingerprint of the xpub if depth is 1 if (depth === 1 && params?.sourceFingerprint !== undefined && params?.sourceFingerprint !== parentFingerprint) { throw new Error(`Provided source fingerprint is not consistent with the xpub. Provided source fingerprint: ${params.sourceFingerprint}, xpub parent fingerprint: ${parentFingerprint}`); } origin = new Keypath_1.Keypath({ path: components, depth, sourceFingerprint: params.sourceFingerprint || undefined, }); } let _isPrivate = false; if (params?.isPrivate !== undefined) { _isPrivate = params.isPrivate; } else { // Check by bitcoin version bytes // TODO: to support all coins we need to keep track of all the version bytes // TODO: get them from coininfo package _isPrivate = version.equals(BTCVersionBytes.MAINNET_XPRIV) || version.equals(BTCVersionBytes.TESTNET_XPRIV); } const xpubHdKeyParams = { // isMaster: false, // set it to undefined if true ( default value) isPrivateKey: _isPrivate ? true : undefined, keyData: Buffer.from(keyData), chainCode: Buffer.from(chainCode), origin, parentFingerprint: parentFingerprint, // children: new Keypath({ path: '/0/*' }) //note: xpub, //name: xpub }; // Lets Generate the xpubs HD KEY const xpubHdKey = new HDKey(xpubHdKeyParams); return xpubHdKey; } /** * Converts the HDKey instance to an extended public key (xpub). * @param {{ versionBytes?: Buffer }} [params] - Optional parameters. * @returns {string} The extended public key. * @throws {Error} If the chain code or origin is missing. * * https://github.com/bitcoinjs/bip32/blob/master/ts-src/bip32.ts#L238 */ toXpub(params) { // If version bytes are provided use that otherwise, If masterkey or private key use xpriv otherwise use xpub const version = params?.versionBytes || (this.getIsMaster() || this.getIsPrivateKey() ? BTCVersionBytes.MAINNET_XPRIV : BTCVersionBytes.MAINNET_XPUB); // Check if chain code is present otherwise we cannot generate xpub if (this.getChainCode() == undefined) { throw new Error('Cannot generate xpub without chain code'); } // Get the key data const keyData = this.getKeyData(); // Get the chain code const chainCode = this.getChainCode(); // If its masterkey most of the values will be default if (this.getIsMaster()) { return HDKey.encodeXpub({ version, depth: 0, parentFingerprint: Buffer.alloc(4).fill(0), childNumber: 0, chainCode, keyData, }); } if (this.getOrigin() == undefined) { throw new Error('Cannot generate xpub without origin because of depth and child number'); } // Get the depth from the origin const depth = this.getOrigin()?.getDepth() || this.getOrigin()?.getComponents().length || 0; // If depth is 1 then origin.sourceFingerprint must be same as parentFingerprint const parentFingerprint = this.getParentFingerprint() || 0; // Childnumber is the last index of the origin path const lastIndex = this.getOrigin()?.getComponents().slice(-1)[0]; // Now make sure last index is simple index and check if its hardened if (!lastIndex || !lastIndex.isIndexComponent()) { throw new Error('Invalid origin path, origin should exist and last index should be simple index'); } const index = lastIndex.getIndex() || 0; const childNumber = lastIndex.isHardened() ? index + 0x80000000 : index; return HDKey.encodeXpub({ version: version, depth, parentFingerprint, childNumber, chainCode, keyData, }); } /** * Converts the HDKey instance to an extended public key (xpub). * @param {{ versionBytes?: Buffer }} [params] - Optional parameters. * @returns {string} The extended public key. * @throws {Error} If the chain code or origin is missing. */ getBip32Key(params) { return this.toXpub(params); } /** * Extracts the parent fingerprint from an extended public key (xpub). * @param {string} xpub - The extended public key. * @returns {number} The parent fingerprint. */ static extractParentFingerprint(xpub) { try { const { parentFingerprint } = HDKey.parseXpub(xpub); return parentFingerprint; } catch (e) { console.warn('Error extracting parent fingerprint from xpub', e); } return 0; } /** * Parses an extended public key (xpub). * @param {string} xpub - The extended public key. * @returns {{ version: Buffer; depth: number; parentFingerprint: number; childNumber: number; chainCode: Buffer; keyData: Buffer; checksum: Buffer; isMaster: boolean }} The parsed xpub components. * @throws {Error} If the checksum is invalid. */ static parseXpub(xpub) { // decode xpub from base58 to hex const xpubHex = Buffer.from(base_1.base58.decode(xpub)); // https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-007-hdkey.md // zpub6rBVCActEAEGdH5TJVz3H3H1kBYxmB2AiKEnfFJSFeiKU4vBepoxwCqDBqrgkvmeiUUfvGSUrii7J5anRWgyk8kN63xpXWhpcF2Lgi4gpkE // 4 byte: version bytes (mainnet: 0x0488B21E public, 0x0488ADE4 private; testnet: 0x043587CF public, 0x04358394 private) // 1 byte: depth: 0x00 for master nodes, 0x01 for level-1 derived keys, .... // 4 bytes: the fingerprint of the parent's key (0x00000000 if master key) // 4 bytes: child number. This is ser32(i) for i in xi = xpar/i, with xi the key being serialized. (0x00000000 if master key) // 32 bytes: the chain code // 33 bytes: the public key or private key data (serP(K) for public keys, 0x00 || ser256(k) for private keys) // 04b24746 03 75bb5468 80000000 529cb574542b8163f9f0a6bdc01180137350fdb50cf54186bffc067694d05d35 03fbd43643e702d2f9b7306345963ae77c49c5e2f6736d36b11db617918f8d28a4 c783598f // First 4 bytes are version bytes const version = xpubHex.slice(0, 4); // Next byte is depth (1 byte) const depth = xpubHex.slice(4, 5).readInt8(0); // Next 4 bytes are fingerprint const parentFingerprint = xpubHex.slice(5, 9).readUInt32BE(0); // Next 4 bytes are child number const childNumber = xpubHex.slice(9, 13).readUInt32BE(0); // Next 32 bytes are chain code const chainCode = xpubHex.slice(13, 45); // Next 33 bytes are public key const keyData = xpubHex.slice(45, 78); // Next 4 bytes are checksum (last 4 bytes) const checksum = xpubHex.slice(-4); // Verify checksum const calculatedChecksum = (0, sha256_1.sha256)((0, sha256_1.sha256)(xpubHex.slice(0, -4))).subarray(0, 4); if (!checksum.equals(calculatedChecksum)) { throw new Error('Invalid checksum for xpub'); } // Check if this is a master key key or a derived key // const isMaster = depth === 0 // TODO: check version bytes to determine if its private or public key // But this will only work for bitcoin in this case // #define MAINNET_XPUB 0x0488B21E // #define MAINNET_XPRIV 0x0488ADE4 // #define TESTNET_XPUB 0x043587CF // #define TESTNET_XPRIV 0x04358394 // bool isPrivate = (version == MAINNET_XPRIV || version == TESTNET_XPRIV); // bool isPublic = (version == MAINNET_XPUB || version == TESTNET_XPUB); return { version, depth, parentFingerprint, childNumber, chainCode, keyData, checksum, }; } /** * Encodes the components into an extended public key (xpub). * @param {{ version: Buffer; depth: number; parentFingerprint: number; childNumber: number; chainCode: Buffer; keyData: Buffer }} params - The components to encode. * @returns {string} The encoded extended public key. */ static encodeXpub({ version, depth, parentFingerprint, childNumber, chainCode, keyData, }) { // Get the fingerprint let depthBytes = Buffer.alloc(1); if (typeof depth === 'number') { depthBytes.writeUint8(depth, 0); } else { depthBytes = depth.slice(0, 1); } // Get the child number let childNumberBytes = Buffer.alloc(4); if (typeof childNumber === 'number') { childNumberBytes.writeUInt32BE(childNumber, 0); } else { childNumberBytes = childNumber.slice(0, 4); } let parentFingerprintBytes = Buffer.alloc(4); if (typeof parentFingerprint === 'number') { parentFingerprintBytes.writeUInt32BE(parentFingerprint, 0); } else { parentFingerprintBytes = parentFingerprint.slice(0, 4); } // Concat all the bytes const xpubBytes = Buffer.concat([version, depthBytes, parentFingerprintBytes, childNumberBytes, chainCode, keyData]); // Calculate checksum const checksum = (0, sha256_1.sha256)((0, sha256_1.sha256)(xpubBytes)).subarray(0, 4); // Add checksum to xpub const xpubWithChecksum = Buffer.concat([xpubBytes, checksum]); // Encode xpub to base58 const xpub = base_1.base58.encode(xpubWithChecksum); return xpub; } } exports.HDKey = HDKey; //# sourceMappingURL=HDKey.js.map