/** * g1wallet.js — Ğ1 Wallet client-side implementation. * * DERIVATION (Substrate / Duniter v2 / Ğecko standard): * 12-word BIP39 mnemonic * → mnemonicToEntropy() → 16 bytes of raw entropy * → use entropy directly as Ed25519 seed (the Substrate "mini-secret") * → SubtleCrypto.importKey("raw", seed, "Ed25519", false, ["sign"]) * → SubtleCrypto.sign("Ed25519", key, empty) → NOT used for sig; * instead derive pubkey via sign(empty) trick or via key export * * NOTE ON PUBKEY EXTRACTION: * SubtleCrypto does not provide a direct "get public key from private seed" * for Ed25519. The approach used here: * - Import as CryptoKeyPair using generateKey with extractable=false is not * applicable (generateKey doesn't accept a seed). * - Instead: import the 32-byte seed as a raw "Ed25519" private key with * extractable=false, then derive the public key via importKey with the * PKCS8 DER wrapper (which wraps the seed), then use exportKey("spki") * on the public key half to extract the 32-byte public key bytes. * * Concretely: * 1. Wrap seed in Ed25519 PKCS8 DER envelope (fixed 16-byte header + 32-byte seed) * 2. importKey("pkcs8", der, "Ed25519", true, ["sign"]) → CryptoKeyPair (private) * 3. Derive public key via importKey of the corresponding SPKI, OR: * Use generateKey workaround — import via PKCS8, extract SPKI from the * corresponding key using SubtleCrypto.exportKey approach. * The cleanest path: after PKCS8 import, call SubtleCrypto.sign on a known * message, then verify — but we need the pubkey bytes for SS58. * * ACTUAL APPROACH (cross-browser, verified pattern): * - Wrap seed in PKCS8 DER (RFC 5958, Ed25519 OID 1.3.101.112) * - importKey("pkcs8", der, {name:"Ed25519"}, true, ["sign"]) → private CryptoKey * - The browser does NOT give us the public key from pkcs8 import alone. * - Solution: also import the same seed as a raw "X25519" key... no, wrong curve. * - CORRECT SOLUTION: use the SubtleCrypto generateKey approach with a * known-seed workaround is not available in the Web Crypto spec. * - FALLBACK: use a pure-JS Ed25519 implementation for pubkey derivation only. * The private key for signing is still held as a non-extractable CryptoKey. * * IMPLEMENTED APPROACH (pragmatic, spec-safe): * Phase 1 (this file): entropy → seed → use tweetnacl-style scalar mult * to derive pubkey bytes in pure JS (32 bytes). The private key is then * imported into SubtleCrypto as non-extractable for all signing operations. * The pubkey bytes are SS58-encoded for display and storage. * * For pubkey derivation we use the standard Ed25519 base point scalar * multiplication. Rather than bundle a full nacl library, we use the * minimal required implementation: clamp + scalar_mult on the Ed25519 * base point, which is ~80 lines of standard arithmetic. * * CRITICAL RULES (never relax): * - The private key NEVER leaves the browser. * - The mnemonic NEVER leaves the browser. * - No key material in localStorage, sessionStorage, or cookies. * - The private CryptoKey is non-extractable. * - One document, one confirmation, one click — no batch signing. */ (function () { 'use strict'; // ------------------------------------------------------------------------- // SESSION STATE // Held in module-scope memory only. Gone when the page is closed. // ------------------------------------------------------------------------- var _session = { unlocked: false, pubkey: null, // base58-encoded SS58 Ğ1 address (g1...) pubkeyHex: null, // 32-byte pubkey as hex, for signing reference _cryptoKey: null // CryptoKey (non-extractable Ed25519 private key) }; // ------------------------------------------------------------------------- // ED25519 PUBKEY DERIVATION — minimal pure JS // Standard Ed25519 scalar multiplication on the base point. // Source: derived from the public domain TweetNaCl / SUPERCOP algorithms. // Only the pubkey derivation path is implemented here — all signing goes // through SubtleCrypto. // ------------------------------------------------------------------------- var _ed25519 = (function () { // Field arithmetic in GF(2^255 - 19) // Each element is a 16-element array of 32-bit integers (signed) function gf(init) { var r = new Float64Array(16); if (init) for (var i = 0; i < init.length; i++) r[i] = init[i]; return r; } var _0 = new Uint8Array(16); var _9 = new Uint8Array(32); _9[0] = 9; var gf0 = gf(), gf1 = gf([1]), D = gf([0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]), D2 = gf([0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]), X = gf([0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169]), Y = gf([0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666]), I = gf([0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]); function car25519(o) { var c; for (var i = 0; i < 16; i++) { o[i] += 65536; c = Math.floor(o[i] / 65536); o[(i + 1) * (i < 15 ? 1 : 0)] += c - 1 + 37 * (c - 1) * (i === 15 ? 1 : 0); o[i] -= c * 65536; } } function sel25519(p, q, b) { var t, c = ~(b - 1); for (var i = 0; i < 16; i++) { t = c & (p[i] ^ q[i]); p[i] ^= t; q[i] ^= t; } } function pack25519(o, n) { var i, j, b; var m = gf(), t = gf(); for (i = 0; i < 16; i++) t[i] = n[i]; car25519(t); car25519(t); car25519(t); for (j = 0; j < 2; j++) { m[0] = t[0] - 0xffed; for (i = 1; i < 15; i++) { m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1); m[i - 1] &= 0xffff; } m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1); b = (m[15] >> 16) & 1; m[14] &= 0xffff; sel25519(t, m, 1 - b); } for (i = 0; i < 16; i++) { o[2 * i] = t[i] & 0xff; o[2 * i + 1] = t[i] >> 8; } } function unpackneg(r, p) { var t = gf(), chk = gf(), num = gf(), den = gf(), den2 = gf(), den4 = gf(), den6 = gf(); set25519(r[2], gf1); unpack25519(r[1], p); S(num, r[1]); M(den, num, D); Z(num, num, r[2]); A(den, r[2], den); S(den2, den); S(den4, den2); M(den6, den4, den2); M(chk, den6, num); M(chk, chk, den); pow2523(chk, chk); M(chk, chk, num); M(chk, chk, den); M(chk, chk, den); M(r[0], chk, den); S(t, r[0]); M(t, t, den); if (!neq25519(t, num)) M(r[0], r[0], I); S(t, r[0]); M(t, t, den); if (!neq25519(t, num)) return -1; if (par25519(r[0]) === (p[31] >> 7)) Z(r[0], gf0, r[0]); M(r[3], r[0], r[1]); return 0; } function neq25519(a, b) { var c = new Uint8Array(32), d = new Uint8Array(32); pack25519(c, a); pack25519(d, b); return cryptoNeq(c, 0, d, 0, 32); } function cryptoNeq(x, xi, y, yi, n) { var d = 0; for (var i = 0; i < n; i++) d |= x[xi + i] ^ y[yi + i]; return (1 & ((d - 1) >>> 8)) - 1; } function par25519(a) { var d = new Uint8Array(32); pack25519(d, a); return d[0] & 1; } function unpack25519(o, n) { for (var i = 0; i < 16; i++) o[i] = n[2 * i] + (n[2 * i + 1] << 8); o[15] &= 0x7fff; } function A(o, a, b) { for (var i = 0; i < 16; i++) o[i] = a[i] + b[i]; } function Z(o, a, b) { for (var i = 0; i < 16; i++) o[i] = a[i] - b[i]; } function M(o, a, b) { var t = new Float64Array(31); for (var i = 0; i < 16; i++) for (var j = 0; j < 16; j++) t[i + j] += a[i] * b[j]; for (var i = 0; i < 15; i++) t[i] += 38 * t[i + 16]; for (var i = 0; i < 16; i++) o[i] = t[i]; car25519(o); car25519(o); } function S(o, a) { M(o, a, a); } function pow2523(o, i) { var c = gf(); for (var a = 0; a < 16; a++) c[a] = i[a]; for (var a = 250; a >= 0; a--) { S(c, c); if (a !== 1) M(c, c, i); } for (var a = 0; a < 16; a++) o[a] = c[a]; } function set25519(r, a) { for (var i = 0; i < 16; i++) r[i] = a[i] | 0; } // 64-byte hash (SHA-512) — we delegate to SubtleCrypto for this. // But we need it synchronously for key derivation. We use a // synchronous SHA-512 based on @noble/hashes which is bundled in bip39. // Actually — the bip39 lib does NOT export sha512 externally. // We implement scalar mult with the clamped seed directly (Ed25519 spec): // The "mini-secret" (32 bytes) is hashed with SHA-512 to get the // 64-byte scalar, then the lower 32 bytes are clamped. // We need SHA-512 synchronously. We implement a minimal SHA-512 here. // --- Minimal SHA-512 --- // Based on FIPS 180-4, pure JS, no dependencies. var K512 = [ [0x428a2f98, 0xd728ae22], [0x71374491, 0x23ef65cd], [0xb5c0fbcf, 0xec4d3b2f], [0xe9b5dba5, 0x8189dbbc], [0x3956c25b, 0xf348b538], [0x59f111f1, 0xb605d019], [0x923f82a4, 0xaf194f9b], [0xab1c5ed5, 0xda6d8118], [0xd807aa98, 0xa3030242], [0x12835b01, 0x45706fbe], [0x243185be, 0x4ee4b28c], [0x550c7dc3, 0xd5ffb4e2], [0x72be5d74, 0xf27b896f], [0x80deb1fe, 0x3b1696b1], [0x9bdc06a7, 0x25c71235], [0xc19bf174, 0xcf692694], [0xe49b69c1, 0x9ef14ad2], [0xefbe4786, 0x384f25e3], [0x0fc19dc6, 0x8b8cd5b5], [0x240ca1cc, 0x77ac9c65], [0x2de92c6f, 0x592b0275], [0x4a7484aa, 0x6ea6e483], [0x5cb0a9dc, 0xbd41fbd4], [0x76f988da, 0x831153b5], [0x983e5152, 0xee66dfab], [0xa831c66d, 0x2db43210], [0xb00327c8, 0x98fb213f], [0xbf597fc7, 0xbeef0ee4], [0xc6e00bf3, 0x3da88fc2], [0xd5a79147, 0x930aa725], [0x06ca6351, 0xe003826f], [0x14292967, 0x0a0e6e70], [0x27b70a85, 0x46d22ffc], [0x2e1b2138, 0x5c26c926], [0x4d2c6dfc, 0x5ac42aed], [0x53380d13, 0x9d95b3df], [0x650a7354, 0x8baf63de], [0x766a0abb, 0x3c77b2a8], [0x81c2c92e, 0x47edaee6], [0x92722c85, 0x1482353b], [0xa2bfe8a1, 0x4cf10364], [0xa81a664b, 0xbc423001], [0xc24b8b70, 0xd0f89791], [0xc76c51a3, 0x0654be30], [0xd192e819, 0xd6ef5218], [0xd6990624, 0x5565a910], [0xf40e3585, 0x5771202a], [0x106aa070, 0x32bbd1b8], [0x19a4c116, 0xb8d2d0c8], [0x1e376c08, 0x5141ab53], [0x2748774c, 0xdf8eeb99], [0x34b0bcb5, 0xe19b48a8], [0x391c0cb3, 0xc5c95a63], [0x4ed8aa4a, 0xe3418acb], [0x5b9cca4f, 0x7763e373], [0x682e6ff3, 0xd6b2b8a3], [0x748f82ee, 0x5defb2fc], [0x78a5636f, 0x43172f60], [0x84c87814, 0xa1f0ab72], [0x8cc70208, 0x1a6439ec], [0x90befffa, 0x23631e28], [0xa4506ceb, 0xde82bde9], [0xbef9a3f7, 0xb2c67915], [0xc67178f2, 0xe372532b], [0xca273ece, 0xea26619c], [0xd186b8c7, 0x21c0c207], [0xeada7dd6, 0xcde0eb1e], [0xf57d4f7f, 0xee6ed178], [0x06f067aa, 0x72176fba], [0x0a637dc5, 0xa2c898a6], [0x113f9804, 0xbef90dae], [0x1b710b35, 0x131c471b], [0x28db77f5, 0x23047d84], [0x32caab7b, 0x40c72493], [0x3c9ebe0a, 0x15c9bebc], [0x431d67c4, 0x9c100d4c], [0x4cc5d4be, 0xcb3e42b6], [0x597f299c, 0xfc657e2a], [0x5fcb6fab, 0x3ad6faec], [0x6c44198c, 0x4a475817] ]; function sha512(msg) { // msg: Uint8Array; returns Uint8Array(64) var H = [ [0x6a09e667, 0xf3bcc908], [0xbb67ae85, 0x84caa73b], [0x3c6ef372, 0xfe94f82b], [0xa54ff53a, 0x5f1d36f1], [0x510e527f, 0xade682d1], [0x9b05688c, 0x2b3e6c1f], [0x1f83d9ab, 0xfb41bd6b], [0x5be0cd19, 0x137e2179] ]; var len = msg.length; var bitlen = len * 8; // Padding var padded = []; for (var i = 0; i < len; i++) padded.push(msg[i]); padded.push(0x80); while ((padded.length % 128) !== 112) padded.push(0); // 16 bytes of bit length (big-endian 128-bit), we only support < 2^32 bits padded.push(0, 0, 0, 0, 0, 0, 0, 0); padded.push((bitlen / 0x1000000 / 0x1000000) & 0xff); padded.push((bitlen / 0x1000000 / 0x10000) & 0xff); padded.push((bitlen / 0x1000000 / 0x100) & 0xff); padded.push((bitlen / 0x1000000) & 0xff); padded.push((bitlen / 0x1000000) >>> 0 & 0xff); // ensure no NaN // fix: proper 8-byte big-endian // re-do last 8 bytes properly padded = padded.slice(0, padded.length - 5); var bl = bitlen; for (var i = 3; i >= 0; i--) { padded.push(bl & 0xff); bl = Math.floor(bl / 256); } // Actually redo this properly: var data = new Uint8Array(len + 1 + ((112 - (len + 1) % 128 + 128) % 128) + 16); for (var i = 0; i < len; i++) data[i] = msg[i]; data[len] = 0x80; var blen = len * 8; // write 128-bit big-endian bit length at end (we only use lower 64 bits) data[data.length - 4] = (blen >>> 24) & 0xff; data[data.length - 3] = (blen >>> 16) & 0xff; data[data.length - 2] = (blen >>> 8) & 0xff; data[data.length - 1] = blen & 0xff; function rotr64h(ah, al, r) { return r < 32 ? (ah >>> r) | (al << (32 - r)) : (al >>> (r - 32)) | (ah << (64 - r)); } function rotr64l(ah, al, r) { return r < 32 ? (al >>> r) | (ah << (32 - r)) : (ah >>> (r - 32)) | (al << (64 - r)); } var W = new Array(160); // 80 pairs of [h, l] for (var bi = 0; bi < data.length; bi += 128) { for (var i = 0; i < 16; i++) { W[2*i] = (data[bi + 8*i]<<24) | (data[bi+8*i+1]<<16) | (data[bi+8*i+2]<<8) | data[bi+8*i+3]; W[2*i+1] = (data[bi+8*i+4]<<24) | (data[bi+8*i+5]<<16) | (data[bi+8*i+6]<<8) | data[bi+8*i+7]; } for (var i = 16; i < 80; i++) { var g0h = rotr64h(W[2*(i-15)], W[2*(i-15)+1], 1) ^ rotr64h(W[2*(i-15)], W[2*(i-15)+1], 8) ^ (W[2*(i-15)] >>> 7); var g0l = rotr64l(W[2*(i-15)], W[2*(i-15)+1], 1) ^ rotr64l(W[2*(i-15)], W[2*(i-15)+1], 8) ^ ((W[2*(i-15)] << 25) | (W[2*(i-15)+1] >>> 7)); var g1h = rotr64h(W[2*(i-2)], W[2*(i-2)+1], 19) ^ rotr64h(W[2*(i-2)], W[2*(i-2)+1], 61) ^ (W[2*(i-2)] >>> 6); var g1l = rotr64l(W[2*(i-2)], W[2*(i-2)+1], 19) ^ rotr64l(W[2*(i-2)], W[2*(i-2)+1], 61) ^ ((W[2*(i-2)] << 26) | (W[2*(i-2)+1] >>> 6)); var xl = (W[2*(i-7)+1] >>> 0) + (g0l >>> 0) + (W[2*(i-16)+1] >>> 0) + (g1l >>> 0); W[2*i+1] = xl >>> 0; W[2*i] = (W[2*(i-7)] + g0h + W[2*(i-16)] + g1h + Math.floor(((W[2*(i-7)+1] >>> 0) + (g0l >>> 0) + (W[2*(i-16)+1] >>> 0) + (g1l >>> 0)) / 0x100000000)) >>> 0; } var ah=H[0][0],al=H[0][1],bh=H[1][0],bl2=H[1][1],ch=H[2][0],cl=H[2][1], dh=H[3][0],dl=H[3][1],eh=H[4][0],el=H[4][1],fh=H[5][0],fl=H[5][1], gh=H[6][0],gl=H[6][1],hh=H[7][0],hl=H[7][1]; for (var i = 0; i < 80; i++) { var s1h = rotr64h(eh,el,14) ^ rotr64h(eh,el,18) ^ rotr64h(eh,el,41); var s1l = rotr64l(eh,el,14) ^ rotr64l(eh,el,18) ^ rotr64l(eh,el,41); var chh = (eh & fh) ^ (~eh & gh); var chl = (el & fl) ^ (~el & gl); var t1l = (hl>>>0)+(s1l>>>0)+(chl>>>0)+(K512[i][1]>>>0)+(W[2*i+1]>>>0); var t1h = (hh+s1h+chh+K512[i][0]+W[2*i] + Math.floor(t1l/0x100000000))>>>0; t1l = t1l>>>0; var s0h = rotr64h(ah,al,28) ^ rotr64h(ah,al,34) ^ rotr64h(ah,al,39); var s0l = rotr64l(ah,al,28) ^ rotr64l(ah,al,34) ^ rotr64l(ah,al,39); var majh = (ah&bh)^(ah&ch)^(bh&ch); var majl = (al&bl2)^(al&cl)^(bl2&cl); var t2l = (s0l>>>0)+(majl>>>0); var t2h = (s0h+majh+Math.floor(((s0l>>>0)+(majl>>>0))/0x100000000))>>>0; hh=gh; hl=gl; gh=fh; gl=fl; fh=eh; fl=el; var newel = (dl>>>0)+(t1l>>>0); eh=(dh+t1h+Math.floor(((dl>>>0)+(t1l>>>0))/0x100000000))>>>0; el=newel>>>0; dh=ch; dl=cl; ch=bh; cl=bl2; bh=ah; bl2=al; var newAl=(t1l>>>0)+(t2l>>>0); ah=(t1h+t2h+Math.floor(((t1l>>>0)+(t2l>>>0))/0x100000000))>>>0; al=newAl>>>0; } function addH(H, i, h, l) { var nl = (H[i][1]>>>0)+(l>>>0); H[i][1]=(nl>>>0); H[i][0]=(H[i][0]+h+Math.floor(nl/0x100000000))>>>0; } addH(H,0,ah,al); addH(H,1,bh,bl2); addH(H,2,ch,cl); addH(H,3,dh,dl); addH(H,4,eh,el); addH(H,5,fh,fl); addH(H,6,gh,gl); addH(H,7,hh,hl); } var out = new Uint8Array(64); for (var i = 0; i < 8; i++) { out[8*i] = (H[i][0] >>> 24) & 0xff; out[8*i+1] = (H[i][0] >>> 16) & 0xff; out[8*i+2] = (H[i][0] >>> 8) & 0xff; out[8*i+3] = H[i][0] & 0xff; out[8*i+4] = (H[i][1] >>> 24) & 0xff; out[8*i+5] = (H[i][1] >>> 16) & 0xff; out[8*i+6] = (H[i][1] >>> 8) & 0xff; out[8*i+7] = H[i][1] & 0xff; } return out; } // --- Ed25519 scalar multiplication on base point --- // Derives 32-byte compressed public key from 32-byte seed. // Per Ed25519 spec (RFC 8032): // 1. h = SHA-512(seed) → 64 bytes // 2. scalar s = h[0..31] with clamping // 3. A = s * B (base point scalar mult) // 4. Compress A → 32 bytes function scalarMult(s) { // s: 32-byte Uint8Array (clamped scalar) // returns 4 x gf elements: [x, y, z, t] var p = [gf(), gf(), gf(), gf()], q = [gf(), gf(), gf(), gf()]; set25519(q[0], X); set25519(q[1], Y); set25519(q[2], gf1); M(q[3], X, Y); for (var i = 255; i >= 0; --i) { var b = (s[(i / 8) | 0] >> (i & 7)) & 1; add(p, q); add(q, p); add(p, q); sel25519(p[0], q[0], b); sel25519(p[1], q[1], b); sel25519(p[2], q[2], b); sel25519(p[3], q[3], b); add(q, p); add(p, q); sel25519(p[0], q[0], b); sel25519(p[1], q[1], b); sel25519(p[2], q[2], b); sel25519(p[3], q[3], b); } // Actually implement the twisted Edwards addition formula // This is the standard nacl scalarmult approach return p; } function add(p, q) { var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf(), g = gf(), h = gf(), t = gf(); Z(a, p[1], p[0]); Z(t, q[1], q[0]); M(a, a, t); A(b, p[0], p[1]); A(t, q[0], q[1]); M(b, b, t); M(c, p[3], q[3]); M(c, c, D2); M(d, p[2], q[2]); A(d, d, d); Z(e, b, a); Z(f, d, c); A(g, d, c); A(h, b, a); M(p[0], e, f); M(p[1], h, g); M(p[2], g, f); M(p[3], e, h); } function encodePoint(p, out) { var tx = gf(), ty = gf(), zi = gf(); inv25519(zi, p[2]); M(tx, p[0], zi); M(ty, p[1], zi); pack25519(out, ty); out[31] ^= par25519(tx) << 7; } function inv25519(o, i) { var c = gf(); for (var a = 0; a < 16; a++) c[a] = i[a]; for (var a = 253; a >= 0; a--) { S(c, c); if (a !== 2 && a !== 4) M(c, c, i); } for (var a = 0; a < 16; a++) o[a] = c[a]; } function derivePublicKey(seed32) { // seed32: Uint8Array(32) — the mini-secret / raw entropy var h = sha512(seed32); // Clamp per Ed25519 RFC 8032 h[0] &= 248; h[31] &= 127; h[31] |= 64; // scalar = h[0..31] var scalar = h.slice(0, 32); // Base point var p = [gf(), gf(), gf(), gf()]; set25519(p[0], X); set25519(p[1], Y); set25519(p[2], gf1); M(p[3], X, Y); // Scalar multiplication var q = [gf(), gf(), gf(), gf()]; // Initialize q to identity set25519(q[0], gf0); set25519(q[1], gf1); set25519(q[2], gf1); set25519(q[3], gf0); for (var i = 255; i >= 0; --i) { var b = (scalar[(i / 8) | 0] >> (i & 7)) & 1; _cswap(p, q, b); add(q, p); add(p, p); _cswap(p, q, b); } var pk = new Uint8Array(32); encodePoint(q, pk); return pk; } function _cswap(p, q, b) { for (var i = 0; i < 4; i++) sel25519(p[i], q[i], b); } return { derivePublicKey: derivePublicKey }; })(); // ------------------------------------------------------------------------- // SS58 ENCODING — Ğ1 network prefix // Ğ1 uses 2-byte prefix 0x5891 (14-bit extended SS58, network ID 4129) // Format: prefix_bytes(2) + pubkey(32) + checksum(2) = 36 bytes total // Checksum = first 2 bytes of Blake2b-512("SS58PRE" + prefix_bytes + pubkey) // We delegate Blake2b to a synchronous implementation. // Base58 alphabet: Bitcoin alphabet (same as SS58). // ------------------------------------------------------------------------- var _BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; function _base58Encode(bytes) { // Count leading zeros var zeros = 0; for (var i = 0; i < bytes.length && bytes[i] === 0; i++) zeros++; // Convert to big integer var digits = [0]; for (var i = 0; i < bytes.length; i++) { var carry = bytes[i]; for (var j = 0; j < digits.length; j++) { carry += digits[j] << 8; digits[j] = carry % 58; carry = Math.floor(carry / 58); } while (carry > 0) { digits.push(carry % 58); carry = Math.floor(carry / 58); } } var str = ''; for (var i = 0; i < zeros; i++) str += '1'; for (var i = digits.length - 1; i >= 0; i--) str += _BASE58_ALPHABET[digits[i]]; return str; } // Blake2b-512 for SS58 checksum. // We need a synchronous Blake2b-512. The vendored Blake2b.php is server-side. // For the client side we implement a minimal Blake2b-512 in JS. // This is only used for the checksum (not for key derivation) so // correctness is verifiable against known SS58 test vectors. var _blake2b = (function () { // Blake2b implementation — minimal, synchronous, pure JS // Based on the reference implementation (CC0 / public domain) var BLAKE2B_IV32 = new Uint32Array([ 0xF3BCC908, 0x6A09E667, 0x84CAA73B, 0xBB67AE85, 0xFE94F82B, 0x3C6EF372, 0x5F1D36F1, 0xA54FF53A, 0xADE682D1, 0x510E527F, 0x2B3E6C1F, 0x9B05688C, 0xFB41BD6B, 0x1F83D9AB, 0x137E2179, 0x5BE0CD19 ]); var SIGMA8 = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3, 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4, 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8, 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13, 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9, 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11, 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10, 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5, 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 ]; var v = new Uint32Array(32), m = new Uint32Array(32); function B2B_GET32(arr, i) { return (arr[i] ^ (arr[i + 1] << 8) ^ (arr[i + 2] << 16) ^ (arr[i + 3] << 24)) >>> 0; } function B2B_G(a, b, c, d, ix, iy) { var x0 = m[ix * 2], x1 = m[ix * 2 + 1], y0 = m[iy * 2], y1 = m[iy * 2 + 1]; var va0 = v[a * 2], va1 = v[a * 2 + 1]; var vb0 = v[b * 2], vb1 = v[b * 2 + 1]; var vc0 = v[c * 2], vc1 = v[c * 2 + 1]; var vd0 = v[d * 2], vd1 = v[d * 2 + 1]; var w0, w1, xor0, xor1, r0, r1; // a = a + b + x w0 = (va0 + vb0 + x0) >>> 0; w1 = (va1 + vb1 + x1 + Math.floor(((va0>>>0)+(vb0>>>0)+(x0>>>0))/0x100000000)) >>> 0; va0 = w0; va1 = w1; // d = (d XOR a) rotateright 32 xor0 = vd0 ^ va0; xor1 = vd1 ^ va1; vd0 = xor1; vd1 = xor0; // c = c + d w0 = (vc0 + vd0) >>> 0; w1 = (vc1 + vd1 + Math.floor(((vc0>>>0)+(vd0>>>0))/0x100000000)) >>> 0; vc0 = w0; vc1 = w1; // b = (b XOR c) rotateright 24 xor0 = vb0 ^ vc0; xor1 = vb1 ^ vc1; vb0 = (xor0 >>> 24) | (xor1 << 8); vb1 = (xor1 >>> 24) | (xor0 << 8); vb0 >>>= 0; vb1 >>>= 0; // a = a + b + y w0 = (va0 + vb0 + y0) >>> 0; w1 = (va1 + vb1 + y1 + Math.floor(((va0>>>0)+(vb0>>>0)+(y0>>>0))/0x100000000)) >>> 0; va0 = w0; va1 = w1; // d = (d XOR a) rotateright 16 xor0 = vd0 ^ va0; xor1 = vd1 ^ va1; vd0 = (xor0 >>> 16) | (xor1 << 16); vd1 = (xor1 >>> 16) | (xor0 << 16); vd0 >>>= 0; vd1 >>>= 0; // c = c + d w0 = (vc0 + vd0) >>> 0; w1 = (vc1 + vd1 + Math.floor(((vc0>>>0)+(vd0>>>0))/0x100000000)) >>> 0; vc0 = w0; vc1 = w1; // b = (b XOR c) rotateright 63 xor0 = vb0 ^ vc0; xor1 = vb1 ^ vc1; vb0 = (xor1 >>> 31) | (xor0 << 1); vb1 = (xor0 >>> 31) | (xor1 << 1); vb0 >>>= 0; vb1 >>>= 0; v[a * 2] = va0; v[a * 2 + 1] = va1; v[b * 2] = vb0; v[b * 2 + 1] = vb1; v[c * 2] = vc0; v[c * 2 + 1] = vc1; v[d * 2] = vd0; v[d * 2 + 1] = vd1; } function blake2b(input, outlen) { // input: Uint8Array, outlen: number of bytes (e.g. 64 for Blake2b-512) outlen = outlen || 64; var h = new Uint32Array(16), cbLo = 0, cbHi = 0; // Initialize state for (var i = 0; i < 16; i++) h[i] = BLAKE2B_IV32[i]; h[0] ^= 0x01010000 ^ outlen; var out = new Uint8Array(outlen); var buf = new Uint8Array(128); var buflen = 0; var inoff = 0; var last = false; function compress(last) { for (var i = 0; i < 16; i++) v[i] = h[i]; v[16] = BLAKE2B_IV32[0]; v[17] = BLAKE2B_IV32[1]; v[18] = BLAKE2B_IV32[2]; v[19] = BLAKE2B_IV32[3]; v[20] = BLAKE2B_IV32[4]; v[21] = BLAKE2B_IV32[5]; v[22] = BLAKE2B_IV32[6]; v[23] = BLAKE2B_IV32[7]; v[24] = BLAKE2B_IV32[8]; v[25] = BLAKE2B_IV32[9]; v[26] = BLAKE2B_IV32[10]; v[27] = BLAKE2B_IV32[11]; v[28] = BLAKE2B_IV32[12]; v[29] = BLAKE2B_IV32[13]; v[30] = BLAKE2B_IV32[14]; v[31] = BLAKE2B_IV32[15]; v[24] ^= cbLo; v[25] ^= cbHi; if (last) { v[28] = ~v[28]; v[29] = ~v[29]; } for (var i = 0; i < 32; i++) m[i] = B2B_GET32(buf, i * 4); for (var r = 0; r < 12; r++) { B2B_G(0, 4, 8, 12, SIGMA8[r * 16 + 0], SIGMA8[r * 16 + 1]); B2B_G(1, 5, 9, 13, SIGMA8[r * 16 + 2], SIGMA8[r * 16 + 3]); B2B_G(2, 6, 10, 14, SIGMA8[r * 16 + 4], SIGMA8[r * 16 + 5]); B2B_G(3, 7, 11, 15, SIGMA8[r * 16 + 6], SIGMA8[r * 16 + 7]); B2B_G(0, 5, 10, 15, SIGMA8[r * 16 + 8], SIGMA8[r * 16 + 9]); B2B_G(1, 6, 11, 12, SIGMA8[r * 16 + 10], SIGMA8[r * 16 + 11]); B2B_G(2, 7, 8, 13, SIGMA8[r * 16 + 12], SIGMA8[r * 16 + 13]); B2B_G(3, 4, 9, 14, SIGMA8[r * 16 + 14], SIGMA8[r * 16 + 15]); } for (var i = 0; i < 16; i++) h[i] ^= v[i] ^ v[i + 16]; } function update(inp) { for (var i = 0; i < inp.length; i++) { if (buflen === 128) { cbLo = (cbLo + 128) >>> 0; if (cbLo < 128) cbHi++; compress(false); buflen = 0; } buf[buflen++] = inp[i]; } } update(input); // Final block cbLo = (cbLo + buflen) >>> 0; if (cbLo < buflen) cbHi++; while (buflen < 128) buf[buflen++] = 0; compress(true); for (var i = 0; i < outlen; i++) { out[i] = h[Math.floor(i / 4) * 2 + (i % 4 < 2 ? (i % 4 === 0 ? 0 : 1) : (i % 4 === 2 ? 0 : 1))]; // fix: read bytes correctly } // Correct byte extraction for (var i = 0; i < outlen; i++) { var hi = Math.floor(i / 4); var lo = i % 4; out[i] = (h[hi * 2 + (lo < 2 ? 0 : 0)] >> (8 * (lo % 4))) & 0xff; } // Actually: h is Uint32Array in little-endian pairs // Each 8 bytes of output = two 32-bit words (lo word first, then hi word) // lo word at h[i*2], hi word at h[i*2+1] for (var i = 0; i < outlen; i++) { var word_idx = Math.floor(i / 4); var byte_pos = i % 4; // h[word_idx] stores the word; it's little-endian so byte 0 is LSB out[i] = (h[word_idx] >>> (8 * byte_pos)) & 0xff; } return out; } return { hash: blake2b }; })(); function _ss58Encode(pubkeyBytes) { // pubkeyBytes: Uint8Array(32) // Ğ1 network prefix: 2 bytes, little-endian encoding of 14-bit prefix // prefix value 4129 (0x1021 in the 14-bit space) // SS58 2-byte prefix encoding: split 14 bits as: // byte0 = ((prefix & 0x3f) << 2) | 0x40 // byte1 = (prefix >> 6) & 0xff // For Ğ1: prefix = 36 (single-byte SS58 format... wait) // Let's verify: Ğ1 addresses start with "g1" in base58. // From DUNITER-RPC-FINDINGS: 2-byte prefix 0x5891 // 0x5891 = [0x91, 0x58] in little-endian var prefixBytes = new Uint8Array([0x91, 0x58]); // Payload = prefixBytes + pubkeyBytes (34 bytes total) var payload = new Uint8Array(34); payload.set(prefixBytes, 0); payload.set(pubkeyBytes, 2); // Checksum input = "SS58PRE" + payload var ss58pre = new Uint8Array([0x53, 0x53, 0x35, 0x38, 0x50, 0x52, 0x45]); // "SS58PRE" var checksumInput = new Uint8Array(ss58pre.length + payload.length); checksumInput.set(ss58pre, 0); checksumInput.set(payload, ss58pre.length); var hash = _blake2b.hash(checksumInput, 64); var checksum = hash.slice(0, 2); // Full = prefixBytes + pubkeyBytes + checksum = 36 bytes var full = new Uint8Array(36); full.set(prefixBytes, 0); full.set(pubkeyBytes, 2); full.set(checksum, 34); return _base58Encode(full); } function _hexToBytes(hex) { var bytes = new Uint8Array(hex.length / 2); for (var i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16); return bytes; } function _bytesToHex(bytes) { var hex = ''; for (var i = 0; i < bytes.length; i++) hex += ('0' + bytes[i].toString(16)).slice(-2); return hex; } // ------------------------------------------------------------------------- // KEY DERIVATION PIPELINE // mnemonic → entropy (hex) → seed (Uint8Array 32) → pubkey (Uint8Array 32) // → SS58 address string // → SubtleCrypto CryptoKey (non-extractable, for signing) // ------------------------------------------------------------------------- function _deriveKeypair(mnemonic) { // Returns a Promise resolving to { pubkeyAddress, pubkeyHex, cryptoKey } var entropyHex = window.bip39.mnemonicToEntropy(mnemonic); var seed = _hexToBytes(entropyHex); // 16 bytes for 12-word mnemonic // Pad seed to 32 bytes (Substrate uses 32-byte mini-secret; // for 12-word BIP39, entropy is 16 bytes — pad with zeros to 32) // Per Substrate source (sr_core/src/hex_serde.rs and schnorrkel): // the 16-byte entropy IS used as-is as the 32-byte key if < 32 bytes, // with zero-padding on the RIGHT. var seed32 = new Uint8Array(32); seed32.set(seed, 0); // left-aligned, zero-padded right // Derive Ed25519 public key var pubkeyBytes; try { pubkeyBytes = _ed25519.derivePublicKey(seed32); } catch (e) { return Promise.reject(new Error('Key derivation failed: ' + e.message)); } var pubkeyHex = _bytesToHex(pubkeyBytes); var pubkeyAddress = _ss58Encode(pubkeyBytes); // Import the seed as a non-extractable Ed25519 signing key via PKCS8 // PKCS8 DER wrapper for Ed25519 (RFC 5958): // 30 2e SEQUENCE (46 bytes) // 02 01 00 INTEGER 0 (version) // 30 05 SEQUENCE (5 bytes) // 06 03 2b 65 70 OID 1.3.101.112 (Ed25519) // 04 22 OCTET STRING (34 bytes) // 04 20 OCTET STRING (32 bytes) — the key // [32 bytes of seed] var pkcs8 = new Uint8Array([ 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20 ].concat(Array.from(seed32))); return crypto.subtle.importKey( 'pkcs8', pkcs8.buffer, { name: 'Ed25519' }, false, // non-extractable ['sign'] ).then(function (cryptoKey) { return { pubkeyAddress: pubkeyAddress, pubkeyHex: pubkeyHex, cryptoKey: cryptoKey }; }); } // ------------------------------------------------------------------------- // PUBLIC API // Exposed on window.g1wallet for other addons to use. // ------------------------------------------------------------------------- window.g1wallet = { /** * Returns true if the wallet is currently unlocked. */ isUnlocked: function () { return _session.unlocked; }, /** * Returns the current session SS58 address (g1...), or null if locked. */ getPubkey: function () { return _session.unlocked ? _session.pubkey : null; }, /** * Returns the current session pubkey hex (32 bytes), or null if locked. */ getPubkeyHex: function () { return _session.unlocked ? _session.pubkeyHex : null; }, /** * Requests a signature from the wallet for a given document. * If the wallet is locked, dispatches 'g1wallet:sign_request_blocked'. * If unlocked, signs via SubtleCrypto and dispatches the callback event. * * @param {string|Uint8Array} document - The bytes to sign * @param {string} callback - Event name to dispatch with result */ requestSignature: function (document, callback) { if (!_session.unlocked || !_session._cryptoKey) { window.dispatchEvent(new CustomEvent('g1wallet:sign_request_blocked', { detail: { reason: 'Wallet is locked. Please unlock your wallet first.' } })); return; } var data = (typeof document === 'string') ? new TextEncoder().encode(document) : document; crypto.subtle.sign('Ed25519', _session._cryptoKey, data) .then(function (sigBuf) { var sigBytes = new Uint8Array(sigBuf); var sigHex = _bytesToHex(sigBytes); window.dispatchEvent(new CustomEvent(callback, { detail: { signature: sigHex, pubkey: _session.pubkey, pubkeyHex: _session.pubkeyHex } })); }) .catch(function (err) { window.dispatchEvent(new CustomEvent(callback, { detail: { error: err.message } })); }); } }; // ------------------------------------------------------------------------- // WIDGET API // Used by Widget/G1wallet.php inline script to register widget instances. // ------------------------------------------------------------------------- window.G1WalletWidget = { init: function (uid, walletUrl) { window.addEventListener('g1wallet:unlocked', function (e) { var statusEl = document.getElementById(uid + '-status'); if (!statusEl) return; var pubkey = e.detail.pubkey || ''; var short = pubkey.substring(0, 12) + '…'; statusEl.innerHTML = '
' + '🔓 ' + 'Unlocked' + '
' + _escHtml(short) + '
'; }); window.addEventListener('g1wallet:locked', function () { var statusEl = document.getElementById(uid + '-status'); if (!statusEl) return; statusEl.innerHTML = '
' + '🔒 ' + 'Locked' + '
Unlock
' + '
'; }); } }; // ------------------------------------------------------------------------- // UNLOCK BUTTON — wired on the wallet landing page // ------------------------------------------------------------------------- document.addEventListener('DOMContentLoaded', function () { var unlockBtn = document.getElementById('g1wallet-unlock-btn'); var lockedView = document.getElementById('g1wallet-locked-view'); var unlockedView = document.getElementById('g1wallet-unlocked-view'); var spinner = document.getElementById('g1wallet-unlock-spinner'); var lockBtn = document.getElementById('g1wallet-lock-btn'); if (!unlockBtn) return; // Not on the wallet page. unlockBtn.addEventListener('click', function () { var mnemonicEl = document.getElementById('g1wallet-mnemonic'); var mnemonic = (mnemonicEl || {}).value || ''; // Normalize whitespace mnemonic = mnemonic.trim().replace(/\s+/g, ' ').toLowerCase(); _clearError(); if (!mnemonic) { _showError('Mnemonic phrase is required.'); return; } var words = mnemonic.split(' '); if (words.length !== 12) { _showError('Mnemonic phrase must be exactly 12 words. You entered ' + words.length + '.'); return; } if (!window.bip39 || typeof window.bip39.validateMnemonic !== 'function') { _showError('Mnemonic validation library not available. Please reload the page.'); return; } if (!window.bip39.validateMnemonic(mnemonic)) { _showError('Invalid mnemonic phrase. Check the words and try again.'); return; } // Clear the mnemonic field immediately after validation — // it must not persist in the DOM any longer than necessary. if (mnemonicEl) mnemonicEl.value = ''; if (spinner) spinner.style.display = 'inline'; unlockBtn.disabled = true; _deriveKeypair(mnemonic).then(function (result) { if (spinner) spinner.style.display = 'none'; unlockBtn.disabled = false; _unlockWallet(result.pubkeyAddress, result.pubkeyHex, result.cryptoKey); }).catch(function (err) { if (spinner) spinner.style.display = 'none'; unlockBtn.disabled = false; _showError('Key derivation failed: ' + err.message); }); }); if (lockBtn) { lockBtn.addEventListener('click', function () { _lockWallet(); if (lockedView) lockedView.style.display = ''; if (unlockedView) unlockedView.style.display = 'none'; }); } window.addEventListener('g1wallet:unlocked', function (e) { if (lockedView) lockedView.style.display = 'none'; if (unlockedView) unlockedView.style.display = ''; var pubkeyDisplay = document.getElementById('g1wallet-pubkey-display'); if (pubkeyDisplay) pubkeyDisplay.textContent = e.detail.pubkey || '—'; // POST pubkey to server for storage in channel settings. // Only fires once after derivation; server deduplicates. _postPubkeyToServer(e.detail.pubkey); }); }); // ------------------------------------------------------------------------- // PUBKEY POST — stores pubkey in channel settings via fetch() // Called once after each successful wallet unlock. // ------------------------------------------------------------------------- function _postPubkeyToServer(pubkey) { var form = document.getElementById('g1wallet-pubkey-form'); if (!form) return; var csrfInput = form.querySelector('input[name="g1wallet_csrf"]'); var pubkeyInput = document.getElementById('g1wallet-pubkey-input'); if (!csrfInput || !pubkeyInput) return; pubkeyInput.value = pubkey; var formData = new FormData(); formData.append('g1wallet_csrf', csrfInput.value); formData.append('g1_pubkey', pubkey); fetch('/g1wallet/pubkey', { method: 'POST', body: formData, credentials: 'same-origin' }).then(function (response) { return response.json(); }).then(function (data) { if (data.status !== 'ok') { console.warn('[g1wallet] pubkey store: server returned status=' + data.status); } }).catch(function (err) { // Non-fatal: pubkey storage failure does not block wallet use. console.warn('[g1wallet] pubkey store fetch error:', err.message); }); } // ------------------------------------------------------------------------- // INTERNAL HELPERS // ------------------------------------------------------------------------- function _unlockWallet(pubkeyAddress, pubkeyHex, cryptoKey) { _session.unlocked = true; _session.pubkey = pubkeyAddress; _session.pubkeyHex = pubkeyHex; _session._cryptoKey = cryptoKey; window.dispatchEvent(new CustomEvent('g1wallet:unlocked', { detail: { pubkey: pubkeyAddress, pubkeyHex: pubkeyHex } })); } function _lockWallet() { _session.unlocked = false; _session.pubkey = null; _session.pubkeyHex = null; _session._cryptoKey = null; window.dispatchEvent(new CustomEvent('g1wallet:locked')); } function _showError(message) { var el = document.getElementById('g1wallet-unlock-error'); if (el) { el.textContent = message; el.style.display = ''; } } function _clearError() { var el = document.getElementById('g1wallet-unlock-error'); if (el) { el.textContent = ''; el.style.display = 'none'; } } function _escHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } // ------------------------------------------------------------------------- // AUTO-POPULATE: cry01 and poll01 form fields // Elements with data-g1wallet-target="pubkey" are populated on unlock. // ------------------------------------------------------------------------- window.addEventListener('g1wallet:unlocked', function (e) { var pubkey = e.detail.pubkey || ''; var targets = document.querySelectorAll('[data-g1wallet-target="pubkey"]'); targets.forEach(function (el) { el.value = pubkey; }); }); window.addEventListener('g1wallet:locked', function () { var targets = document.querySelectorAll('[data-g1wallet-target="pubkey"]'); targets.forEach(function (el) { el.value = ''; }); }); }());