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(); } }