UNPKG

libnexa-ts

Version:

A pure and powerful Nexa SDK library.

447 lines (391 loc) 16.9 kB
import HDKeyUtils from "./hdkey.utils"; import { InvalidB58Checksum, InvalidDerivationArgument, InvalidEntropyArgument, InvalidPath, NotEnoughEntropy, TooMuchEntropy } from "./exceptions"; import BufferUtils from "../utils/buffer.utils"; import ValidationUtils from "../utils/validation.utils"; import type { HDPrivateKeyBuffers, HDPrivateKeyDto, HDPrivateKeyMinimalDto, IHDPrivateKey } from "../common/interfaces"; import type { Networkish } from "../common/types"; import Hash from "../crypto/hash"; import PrivateKey from "./privatekey"; import type PublicKey from "./publickey"; import BN from "../crypto/bn.extension"; import Point from "../crypto/point"; import type { Network } from "../core/network/network"; import { networks } from "../core/network/network-manager"; import Base58Check from "../encoding/base58check"; import CommonUtils from "../utils/common.utils"; import HDPublicKey from "./hdpublickey"; export default class HDPrivateKey implements IHDPrivateKey { private static readonly MINIMUM_ENTROPY_BITS = 128; private static readonly BITS_TO_BYTES = 1 / 8; private static readonly MAXIMUM_ENTROPY_BITS = 512; private static readonly PrivateKeySize = 32; private static readonly PrivateKeyStart = HDKeyUtils.ChainCodeEnd + 1; private static readonly PrivateKeyEnd = this.PrivateKeyStart + this.PrivateKeySize; private static readonly ChecksumStart = this.PrivateKeyEnd; private static readonly ChecksumEnd = this.ChecksumStart + HDKeyUtils.CheckSumSize; private _hdPublicKey?: HDPublicKey; readonly privateKey!: PrivateKey; readonly publicKey!: PublicKey; readonly network!: Network; readonly depth!: number; readonly parentFingerPrint!: Uint8Array; readonly fingerPrint!: Uint8Array; readonly chainCode!: Uint8Array; readonly childIndex!: number; readonly checksum!: Uint8Array; readonly xprivkey!: string; /** * Represents an instance of an hierarchically derived private key. * * More info on https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki */ constructor(arg?: string | Uint8Array | IHDPrivateKey | HDPrivateKeyDto | HDPrivateKeyMinimalDto | Networkish) { if (arg instanceof HDPrivateKey) { return arg; } let params = HDPrivateKey._classifyArgument(arg); this.privateKey = params.privateKey as PrivateKey; this.publicKey = (params.publicKey ?? params.privateKey.toPublicKey()) as PublicKey; this.network = params.network; this.depth = params.depth; this.parentFingerPrint = params.parentFingerPrint; this.fingerPrint = params.fingerPrint; this.chainCode = params.chainCode; this.childIndex = params.childIndex; this.xprivkey = params.xprivkey; this.checksum = params.checksum; } private static _classifyArgument(arg?: string | Uint8Array | IHDPrivateKey | HDPrivateKeyDto | HDPrivateKeyMinimalDto | Networkish): IHDPrivateKey { if (!arg) { return this._generateRandomly(); } if (networks.get(arg as Networkish)) { return this._generateRandomly(arg as Networkish); } if (typeof arg === 'string' || BufferUtils.isBuffer(arg)) { let xprivkey = typeof arg === 'string' ? arg : BufferUtils.bufferToUtf8(arg); if (HDKeyUtils.isValidSerialized(xprivkey, undefined, true)) { return this._buildFromSerialized(xprivkey); } if (CommonUtils.isValidJSON(arg as string)) { return this._buildFromObject(JSON.parse(arg as string)); } throw HDKeyUtils.getSerializedError(arg, undefined, true); } if (this._isIHDPrivKey(arg)) { return arg; } if (this._isMinimalDtoObject(arg)) { return this._buildFromMinimalObject(arg); } if (this._isDtoObject(arg)) { return this._buildFromObject(arg); } throw new TypeError('Invalid argument type for creation, must be string, json, buffer, or object'); } public get hdPublicKey(): HDPublicKey { return this.getHDPublicKey(); } public get xpubkey(): string { return this.getHDPublicKey().xpubkey; } private static _isDtoObject(data: unknown): data is HDPrivateKeyDto { return typeof data === 'object' && data !== null && 'xprivkey' in data && 'privateKey' in data && typeof data.privateKey === 'string'; } private static _isMinimalDtoObject(data: unknown): data is HDPrivateKeyMinimalDto { return typeof data === 'object' && data !== null && !('xprivkey' in data) && 'privateKey' in data && typeof data.privateKey === 'string'; } private static _isIHDPrivKey(data: unknown): data is IHDPrivateKey { return typeof data === 'object' && data !== null && 'xprivkey' in data && 'privateKey' in data && data.privateKey instanceof PrivateKey; } /** * Verifies that a given path is valid. * * @param arg * @param hardened */ public static isValidPath(arg: string | number, hardened?: boolean): boolean { if (typeof arg === 'string') { let indexes = HDKeyUtils.getDerivationIndexes(arg); return indexes !== null && indexes.every(i => this.isValidPath(i)); } if (typeof arg === 'number') { if (arg < HDKeyUtils.Hardened && hardened === true) { arg += HDKeyUtils.Hardened; } return arg >= 0 && arg < HDKeyUtils.MaxIndex; } return false; } public static fromString(xprivkey: string): HDPrivateKey { ValidationUtils.validateArgument(typeof xprivkey === 'string', 'No valid string was provided'); return new HDPrivateKey(xprivkey); } /** * Returns the string representation of this private key (ext privkey). */ public toString(): string { return this.xprivkey; } /** * Build a HDPrivateKey from a buffer * * @param buf */ public static fromBuffer(buf: Uint8Array): HDPrivateKey { ValidationUtils.validateArgument(BufferUtils.isBuffer(buf), 'No valid Buffer was provided'); return new HDPrivateKey(buf); } /** * Returns a buffer representation of the HDPrivateKey */ public toBuffer(): Uint8Array { return BufferUtils.utf8ToBuffer(this.xprivkey); } public toJSON = this.toObject; /** * Returns a plain object with a representation of this private key. */ public toObject(): HDPrivateKeyDto { return { network: this.network.name, depth: this.depth, fingerPrint: BufferUtils.integerFromBuffer(this.fingerPrint), parentFingerPrint: BufferUtils.integerFromBuffer(this.parentFingerPrint), childIndex: this.childIndex, chainCode: BufferUtils.bufferToHex(this.chainCode), privateKey: this.privateKey.toString(), checksum: BufferUtils.integerFromBuffer(this.checksum), xprivkey: this.xprivkey, }; } public static fromObject(arg: HDPrivateKeyDto): HDPrivateKey { ValidationUtils.validateArgument(typeof arg === 'object' && arg !== null, 'No valid argument was provided'); return new HDPrivateKey(arg); } public static fromMinimalObject(arg: HDPrivateKeyMinimalDto): HDPrivateKey { ValidationUtils.validateArgument(typeof arg === 'object' && arg !== null, 'No valid argument was provided'); let params = this._buildFromMinimalObject(arg); return new HDPrivateKey(params); } /** * Generate a private key from a seed, as described in BIP32 * * @param seed * @param network * @return HDPrivateKey */ public static fromSeed(seed: string | Uint8Array, network: Networkish = networks.defaultNetwork): HDPrivateKey { let params = this._buildFromSeed(seed, network); return new HDPrivateKey(params); } /** * Get a derived child based on a string or number. * * If the first argument is a string, it's parsed as the full path of * derivation. Valid values for this argument include "m" (which returns the * same private key), "m/0/1/40/2'/1000", where the ' quote means a hardened * derivation. * * If the first argument is a number, the child with that index will be * derived. If the second argument is truthy, the hardened version will be * derived. See the example usage for clarification. * * @example * ```javascript * let parent = new HDPrivateKey('xprv...'); * let child_0_1_2h = parent.deriveChild(0).deriveChild(1).deriveChild(2, true); * let copy_of_child_0_1_2h = parent.deriveChild("m/0/1/2'"); * assert(child_0_1_2h.xprivkey === copy_of_child_0_1_2h.xprivkey); * ``` * * @param arg * @param hardened * @return HDPrivateKey */ public deriveChild(arg: string | number, hardened?: boolean): HDPrivateKey { if (typeof arg === 'number') { return this._deriveWithNumber(arg, hardened); } if (typeof arg === 'string') { return this._deriveFromString(arg); } throw new InvalidDerivationArgument(arg); } private _deriveWithNumber(index: number, hardened?: boolean): HDPrivateKey { if (!HDPrivateKey.isValidPath(index, hardened)) { throw new InvalidPath(index); } hardened = index >= HDKeyUtils.Hardened ? true : hardened; if (index < HDKeyUtils.Hardened && hardened === true) { index += HDKeyUtils.Hardened; } let indexBuffer = BufferUtils.integerAsBuffer(index); let data; if (hardened) { // This will use a 32 byte zero padded serialization of the private key let privateKeyBuffer = this.privateKey.toBuffer(); ValidationUtils.validateState(privateKeyBuffer.length === 32, 'length of private key buffer is expected to be 32 bytes'); data = BufferUtils.concat([Uint8Array.from([0]), privateKeyBuffer, indexBuffer]); } else { data = BufferUtils.concat([this.publicKey.toBuffer(), indexBuffer]); } let hash = Hash.sha512hmac(data, this.chainCode); let leftPart = BN.fromBuffer(hash.subarray(0, 32), { size: 32 }); let chainCode = hash.subarray(32, 64); let privateKey = leftPart.add(this.privateKey.toBigNumber()).umod(Point.getN()).toByteArray({ size: 32 }); if (!PrivateKey.isValid(privateKey)) { // Index at this point is already hardened, we can pass null as the hardened arg return this._deriveWithNumber(index + 1); } let buffers: HDPrivateKeyBuffers = { version: BufferUtils.integerAsBuffer(this.network.xprivkey), depth: BufferUtils.integerAsSingleByteBuffer(this.depth + 1), parentFingerPrint: this.fingerPrint, childIndex: BufferUtils.integerAsBuffer(index), privateKey: privateKey, chainCode: chainCode, }; let derived = HDPrivateKey._buildFromBuffers(buffers); return new HDPrivateKey(derived); } private _deriveFromString(path: string): HDPrivateKey { if (!HDPrivateKey.isValidPath(path)) { throw new InvalidPath(path); } const deriveKeys = (baseKey: HDPrivateKey, indexes: number[]): HDPrivateKey => { return indexes.reduce((prev, idx) => prev._deriveWithNumber(idx), baseKey); } let indexes = HDKeyUtils.getDerivationIndexes(path); return deriveKeys(this, indexes!); } private static _buildFromSeed(seed: string | Uint8Array, network: Networkish = networks.defaultNetwork): IHDPrivateKey { if (CommonUtils.isHexa(seed)) { seed = BufferUtils.hexToBuffer(seed as string); } if (!BufferUtils.isBuffer(seed)) { throw new InvalidEntropyArgument(seed); } if (seed.length < this.MINIMUM_ENTROPY_BITS * this.BITS_TO_BYTES) { throw new NotEnoughEntropy(seed); } if (seed.length > this.MAXIMUM_ENTROPY_BITS * this.BITS_TO_BYTES) { throw new TooMuchEntropy(seed); } let hash = Hash.sha512hmac(seed, BufferUtils.utf8ToBuffer('Bitcoin seed')); let buffers: HDPrivateKeyBuffers = { version: BufferUtils.integerAsBuffer((networks.get(network) || networks.defaultNetwork).xprivkey), depth: BufferUtils.integerAsSingleByteBuffer(0), parentFingerPrint: BufferUtils.integerAsBuffer(0), childIndex: BufferUtils.integerAsBuffer(0), privateKey: hash.subarray(0, 32), chainCode: hash.subarray(32, 64), }; return this._buildFromBuffers(buffers); } private static _buildFromObject(arg: HDPrivateKeyDto): IHDPrivateKey { return { network: networks.get(arg.network)!, depth: arg.depth, fingerPrint: BufferUtils.integerAsBuffer(arg.fingerPrint), parentFingerPrint: BufferUtils.integerAsBuffer(arg.parentFingerPrint), childIndex: arg.childIndex, chainCode: BufferUtils.hexToBuffer(arg.chainCode), xprivkey: arg.xprivkey, checksum: BufferUtils.integerAsBuffer(arg.checksum), privateKey: PrivateKey.fromString(arg.privateKey, arg.network) } } private static _buildFromMinimalObject(arg: HDPrivateKeyMinimalDto): IHDPrivateKey { let buffers = { version: BufferUtils.integerAsBuffer(networks.get(arg.network)!.xprivkey), depth: BufferUtils.integerAsSingleByteBuffer(arg.depth), parentFingerPrint: BufferUtils.integerAsBuffer(arg.parentFingerPrint), childIndex: BufferUtils.integerAsBuffer(arg.childIndex), chainCode: BufferUtils.hexToBuffer(arg.chainCode), privateKey: BufferUtils.hexToBuffer(arg.privateKey), }; return this._buildFromBuffers(buffers); } private static _buildFromSerialized(xprivkey: string): IHDPrivateKey { let decoded = Base58Check.decode(xprivkey); let buffers: HDPrivateKeyBuffers = { version: decoded.subarray(HDKeyUtils.VersionStart, HDKeyUtils.VersionEnd), depth: decoded.subarray(HDKeyUtils.DepthStart, HDKeyUtils.DepthEnd), parentFingerPrint: decoded.subarray(HDKeyUtils.ParentFingerPrintStart, HDKeyUtils.ParentFingerPrintEnd), childIndex: decoded.subarray(HDKeyUtils.ChildIndexStart, HDKeyUtils.ChildIndexEnd), chainCode: decoded.subarray(HDKeyUtils.ChainCodeStart, HDKeyUtils.ChainCodeEnd), privateKey: decoded.subarray(this.PrivateKeyStart, this.PrivateKeyEnd), checksum: decoded.subarray(this.ChecksumStart, this.ChecksumEnd), } return this._buildFromBuffers(buffers); } /** * Receives a object with buffers in all the properties and populates the * internal structure */ private static _buildFromBuffers(arg: HDPrivateKeyBuffers): IHDPrivateKey { HDPrivateKey._validateBufferArguments(arg); let sequence = [ arg.version, arg.depth, arg.parentFingerPrint, arg.childIndex, arg.chainCode, new Uint8Array(1), arg.privateKey ]; let concat = BufferUtils.concat(sequence); if (!arg.checksum?.length) { arg.checksum = Base58Check.checksum(concat); } else if (BufferUtils.bufferToHex(arg.checksum) !== BufferUtils.bufferToHex(Base58Check.checksum(concat))) { throw new InvalidB58Checksum(concat); } let network = networks.get(BufferUtils.integerFromBuffer(arg.version))!; let privateKey = PrivateKey.from(BN.fromBuffer(arg.privateKey), network); let publicKey = privateKey.toPublicKey(); return { xprivkey: Base58Check.encode(concat), network: network, depth: BufferUtils.integerFromSingleByteBuffer(arg.depth), privateKey: privateKey, publicKey: publicKey, fingerPrint: Hash.sha256ripemd160(publicKey.toBuffer()).subarray(0, HDKeyUtils.ParentFingerPrintSize), chainCode: arg.chainCode, childIndex: BufferUtils.integerFromBuffer(arg.childIndex), parentFingerPrint: arg.parentFingerPrint, checksum: arg.checksum } } private static _validateBufferArguments(arg: HDPrivateKeyBuffers): void { const checkBuffer = (name: string, size: number): void => { let buff = arg[name as keyof HDPrivateKeyBuffers]; ValidationUtils.validateArgument(BufferUtils.isBuffer(buff), name + ' argument is not a buffer'); ValidationUtils.validateArgument(buff!.length === size, name + ' has not the expected size: found ' + buff!.length + ', expected ' + size); }; checkBuffer('version', HDKeyUtils.VersionSize); checkBuffer('depth', HDKeyUtils.DepthSize); checkBuffer('parentFingerPrint', HDKeyUtils.ParentFingerPrintSize); checkBuffer('childIndex', HDKeyUtils.ChildIndexSize); checkBuffer('chainCode', HDKeyUtils.ChainCodeSize); checkBuffer('privateKey', this.PrivateKeySize); if (arg.checksum?.length) { checkBuffer('checksum', HDKeyUtils.CheckSumSize); } } private static _generateRandomly(network?: Networkish): IHDPrivateKey { return this._buildFromSeed(BufferUtils.getRandomBuffer(64), network); } /** * Will return the corresponding hd public key * * @returns An extended public key generated from the hd private key */ public getHDPublicKey(): HDPublicKey { if (!this._hdPublicKey) { this._hdPublicKey = new HDPublicKey(this); } return this._hdPublicKey; } /** * Returns the console representation of this extended private key. */ public inspect(): string { return `<HDPrivateKey: ${this}>`; } }