79 lines
3.2 KiB
PHP
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);
|
|
}
|