diff --git a/hubzilla/addon/g1wallet/view/js/g1wallet.js b/hubzilla/addon/g1wallet/view/js/g1wallet.js new file mode 100644 index 0000000..cdddc21 --- /dev/null +++ b/hubzilla/addon/g1wallet/view/js/g1wallet.js @@ -0,0 +1,241 @@ +/** + * g1wallet.js — Ğ1 Wallet client-side skeleton. + * + * At this stage: + * - The g1wallet namespace and session object are declared. + * - The unlock button is wired — it intercepts the click but does NOT derive keys yet. + * - The event API stubs are in place for cry01 and poll01 to listen to. + * - scrypt-js is loaded (vendor/scrypt-js-3.0.1.min.js) but not yet called. + * + * CRITICAL RULES (never relax these): + * - The private key NEVER leaves the browser. + * - The pseudo and password NEVER leave the browser. + * - No key material in localStorage, sessionStorage, or cookies. + * - The private key object is marked non-extractable in WebCrypto. + * - One document, one confirmation, one click — no batch signing. + */ + +(function () { + 'use strict'; + + // ------------------------------------------------------------------------ + // SESSION STATE + // Session is held in module-scope memory only. + // It is gone when the page is closed or navigated away from. + // ------------------------------------------------------------------------ + + var _session = { + unlocked: false, + pubkey: null, // base58-encoded Ed25519 public key + // _privkey is intentionally not stored here — it lives only in the + // WebCrypto CryptoKey object (non-extractable) once derivation is implemented. + _cryptoKey: null // CryptoKey (non-extractable) — null until derivation + }; + + // ------------------------------------------------------------------------ + // 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 pubkey (base58 string), or null if locked. + */ + getPubkey: function () { + return _session.unlocked ? _session.pubkey : null; + }, + + /** + * Requests a signature from the wallet for a given document. + * If the wallet is locked, dispatches 'g1wallet:sign_request_blocked' instead. + * Once signing is implemented, dispatches the result via the callback event. + * + * @param {string} document - The document string to sign + * @param {string} callback - Event name to dispatch with the signed 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; + } + // TODO: implement signing via SubtleCrypto.sign() with Ed25519. + // The signed bytes are base64-encoded and dispatched via the callback event. + console.warn('[g1wallet] requestSignature: signing not yet implemented.'); + } + }; + + // ------------------------------------------------------------------------ + // WIDGET API + // Used by Widget/G1wallet.php inline script to register widget instances. + // ------------------------------------------------------------------------ + + window.G1WalletWidget = { + init: function (uid, walletUrl) { + // Listen for session events and update the widget DOM. + 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 errorEl = document.getElementById('g1wallet-unlock-error'); + var lockBtn = document.getElementById('g1wallet-lock-btn'); + + if (!unlockBtn) return; // Not on the wallet page. + + unlockBtn.addEventListener('click', function () { + var pseudo = (document.getElementById('g1wallet-pseudo') || {}).value || ''; + var password = (document.getElementById('g1wallet-password') || {}).value || ''; + + _clearError(); + + if (!pseudo || !password) { + _showError('Both pseudo and password are required.'); + return; + } + + // TODO: call scrypt derivation here. + // scrypt(pseudo, password, N=4096, r=16, p=1) → 32-byte seed → Ed25519 keypair + // For now: log and show a placeholder message. + console.log('[g1wallet] Unlock clicked. Derivation not yet implemented.'); + + if (spinner) spinner.style.display = 'inline'; + unlockBtn.disabled = true; + + // Simulate async derivation (placeholder). + setTimeout(function () { + if (spinner) spinner.style.display = 'none'; + unlockBtn.disabled = false; + _showError('Key derivation not yet implemented. This is a skeleton build.'); + }, 500); + }); + + if (lockBtn) { + lockBtn.addEventListener('click', function () { + _lockWallet(); + if (lockedView) lockedView.style.display = ''; + if (unlockedView) unlockedView.style.display = 'none'; + // Clear credential fields. + var pseudoEl = document.getElementById('g1wallet-pseudo'); + var passwordEl = document.getElementById('g1wallet-password'); + if (pseudoEl) pseudoEl.value = ''; + if (passwordEl) passwordEl.value = ''; + }); + } + + // Listen for wallet:unlocked event (dispatched by _unlockWallet below). + 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 || '—'; + }); + }); + + // ------------------------------------------------------------------------ + // INTERNAL HELPERS + // ------------------------------------------------------------------------ + + function _unlockWallet(pubkey, cryptoKey) { + _session.unlocked = true; + _session.pubkey = pubkey; + _session._cryptoKey = cryptoKey; + window.dispatchEvent(new CustomEvent('g1wallet:unlocked', { + detail: { pubkey: pubkey } + })); + } + + function _lockWallet() { + _session.unlocked = false; + _session.pubkey = 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 + // Other addons place data-g1wallet-target="pubkey" on their pubkey input fields. + // When the wallet unlocks, those fields are populated automatically. + // ------------------------------------------------------------------------ + + 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 = ''; + }); + }); + +}());