452 lines
13 KiB
TypeScript
452 lines
13 KiB
TypeScript
import { PublicKey } from './public-key.js';
|
|
|
|
import {
|
|
binToHex,
|
|
decodePrivateKeyWif,
|
|
encodePrivateKeyWif,
|
|
generatePrivateKey,
|
|
hexToBin,
|
|
isHex,
|
|
utf8ToBin,
|
|
bigIntToCompactUint,
|
|
secp256k1,
|
|
sha256,
|
|
} from '@bitauth/libauth';
|
|
|
|
import type {
|
|
RecoverableSignature,
|
|
WalletImportFormatType,
|
|
} from '@bitauth/libauth';
|
|
|
|
/**
|
|
* Private Key entity.
|
|
*/
|
|
export class PrivateKey {
|
|
// The raw bytes representing the Private Key.
|
|
public readonly bytes: Uint8Array;
|
|
|
|
/**
|
|
* Construct a new Private Key.
|
|
*
|
|
* @throws {Error} If Private Key cannot be constructed.
|
|
*
|
|
* @param bytes {Uint8Array} The raw bytes (32) for the Private Key.
|
|
*/
|
|
constructor(bytes: Uint8Array) {
|
|
// Ensure that the Private Key is valid.
|
|
if (!secp256k1.validatePrivateKey(bytes)) {
|
|
throw new Error(
|
|
`Private Key is invalid: Must be >= 0x01 and <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140.`,
|
|
);
|
|
}
|
|
|
|
this.bytes = bytes;
|
|
}
|
|
|
|
/**
|
|
* Create a Private Key Entity from the given bytes.
|
|
*
|
|
* @param privateKeyBytes {string} Raw Private Key bytes.
|
|
*
|
|
* @throws {Error} If Private Key could not be created.
|
|
*
|
|
* @returns {PrivateKey} The created Private Key Entity.
|
|
*/
|
|
public static fromBytes(privateKeyBytes: Uint8Array): PrivateKey {
|
|
return new PrivateKey(privateKeyBytes);
|
|
}
|
|
|
|
/**
|
|
* Create a Private Key Entity from the given bytes (as hexadecimal string).
|
|
*
|
|
* @param privateKeyHex {string} Raw Private Key bytes encoded as hexadecimal string.
|
|
*
|
|
* @throws {Error} If Private Key could not be created.
|
|
*
|
|
* @returns {PrivateKey} The created Private Key Entity.
|
|
*/
|
|
public static fromHex(privateKeyHex: string): PrivateKey {
|
|
// Ensure that the given string is hex.
|
|
if (!isHex(privateKeyHex)) {
|
|
throw new Error(
|
|
`The given string (${privateKeyHex}) is not valid hexadecimal.`,
|
|
);
|
|
}
|
|
|
|
// Convert the hex to binary (Uint8Array).
|
|
const privateKeyBin = hexToBin(privateKeyHex);
|
|
|
|
// Create a Private Key Entity from the binary.
|
|
return new PrivateKey(privateKeyBin);
|
|
}
|
|
|
|
/**
|
|
* Create a Private Key from a given WIF (Note that the network information from the WIF will be lost).
|
|
*
|
|
* @throws {Error} If Private Key cannot be created.
|
|
*
|
|
* @returns {PrivateKey} The Private Key
|
|
*/
|
|
public static fromWIF(
|
|
wif: string,
|
|
removePrefixes = ['bitcoincash:', 'wif:'],
|
|
): PrivateKey {
|
|
// Remove the prefixes from the WIF (if they exist).
|
|
const wifWithoutPrefixes = removePrefixes.reduce((result, prefix) => {
|
|
return result.startsWith(prefix) ? result.slice(prefix.length) : result;
|
|
}, wif);
|
|
|
|
// Attempt to decode the WIF.
|
|
const decodeResult = decodePrivateKeyWif(wifWithoutPrefixes);
|
|
|
|
// If a string is returned, this indicates an error...
|
|
if (typeof decodeResult === 'string') {
|
|
throw new Error(decodeResult);
|
|
}
|
|
|
|
// Ensure the Private Key is valid by checking the length (32 bytes = uncompressed private key, 33 bytes = compressed private key).
|
|
// NOTE: This is to work around a LibAuth bug where decodePrivateKeyWif does not verify the version of the Base58 string.
|
|
// This means that we will get a false positive on Base58/Legacy addresses as being valid WIFs.
|
|
// Once the below PR is merged into LibAuth (and we've updated to the new version), we can remove the following code.
|
|
// https://github.com/bitauth/libauth/pull/147
|
|
const isValidPrivateKey =
|
|
decodeResult.privateKey.length === 32 ||
|
|
decodeResult.privateKey.length === 33;
|
|
|
|
// If it is not a valid private key, then it is not a WIF,
|
|
if (!isValidPrivateKey) {
|
|
throw new Error(
|
|
`Expected 32 or 33 bytes WIF, received ${decodeResult.privateKey.length}`,
|
|
);
|
|
}
|
|
|
|
// Return a new Private Key entity.
|
|
return new PrivateKey(decodeResult.privateKey);
|
|
}
|
|
|
|
/**
|
|
* Generate a random Private Key.
|
|
*
|
|
* @returns {PrivateKey} The randomly generated Private Key
|
|
*/
|
|
public static generateRandom(): PrivateKey {
|
|
const bytes = generatePrivateKey();
|
|
|
|
return new PrivateKey(bytes);
|
|
}
|
|
|
|
public static isWIF(wif: string, removePrefixes = ['bitcoincash:', 'wif:']) {
|
|
try {
|
|
this.fromWIF(wif, removePrefixes);
|
|
return true;
|
|
} catch (_error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Derive the public key from this private key.
|
|
*
|
|
* @throws {Error} If Public Key cannot be derived.
|
|
*
|
|
* @returns {PublicKey} The derived Public Key.
|
|
*/
|
|
public derivePublicKey(): PublicKey {
|
|
// Attempt to derive the public key.
|
|
const deriveResult = secp256k1.derivePublicKeyCompressed(this.bytes);
|
|
|
|
// If a string is returned, this indicates an error...
|
|
if (typeof deriveResult === 'string') {
|
|
throw new Error(deriveResult);
|
|
}
|
|
|
|
// Return the derived Public Key.
|
|
return new PublicKey(deriveResult);
|
|
}
|
|
|
|
/**
|
|
* Sign a message using a recoverable compact signature format.
|
|
*
|
|
* @param message The message to sign, either as a Uint8Array or string.
|
|
* If a string is provided, it will be converted to UTF-8 bytes.
|
|
*
|
|
* @throws {Error} If message cannot be signed.
|
|
*
|
|
* @returns The generated recoverable compact signature.
|
|
*/
|
|
public signMessageRecoverableCompact(message: Uint8Array | string) {
|
|
if (typeof message === 'string') {
|
|
message = utf8ToBin(message);
|
|
}
|
|
|
|
const messageHash = sha256.hash(message);
|
|
|
|
return this.signMessageHashRecoverableCompact(messageHash);
|
|
}
|
|
|
|
/**
|
|
* Sign a message using a compact signature format.
|
|
*
|
|
* @param message The message to sign, either as a Uint8Array or string.
|
|
* If a string is provided, it will be converted to UTF-8 bytes.
|
|
*
|
|
* @throws {Error} If message cannot be signed.
|
|
*
|
|
* @returns The generated compact signature.
|
|
*/
|
|
public signMessageCompact(message: Uint8Array | string) {
|
|
if (typeof message === 'string') {
|
|
message = utf8ToBin(message);
|
|
}
|
|
|
|
const messageHash = sha256.hash(message);
|
|
|
|
return this.signMessageHashCompact(messageHash);
|
|
}
|
|
|
|
/**
|
|
* Sign a message using the Schnorr signature scheme.
|
|
*
|
|
* @param message The message to sign, either as a Uint8Array or string.
|
|
* If a string is provided, it will be converted to UTF-8 bytes.
|
|
*
|
|
* @throws {Error} If message cannot be signed.
|
|
*
|
|
* @returns The generated Schnorr signature.
|
|
*/
|
|
public signMessageSchnorr(message: Uint8Array | string) {
|
|
if (typeof message === 'string') {
|
|
message = utf8ToBin(message);
|
|
}
|
|
|
|
const messageHash = sha256.hash(message);
|
|
|
|
return this.signMessageHashSchnorr(messageHash);
|
|
}
|
|
|
|
/**
|
|
* Sign a message using DER (Distinguished Encoding Rules) format.
|
|
*
|
|
* @param message The message to sign, either as a Uint8Array or string.
|
|
* If a string is provided, it will be converted to UTF-8 bytes.
|
|
*
|
|
* @throws {Error} If message cannot be signed.
|
|
*
|
|
* @returns The generated DER signature.
|
|
*/
|
|
public signMessageDER(message: Uint8Array | string) {
|
|
if (typeof message === 'string') {
|
|
message = utf8ToBin(message);
|
|
}
|
|
|
|
const messageHash = sha256.hash(message);
|
|
|
|
return this.signMessageHashDER(messageHash);
|
|
}
|
|
|
|
/**
|
|
* Sign a message hash with this Private Key using an Electron Cash Signature.
|
|
*
|
|
* This applies the Bitcoin and Electron Magic Bytes.
|
|
*
|
|
* NOTE: Electron can verify this message successfully, but generates them differently internally.
|
|
*
|
|
* @param message The message.
|
|
*
|
|
* @throws {Error} If message hash cannot be signed.
|
|
*
|
|
* @returns {Uint8Array} The generated signature.
|
|
*/
|
|
public signMessageElectron(message: string): Uint8Array {
|
|
// Compile the message.
|
|
const magicBytes = utf8ToBin('\x18Bitcoin Signed Message:\n');
|
|
const messageBytes = utf8ToBin(message);
|
|
const messageLengthBytes = bigIntToCompactUint(BigInt(messageBytes.length)); // hexToBin(messageBytes.length.toString(16)); //
|
|
const messageFinal = new Uint8Array([
|
|
...magicBytes,
|
|
...messageLengthBytes,
|
|
...messageBytes,
|
|
]);
|
|
|
|
// Hash the message.
|
|
// NOTE: That we hash256 (2 x sha256) the message.
|
|
const messageHash = sha256.hash(sha256.hash(messageFinal));
|
|
|
|
// Use Recoverable Compact Signature.
|
|
const signature = this.signMessageHashRecoverableCompact(messageHash);
|
|
|
|
// Apply Electon encoding.
|
|
const electronEncoding = new Uint8Array([
|
|
...[31 + signature.recoveryId],
|
|
...signature.signature,
|
|
]);
|
|
|
|
// Return the signature.
|
|
return electronEncoding;
|
|
}
|
|
|
|
/**
|
|
* Sign a message hash with this Private Key using a Recoverable Compact Signature.
|
|
*
|
|
* @param messageHash {Uint8Array} The hash of the message.
|
|
*
|
|
* @throws {Error} If message hash cannot be signed.
|
|
*
|
|
* @returns {Uint8Array} The generated signature.
|
|
*/
|
|
public signMessageHashRecoverableCompact(
|
|
messageHash: Uint8Array | string,
|
|
): RecoverableSignature {
|
|
// If a string was given, convert it from hex to bytes.
|
|
// TODO: Consider using Bytes class - it ensures valid hex.
|
|
if (typeof messageHash === 'string') {
|
|
messageHash = hexToBin(messageHash);
|
|
}
|
|
|
|
// Attempt to sign the message.
|
|
const signResult = secp256k1.signMessageHashRecoverableCompact(
|
|
this.bytes,
|
|
messageHash,
|
|
);
|
|
|
|
// If a string is returned, this indicates an error...
|
|
if (typeof signResult === 'string') {
|
|
throw new Error(signResult);
|
|
}
|
|
|
|
// Return the signature.
|
|
return signResult;
|
|
}
|
|
|
|
/**
|
|
* Sign a message hash with this Private Key using a Compact Signature.
|
|
*
|
|
* @param messageHash {Uint8Array} The hash of the message.
|
|
*
|
|
* @throws {Error} If message hash cannot be signed.
|
|
*
|
|
* @returns {Uint8Array} The generated signature.
|
|
*/
|
|
public signMessageHashCompact(messageHash: Uint8Array | string): Uint8Array {
|
|
// If a string was given, convert it from hex to bytes.
|
|
// TODO: Consider using Bytes class - it ensures valid hex.
|
|
if (typeof messageHash === 'string') {
|
|
messageHash = hexToBin(messageHash);
|
|
}
|
|
|
|
// Attempt to sign the message.
|
|
const signResult = secp256k1.signMessageHashCompact(
|
|
this.bytes,
|
|
messageHash,
|
|
);
|
|
|
|
// If a string is returned, this indicates an error...
|
|
if (typeof signResult === 'string') {
|
|
throw new Error(signResult);
|
|
}
|
|
|
|
// Return the signature.
|
|
return signResult;
|
|
}
|
|
|
|
/**
|
|
* Sign a message hash with this Private Key using a Schnorr Signature.
|
|
*
|
|
* @param messageHash {Uint8Array} The hash of the message.
|
|
*
|
|
* @throws {Error} If message hash cannot be signed.
|
|
*
|
|
* @returns {Uint8Array} The generated signature.
|
|
*/
|
|
public signMessageHashSchnorr(messageHash: Uint8Array | string): Uint8Array {
|
|
// If a string was given, convert it from hex to bytes.
|
|
// TODO: Consider using Bytes class - it ensures valid hex.
|
|
if (typeof messageHash === 'string') {
|
|
messageHash = hexToBin(messageHash);
|
|
}
|
|
|
|
// Attempt to sign the message.
|
|
const signResult = secp256k1.signMessageHashSchnorr(
|
|
this.bytes,
|
|
messageHash,
|
|
);
|
|
|
|
// If a string is returned, this indicates an error...
|
|
if (typeof signResult === 'string') {
|
|
throw new Error(signResult);
|
|
}
|
|
|
|
// Return the signature.
|
|
return signResult;
|
|
}
|
|
|
|
/**
|
|
* Sign a message hash with this Private Key using a DER Signature.
|
|
*
|
|
* @param messageHash {Uint8Array} The hash of the message.
|
|
*
|
|
* @throws {Error} If message hash cannot be signed.
|
|
*
|
|
* @returns {Uint8Array} The generated signature.
|
|
*/
|
|
public signMessageHashDER(messageHash: Uint8Array | string): Uint8Array {
|
|
// If a string was given, convert it from hex to bytes.
|
|
// TODO: Consider using Bytes class - it ensures valid hex.
|
|
if (typeof messageHash === 'string') {
|
|
messageHash = hexToBin(messageHash);
|
|
}
|
|
|
|
const signResult = secp256k1.signMessageHashDER(this.bytes, messageHash);
|
|
|
|
// If a string is returned, this indicates an error...
|
|
if (typeof signResult === 'string') {
|
|
throw new Error(signResult);
|
|
}
|
|
|
|
// Return the signature.
|
|
return signResult;
|
|
}
|
|
|
|
/**
|
|
* Convert the raw bytes of this Private Key to hexadecimal encoding.
|
|
*
|
|
* @returns {string} The raw bytes of the Private Key as a hexadecimal string.
|
|
*/
|
|
public toHex(): string {
|
|
// Return the hexadecimal encoded bytes of this Private Key.
|
|
return binToHex(this.bytes);
|
|
}
|
|
|
|
/**
|
|
* Convert this Private Key to a raw 32 byte representation.
|
|
*
|
|
* @returns {Uint8Array} The raw Private Key as a Uint8Array.
|
|
*/
|
|
public toBytes(): Uint8Array {
|
|
// Return the raw bytes for this Private Key.
|
|
return this.bytes;
|
|
}
|
|
|
|
/**
|
|
* Convert this Private Key to a WIF format.
|
|
*
|
|
* @param type {WalletImportFormatType} The type of WIF to create.
|
|
*
|
|
* @returns {string} The Private Key in WIF format.
|
|
*/
|
|
public toWif(type: WalletImportFormatType = 'mainnet'): string {
|
|
// Return the encoded Private Key WIF.
|
|
return encodePrivateKeyWif(this.bytes, type);
|
|
}
|
|
|
|
/**
|
|
* Returns the Private Key as a mainnet WIF.
|
|
*
|
|
* @returns {string} The Private Key as a mainnet WIF.
|
|
*/
|
|
public toString(): string {
|
|
// Return the encoded Mainnet Private Key WIF.
|
|
return this.toWif();
|
|
}
|
|
}
|