UNPKG

j-bitcoin

Version:

Comprehensive JavaScript/TypeScript Bitcoin (BTC) wallet library with custodial and non-custodial support, hierarchical deterministic keys, threshold signatures, and advanced cryptographic features

299 lines (257 loc) 9.78 kB
/** * @fileoverview ECDSA signature implementation for Bitcoin * @version 1.0.0 * @author yfbsei * @license ISC */ import { createHash } from 'node:crypto'; import { secp256k1 } from '@noble/curves/secp256k1'; import { b58decode } from '../../../encoding/base58.js'; import { CRYPTO_CONSTANTS, NETWORK_VERSIONS } from '../../constants.js'; /** * Custom error class for ECDSA operations * @class ECDSAError * @extends Error */ class ECDSAError extends Error { /** * Create an ECDSA error * @param {string} message - Error message * @param {string} code - Error code * @param {Object} [details={}] - Additional details */ constructor(message, code, details = {}) { super(message); this.name = 'ECDSAError'; this.code = code; this.details = details; } } /** * Validate a private key format and value * @param {string|Buffer|Uint8Array} privateKey - Private key to validate * @returns {Buffer} Validated 32-byte private key * @throws {ECDSAError} If key is invalid */ function validatePrivateKey(privateKey) { let keyBuffer; if (typeof privateKey === 'string') { if (privateKey.startsWith('5') || privateKey.startsWith('K') || privateKey.startsWith('L')) { keyBuffer = decodeWIFPrivateKey(privateKey); } else if (/^[0-9a-fA-F]{64}$/.test(privateKey)) { keyBuffer = Buffer.from(privateKey, 'hex'); } else { throw new ECDSAError('Invalid private key format', 'INVALID_FORMAT'); } } else if (Buffer.isBuffer(privateKey)) { keyBuffer = privateKey; } else if (privateKey instanceof Uint8Array) { keyBuffer = Buffer.from(privateKey); } else { throw new ECDSAError('Private key must be Buffer, Uint8Array, or string', 'INVALID_TYPE'); } if (keyBuffer.length !== CRYPTO_CONSTANTS.PRIVATE_KEY_LENGTH) { throw new ECDSAError(`Invalid private key length: ${keyBuffer.length}`, 'INVALID_LENGTH'); } const keyBigInt = BigInt('0x' + keyBuffer.toString('hex')); const curveOrder = BigInt('0x' + CRYPTO_CONSTANTS.SECP256K1_ORDER); if (keyBigInt <= 0n || keyBigInt >= curveOrder) { throw new ECDSAError('Private key out of valid range', 'OUT_OF_RANGE'); } return keyBuffer; } /** * Validate a public key format and value * @param {string|Buffer|Uint8Array} publicKey - Public key to validate * @returns {Buffer} Validated public key * @throws {ECDSAError} If key is invalid */ function validatePublicKey(publicKey) { let keyBuffer; if (typeof publicKey === 'string') { keyBuffer = Buffer.from(publicKey, 'hex'); } else if (Buffer.isBuffer(publicKey)) { keyBuffer = publicKey; } else if (publicKey instanceof Uint8Array) { keyBuffer = Buffer.from(publicKey); } else { throw new ECDSAError('Public key must be Buffer, Uint8Array, or string', 'INVALID_TYPE'); } if (keyBuffer.length !== 33 && keyBuffer.length !== 65) { throw new ECDSAError(`Invalid public key length: ${keyBuffer.length}`, 'INVALID_LENGTH'); } try { secp256k1.ProjectivePoint.fromHex(keyBuffer); } catch { throw new ECDSAError('Invalid public key point', 'INVALID_POINT'); } return keyBuffer; } /** * Decode a WIF-encoded private key * @param {string} wif - WIF-encoded private key * @returns {Buffer} 32-byte private key * @throws {ECDSAError} If WIF format is invalid */ function decodeWIFPrivateKey(wif) { const decoded = b58decode(wif); if (decoded.length !== 33 && decoded.length !== 34) { throw new ECDSAError('Invalid WIF length', 'INVALID_WIF'); } const versionByte = decoded[0]; const isMainnet = versionByte === NETWORK_VERSIONS.main.versions.WIF_PRIVATE_KEY; const isTestnet = versionByte === NETWORK_VERSIONS.test.versions.WIF_PRIVATE_KEY; if (!isMainnet && !isTestnet) { throw new ECDSAError('Invalid WIF version byte', 'INVALID_VERSION'); } const hasCompressedFlag = decoded.length === 34; if (hasCompressedFlag && decoded[33] !== 0x01) { throw new ECDSAError('Invalid compression flag', 'INVALID_COMPRESSION'); } return decoded.slice(1, 33); } /** * ECDSA signature operations for Bitcoin * @namespace ECDSA */ const ECDSA = { /** * Sign a message hash with ECDSA * @param {string|Buffer|Uint8Array} privateKey - Private key * @param {string|Buffer} messageHash - 32-byte hash to sign * @param {Object} [options={}] - Signing options * @returns {Object} Signature with r, s, recovery, signature, der * @throws {ECDSAError} If inputs are invalid */ sign(privateKey, messageHash, options = {}) { const keyBuffer = validatePrivateKey(privateKey); let hashBuffer; if (typeof messageHash === 'string') { hashBuffer = Buffer.from(messageHash, 'hex'); } else { hashBuffer = Buffer.from(messageHash); } if (hashBuffer.length !== 32) { throw new ECDSAError('Message hash must be 32 bytes', 'INVALID_HASH'); } const signature = secp256k1.sign(hashBuffer, keyBuffer, { lowS: true, extraEntropy: options.extraEntropy }); return { r: signature.r, s: signature.s, recovery: signature.recovery, signature: Buffer.from(signature.toCompactRawBytes()), der: Buffer.from(signature.toDERRawBytes()) }; }, /** * Verify an ECDSA signature * @param {Object|Buffer|Uint8Array} signature - Signature to verify * @param {string|Buffer} messageHash - 32-byte message hash * @param {string|Buffer|Uint8Array} publicKey - Public key * @returns {boolean} True if signature is valid */ verify(signature, messageHash, publicKey) { const pubKeyBuffer = validatePublicKey(publicKey); let hashBuffer; if (typeof messageHash === 'string') { hashBuffer = Buffer.from(messageHash, 'hex'); } else { hashBuffer = Buffer.from(messageHash); } if (hashBuffer.length !== 32) { throw new ECDSAError('Message hash must be 32 bytes', 'INVALID_HASH'); } let sigBytes; if (signature.r !== undefined && signature.s !== undefined) { const sig = new secp256k1.Signature(signature.r, signature.s); sigBytes = sig.toCompactRawBytes(); } else if (Buffer.isBuffer(signature) || signature instanceof Uint8Array) { sigBytes = signature; } else { throw new ECDSAError('Invalid signature format', 'INVALID_SIGNATURE'); } try { return secp256k1.verify(sigBytes, hashBuffer, pubKeyBuffer); } catch { return false; } }, /** * Recover public key from signature * @param {Object} signature - Signature with r, s components * @param {string|Buffer} messageHash - Original message hash * @param {number} recovery - Recovery parameter (0-3) * @returns {Buffer} Recovered compressed public key * @throws {ECDSAError} If signature format is invalid */ recoverPublicKey(signature, messageHash, recovery) { let hashBuffer; if (typeof messageHash === 'string') { hashBuffer = Buffer.from(messageHash, 'hex'); } else { hashBuffer = Buffer.from(messageHash); } let sig; if (signature.r !== undefined && signature.s !== undefined) { sig = new secp256k1.Signature(signature.r, signature.s, recovery); } else { throw new ECDSAError('Invalid signature format for recovery', 'INVALID_SIGNATURE'); } const recoveredPoint = sig.recoverPublicKey(hashBuffer); return Buffer.from(recoveredPoint.toRawBytes(true)); }, /** * Get public key from private key * @param {string|Buffer|Uint8Array} privateKey - Private key * @param {boolean} [compressed=true] - Return compressed format * @returns {Buffer} Public key */ getPublicKey(privateKey, compressed = true) { const keyBuffer = validatePrivateKey(privateKey); return Buffer.from(secp256k1.getPublicKey(keyBuffer, compressed)); }, /** * Sign a message with Bitcoin message prefix * @param {string|Buffer|Uint8Array} privateKey - Private key * @param {string|Buffer} message - Message to sign * @returns {Object} Signature */ signMessage(privateKey, message) { const messageBuffer = typeof message === 'string' ? Buffer.from(message, 'utf8') : message; const prefix = Buffer.from('\x18Bitcoin Signed Message:\n', 'utf8'); const lengthBuffer = Buffer.from([messageBuffer.length]); const fullMessage = Buffer.concat([prefix, lengthBuffer, messageBuffer]); const messageHash = createHash('sha256') .update(createHash('sha256').update(fullMessage).digest()) .digest(); return this.sign(privateKey, messageHash); }, /** * Verify a Bitcoin signed message * @param {Object} signature - Signature to verify * @param {string|Buffer} message - Original message * @param {string|Buffer|Uint8Array} publicKey - Public key * @returns {boolean} True if valid */ verifyMessage(signature, message, publicKey) { const messageBuffer = typeof message === 'string' ? Buffer.from(message, 'utf8') : message; const prefix = Buffer.from('\x18Bitcoin Signed Message:\n', 'utf8'); const lengthBuffer = Buffer.from([messageBuffer.length]); const fullMessage = Buffer.concat([prefix, lengthBuffer, messageBuffer]); const messageHash = createHash('sha256') .update(createHash('sha256').update(fullMessage).digest()) .digest(); return this.verify(signature, messageHash, publicKey); } }; export { ECDSA, ECDSAError, validatePrivateKey, validatePublicKey, decodeWIFPrivateKey }; export default ECDSA;