Files
xo-stack/packages/primitives/src/private-key.ts
T
2024-09-12 21:33:11 +10:00

324 lines
8.8 KiB
TypeScript

import { PublicKey } from './public-key.js';
import {
binToHex,
decodePrivateKeyWif,
encodePrivateKeyWif,
generatePrivateKey,
hexToBin,
isHex,
binToUtf8,
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);
}
// 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);
}
/**
* 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 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
): RecoverableSignature {
// 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): Uint8Array {
// 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): Uint8Array {
// 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): Uint8Array {
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;
}
/**
* 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;
}
/**
* 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();
}
}