From e3128639df36532da4a631a50ec9d4d236e84ed2 Mon Sep 17 00:00:00 2001 From: TheRON Date: Sun, 14 Jun 2026 07:07:43 -0400 Subject: [PATCH] Updated --- hubzilla/addon/g1wallet/view/css/g1wallet.js | 442 +++++++++++++++++++ 1 file changed, 442 insertions(+) create mode 100644 hubzilla/addon/g1wallet/view/css/g1wallet.js diff --git a/hubzilla/addon/g1wallet/view/css/g1wallet.js b/hubzilla/addon/g1wallet/view/css/g1wallet.js new file mode 100644 index 0000000..c49ce91 --- /dev/null +++ b/hubzilla/addon/g1wallet/view/css/g1wallet.js @@ -0,0 +1,442 @@ +/** + * g1wallet.js — Ğ1 Wallet client-side implementation. + * + * DERIVATION (Substrate / Duniter v2 / Ğecko standard): + * 12-word BIP39 mnemonic + * → mnemonicToEntropy() → 16 bytes of raw entropy (hex) + * → zero-pad right to 32 bytes → Ed25519 seed (the Substrate "mini-secret") + * → nacl.sign.keyPair.fromSeed(seed) → { publicKey: Uint8Array(32) } + * → SS58-encode publicKey with Ğ1 prefix bytes [0x58, 0x91] → g1... address + * + * Signing uses SubtleCrypto (Ed25519, non-extractable CryptoKey). + * Key derivation uses TweetNaCl (nacl-fast.min.js, vendored, pinned v1.0.3). + * + * CRITICAL RULES: + * - 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 — module-scope memory only, gone on page close + // ------------------------------------------------------------------------- + + var _session = { + unlocked: false, + pubkey: null, // SS58 g1... address + pubkeyHex: null, // 32-byte pubkey as hex + _cryptoKey: null // SubtleCrypto CryptoKey (non-extractable Ed25519) + }; + + // ------------------------------------------------------------------------- + // SS58 ENCODING + // Ğ1 prefix bytes: [0x58, 0x91] (verified against live mainnet address) + // Format: prefix(2) + pubkey(32) + checksum(2) = 36 bytes, base58-encoded + // Checksum: first 2 bytes of Blake2b-512("SS58PRE" + prefix + pubkey) + // ------------------------------------------------------------------------- + + var _BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + + function _base58Encode(bytes) { + var zeros = 0; + for (var i = 0; i < bytes.length && bytes[i] === 0; i++) zeros++; + 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 — minimal synchronous pure-JS implementation. + // Used only for SS58 checksum (2 bytes of a 64-byte hash). + var _blake2b512 = (function () { + var IV = new Uint32Array([ + 0xF3BCC908, 0x6A09E667, 0x84CAA73B, 0xBB67AE85, + 0xFE94F82B, 0x3C6EF372, 0x5F1D36F1, 0xA54FF53A, + 0xADE682D1, 0x510E527F, 0x2B3E6C1F, 0x9B05688C, + 0xFB41BD6B, 0x1F83D9AB, 0x137E2179, 0x5BE0CD19 + ]); + var SIGMA = [ + 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 + ]; + + function G(v, a, b, c, d, x0, x1, y0, y1) { + // 64-bit add via 32-bit pairs (hi, lo) + function add64(ah, al, bh, bl) { + var lo = (al >>> 0) + (bl >>> 0); + return { h: (ah + bh + (lo / 0x100000000 | 0)) >>> 0, l: lo >>> 0 }; + } + function rotr64(h, l, r) { + if (r < 32) return { h: (h >>> r) | (l << (32-r)), l: (l >>> r) | (h << (32-r)) }; + return { h: (l >>> (r-32)) | (h << (64-r)), l: (h >>> (r-32)) | (l << (64-r)) }; + } + var va = {h:v[a*2],l:v[a*2+1]}, vb = {h:v[b*2],l:v[b*2+1]}; + var vc = {h:v[c*2],l:v[c*2+1]}, vd = {h:v[d*2],l:v[d*2+1]}; + var mx = {h:x0,l:x1}, my = {h:y0,l:y1}; + va = add64(va.h,va.l, vb.h,vb.l); va = add64(va.h,va.l, mx.h,mx.l); + vd = rotr64(vd.h^va.h, vd.l^va.l, 32); + vc = add64(vc.h,vc.l, vd.h,vd.l); + vb = rotr64(vb.h^vc.h, vb.l^vc.l, 24); + va = add64(va.h,va.l, vb.h,vb.l); va = add64(va.h,va.l, my.h,my.l); + vd = rotr64(vd.h^va.h, vd.l^va.l, 16); + vc = add64(vc.h,vc.l, vd.h,vd.l); + vb = rotr64(vb.h^vc.h, vb.l^vc.l, 63); + v[a*2]=va.h; v[a*2+1]=va.l; v[b*2]=vb.h; v[b*2+1]=vb.l; + v[c*2]=vc.h; v[c*2+1]=vc.l; v[d*2]=vd.h; v[d*2+1]=vd.l; + } + + function compress(h, m, t, last) { + var v = new Uint32Array(32); + for (var i = 0; i < 16; i++) v[i] = h[i]; + for (var i = 0; i < 16; i++) v[16+i] = IV[i]; + v[24] ^= t & 0xffffffff; v[25] ^= Math.floor(t / 0x100000000); + if (last) { v[28] = ~v[28]; v[29] = ~v[29]; } + for (var r = 0; r < 12; r++) { + var s = SIGMA.slice(r*16, r*16+16); + G(v,0,4,8,12, m[s[0]*2],m[s[0]*2+1], m[s[1]*2],m[s[1]*2+1]); + G(v,1,5,9,13, m[s[2]*2],m[s[2]*2+1], m[s[3]*2],m[s[3]*2+1]); + G(v,2,6,10,14, m[s[4]*2],m[s[4]*2+1], m[s[5]*2],m[s[5]*2+1]); + G(v,3,7,11,15, m[s[6]*2],m[s[6]*2+1], m[s[7]*2],m[s[7]*2+1]); + G(v,0,5,10,15, m[s[8]*2],m[s[8]*2+1], m[s[9]*2],m[s[9]*2+1]); + G(v,1,6,11,12, m[s[10]*2],m[s[10]*2+1], m[s[11]*2],m[s[11]*2+1]); + G(v,2,7,8,13, m[s[12]*2],m[s[12]*2+1], m[s[13]*2],m[s[13]*2+1]); + G(v,3,4,9,14, m[s[14]*2],m[s[14]*2+1], m[s[15]*2],m[s[15]*2+1]); + } + for (var i = 0; i < 16; i++) h[i] ^= v[i] ^ v[16+i]; + } + + function hash(input) { + var h = new Uint32Array(16); + for (var i = 0; i < 16; i++) h[i] = IV[i]; + h[0] ^= 0x01010040; // fanout=1, depth=1, outlen=64 + + var buf = new Uint8Array(128), buflen = 0, total = 0; + + function update(data) { + for (var i = 0; i < data.length; i++) { + if (buflen === 128) { + total += 128; + var m = new Uint32Array(32); + for (var j = 0; j < 32; j++) + m[j] = buf[j*4] | (buf[j*4+1]<<8) | (buf[j*4+2]<<16) | (buf[j*4+3]<<24); + compress(h, m, total, false); + buflen = 0; + } + buf[buflen++] = data[i]; + } + } + + update(input); + total += buflen; + while (buflen < 128) buf[buflen++] = 0; + var m = new Uint32Array(32); + for (var j = 0; j < 32; j++) + m[j] = buf[j*4] | (buf[j*4+1]<<8) | (buf[j*4+2]<<16) | (buf[j*4+3]<<24); + compress(h, m, total, true); + + var out = new Uint8Array(64); + for (var i = 0; i < 16; i++) { + out[i*4] = h[i] & 0xff; + out[i*4+1] = (h[i] >>> 8) & 0xff; + out[i*4+2] = (h[i] >>> 16)& 0xff; + out[i*4+3] = (h[i] >>> 24)& 0xff; + } + return out; + } + + return { hash: hash }; + })(); + + function _ss58Encode(pubkeyBytes) { + // Ğ1 prefix bytes [0x58, 0x91] — verified against live mainnet. + var prefixBytes = new Uint8Array([0x58, 0x91]); + var payload = new Uint8Array(34); + payload.set(prefixBytes, 0); + payload.set(pubkeyBytes, 2); + + var ss58pre = new TextEncoder().encode('SS58PRE'); + var checksumInput = new Uint8Array(ss58pre.length + payload.length); + checksumInput.set(ss58pre, 0); + checksumInput.set(payload, ss58pre.length); + + var hashBytes = _blake2b512.hash(checksumInput); + var full = new Uint8Array(36); + full.set(prefixBytes, 0); + full.set(pubkeyBytes, 2); + full.set(hashBytes.slice(0, 2), 34); + return _base58Encode(full); + } + + function _hexToBytes(hex) { + var b = new Uint8Array(hex.length / 2); + for (var i = 0; i < hex.length; i += 2) b[i/2] = parseInt(hex.slice(i,i+2), 16); + return b; + } + + function _bytesToHex(bytes) { + var s = ''; + for (var i = 0; i < bytes.length; i++) s += ('0' + bytes[i].toString(16)).slice(-2); + return s; + } + + // ------------------------------------------------------------------------- + // KEY DERIVATION + // Uses TweetNaCl (nacl-fast.min.js) for Ed25519 pubkey derivation. + // nacl.sign.keyPair.fromSeed(seed) is the correct primitive: + // seed (32 bytes) → { publicKey: Uint8Array(32), secretKey: Uint8Array(64) } + // The secretKey is discarded — signing uses SubtleCrypto with a + // non-extractable CryptoKey imported from the same seed. + // ------------------------------------------------------------------------- + + function _deriveKeypair(mnemonic) { + if (!window.nacl || typeof window.nacl.sign === 'undefined') { + return Promise.reject(new Error('TweetNaCl library not loaded. Please reload the page.')); + } + + var entropyHex = window.bip39.mnemonicToEntropy(mnemonic); + var entropy = _hexToBytes(entropyHex); // 16 bytes for 12-word mnemonic + + // Substrate mini-secret: entropy zero-padded right to 32 bytes. + var seed32 = new Uint8Array(32); + seed32.set(entropy, 0); + + // Derive Ed25519 pubkey via TweetNaCl. + var keypair = window.nacl.sign.keyPair.fromSeed(seed32); + var pubkeyBytes = keypair.publicKey; // Uint8Array(32) + var pubkeyHex = _bytesToHex(pubkeyBytes); + var pubkeyAddress = _ss58Encode(pubkeyBytes); + + // Import seed as non-extractable Ed25519 CryptoKey for signing. + // PKCS8 DER wrapper for Ed25519 (RFC 5958): + 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, ['sign'] + ).then(function (cryptoKey) { + return { pubkeyAddress: pubkeyAddress, pubkeyHex: pubkeyHex, cryptoKey: cryptoKey }; + }); + } + + // ------------------------------------------------------------------------- + // PUBLIC API + // ------------------------------------------------------------------------- + + window.g1wallet = { + isUnlocked: function () { return _session.unlocked; }, + getPubkey: function () { return _session.unlocked ? _session.pubkey : null; }, + getPubkeyHex:function () { return _session.unlocked ? _session.pubkeyHex : null; }, + + 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) { + window.dispatchEvent(new CustomEvent(callback, { + detail: { signature: _bytesToHex(new Uint8Array(sigBuf)), pubkey: _session.pubkey, pubkeyHex: _session.pubkeyHex } + })); + }) + .catch(function (err) { + window.dispatchEvent(new CustomEvent(callback, { detail: { error: err.message } })); + }); + } + }; + + // ------------------------------------------------------------------------- + // WIDGET API + // ------------------------------------------------------------------------- + + window.G1WalletWidget = { + init: function (uid, walletUrl) { + window.addEventListener('g1wallet:unlocked', function (e) { + var el = document.getElementById(uid + '-status'); + if (!el) return; + var short = (e.detail.pubkey || '').substring(0, 12) + '…'; + el.innerHTML = '
' + + '🔓 ' + + 'Unlocked' + + '
' + _escHtml(short) + '
'; + }); + window.addEventListener('g1wallet:locked', function () { + var el = document.getElementById(uid + '-status'); + if (!el) return; + el.innerHTML = '
' + + '🔒 ' + + 'Locked' + + '
'; + }); + } + }; + + // ------------------------------------------------------------------------- + // UNLOCK BUTTON + // ------------------------------------------------------------------------- + + 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; + + unlockBtn.addEventListener('click', function () { + var mnemonicEl = document.getElementById('g1wallet-mnemonic'); + var mnemonic = (mnemonicEl || {}).value || ''; + 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 mnemonic field immediately after validation. + 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 pd = document.getElementById('g1wallet-pubkey-display'); + if (pd) pd.textContent = e.detail.pubkey || '—'; + _postPubkeyToServer(e.detail.pubkey); + }); + }); + + // ------------------------------------------------------------------------- + // PUBKEY POST + // ------------------------------------------------------------------------- + + 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 fd = new FormData(); + fd.append('g1wallet_csrf', csrfInput.value); + fd.append('g1_pubkey', pubkey); + fetch('/g1wallet/pubkey', { method: 'POST', body: fd, credentials: 'same-origin' }) + .then(function (r) { return r.json(); }) + .then(function (d) { if (d.status !== 'ok') console.warn('[g1wallet] pubkey store:', d.status); }) + .catch(function (e) { console.warn('[g1wallet] pubkey store error:', e.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(msg) { + var el = document.getElementById('g1wallet-unlock-error'); + if (el) { el.textContent = msg; el.style.display = ''; } + } + + function _clearError() { + var el = document.getElementById('g1wallet-unlock-error'); + if (el) { el.textContent = ''; el.style.display = 'none'; } + } + + function _escHtml(s) { + return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); + } + + // Auto-populate data-g1wallet-target="pubkey" fields on other addons. + window.addEventListener('g1wallet:unlocked', function (e) { + document.querySelectorAll('[data-g1wallet-target="pubkey"]') + .forEach(function (el) { el.value = e.detail.pubkey || ''; }); + }); + window.addEventListener('g1wallet:locked', function () { + document.querySelectorAll('[data-g1wallet-target="pubkey"]') + .forEach(function (el) { el.value = ''; }); + }); + +}());