/** * 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. * - bip39 is loaded (vendor/bip39-3.1.0.min.js, English wordlist only) for * mnemonic validation and entropy extraction, but full key derivation is * not yet called. * * DERIVATION (not yet implemented — see RFC 0015): * Per Duniter HD Wallet RFC 0015 (Dubp_HD_Wallet, git.duniter.org/documents/rfcs), * the wallet's keypair is derived from a 12-word BIP39 mnemonic (English * wordlist). The mnemonic's ENTROPY (16 bytes for 12 words) — not a PBKDF2 * seed — is the input to a BIP32-Ed25519 derivation (Khovratovich/Law * construction, same family as Cardano's HD wallets). * * The exact BIP32-Ed25519 derivation path used by Ğecko/Cesium2 has not yet * been confirmed against RFC 0015 and MUST be verified before this stub is * replaced with real derivation code. Do not implement against assumptions. * * CRITICAL RULES (never relax these): * - The private key NEVER leaves the browser. * - The mnemonic NEVER leaves 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, // using the key derived per RFC 0015 (BIP32-Ed25519 over mnemonic entropy). // 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 mnemonicEl = document.getElementById('g1wallet-mnemonic'); var mnemonic = (mnemonicEl || {}).value || ''; // Normalize whitespace: collapse multiple spaces/newlines to single spaces, trim. 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; } // Validate against the BIP39 English wordlist and checksum. if (window.bip39 && typeof window.bip39.validateMnemonic === 'function') { if (!window.bip39.validateMnemonic(mnemonic)) { _showError('Invalid mnemonic phrase. Check the words and try again.'); return; } } else { _showError('Mnemonic validation library not available. Please reload the page.'); return; } // TODO: derive keypair from mnemonic entropy per RFC 0015 (BIP32-Ed25519). // var entropy = window.bip39.mnemonicToEntropy(mnemonic); // ... BIP32-Ed25519 derivation (path TBD, pending RFC 0015 confirmation) ... // ... resulting Ed25519 keypair imported as non-extractable CryptoKey ... console.log('[g1wallet] Unlock clicked. Mnemonic validated. 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('Mnemonic is valid. 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 the mnemonic field. var mnemonicEl = document.getElementById('g1wallet-mnemonic'); if (mnemonicEl) mnemonicEl.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 = ''; }); }); }());