'body required'], 400); $recipient = getTenantByUsername($db, $recipientName); if (!$recipient) jsonResponse(['error' => "no such user: $recipientName"], 404); $interKey = getInterTenantKey($db, $MASTER_KEY); if (!$interKey) jsonResponse(['error' => 'relay super-shared key not set'], 500); $storedTitle = clientEncrypt($title, $interKey); $storedBody = clientEncrypt($body, $interKey); $db->prepare('INSERT INTO messages (recipient_id, sender_id, sender_username, title, body, color, tags, relay_uid) VALUES (:r, :s, :sn, :t, :b, :c, :tg, :uid)') ->execute([ 'r' => $recipient['id'], 's' => $sessionTenant['id'], 'sn' => $sessionTenant['username'], 't' => $storedTitle, 'b' => $storedBody, 'c' => $input['color'] ?? '#38bdf8', 'tg' => $input['tags'] ?? '', 'uid' => $input['relay_uid'] ?? null, ]); jsonResponse(['ok' => true, 'id' => (int)$db->lastInsertId(), 'recipient' => $recipientName]); } // ── Client API auth (X-Relay-User + X-Relay-Key) ── $clientTenant = authClient($db); if ($route === '/api/messages' && $method === 'POST') { if (!$clientTenant) jsonResponse(['error' => 'unauthorized'], 401); $input = json_decode(file_get_contents('php://input'), true) ?: []; $encryptedTitle = (string)($input['title'] ?? ''); $encryptedBody = (string)($input['body'] ?? ''); $recipientName = trim((string)($input['recipient'] ?? $clientTenant['username'])); if ($encryptedBody === '') jsonResponse(['error' => 'body required'], 400); $recipient = getTenantByUsername($db, $recipientName); if (!$recipient) jsonResponse(['error' => "no such user: $recipientName"], 404); // Decrypt with sender's secret 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); $plainTitle = clientDecrypt($encryptedTitle, $senderSecret); $plainBody = clientDecrypt($encryptedBody, $senderSecret); // Re-encrypt with the inter-tenant (super-shared) key for at-rest storage $interKey = getInterTenantKey($db, $MASTER_KEY); if (!$interKey) jsonResponse(['error' => 'relay super-shared key not set — admin must complete setup'], 500); $storedTitle = clientEncrypt($plainTitle, $interKey); $storedBody = clientEncrypt($plainBody, $interKey); $db->prepare('INSERT INTO messages (recipient_id, sender_id, sender_username, title, body, color, tags, relay_uid) VALUES (:r, :s, :sn, :t, :b, :c, :tg, :uid)') ->execute([ 'r' => $recipient['id'], 's' => $clientTenant['id'], 'sn' => $clientTenant['username'], 't' => $storedTitle, 'b' => $storedBody, 'c' => $input['color'] ?? '#38bdf8', 'tg' => $input['tags'] ?? '', 'uid' => $input['relay_uid'] ?? null, ]); jsonResponse(['ok' => true, 'id' => (int)$db->lastInsertId(), 'recipient' => $recipientName]); } if ($route === '/api/messages' && $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 = $_GET['since'] ?? null; if ($since) { $stmt = $db->prepare("SELECT * FROM messages WHERE recipient_id = :r AND created_at > :s ORDER BY created_at ASC"); $stmt->execute(['r' => $clientTenant['id'], 's' => $since]); } else { $stmt = $db->prepare("SELECT * FROM messages WHERE recipient_id = :r ORDER BY created_at ASC"); $stmt->execute(['r' => $clientTenant['id']]); } $msgs = $stmt->fetchAll(); // Decrypt with inter-tenant key, re-encrypt with recipient's secret $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 = []; foreach ($msgs as $msg) { $plainTitle = clientDecrypt($msg['title'] ?? '', $interKey); $plainBody = clientDecrypt($msg['body'] ?? '', $interKey); $msg['title'] = clientEncrypt($plainTitle, $recipientSecret); $msg['body'] = clientEncrypt($plainBody, $recipientSecret); $out[] = $msg; } $db->prepare("UPDATE messages SET fetched = 1 WHERE recipient_id = :r AND fetched = 0") ->execute(['r' => $clientTenant['id']]); jsonResponse($out); } if (preg_match('#^/api/messages/(\d+)$#', $route, $m) && $method === 'DELETE') { $tenant = $clientTenant ?: $sessionTenant; if (!$tenant) jsonResponse(['error' => 'unauthorized'], 401); $id = (int)$m[1]; $db->prepare('DELETE FROM messages WHERE id = :id AND recipient_id = :r') ->execute(['id' => $id, 'r' => $tenant['id']]); jsonResponse(['ok' => true]); } if ($route === '/api/attachments' && $method === 'POST') { if (!$clientTenant) jsonResponse(['error' => 'unauthorized'], 401); $input = json_decode(file_get_contents('php://input'), true) ?: []; $relayUid = (string)($input['relay_uid'] ?? ''); $filename = (string)($input['filename'] ?? ''); $data = (string)($input['data'] ?? ''); $recipientName = trim((string)($input['recipient'] ?? $clientTenant['username'])); if (!$relayUid || !$filename || !$data) jsonResponse(['error' => 'relay_uid, filename, and data required'], 400); $recipient = getTenantByUsername($db, $recipientName); if (!$recipient) jsonResponse(['error' => "no such user: $recipientName"], 404); // Decrypt-reencrypt hop, same shape as messages: arrives encrypted with // sender's secret, gets stored at-rest under the inter-tenant key. 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); $plainBytes = clientDecrypt($data, $senderSecret); if ($plainBytes === '') jsonResponse(['error' => 'attachment decrypt failed (sender secret mismatch?)'], 400); $storedData = clientEncrypt($plainBytes, $interKey); $db->prepare('DELETE FROM attachments WHERE recipient_id = :r AND relay_uid = :uid AND filename = :fn') ->execute(['r' => $recipient['id'], 'uid' => $relayUid, 'fn' => $filename]); $db->prepare('INSERT INTO attachments (recipient_id, relay_uid, filename, data) VALUES (:r, :uid, :fn, :data)') ->execute(['r' => $recipient['id'], 'uid' => $relayUid, 'fn' => $filename, 'data' => $storedData]); jsonResponse(['ok' => true, 'id' => (int)$db->lastInsertId()]); } if ($route === '/api/attachments' && $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); } $relayUid = $_GET['relay_uid'] ?? ''; if (!$relayUid) jsonResponse(['error' => 'relay_uid required'], 400); $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); $stmt = $db->prepare('SELECT id, relay_uid, filename, data, created_at FROM attachments WHERE recipient_id = :r AND relay_uid = :uid'); $stmt->execute(['r' => $clientTenant['id'], 'uid' => $relayUid]); $rows = $stmt->fetchAll(); $out = []; foreach ($rows as $row) { $plainBytes = clientDecrypt($row['data'] ?? '', $interKey); $row['data'] = clientEncrypt($plainBytes, $recipientSecret); $out[] = $row; } jsonResponse($out); }