275 lines
12 KiB
JavaScript
275 lines
12 KiB
JavaScript
/**
|
|
* 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 =
|
|
'<div class="g1wallet-status g1wallet-status-unlocked">' +
|
|
'<span class="g1wallet-status-indicator">🔓</span> ' +
|
|
'<span class="text-success small">Unlocked</span>' +
|
|
'<div class="g1wallet-cached-key text-muted small font-monospace mt-1">' +
|
|
_escHtml(short) +
|
|
'</div></div>';
|
|
});
|
|
|
|
window.addEventListener('g1wallet:locked', function () {
|
|
var statusEl = document.getElementById(uid + '-status');
|
|
if (!statusEl) return;
|
|
statusEl.innerHTML =
|
|
'<div class="g1wallet-status g1wallet-status-locked">' +
|
|
'<span class="g1wallet-status-indicator">🔒</span> ' +
|
|
'<span class="text-muted small">Locked</span>' +
|
|
'<div class="mt-1"><a href="' + _escHtml(walletUrl) + '" ' +
|
|
'class="btn btn-xs btn-outline-secondary btn-sm">Unlock</a></div>' +
|
|
'</div>';
|
|
});
|
|
}
|
|
};
|
|
|
|
// ------------------------------------------------------------------------
|
|
// 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, '>')
|
|
.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 = '';
|
|
});
|
|
});
|
|
|
|
}());
|