Files
kane-diagnostics/hubzilla/addon/g1wallet/g1wallet_spool.php
2026-06-14 03:52:08 -04:00

153 lines
5.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* g1wallet_spool.php — POST handlers for g1wallet.
* pubkey store: receives public key (SS58 g1... address) after unlock, stores in channel settings.
* broadcast relay: receives signed Duniter transaction, relays to orchestrator.
*
* The private key never reaches this file. Ever.
*/
function g1wallet_handle_pubkey_post($access) {
// 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) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Public key is required.']);
killme();
}
// Ğ1 SS58 addresses: base58-encoded, 36 bytes decoded, begin with "g1".
// Encoded length is 4647 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();
}
// 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);
header('Content-Type: application/json');
echo json_encode(['status' => 'ok']);
killme();
}
function g1wallet_handle_broadcast_post() {
// Receives a signed Duniter transaction document (base64-encoded) from the browser.
// Validates the node token, relays to the orchestrator, returns the transaction hash.
//
// The browser signs the document with the participant's private key (WebCrypto).
// Only the signed bytes arrive here — never the private key.
$signed_doc = trim($_POST['signed_doc'] ?? '');
$doc_type = trim($_POST['doc_type'] ?? ''); // e.g. 'transfer', 'certification'
if (!$signed_doc || !$doc_type) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'signed_doc and doc_type are required.']);
killme();
}
// 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($result);
killme();
}