Files
hyprcrd/relay/inc/crypto.php
T

79 lines
3.2 KiB
PHP

<?php
// Crypto helpers: at-rest encryption with master key, plus the
// HYPRCRD-RELAY-V1 envelope format used by both the JS client and
// the relay's internal re-encryption hops.
if (!defined('RELAY_BOOTSTRAPPED')) { http_response_code(404); exit; }
/**
* Encrypt a value at-rest with the master key. Used for tenant secrets
* and the inter-tenant key in the DB.
*/
function masterEncrypt(string $plain, string $masterKeyHex): string {
$key = hex2bin($masterKeyHex);
$iv = random_bytes(16);
$cipher = openssl_encrypt($plain, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($iv . $cipher);
}
function masterDecrypt(string $encoded, string $masterKeyHex): string {
$key = hex2bin($masterKeyHex);
$raw = base64_decode($encoded);
if (!$raw || strlen($raw) < 17) return '';
$iv = substr($raw, 0, 16);
$cipher = substr($raw, 16);
return openssl_decrypt($cipher, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv) ?: '';
}
/**
* Match the JS client's encryption format exactly:
* key = first 32 chars of hex(SHA-256(secret)) used as 32-byte ASCII key
* envelope = "HYPRCRD-RELAY-V1\n" + base64(iv || ciphertext)
*/
function clientEncrypt(string $plain, string $secret): string {
$key = substr(hash('sha256', $secret), 0, 32);
$iv = random_bytes(16);
$cipher = openssl_encrypt($plain, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
return "HYPRCRD-RELAY-V1\n" . base64_encode($iv . $cipher);
}
function clientDecrypt(string $envelope, string $secret): string {
if (!str_starts_with($envelope, 'HYPRCRD-RELAY-V1')) return $envelope;
$b64 = trim(substr($envelope, strlen('HYPRCRD-RELAY-V1') + 1));
$raw = base64_decode($b64);
if (!$raw || strlen($raw) < 17) return '';
$key = substr(hash('sha256', $secret), 0, 32);
$iv = substr($raw, 0, 16);
$cipher = substr($raw, 16);
$result = openssl_decrypt($cipher, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
return $result !== false ? $result : '';
}
/**
* Inter-tenant key: relay-wide shared "super secret" key. Set by the admin
* during initial setup (it's the third field in the setup form). Used to
* re-encrypt messages at rest so they can be routed between tenants.
*
* Returns null if not set yet (e.g. legacy v2 installs that haven't been
* upgraded through the new setup wizard).
*/
function getInterTenantKey(PDO $db, string $masterKey): ?string {
$stored = $db->query("SELECT value FROM settings WHERE key = 'inter_tenant_key'")->fetchColumn();
if (!$stored) return null;
$decrypted = masterDecrypt($stored, $masterKey);
return $decrypted !== '' ? $decrypted : null;
}
/** Set/replace the inter-tenant key. Stored encrypted with the master key. */
function setInterTenantKey(PDO $db, string $masterKey, string $newKey): void {
$encrypted = masterEncrypt($newKey, $masterKey);
$db->prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('inter_tenant_key', :v)")
->execute(['v' => $encrypted]);
}
/** True if the relay-wide super secret has been configured. */
function hasInterTenantKey(PDO $db): bool {
$stored = $db->query("SELECT value FROM settings WHERE key = 'inter_tenant_key'")->fetchColumn();
return !empty($stored);
}