libnexa-ts
Version:
A pure and powerful Nexa SDK library.
447 lines (391 loc) • 16.9 kB
text/typescript
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}>`;
}
}