From a44ab4b7608b4728053e95024d69402aeb44c4a0 Mon Sep 17 00:00:00 2001 From: TheRON Date: Sun, 14 Jun 2026 03:52:08 -0400 Subject: [PATCH] Updated --- hubzilla/addon/g1wallet/g1wallet.php | 29 ++-- hubzilla/addon/g1wallet/g1wallet_renderer.php | 45 ++++--- hubzilla/addon/g1wallet/g1wallet_spool.php | 127 +++++++++++++++--- 3 files changed, 148 insertions(+), 53 deletions(-) diff --git a/hubzilla/addon/g1wallet/g1wallet.php b/hubzilla/addon/g1wallet/g1wallet.php index 59eb52c..254b6ff 100644 --- a/hubzilla/addon/g1wallet/g1wallet.php +++ b/hubzilla/addon/g1wallet/g1wallet.php @@ -3,7 +3,7 @@ /** * Name: Ğ1 Wallet * Description: Self-sovereign Ğ1 wallet for SASE-verified participants. Key derivation and signing in the browser. The platform never touches your keys. - * Version: 0.1.0 + * Version: 0.2.0 * MinVersion: 11.0 * MaxVersion: 12.0 */ @@ -36,9 +36,9 @@ function g1wallet_load_pdl(&$b) { } } -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // HELPERS -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- function g1wallet_h($value) { // HTML-escapes a value for safe output. @@ -53,9 +53,9 @@ function g1wallet_load_config() { return (json_last_error() === JSON_ERROR_NONE) ? $cfg : []; } -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // ACCESS -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- function g1wallet_access_state() { // Returns operator, participant, or public. @@ -80,13 +80,13 @@ function g1wallet_access_state() { $associations = $cfg['associations'] ?? []; if (empty($associations)) return 'public'; - // Direct pgrp_member query — works for guest tokens. + // Direct pggrp_member query — works for guest tokens. foreach ($associations as $slug => $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", + $r = q("SELECT xchan FROM pggrp_member WHERE gid = %d AND xchan = '%s' LIMIT 1", intval($gid), dbesc($observer) ); @@ -98,17 +98,22 @@ function g1wallet_access_state() { 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')) { + // bip39 must load before g1wallet.js (g1wallet.js calls window.bip39). head_add_js('/addon/g1wallet/vendor/bip39-3.1.0.min.js'); head_add_js('/addon/g1wallet/view/js/g1wallet.js'); + // Note: vendor/scrypt-js-3.0.1.min.js is NOT loaded. + // scrypt is the obsolete Cesium1 / Duniter v1 derivation algorithm. + // Duniter v2 / Ğecko uses entropy-as-seed (no KDF). The file is + // retained in vendor/ for reference but is not wired anywhere. } $access = g1wallet_access_state(); @@ -136,7 +141,7 @@ function g1wallet_content() { return g1wallet_handle_broadcast_post(); case 'pubkey': - // POST: store participant's public key in channel settings after unlock. + // POST: store public key in channel settings after unlock. if ($_SERVER['REQUEST_METHOD'] !== 'POST') { return g1wallet_render_error('POST required.'); } @@ -151,9 +156,9 @@ function g1wallet_content() { } } -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // CSRF -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- function g1wallet_csrf_token() { // Generates and stores a CSRF token for the current session. diff --git a/hubzilla/addon/g1wallet/g1wallet_renderer.php b/hubzilla/addon/g1wallet/g1wallet_renderer.php index e435783..8786e97 100644 --- a/hubzilla/addon/g1wallet/g1wallet_renderer.php +++ b/hubzilla/addon/g1wallet/g1wallet_renderer.php @@ -6,33 +6,31 @@ * Knows nothing about crypto — that lives entirely in g1wallet.js. */ -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // ACCESS WALL -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- function g1wallet_render_access_wall() { + $directory_url = g1wallet_h(z_root() . '/channel/theron'); return '
'; } -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // WALLET LANDING -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- function g1wallet_render_landing($access) { // Wallet landing: shows unlock form or unlocked interface depending on JS session state. - // At skeleton stage, always shows the unlock form. - // Once g1wallet.js is wired, the JS will swap to the unlocked view on successful derivation. + // JS swaps to the unlocked view on successful derivation. $out = '
'; $out .= '
'; @@ -63,18 +61,21 @@ function g1wallet_render_landing($access) { return $out; } -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // UNLOCK FORM -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- function g1wallet_render_unlock_form() { // Renders the wallet unlock form. // The form is handled entirely by g1wallet.js — it does NOT POST to the server. // The mnemonic never leaves the browser. // - // Per Duniter HD Wallet RFC 0015 (Dubp_HD_Wallet), the wallet's keypair is - // derived from a 12-word BIP39 mnemonic (English wordlist), using its - // entropy as input to a BIP32-Ed25519 derivation — not a raw PBKDF2 seed. + // Derivation (Duniter v2 / Substrate / Ğecko standard): + // 12-word BIP39 mnemonic + // → mnemonicToEntropy() → 16 bytes of raw entropy + // → use entropy directly as 32-byte Ed25519 seed (zero-padded right) + // → derive Ed25519 keypair (SubtleCrypto, non-extractable private key) + // → SS58-encode public key with Ğ1 network prefix → g1... address $out = '
'; $out .= '

Unlock Your Wallet

'; @@ -99,13 +100,13 @@ function g1wallet_render_unlock_form() { return $out; } -// ---------------------------------------------------------------------------- -// UNLOCKED INTERFACE (PLACEHOLDER) -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// UNLOCKED INTERFACE +// ----------------------------------------------------------------------------- function g1wallet_render_unlocked_placeholder($access) { - // Placeholder for the unlocked wallet interface. - // Populated by g1wallet.js once key derivation is implemented. + // Unlocked wallet interface. + // Populated by g1wallet.js after key derivation. $out = '
'; $out .= '
'; @@ -114,7 +115,7 @@ function g1wallet_render_unlocked_placeholder($access) { $out .= '
'; $out .= '
'; - $out .= ''; + $out .= ''; $out .= '

'; $out .= '
'; @@ -141,9 +142,9 @@ function g1wallet_render_unlocked_placeholder($access) { return $out; } -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // ERROR -// ---------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- function g1wallet_render_error($message) { // Shows a plain-language error. Never shows a blank page or stack trace. diff --git a/hubzilla/addon/g1wallet/g1wallet_spool.php b/hubzilla/addon/g1wallet/g1wallet_spool.php index 860e70f..e03dcda 100644 --- a/hubzilla/addon/g1wallet/g1wallet_spool.php +++ b/hubzilla/addon/g1wallet/g1wallet_spool.php @@ -2,36 +2,72 @@ /** * g1wallet_spool.php — POST handlers for g1wallet. - * pubkey store: receives public key after unlock, stores in channel settings. + * pubkey store: receives public key (SS58 g1... address) after unlock, stores in channel settings. * broadcast relay: receives signed Duniter transaction, relays to orchestrator. * - * At skeleton stage both handlers return placeholder responses. * The private key never reaches this file. Ever. */ function g1wallet_handle_pubkey_post($access) { - // Stores the participant's Ğ1 public key in their Hubzilla channel settings. - // Called once after first wallet unlock (and on re-unlock if key changes). + // Stores the participant's Ğ1 public key (SS58 address) in their Hubzilla + // channel settings. Called once after each wallet unlock (server deduplicates + // by checking the existing value before writing). // The public key is the only wallet-related thing the server ever stores. $pubkey = trim($_POST['g1_pubkey'] ?? ''); if (!$pubkey) { - return g1wallet_render_error('Public key is required.'); + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'Public key is required.']); + killme(); } - // Basic length check — Ğ1 public keys are 43–44 characters in base58. - if (strlen($pubkey) < 43 || strlen($pubkey) > 64) { - return g1wallet_render_error('Invalid public key format.'); + // Ğ1 SS58 addresses: base58-encoded, 36 bytes decoded, begin with "g1". + // Encoded length is 46–47 characters. Reject anything outside that range. + $len = strlen($pubkey); + if ($len < 46 || $len > 48) { + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'Invalid public key format.']); + killme(); } - // TODO: store $pubkey in Hubzilla channel settings using set_pconfig() or equivalent. - // Placeholder: log and return success shell. - // set_pconfig(local_channel(), 'g1wallet', 'g1_pubkey', $pubkey); + // Must begin with "g1" (SS58 with Ğ1 network prefix). + if (strncmp($pubkey, 'g1', 2) !== 0) { + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'Invalid public key prefix.']); + killme(); + } + + // Only base58 characters (Bitcoin alphabet, no 0/O/I/l). + if (!preg_match('/^[1-9A-HJ-NP-Za-km-z]+$/', $pubkey)) { + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'Invalid public key characters.']); + killme(); + } + + // Determine channel_id to write against. + // Operators use their own channel_id. Participants also use local_channel(). + // We do not write other channels' keys — only the caller's own. + $channel_id = local_channel(); + if (!$channel_id) { + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'Not authenticated.']); + killme(); + } + + // Deduplicate: if the stored key matches, return ok without writing. + $existing = get_pconfig($channel_id, 'g1wallet', 'g1_pubkey'); + if ($existing === $pubkey) { + header('Content-Type: application/json'); + echo json_encode(['status' => 'ok', 'note' => 'Key unchanged.']); + killme(); + } + + // Store the public key. + set_pconfig($channel_id, 'g1wallet', 'g1_pubkey', $pubkey); - // Return JSON for fetch() caller in g1wallet.js. header('Content-Type: application/json'); - echo json_encode(['status' => 'ok', 'note' => 'Pubkey storage not yet implemented.']); + echo json_encode(['status' => 'ok']); killme(); } @@ -51,13 +87,66 @@ function g1wallet_handle_broadcast_post() { killme(); } - // TODO: load config, relay to orchestrator POST /g1wallet/broadcast. - // $config = g1wallet_load_config(); - // $orchestrator_url = $config['orchestrator_url'] ?? ''; - // $node_token = $config['node_token'] ?? ''; - // ... HTTP relay to orchestrator ... + // Validate doc_type is a known type. + $allowed_types = ['transfer', 'certification']; + if (!in_array($doc_type, $allowed_types, true)) { + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'Unknown doc_type.']); + killme(); + } + + // Load config for orchestrator endpoint. + $config = g1wallet_load_config(); + $orchestrator_url = rtrim($config['orchestrator_url'] ?? '', '/'); + $node_token = $config['node_token'] ?? ''; + + if (!$orchestrator_url || !$node_token) { + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'Orchestrator not configured.']); + killme(); + } + + // Relay to orchestrator POST /g1wallet/broadcast. + $payload = json_encode([ + 'signed_doc' => $signed_doc, + 'doc_type' => $doc_type, + ]); + + $ch = curl_init($orchestrator_url . '/g1wallet/broadcast'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'X-Node-Token: ' . $node_token, + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + + $raw = curl_exec($ch); + $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $err = curl_error($ch); + curl_close($ch); + + if ($err) { + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'Orchestrator unreachable.']); + killme(); + } + + if ($http !== 200) { + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'Orchestrator returned HTTP ' . intval($http) . '.']); + killme(); + } + + $result = json_decode($raw, true); + if (json_last_error() !== JSON_ERROR_NONE || !isset($result['status'])) { + header('Content-Type: application/json'); + echo json_encode(['status' => 'error', 'message' => 'Invalid orchestrator response.']); + killme(); + } header('Content-Type: application/json'); - echo json_encode(['status' => 'ok', 'note' => 'Broadcast relay not yet implemented.']); + echo json_encode($result); killme(); }