diff --git a/hubzilla/addon/g1wallet/README.md b/hubzilla/addon/g1wallet/README.md new file mode 100644 index 0000000..9441da9 --- /dev/null +++ b/hubzilla/addon/g1wallet/README.md @@ -0,0 +1,45 @@ +# g1wallet — Self-Sovereign Ğ1 Wallet + +**Version:** 0.1.0 (skeleton) +**Spec:** `G1WALLET-SPEC.md` in the repo root + +--- + +## What This Is + +A self-sovereign Ğ1 wallet embedded in the Civic Infrastructure. Every SASE-verified +participant — including the operator — holds their own Ğ1 identity under their own +credentials, in their own browser, under their own control. The platform never touches +the keys. + +## What Is Implemented at This Tag + +- Addon registration, routing, access state +- Wallet unlock form (UI only — no crypto yet) +- Unlocked interface placeholder +- Compact sidebar widget (locked/unlocked status) +- CivicNav registry entry + +## What Is Not Yet Implemented + +- scrypt key derivation (vendor/scrypt-js-3.0.1.min.js is present but not wired) +- Ed25519 signing via WebCrypto +- Wallet session state (JS) +- Pubkey POST to /g1wallet/pubkey +- Balance fetch from orchestrator +- Broadcast relay to Duniter network + +## Build Sequence (from G1WALLET-SPEC.md) + +1. `view/js/g1wallet.js` — key derivation, signing, session management, event API +2. `vendor/scrypt-js-3.0.1.min.js` — already present, pinned +3. `g1wallet.php` — entry point (done) +4. `g1wallet_renderer.php` — unlock form, unlocked interface (skeleton done) +5. `g1wallet_spool.php` — pubkey store, broadcast relay (stubs done) +6. Orchestrator additions — broadcast and balance endpoints in main.py +7. Widget, PDL, CSS (done) +8. Integration test against Duniter standard key derivation + +## Runtime Config + +Copy `config.json.template` to `config.json` on the host. Do not commit `config.json`. diff --git a/hubzilla/addon/g1wallet/g1wallet.php b/hubzilla/addon/g1wallet/g1wallet.php new file mode 100644 index 0000000..df4fcb2 --- /dev/null +++ b/hubzilla/addon/g1wallet/g1wallet.php @@ -0,0 +1,171 @@ + $assoc) { + $groups = $assoc['groups'] ?? []; + foreach (['corpus_builder', 'sase_participant', 'civic_professional'] as $group_key) { + $gid = intval($groups[$group_key] ?? 0); + if ($gid) { + $r = q("SELECT xchan FROM pgrp_member WHERE gid = %d AND xchan = '%s' LIMIT 1", + intval($gid), + dbesc($observer) + ); + if ($r) return 'participant'; + } + } + } + + return 'public'; +} + +// ---------------------------------------------------------------------------- +// CONTENT ROUTER +// ---------------------------------------------------------------------------- + +function g1wallet_content() { + if (function_exists('head_add_css')) { + head_add_css('/addon/g1wallet/view/css/g1wallet.css'); + } + if (function_exists('head_add_js')) { + head_add_js('/addon/g1wallet/vendor/scrypt-js-3.0.1.min.js'); + head_add_js('/addon/g1wallet/view/js/g1wallet.js'); + } + + $access = g1wallet_access_state(); + $sub_route = strtolower(argv(1) ?? ''); + + if ($access === 'public') { + return g1wallet_render_access_wall(); + } + + switch ($sub_route) { + + case 'balance': + // GET: return cached balance for the current session pubkey. + // Placeholder — orchestrator query not yet implemented. + return g1wallet_render_error('Balance fetch not yet implemented.'); + + case 'broadcast': + // POST: relay signed Duniter transaction to orchestrator. + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + return g1wallet_render_error('POST required.'); + } + if (!g1wallet_verify_csrf()) { + return g1wallet_render_error('Invalid form token. Please reload and try again.'); + } + return g1wallet_handle_broadcast_post(); + + case 'pubkey': + // POST: store participant's public key in channel settings after unlock. + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + return g1wallet_render_error('POST required.'); + } + if (!g1wallet_verify_csrf()) { + return g1wallet_render_error('Invalid form token. Please reload and try again.'); + } + return g1wallet_handle_pubkey_post($access); + + default: + // Wallet landing: unlock form or unlocked interface. + return g1wallet_render_landing($access); + } +} + +// ---------------------------------------------------------------------------- +// CSRF +// ---------------------------------------------------------------------------- + +function g1wallet_csrf_token() { + // Generates and stores a CSRF token for the current session. + if (empty($_SESSION['g1wallet_csrf'])) { + $_SESSION['g1wallet_csrf'] = bin2hex(random_bytes(16)); + } + return ''; +} + +function g1wallet_verify_csrf() { + // Returns true if the CSRF token in POST matches the session token. + return isset($_POST['g1wallet_csrf'], $_SESSION['g1wallet_csrf']) + && hash_equals($_SESSION['g1wallet_csrf'], $_POST['g1wallet_csrf']); +} diff --git a/hubzilla/addon/g1wallet/g1wallet_renderer.php b/hubzilla/addon/g1wallet/g1wallet_renderer.php new file mode 100644 index 0000000..874ef9f --- /dev/null +++ b/hubzilla/addon/g1wallet/g1wallet_renderer.php @@ -0,0 +1,158 @@ + +
Your self-sovereign Ğ1 identity. Keys are derived in your browser and never leave your device.
'; + $out .= 'Enter your Ğ1 credentials. These are used only in your browser to derive your keypair. They are never sent to the server.
'; + + $out .= '—
'; + $out .= 'Not yet loaded.
'; + $out .= ''; + $out .= 'You are the operator. Your Ğ1 public key is stored in your channel settings, not in config.
'; + $out .= '