verify-bitcoin-message
Version:
A dependency-free Bitcoin message signature verifier that works in browsers
684 lines (681 loc) • 13.8 kB
JavaScript
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
};