512 lines
19 KiB
JavaScript
512 lines
19 KiB
JavaScript
|
|
// The wallet that signs Sirius Press sign-ins, running in the visitor's page.
|
||
|
|
//
|
||
|
|
// Why this exists at all: a self-hosted Sirius Press site must be able to log
|
||
|
|
// its own users in without calling home. Redirecting to sirius.x to sign would
|
||
|
|
// make every install depend on Silent Mode's portal being up and willing —
|
||
|
|
// which is exactly the arrangement this whole project exists to get away from.
|
||
|
|
// So the signing happens here, in the browser, against a phrase the server
|
||
|
|
// never sees.
|
||
|
|
//
|
||
|
|
// No build step and no dependencies, deliberately: a wallet you cannot read is
|
||
|
|
// a wallet you cannot trust, and a site owner should be able to open this file
|
||
|
|
// and follow every line from phrase to signature. BigInt does the curve maths,
|
||
|
|
// WebCrypto does SHA-256, HMAC and PBKDF2. RIPEMD-160 is implemented below
|
||
|
|
// because WebCrypto does not offer it and address derivation needs it.
|
||
|
|
//
|
||
|
|
// The phrase never leaves the page. It is not posted, not stored in a cookie,
|
||
|
|
// and — unless the visitor asks to stay signed in — not written to disk at
|
||
|
|
// all. What crosses the wire is a signature over a challenge the server
|
||
|
|
// issued, which proves control of the key and nothing else.
|
||
|
|
//
|
||
|
|
// window.SiriusWallet = { fromPhrase, generatePhrase, validatePhrase,
|
||
|
|
// external, addressFromPublicKey }
|
||
|
|
|
||
|
|
(() => {
|
||
|
|
"use strict";
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------- secp256k1
|
||
|
|
|
||
|
|
const P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn;
|
||
|
|
const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
||
|
|
const GX = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n;
|
||
|
|
const GY = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n;
|
||
|
|
const HALF_N = N >> 1n;
|
||
|
|
|
||
|
|
const mod = (a, m = P) => ((a % m) + m) % m;
|
||
|
|
|
||
|
|
function powMod(base, exp, m) {
|
||
|
|
let result = 1n;
|
||
|
|
let b = mod(base, m);
|
||
|
|
let e = exp;
|
||
|
|
while (e > 0n) {
|
||
|
|
if (e & 1n) result = (result * b) % m;
|
||
|
|
b = (b * b) % m;
|
||
|
|
e >>= 1n;
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Both moduli here are prime, so Fermat gives the inverse without an
|
||
|
|
// extended-Euclid routine.
|
||
|
|
const invMod = (a, m) => powMod(a, m - 2n, m);
|
||
|
|
|
||
|
|
// Points are Jacobian [X, Y, Z]; affine is X/Z^2, Y/Z^3. One inversion per
|
||
|
|
// scalar multiply instead of one per bit.
|
||
|
|
const INF = [1n, 1n, 0n];
|
||
|
|
|
||
|
|
function jDouble([x, y, z]) {
|
||
|
|
if (z === 0n || y === 0n) return INF;
|
||
|
|
const a = mod(x * x);
|
||
|
|
const b = mod(y * y);
|
||
|
|
const c = mod(b * b);
|
||
|
|
const d = mod(2n * (mod((x + b) * (x + b)) - a - c));
|
||
|
|
const e = mod(3n * a);
|
||
|
|
const f = mod(e * e);
|
||
|
|
const x3 = mod(f - 2n * d);
|
||
|
|
const y3 = mod(e * (d - x3) - 8n * c);
|
||
|
|
const z3 = mod(2n * y * z);
|
||
|
|
return [x3, y3, z3];
|
||
|
|
}
|
||
|
|
|
||
|
|
function jAdd(p1, p2) {
|
||
|
|
if (p1[2] === 0n) return p2;
|
||
|
|
if (p2[2] === 0n) return p1;
|
||
|
|
const [x1, y1, z1] = p1;
|
||
|
|
const [x2, y2, z2] = p2;
|
||
|
|
const z1z1 = mod(z1 * z1);
|
||
|
|
const z2z2 = mod(z2 * z2);
|
||
|
|
const u1 = mod(x1 * z2z2);
|
||
|
|
const u2 = mod(x2 * z1z1);
|
||
|
|
const s1 = mod(y1 * z2 * z2z2);
|
||
|
|
const s2 = mod(y2 * z1 * z1z1);
|
||
|
|
if (u1 === u2) return s1 === s2 ? jDouble(p1) : INF;
|
||
|
|
const h = mod(u2 - u1);
|
||
|
|
const i = mod(mod(2n * h) * mod(2n * h));
|
||
|
|
const j = mod(h * i);
|
||
|
|
const r = mod(2n * (s2 - s1));
|
||
|
|
const v = mod(u1 * i);
|
||
|
|
const x3 = mod(r * r - j - 2n * v);
|
||
|
|
const y3 = mod(r * (v - x3) - 2n * s1 * j);
|
||
|
|
const z3 = mod((mod((z1 + z2) * (z1 + z2)) - z1z1 - z2z2) * h);
|
||
|
|
return [x3, y3, z3];
|
||
|
|
}
|
||
|
|
|
||
|
|
function toAffine([x, y, z]) {
|
||
|
|
if (z === 0n) return null;
|
||
|
|
const zi = invMod(z, P);
|
||
|
|
const zi2 = mod(zi * zi);
|
||
|
|
return [mod(x * zi2), mod(y * zi2 * zi)];
|
||
|
|
}
|
||
|
|
|
||
|
|
function mulPoint(k, px, py) {
|
||
|
|
k = mod(k, N);
|
||
|
|
if (k === 0n) return null;
|
||
|
|
const base = [px, py, 1n];
|
||
|
|
let acc = INF;
|
||
|
|
for (const bit of k.toString(2)) {
|
||
|
|
acc = jDouble(acc);
|
||
|
|
if (bit === "1") acc = jAdd(acc, base);
|
||
|
|
}
|
||
|
|
return toAffine(acc);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ------------------------------------------------------------------- bytes
|
||
|
|
|
||
|
|
const enc = new TextEncoder();
|
||
|
|
const hexToBytes = (hex) =>
|
||
|
|
Uint8Array.from(hex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
|
||
|
|
const bytesToHex = (b) =>
|
||
|
|
[...b].map((x) => x.toString(16).padStart(2, "0")).join("");
|
||
|
|
const bytesToBig = (b) => BigInt("0x" + (bytesToHex(b) || "0"));
|
||
|
|
const bigToBytes = (n, len = 32) => hexToBytes(n.toString(16).padStart(len * 2, "0"));
|
||
|
|
const concat = (...arrays) => {
|
||
|
|
const total = arrays.reduce((n, a) => n + a.length, 0);
|
||
|
|
const out = new Uint8Array(total);
|
||
|
|
let at = 0;
|
||
|
|
for (const a of arrays) { out.set(a, at); at += a.length; }
|
||
|
|
return out;
|
||
|
|
};
|
||
|
|
const toBase64 = (b) => btoa(String.fromCharCode(...b));
|
||
|
|
|
||
|
|
const sha256 = async (bytes) =>
|
||
|
|
new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
||
|
|
|
||
|
|
async function hmac(hash, key, data) {
|
||
|
|
const k = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash }, false, ["sign"]);
|
||
|
|
return new Uint8Array(await crypto.subtle.sign("HMAC", k, data));
|
||
|
|
}
|
||
|
|
|
||
|
|
// --------------------------------------------------------------- RIPEMD-160
|
||
|
|
//
|
||
|
|
// WebCrypto has no RIPEMD-160 and an address cannot be derived without it.
|
||
|
|
// Straight transcription of the reference implementation.
|
||
|
|
|
||
|
|
function ripemd160(message) {
|
||
|
|
const rl = [
|
||
|
|
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 rr = [
|
||
|
|
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 sl = [
|
||
|
|
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 sr = [
|
||
|
|
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 kl = [0x00000000, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e];
|
||
|
|
const kr = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0x00000000];
|
||
|
|
|
||
|
|
const rol = (x, n) => ((x << n) | (x >>> (32 - n))) >>> 0;
|
||
|
|
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);
|
||
|
|
};
|
||
|
|
|
||
|
|
// Pad to 64-byte blocks: 0x80, zeros, then a 64-bit little-endian length.
|
||
|
|
const len = message.length;
|
||
|
|
const withPad = new Uint8Array((((len + 8) >> 6) + 1) << 6);
|
||
|
|
withPad.set(message);
|
||
|
|
withPad[len] = 0x80;
|
||
|
|
const view = new DataView(withPad.buffer);
|
||
|
|
view.setUint32(withPad.length - 8, (len << 3) >>> 0, true);
|
||
|
|
view.setUint32(withPad.length - 4, Math.floor((len * 8) / 0x100000000), true);
|
||
|
|
|
||
|
|
let h = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
|
||
|
|
const x = new Array(16);
|
||
|
|
|
||
|
|
for (let block = 0; block < withPad.length; block += 64) {
|
||
|
|
for (let i = 0; i < 16; i++) x[i] = view.getUint32(block + i * 4, true);
|
||
|
|
let [al, bl, cl, dl, el] = h;
|
||
|
|
let [ar, br, cr, dr, er] = h;
|
||
|
|
for (let j = 0; j < 80; j++) {
|
||
|
|
const round = Math.floor(j / 16);
|
||
|
|
let t = (al + f(j, bl, cl, dl) + x[rl[j]] + kl[round]) >>> 0;
|
||
|
|
t = (rol(t, sl[j]) + el) >>> 0;
|
||
|
|
al = el; el = dl; dl = rol(cl, 10); cl = bl; bl = t;
|
||
|
|
t = (ar + f(79 - j, br, cr, dr) + x[rr[j]] + kr[round]) >>> 0;
|
||
|
|
t = (rol(t, sr[j]) + er) >>> 0;
|
||
|
|
ar = er; er = dr; dr = rol(cr, 10); cr = br; br = t;
|
||
|
|
}
|
||
|
|
// The new state is a rotation of the old one: h0 takes the value built
|
||
|
|
// from h1, h1 from h2, and so on, with h4 wrapping back to h0.
|
||
|
|
h = [
|
||
|
|
(h[1] + cl + dr) >>> 0,
|
||
|
|
(h[2] + dl + er) >>> 0,
|
||
|
|
(h[3] + el + ar) >>> 0,
|
||
|
|
(h[4] + al + br) >>> 0,
|
||
|
|
(h[0] + bl + cr) >>> 0,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
const out = new Uint8Array(20);
|
||
|
|
const ov = new DataView(out.buffer);
|
||
|
|
h.forEach((word, i) => ov.setUint32(i * 4, word, true));
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
// --------------------------------------------------------------- CashAddr
|
||
|
|
|
||
|
|
const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||
|
|
|
||
|
|
function polyMod(values) {
|
||
|
|
// 40-bit accumulator, split across two 32-bit halves because JavaScript's
|
||
|
|
// bitwise operators truncate to 32 bits and BigInt here would be slower
|
||
|
|
// than the arithmetic is worth.
|
||
|
|
let c = 1n;
|
||
|
|
for (const d of values) {
|
||
|
|
const c0 = c >> 35n;
|
||
|
|
c = ((c & 0x07ffffffffn) << 5n) ^ BigInt(d);
|
||
|
|
if (c0 & 0x01n) c ^= 0x98f2bc8e61n;
|
||
|
|
if (c0 & 0x02n) c ^= 0x79b76d99e2n;
|
||
|
|
if (c0 & 0x04n) c ^= 0xf33e5fb3c4n;
|
||
|
|
if (c0 & 0x08n) c ^= 0xae2eabe2a8n;
|
||
|
|
if (c0 & 0x10n) c ^= 0x1e4f43e470n;
|
||
|
|
}
|
||
|
|
return c ^ 1n;
|
||
|
|
}
|
||
|
|
|
||
|
|
const expandPrefix = (prefix) => [...prefix].map((ch) => ch.charCodeAt(0) & 0x1f).concat([0]);
|
||
|
|
|
||
|
|
function convertBits(values, from, to, pad) {
|
||
|
|
let acc = 0;
|
||
|
|
let bits = 0;
|
||
|
|
const out = [];
|
||
|
|
const max = (1 << to) - 1;
|
||
|
|
for (const v of values) {
|
||
|
|
if (v < 0 || v >> from !== 0) return null;
|
||
|
|
acc = (acc << from) | v;
|
||
|
|
bits += from;
|
||
|
|
while (bits >= to) {
|
||
|
|
bits -= to;
|
||
|
|
out.push((acc >> bits) & max);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (pad) {
|
||
|
|
if (bits > 0) out.push((acc << (to - bits)) & max);
|
||
|
|
} else if (bits >= from || ((acc << (to - bits)) & max)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
function encodeCashAddr(prefix, hash160, type = 0) {
|
||
|
|
const payload = concat(Uint8Array.of(type << 3), hash160);
|
||
|
|
const data = convertBits([...payload], 8, 5, true);
|
||
|
|
const checksum = polyMod(expandPrefix(prefix).concat(data, [0, 0, 0, 0, 0, 0, 0, 0]));
|
||
|
|
const cs = [];
|
||
|
|
for (let i = 0; i < 8; i++) cs.push(Number((checksum >> BigInt(5 * (7 - i))) & 0x1fn));
|
||
|
|
return prefix + ":" + data.concat(cs).map((v) => CHARSET[v]).join("");
|
||
|
|
}
|
||
|
|
|
||
|
|
const addressFromPublicKey = async (pub, prefix) =>
|
||
|
|
encodeCashAddr(prefix, ripemd160(await sha256(pub)), 0);
|
||
|
|
|
||
|
|
// ------------------------------------------------------------------- keys
|
||
|
|
|
||
|
|
async function publicKey(priv) {
|
||
|
|
const pt = mulPoint(bytesToBig(priv), GX, GY);
|
||
|
|
if (!pt) throw new Error("that key does not produce a valid public key");
|
||
|
|
return concat(Uint8Array.of(pt[1] & 1n ? 3 : 2), bigToBytes(pt[0]));
|
||
|
|
}
|
||
|
|
|
||
|
|
// BIP-32 CKDpriv. Hardened steps use the private key, normal steps the
|
||
|
|
// public one — which is why this has to be async all the way down.
|
||
|
|
async function deriveChild(node, index) {
|
||
|
|
const hardened = index >= 0x80000000;
|
||
|
|
const indexBytes = new Uint8Array(4);
|
||
|
|
new DataView(indexBytes.buffer).setUint32(0, index, false);
|
||
|
|
const data = hardened
|
||
|
|
? concat(Uint8Array.of(0), node.key, indexBytes)
|
||
|
|
: concat(await publicKey(node.key), indexBytes);
|
||
|
|
const i = await hmac("SHA-512", node.chain, data);
|
||
|
|
const tweak = bytesToBig(i.slice(0, 32));
|
||
|
|
if (tweak >= N) throw new Error("derivation hit an invalid tweak");
|
||
|
|
const child = mod(tweak + bytesToBig(node.key), N);
|
||
|
|
if (child === 0n) throw new Error("derivation produced a zero key");
|
||
|
|
return { key: bigToBytes(child), chain: i.slice(32) };
|
||
|
|
}
|
||
|
|
|
||
|
|
async function derivePath(seed, path) {
|
||
|
|
const i = await hmac("SHA-512", enc.encode("Bitcoin seed"), seed);
|
||
|
|
let node = { key: i.slice(0, 32), chain: i.slice(32) };
|
||
|
|
for (const part of path.split("/").slice(1)) {
|
||
|
|
if (!part) continue;
|
||
|
|
const hardened = /['h]$/i.test(part);
|
||
|
|
const num = parseInt(part.replace(/['h]$/i, ""), 10);
|
||
|
|
if (!Number.isInteger(num) || num < 0) throw new Error("bad derivation path: " + path);
|
||
|
|
node = await deriveChild(node, hardened ? num + 0x80000000 : num);
|
||
|
|
}
|
||
|
|
return node;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function seedFromPhrase(phrase, passphrase = "") {
|
||
|
|
const key = await crypto.subtle.importKey("raw", enc.encode(phrase), "PBKDF2", false, ["deriveBits"]);
|
||
|
|
const bits = await crypto.subtle.deriveBits(
|
||
|
|
{ name: "PBKDF2", salt: enc.encode("mnemonic" + passphrase), iterations: 2048, hash: "SHA-512" },
|
||
|
|
key,
|
||
|
|
512,
|
||
|
|
);
|
||
|
|
return new Uint8Array(bits);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ------------------------------------------------------------- signing
|
||
|
|
|
||
|
|
const MAGIC = "Bitcoin Signed Message:\n";
|
||
|
|
|
||
|
|
function varStr(bytes) {
|
||
|
|
if (bytes.length < 0xfd) return concat(Uint8Array.of(bytes.length), bytes);
|
||
|
|
const len = new Uint8Array(3);
|
||
|
|
len[0] = 0xfd;
|
||
|
|
new DataView(len.buffer).setUint16(1, bytes.length, true);
|
||
|
|
return concat(len, bytes);
|
||
|
|
}
|
||
|
|
|
||
|
|
// The digest every BCH wallet signs for a text message: double SHA-256 over
|
||
|
|
// varstr(magic) || varstr(message). Sirius Press verifies against this, so a
|
||
|
|
// signature made in Electron Cash or the Theseus wallet works identically.
|
||
|
|
async function messageDigest(message) {
|
||
|
|
const payload = concat(varStr(enc.encode(MAGIC)), varStr(enc.encode(message)));
|
||
|
|
return sha256(await sha256(payload));
|
||
|
|
}
|
||
|
|
|
||
|
|
// RFC 6979: the nonce comes from the key and the message, never from the
|
||
|
|
// page's random source. Two signatures of the same thing are identical, and
|
||
|
|
// a bad RNG cannot leak the key.
|
||
|
|
async function* nonces(digest, priv) {
|
||
|
|
const h1 = bigToBytes(mod(bytesToBig(digest), N));
|
||
|
|
let v = new Uint8Array(32).fill(1);
|
||
|
|
let k = new Uint8Array(32).fill(0);
|
||
|
|
k = await hmac("SHA-256", k, concat(v, Uint8Array.of(0), priv, h1));
|
||
|
|
v = await hmac("SHA-256", k, v);
|
||
|
|
k = await hmac("SHA-256", k, concat(v, Uint8Array.of(1), priv, h1));
|
||
|
|
v = await hmac("SHA-256", k, v);
|
||
|
|
for (let i = 0; i < 64; i++) {
|
||
|
|
v = await hmac("SHA-256", k, v);
|
||
|
|
const candidate = bytesToBig(v);
|
||
|
|
if (candidate > 0n && candidate < N) yield candidate;
|
||
|
|
k = await hmac("SHA-256", k, concat(v, Uint8Array.of(0)));
|
||
|
|
v = await hmac("SHA-256", k, v);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function signDigest(digest, priv) {
|
||
|
|
const z = mod(bytesToBig(digest), N);
|
||
|
|
const d = bytesToBig(priv);
|
||
|
|
for await (const k of nonces(digest, priv)) {
|
||
|
|
const pt = mulPoint(k, GX, GY);
|
||
|
|
if (!pt) continue;
|
||
|
|
const r = mod(pt[0], N);
|
||
|
|
if (r === 0n) continue;
|
||
|
|
let s = mod(invMod(k, N) * (z + r * d), N);
|
||
|
|
if (s === 0n) continue;
|
||
|
|
let recid = (pt[1] & 1n ? 1 : 0) | (pt[0] >= N ? 2 : 0);
|
||
|
|
if (s > HALF_N) {
|
||
|
|
s = N - s;
|
||
|
|
recid ^= 1;
|
||
|
|
}
|
||
|
|
return concat(Uint8Array.of(27 + 4 + recid), bigToBytes(r), bigToBytes(s));
|
||
|
|
}
|
||
|
|
throw new Error("could not produce a signature");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ------------------------------------------------------------- public API
|
||
|
|
|
||
|
|
const normalizePhrase = (p) => String(p || "").trim().toLowerCase().replace(/\s+/g, " ");
|
||
|
|
|
||
|
|
function validatePhrase(phrase) {
|
||
|
|
const clean = normalizePhrase(phrase);
|
||
|
|
if (!clean) return { ok: false, error: "Enter your recovery phrase." };
|
||
|
|
const words = clean.split(" ");
|
||
|
|
if (![12, 15, 18, 21, 24].includes(words.length)) {
|
||
|
|
return { ok: false, error: `A recovery phrase has 12, 15, 18, 21 or 24 words — this has ${words.length}.` };
|
||
|
|
}
|
||
|
|
const list = window.SIRIUS_BIP39_EN;
|
||
|
|
if (Array.isArray(list)) {
|
||
|
|
for (let i = 0; i < words.length; i++) {
|
||
|
|
if (!list.includes(words[i])) {
|
||
|
|
return { ok: false, error: `Word ${i + 1}, “${words[i]}”, is not a recovery-phrase word.` };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return { ok: true, error: "" };
|
||
|
|
}
|
||
|
|
|
||
|
|
/** A fresh phrase with a valid BIP-39 checksum, from the browser's CSPRNG. */
|
||
|
|
async function generatePhrase(wordCount = 12) {
|
||
|
|
const list = window.SIRIUS_BIP39_EN;
|
||
|
|
if (!Array.isArray(list) || list.length !== 2048) {
|
||
|
|
throw new Error("the word list has not loaded, so a phrase cannot be generated safely");
|
||
|
|
}
|
||
|
|
const entropyBits = (wordCount / 3) * 32;
|
||
|
|
const entropy = crypto.getRandomValues(new Uint8Array(entropyBits / 8));
|
||
|
|
const hash = await sha256(entropy);
|
||
|
|
let bits = [...entropy].map((b) => b.toString(2).padStart(8, "0")).join("");
|
||
|
|
bits += hash[0].toString(2).padStart(8, "0").slice(0, entropyBits / 32);
|
||
|
|
const words = [];
|
||
|
|
for (let i = 0; i < bits.length / 11; i++) {
|
||
|
|
words.push(list[parseInt(bits.slice(i * 11, i * 11 + 11), 2)]);
|
||
|
|
}
|
||
|
|
return words.join(" ");
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Open a wallet from a phrase.
|
||
|
|
*
|
||
|
|
* @returns {{address: string, sign: (message: string) => Promise<string>}}
|
||
|
|
*/
|
||
|
|
async function fromPhrase(phrase, { prefix = "bitcoincash", path = "m/44'/145'/0'/0/0" } = {}) {
|
||
|
|
const clean = normalizePhrase(phrase);
|
||
|
|
const check = validatePhrase(clean);
|
||
|
|
if (!check.ok) throw new Error(check.error);
|
||
|
|
const node = await derivePath(await seedFromPhrase(clean), path);
|
||
|
|
const pub = await publicKey(node.key);
|
||
|
|
const address = await addressFromPublicKey(pub, prefix);
|
||
|
|
return {
|
||
|
|
address,
|
||
|
|
async sign(message) {
|
||
|
|
return toBase64(await signDigest(await messageDigest(message), node.key));
|
||
|
|
},
|
||
|
|
/**
|
||
|
|
* Sign an already-computed 32-byte digest.
|
||
|
|
*
|
||
|
|
* The gateway's upload envelope (BNS-SITE1) is a single SHA-256 over a
|
||
|
|
* fixed line format, not the BIP-137 message scheme — so the static
|
||
|
|
* exporter, signing from the browser, needs this rather than sign().
|
||
|
|
* Handing a wallet a bare digest is a sharp tool: nothing about the
|
||
|
|
* bytes is legible to the person approving it, which is exactly why
|
||
|
|
* the interactive login path does not use it.
|
||
|
|
*/
|
||
|
|
async signRaw(digest) {
|
||
|
|
if (!(digest instanceof Uint8Array) || digest.length !== 32) {
|
||
|
|
throw new Error("signRaw expects a 32-byte digest");
|
||
|
|
}
|
||
|
|
return toBase64(await signDigest(digest, node.key));
|
||
|
|
},
|
||
|
|
/** sha256 of arbitrary bytes, so callers need no second hash library. */
|
||
|
|
sha256,
|
||
|
|
/** Drop the key material once the page is done with it. */
|
||
|
|
forget() {
|
||
|
|
node.key.fill(0);
|
||
|
|
node.chain.fill(0);
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A wallet the browser already provides, if there is one.
|
||
|
|
*
|
||
|
|
* Theseus exposes `window.bitcoincash` on `.x` origins, which signs with the
|
||
|
|
* same BIP-137 scheme. Using it means the key stays in the browser's own
|
||
|
|
* wallet and never touches this page — strictly better than asking for a
|
||
|
|
* phrase, so the login screen offers it first when it is there.
|
||
|
|
*/
|
||
|
|
function external() {
|
||
|
|
const bridge = window.bitcoincash;
|
||
|
|
if (!bridge || typeof bridge.signMessage !== "function") return null;
|
||
|
|
return {
|
||
|
|
name: "Theseus",
|
||
|
|
async address() {
|
||
|
|
const a = await bridge.getAddress();
|
||
|
|
return typeof a === "string" ? a : a && a.address ? a.address : "";
|
||
|
|
},
|
||
|
|
async sign(message) {
|
||
|
|
const r = await bridge.signMessage(message);
|
||
|
|
if (typeof r === "string") return r;
|
||
|
|
if (r && r.signature) return r.signature;
|
||
|
|
throw new Error("the wallet returned a signature this page did not understand");
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
window.SiriusWallet = {
|
||
|
|
fromPhrase,
|
||
|
|
generatePhrase,
|
||
|
|
validatePhrase,
|
||
|
|
normalizePhrase,
|
||
|
|
external,
|
||
|
|
addressFromPublicKey,
|
||
|
|
// Exposed for the test page in tests/browser/, not used by the UI.
|
||
|
|
_internals: { signDigest, messageDigest, publicKey, ripemd160, encodeCashAddr },
|
||
|
|
};
|
||
|
|
})();
|