import { Bytes } from './bytes.js'; import { PublicKey } from './public-key.js'; import { type RecoverableSignature, type WalletImportFormatType, decodePrivateKeyWif, encodePrivateKeyWif, generatePrivateKey, bigIntToCompactUint, secp256k1, sha256, } from '@bitauth/libauth'; /** * Private Key entity. * * @example * * ```ts * // Import our primitives * import { PrivateKey } from '@xocash/primitives'; * * // Create a Private Key from a WIF. * const privateKey = PrivateKey.from('wif'); * * // Derive the Public Key * const publicKey = PrivateKey.derivePublicKey(); * * // Output the CashAddr. * console.log(publicKey.deriveAddress().toCashAddr()); * ``` */ export class PrivateKey { // The raw bytes representing the Private Key. protected 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. */ protected 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; } /** * Instantiates a Private Key from either Bytes, Hex or WIF. * * @param privateKey The private key as either Bytes, Hex or WIF. * * @throws {Error} If the private key cannot be decoded. * * @returns The created Private Key Primitive. */ public static from(privateKey: Uint8Array | string) { if (privateKey instanceof Uint8Array) { return this.fromBytes(privateKey); } if (this.isWIF(privateKey)) { return this.fromWIF(privateKey); } return this.fromHex(privateKey); } /** * 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 this(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 { // Convert the hex to binary (Uint8Array). const privateKeyBin = Bytes.fromHex(privateKeyHex).toBytes(); // Create a Private Key Entity from the binary. return new this(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:', 'bch-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 this(decodeResult.privateKey); } /** * Creates an instance from serialized data. * * @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted. * * @param bytes - Serialized Primitive Data * * @returns A new instance of this primitive. */ public static fromRaw(bytes: Uint8Array) { return new this(bytes); } /** * Generate a random Private Key. * * @returns {PrivateKey} The randomly generated Private Key */ public static generateRandom(): PrivateKey { const bytes = generatePrivateKey(); return new this(bytes); } public static isWIF(wif: string, removePrefixes = ['bitcoincash:', 'wif:']) { try { this.fromWIF(wif, removePrefixes); return true; } catch (_error) { return false; } } /** * 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 Bytes.fromBytes(this.bytes).toHex(); } /** * 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(); } /** * Converts this primitive to a serializable raw object representation. * * @returns Serialzied primitive data. */ toRaw() { return this.bytes; } /** * 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 PublicKey.fromRaw(deriveResult); } /** * Sign a message using a recoverable compact signature format. * * @remarks * * This method will SHA256 hash the message for you. * * @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 = Bytes.fromUtf8(message).toBytes(); } const messageHash = sha256.hash(message); return this.signMessageHashRecoverableCompact(messageHash); } /** * Sign a message using a compact signature format. * * @remarks * * This method will SHA256 hash the message for you. * * @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 = Bytes.fromUtf8(message).toBytes(); } const messageHash = sha256.hash(message); return this.signMessageHashCompact(messageHash); } /** * Sign a message using the Schnorr signature scheme. * * @remarks * * This method will SHA256 hash the message for you. * * @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 = Bytes.fromUtf8(message).toBytes(); } const messageHash = sha256.hash(message); return this.signMessageHashSchnorr(messageHash); } /** * Sign a message using DER (Distinguished Encoding Rules) format. * * @remarks * * This method will SHA256 hash the message for you. * * @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 = Bytes.fromUtf8(message).toBytes(); } 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 = Bytes.fromUtf8( '\x18Bitcoin Signed Message:\n', ).toBytes(); const messageBytes = Bytes.fromUtf8(message).toBytes(); 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 = Bytes.fromHex(messageHash).toBytes(); } if (messageHash.length !== 32) { throw new Error('messageHash must be a hash of length 32 bytes'); } // 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 = Bytes.fromHex(messageHash).toBytes(); } if (messageHash.length !== 32) { throw new Error('messageHash must be a hash of length 32 bytes'); } // 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 = Bytes.fromHex(messageHash).toBytes(); } if (messageHash.length !== 32) { throw new Error('messageHash must be a hash of length 32 bytes'); } // 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 = Bytes.fromHex(messageHash).toBytes(); } if (messageHash.length !== 32) { throw new Error('messageHash must be a hash of length 32 bytes'); } 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; } }