N // // Same decrypt-reencrypt hop as messages.php: arrives encrypted with the // senders client secret, stored at-rest under the inter-tenant key, returned // re-encrypted with the recipients client secret. Relay sees plaintext only // during the routing hop. if (!defined('RELAY_BOOTSTRAPPED')) { http_response_code(404); exit; } // $clientTenant is set by messages.php earlier in the chain via authClient(). // We re-resolve here to keep this file self-contained. $clientTenant = $clientTenant ?? authClient($db); if ($route === '/api/chat' && $method === 'POST') { if (!$clientTenant) jsonResponse(['error' => 'unauthorized'], 401); $input = json_decode(file_get_contents('php://input'), true) ?: []; $encryptedBody = (string)($input['body'] ?? ''); $recipientName = trim((string)($input['recipient'] ?? '')); if ($encryptedBody === '') jsonResponse(['error' => 'body required'], 400); if ($recipientName === '') jsonResponse(['error' => 'recipient required'], 400); $recipient = getTenantByUsername($db, $recipientName); if (!$recipient) jsonResponse(['error' => "no such user: $recipientName"], 404); if (empty($clientTenant['secret_encrypted'])) { jsonResponse(['error' => 'sender has no client secret set — log into the relay UI and configure one in your account'], 400); } $senderSecret = masterDecrypt($clientTenant['secret_encrypted'], $MASTER_KEY); if (!$senderSecret) jsonResponse(['error' => 'sender secret unavailable'], 500); $interKey = getInterTenantKey($db, $MASTER_KEY); if (!$interKey) jsonResponse(['error' => 'relay super-shared key not set — admin must complete setup'], 500); $plain = clientDecrypt($encryptedBody, $senderSecret); if ($plain === '') jsonResponse(['error' => 'body decrypt failed (sender secret mismatch?)'], 400); $stored = clientEncrypt($plain, $interKey); $db->prepare('INSERT INTO chat_messages (recipient_id, sender_id, sender_username, body) VALUES (:r, :s, :sn, :b)') ->execute([ 'r' => $recipient['id'], 's' => $clientTenant['id'], 'sn' => $clientTenant['username'], 'b' => $stored, ]); jsonResponse(['ok' => true, 'id' => (int)$db->lastInsertId()]); } if ($route === '/api/chat' && $method === 'GET') { if (!$clientTenant) jsonResponse(['error' => 'unauthorized'], 401); if (empty($clientTenant['secret_encrypted'])) { jsonResponse(['error' => 'no client secret set — log into the relay UI and configure one in your account'], 400); } $since = (int)($_GET['since'] ?? 0); $stmt = $db->prepare('SELECT id, sender_username, body, created_at FROM chat_messages WHERE recipient_id = :r AND id > :s ORDER BY id ASC LIMIT 500'); $stmt->execute(['r' => $clientTenant['id'], 's' => $since]); $rows = $stmt->fetchAll(); $interKey = getInterTenantKey($db, $MASTER_KEY); if (!$interKey) jsonResponse(['error' => 'relay super-shared key not set'], 500); $recipientSecret = masterDecrypt($clientTenant['secret_encrypted'], $MASTER_KEY); if (!$recipientSecret) jsonResponse(['error' => 'recipient secret unavailable'], 500); $out = []; $maxId = $since; foreach ($rows as $row) { $plain = clientDecrypt($row['body'] ?? '', $interKey); $row['body'] = clientEncrypt($plain, $recipientSecret); $out[] = $row; if ((int)$row['id'] > $maxId) $maxId = (int)$row['id']; } // Mark fetched so cleanup can prune them sooner if desired. We still // keep them around for the 7-day window so a re-poll with since=0 // backfills history if a client wipes its local cache. if ($out) { $db->prepare('UPDATE chat_messages SET fetched = 1 WHERE recipient_id = :r AND id <= :m AND fetched = 0') ->execute(['r' => $clientTenant['id'], 'm' => $maxId]); } jsonResponse(['messages' => $out, 'max_id' => $maxId]); }