UNPKG

verify-bitcoin-message

Version:

A dependency-free Bitcoin message signature verifier that works in browsers

684 lines (681 loc) 13.8 kB
var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true, configurable: true, set: (newValue) => all[name] = () => newValue }); }; // rpc.ts var exports_rpc = {}; __export(exports_rpc, { verify: () => verify, default: () => verifySafe }); var defaultLocalConnection = { url: "http://127.0.0.1:8332/", user: "bitcoinuser", password: "bitcoinpassword" }; async function verify({ address, signature, message }, { url, user, password } = defaultLocalConnection) { const messageStr = typeof message === "string" ? message : new TextDecoder().decode(message); const body = JSON.stringify({ jsonrpc: "1.0", id: "curltest", method: "verifymessage", params: [address, signature, messageStr] }); const response = await fetch(url, { body, method: "POST", headers: { "Content-Type": "text/plain", Authorization: "Basic " + btoa(`${user}:${password}`) } }); const { result, error } = await response.json(); return result ?? fail(error); } async function verifySafe(params, connection) { try { return await verify(params, connection); } catch (error) { return false; } } // verify.ts function fail(error) { throw error instanceof Error ? error : new Error(error); } function assert(condition, error = "Assertion failed") { if (!condition) fail(error); } async function verify2({ message, address, signature }) { const sigBytes = base64ToBytes(signature); assert(sigBytes.length === 65, `Invalid signature length: ${sigBytes.length}, expected 65`); const recoveryFlag = sigBytes[0]; assert(recoveryFlag >= 27, "Invalid recovery flag"); assert(recoveryFlag <= 34, "Invalid recovery flag"); const signatureData = sigBytes.slice(1); const messageHash = await createMessageHash(typeof message === "string" ? encoder.encode(message) : message); for (let testRecoveryId = 0;testRecoveryId < 4; testRecoveryId++) { const publicKey = recoverPublicKey(messageHash, signatureData, testRecoveryId); if (publicKey) { for (const compressed of [true, false]) { const testAddress = await publicKeyToAddress(publicKey, compressed); if (testAddress === address) { return true; } } } } fail("Unable to recover public key from signature"); } async function verifySafe2(params, log = true) { try { return await verify2(params); } catch (error) { if (log) console.error(error); return false; } } var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; var encoder = new TextEncoder; async function sha256(data) { return crypto.subtle.digest("SHA-256", data); } async function doubleSha256(data) { return sha256(data).then(sha256); } function bytesToHex(bytes) { return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); } function ripemd160(data) { const K_LEFT = [0, 1518500249, 1859775393, 2400959708, 2840853838]; const K_RIGHT = [1352829926, 1548603684, 1836072691, 2053994217, 0]; const R_LEFT = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13 ]; const R_RIGHT = [ 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11 ]; const S_LEFT = [ 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 ]; const S_RIGHT = [ 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 ]; const rotateLeft = (n, b) => n << b | n >>> 32 - b; const f = (j, x, y, z) => { if (j < 16) return x ^ y ^ z; if (j < 32) return x & y | ~x & z; if (j < 48) return (x | ~y) ^ z; if (j < 64) return x & z | y & ~z; return x ^ (y | ~z); }; const msgLen = data.length; const bitLen = msgLen * 8; const paddedLen = Math.ceil((msgLen + 9) / 64) * 64; const padded = new Uint8Array(paddedLen); padded.set(data); padded[msgLen] = 128; const view = new DataView(padded.buffer); view.setUint32(paddedLen - 8, bitLen & 4294967295, true); view.setUint32(paddedLen - 4, Math.floor(bitLen / 4294967296), true); let h0 = 1732584193; let h1 = 4023233417; let h2 = 2562383102; let h3 = 271733878; let h4 = 3285377520; for (let chunk = 0;chunk < paddedLen; chunk += 64) { const w = new Array(16); for (let i = 0;i < 16; i++) { w[i] = view.getUint32(chunk + i * 4, true); } let al = h0, bl = h1, cl = h2, dl = h3, el = h4; let ar = h0, br = h1, cr = h2, dr = h3, er = h4; for (let j = 0;j < 80; j++) { let t2 = al + f(j, bl, cl, dl) + w[R_LEFT[j]] + K_LEFT[Math.floor(j / 16)] >>> 0; t2 = rotateLeft(t2, S_LEFT[j]) + el; al = el; el = dl; dl = rotateLeft(cl, 10); cl = bl; bl = t2 >>> 0; t2 = ar + f(79 - j, br, cr, dr) + w[R_RIGHT[j]] + K_RIGHT[Math.floor(j / 16)] >>> 0; t2 = rotateLeft(t2, S_RIGHT[j]) + er; ar = er; er = dr; dr = rotateLeft(cr, 10); cr = br; br = t2 >>> 0; } const t = h1 + cl + dr >>> 0; h1 = h2 + dl + er >>> 0; h2 = h3 + el + ar >>> 0; h3 = h4 + al + br >>> 0; h4 = h0 + bl + cr >>> 0; h0 = t; } const result = new Uint8Array(20); const resultView = new DataView(result.buffer); resultView.setUint32(0, h0, true); resultView.setUint32(4, h1, true); resultView.setUint32(8, h2, true); resultView.setUint32(12, h3, true); resultView.setUint32(16, h4, true); return result; } function base64ToBytes(base64) { const binaryString = atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0;i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; } var SECP256K1_P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn; var SECP256K1_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; var G = { x: 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n, y: 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n }; function modInverse(a, m) { if (a < 0n) a = (a % m + m) % m; let [old_r, r] = [a, m]; let [old_s, s] = [1n, 0n]; while (r !== 0n) { const quotient = old_r / r; [old_r, r] = [r, old_r - quotient * r]; [old_s, s] = [s, old_s - quotient * s]; } return old_r > 1n ? 0n : old_s < 0n ? old_s + m : old_s; } function pointAdd(p1, p2) { if (!p1) return p2; if (!p2) return p1; if (p1.x === p2.x) { if (p1.y === p2.y) { const s2 = 3n * p1.x * p1.x * modInverse(2n * p1.y, SECP256K1_P) % SECP256K1_P; const x32 = (s2 * s2 - 2n * p1.x) % SECP256K1_P; const y32 = (s2 * (p1.x - x32) - p1.y) % SECP256K1_P; return { x: x32 < 0n ? x32 + SECP256K1_P : x32, y: y32 < 0n ? y32 + SECP256K1_P : y32 }; } else { return null; } } const s = (p2.y - p1.y) * modInverse(p2.x - p1.x, SECP256K1_P) % SECP256K1_P; const x3 = (s * s - p1.x - p2.x) % SECP256K1_P; const y3 = (s * (p1.x - x3) - p1.y) % SECP256K1_P; return { x: x3 < 0n ? x3 + SECP256K1_P : x3, y: y3 < 0n ? y3 + SECP256K1_P : y3 }; } function pointMultiply(k, point) { if (k === 0n) return null; if (k === 1n) return point; let result = null; let addend = point; while (k > 0n) { if (k & 1n) { result = pointAdd(result, addend); } addend = pointAdd(addend, addend); k >>= 1n; } return result; } function modPow(base, exp, mod) { let result = 1n; base = base % mod; while (exp > 0n) { if (exp % 2n === 1n) { result = result * base % mod; } exp = exp >> 1n; base = base * base % mod; } return result; } function recoverPublicKey(messageHash, signature, recoveryId) { if (signature.length !== 64) return null; const r = BigInt("0x" + bytesToHex(signature.slice(0, 32))); const s = BigInt("0x" + bytesToHex(signature.slice(32, 64))); const e = BigInt("0x" + bytesToHex(messageHash)); if (r >= SECP256K1_N || s >= SECP256K1_N) return null; const x = r + BigInt(recoveryId >> 1) * SECP256K1_N; if (x >= SECP256K1_P) return null; const ySq = (x * x * x + 7n) % SECP256K1_P; let y = modPow(ySq, (SECP256K1_P + 1n) / 4n, SECP256K1_P); if (y % 2n !== BigInt(recoveryId & 1)) { y = SECP256K1_P - y; } const R = { x, y }; const rInv = modInverse(r, SECP256K1_N); const sR = pointMultiply(s, R); const eG = pointMultiply(e, G); if (!sR || !eG) return null; const negEG = { x: eG.x, y: SECP256K1_P - eG.y }; const diff = pointAdd(sR, negEG); if (!diff) return null; return pointMultiply(rInv, diff); } function encodeVarint(n) { if (n < 253) { return new Uint8Array([n]); } else if (n <= 65535) { return new Uint8Array([253, n & 255, n >> 8 & 255]); } else if (n <= 4294967295) { return new Uint8Array([254, n & 255, n >> 8 & 255, n >> 16 & 255, n >> 24 & 255]); } else { throw new Error("Number too large for varint encoding"); } } async function createMessageHash(messageBytes) { const prefix = `Bitcoin Signed Message: `; const prefixBytes = encoder.encode(prefix); const prefixLength = encodeVarint(prefixBytes.length); const messageLength = encodeVarint(messageBytes.length); const fullMessage = new Uint8Array(prefixLength.length + prefixBytes.length + messageLength.length + messageBytes.length); let offset = 0; fullMessage.set(prefixLength, offset); offset += prefixLength.length; fullMessage.set(prefixBytes, offset); offset += prefixBytes.length; fullMessage.set(messageLength, offset); offset += messageLength.length; fullMessage.set(messageBytes, offset); const hashBuffer = await doubleSha256(fullMessage.buffer); return new Uint8Array(hashBuffer); } async function publicKeyToAddress(publicKey, compressed = true) { let publicKeyBytes; if (compressed) { publicKeyBytes = new Uint8Array(33); publicKeyBytes[0] = publicKey.y % 2n === 0n ? 2 : 3; const xBytes = publicKey.x.toString(16).padStart(64, "0"); for (let i = 0;i < 32; i++) { publicKeyBytes[i + 1] = parseInt(xBytes.substring(i * 2, i * 2 + 2), 16); } } else { publicKeyBytes = new Uint8Array(65); publicKeyBytes[0] = 4; const xBytes = publicKey.x.toString(16).padStart(64, "0"); for (let i = 0;i < 32; i++) { publicKeyBytes[i + 1] = parseInt(xBytes.substring(i * 2, i * 2 + 2), 16); } const yBytes = publicKey.y.toString(16).padStart(64, "0"); for (let i = 0;i < 32; i++) { publicKeyBytes[i + 33] = parseInt(yBytes.substring(i * 2, i * 2 + 2), 16); } } const sha256Hash = await sha256(publicKeyBytes.buffer); const ripemd160Hash = ripemd160(new Uint8Array(sha256Hash)); const versioned = new Uint8Array(21); versioned[0] = 0; versioned.set(ripemd160Hash.slice(0, 20), 1); const checksumBuffer = await doubleSha256(versioned.buffer); const checksum = new Uint8Array(checksumBuffer); const fullAddress = new Uint8Array(25); fullAddress.set(versioned, 0); fullAddress.set(checksum.slice(0, 4), 21); return base58Encode(fullAddress); } function base58Encode(bytes) { const alphabet = BASE58_ALPHABET; const base = BigInt(alphabet.length); let num = 0n; for (let i = 0;i < bytes.length; i++) { num = num * 256n + BigInt(bytes[i]); } let result = ""; while (num > 0n) { const remainder = num % base; result = alphabet[Number(remainder)] + result; num = num / base; } for (let i = 0;i < bytes.length && bytes[i] === 0; i++) { result = "1" + result; } return result; } export { verifySafe2 as verifySafe, exports_rpc as rpc, fail, verify2 as default, assert };