From ef238f3b9c686eb6ef161e480c0cd18b2efccd4d Mon Sep 17 00:00:00 2001 From: th Date: Fri, 10 Apr 2026 18:50:04 +0200 Subject: [PATCH] image push fix --- public/index.php | 25 ++++++++++++++++++------- relay/inc/routes/messages.php | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/public/index.php b/public/index.php index ad2c0c4..502893e 100644 --- a/public/index.php +++ b/public/index.php @@ -953,7 +953,15 @@ if (str_starts_with($route, '/api/')) { if (!preg_match('/^[a-f0-9]+\.\w+$/', $fn)) continue; $dest = $uploadDir . '/' . $fn; if (file_exists($dest)) continue; - $raw = base64_decode($att['data']); + // New format: HYPRCRD-RELAY-V1 envelope (decrypt-reencrypted by relay) + // Old format: raw base64(iv || cipher) + $payload = $att['data'] ?? ''; + if (str_starts_with($payload, 'HYPRCRD-RELAY-V1')) { + $b64 = trim(substr($payload, strlen('HYPRCRD-RELAY-V1') + 1)); + $raw = base64_decode($b64); + } else { + $raw = base64_decode($payload); + } if (strlen($raw) >= 17) { $decrypted = openssl_decrypt(substr($raw, 16), 'aes-256-cbc', $encKey, OPENSSL_RAW_DATA, substr($raw, 0, 16)); if ($decrypted !== false) file_put_contents($dest, $decrypted); @@ -1060,7 +1068,9 @@ if (str_starts_with($route, '/api/')) { $db->prepare("INSERT OR IGNORE INTO relay_pushed (relay_msg_id) VALUES (?)")->execute([$resData['id']]); } - // Upload attachments (images referenced in body) + // Upload attachments (images referenced in body). + // Same envelope format as title/body so the relay can decrypt with + // the sender's secret and re-encrypt with the recipient's secret. $cardBody = $card['body'] ?? ''; preg_match_all('#uploads/([a-f0-9]+\.\w+)#', $cardBody, $attMatches); $uploadDir = __DIR__ . '/uploads'; @@ -1068,13 +1078,14 @@ if (str_starts_with($route, '/api/')) { $filePath = $uploadDir . '/' . $filename; if (!file_exists($filePath)) continue; $raw = file_get_contents($filePath); - $iv = random_bytes(16); - $cipher = openssl_encrypt($raw, 'aes-256-cbc', $encKey, OPENSSL_RAW_DATA, $iv); - $attPayload = json_encode([ + $envelope = $encryptField($raw); + $attData = [ 'relay_uid' => $relayUid, 'filename' => $filename, - 'data' => base64_encode($iv . $cipher), - ]); + 'data' => $envelope, + ]; + if ($recipient) $attData['recipient'] = $recipient; + $attPayload = json_encode($attData); $ach = curl_init(rtrim($relayUrl, '/') . '/api/attachments'); curl_setopt_array($ach, [ CURLOPT_RETURNTRANSFER => true, diff --git a/relay/inc/routes/messages.php b/relay/inc/routes/messages.php index 954374f..ce25064 100644 --- a/relay/inc/routes/messages.php +++ b/relay/inc/routes/messages.php @@ -143,18 +143,49 @@ if ($route === '/api/attachments' && $method === 'POST') { $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' => $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]); - jsonResponse($stmt->fetchAll()); + $rows = $stmt->fetchAll(); + + $out = []; + foreach ($rows as $row) { + $plainBytes = clientDecrypt($row['data'] ?? '', $interKey); + $row['data'] = clientEncrypt($plainBytes, $recipientSecret); + $out[] = $row; + } + jsonResponse($out); }