From b02f264b23e07bd03103ed9bcf3ad706b29927ce Mon Sep 17 00:00:00 2001 From: Tom Hempel Date: Wed, 24 Jun 2026 09:37:08 +0200 Subject: [PATCH] improved security --- common.php | 10 ++++-- lib/encrypted_payload.php | 41 +++++++++++++++++++++++-- tests/Integration/SecurityPathsTest.php | 2 +- tests/Integration/TokenTest.php | 5 +++ tests/Unit/EncryptedPayloadTest.php | 14 +++++++++ 5 files changed, 65 insertions(+), 7 deletions(-) diff --git a/common.php b/common.php index 9b8a9ef..85dc345 100644 --- a/common.php +++ b/common.php @@ -153,6 +153,10 @@ function hkdf_session_key_from_token(string $tokenHex, string $info = 'qdb-aes', // --- Token-Storage (DB-backed via session table) --- +function token_storage_key(string $token): string { + return hash('sha256', $token); +} + function token_add(string $token, int $ttlSeconds = 86400, array $extra = []): void { require_once __DIR__ . '/db_init.php'; [$pdo, $tmpDb, $lockFp] = qdb_open(true); @@ -161,7 +165,7 @@ function token_add(string $token, int $ttlSeconds = 86400, array $extra = []): v "INSERT INTO session (token, userID, role, entityID, createdAt, expiresAt, temp) VALUES (:t, :uid, :role, :eid, :ca, :ea, :tmp)" )->execute([ - ':t' => $token, + ':t' => token_storage_key($token), ':uid' => $extra['userID'] ?? '', ':role' => $extra['role'] ?? '', ':eid' => $extra['entityID'] ?? '', @@ -178,7 +182,7 @@ function token_get_record(string $token): ?array { require_once __DIR__ . '/db_init.php'; [$pdo, $tmpDb, $lockFp] = qdb_open(false); $stmt = $pdo->prepare("SELECT * FROM session WHERE token = :t AND expiresAt >= :now"); - $stmt->execute([':t' => $token, ':now' => time()]); + $stmt->execute([':t' => token_storage_key($token), ':now' => time()]); $row = $stmt->fetch(PDO::FETCH_ASSOC); qdb_discard($tmpDb, $lockFp); if (!$row) return null; @@ -197,7 +201,7 @@ function token_get_record(string $token): ?array { function token_revoke(string $token): void { require_once __DIR__ . '/db_init.php'; [$pdo, $tmpDb, $lockFp] = qdb_open(true); - $pdo->prepare("DELETE FROM session WHERE token = :t")->execute([':t' => $token]); + $pdo->prepare("DELETE FROM session WHERE token = :t")->execute([':t' => token_storage_key($token)]); qdb_save($tmpDb, $lockFp); } diff --git a/lib/encrypted_payload.php b/lib/encrypted_payload.php index dd9d3e9..f007235 100644 --- a/lib/encrypted_payload.php +++ b/lib/encrypted_payload.php @@ -1,14 +1,16 @@ true, - 'payload' => base64_encode(aes256_cbc_encrypt_bytes($plainJson, $key)), + 'alg' => 'A256GCM', + 'payload' => base64_encode(qdb_aes256_gcm_encrypt_bytes($plainJson, $key)), ]; } @@ -21,5 +23,38 @@ function qdb_decrypt_sensitive_envelope(array $envelope, string $tokenHex): stri throw new InvalidArgumentException('Invalid base64 payload'); } $key = hkdf_session_key_from_token($tokenHex); - return aes256_cbc_decrypt_bytes($raw, $key); + $alg = isset($envelope['alg']) && is_string($envelope['alg']) ? $envelope['alg'] : 'A256CBC'; + return match ($alg) { + 'A256GCM' => qdb_aes256_gcm_decrypt_bytes($raw, $key), + 'A256CBC' => aes256_cbc_decrypt_bytes($raw, $key), + default => throw new InvalidArgumentException('Unsupported encrypted envelope algorithm'), + }; +} + +function qdb_aes256_gcm_encrypt_bytes(string $plain, string $key): string { + $key = str_pad(substr($key, 0, 32), 32, "\0"); + $iv = random_bytes(12); + $tag = ''; + $cipher = openssl_encrypt($plain, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, ''); + if ($cipher === false || strlen($tag) !== 16) { + throw new RuntimeException('openssl_encrypt failed'); + } + return $iv . $cipher . $tag; +} + +function qdb_aes256_gcm_decrypt_bytes(string $data, string $key): string { + $ivLen = 12; + $tagLen = 16; + if (strlen($data) <= $ivLen + $tagLen) { + throw new InvalidArgumentException('cipher too short'); + } + $key = str_pad(substr($key, 0, 32), 32, "\0"); + $iv = substr($data, 0, $ivLen); + $tag = substr($data, -$tagLen); + $ct = substr($data, $ivLen, -$tagLen); + $plain = openssl_decrypt($ct, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, ''); + if ($plain === false) { + throw new InvalidArgumentException('openssl_decrypt failed'); + } + return $plain; } diff --git a/tests/Integration/SecurityPathsTest.php b/tests/Integration/SecurityPathsTest.php index f149edf..61217a5 100644 --- a/tests/Integration/SecurityPathsTest.php +++ b/tests/Integration/SecurityPathsTest.php @@ -18,7 +18,7 @@ final class SecurityPathsTest extends QdbTestCase 'INSERT INTO session (token, userID, role, entityID, createdAt, expiresAt, temp) VALUES (:t, :uid, :role, :eid, :ca, :ea, 0)' )->execute([ - ':t' => $token, + ':t' => token_storage_key($token), ':uid' => $f->adminUserId, ':role' => 'admin', ':eid' => 'ent', diff --git a/tests/Integration/TokenTest.php b/tests/Integration/TokenTest.php index 7a787a2..df7f3c9 100644 --- a/tests/Integration/TokenTest.php +++ b/tests/Integration/TokenTest.php @@ -20,6 +20,11 @@ final class TokenTest extends QdbTestCase $rec = token_get_record($token); $this->assertNotNull($rec); $this->assertSame('admin', $rec['role']); + [$pdo, $tmpDb, $lockFp] = qdb_open(false); + $stored = $pdo->query('SELECT token FROM session LIMIT 1')->fetchColumn(); + qdb_discard($tmpDb, $lockFp); + $this->assertSame(token_storage_key($token), $stored); + $this->assertNotSame($token, $stored); token_revoke($token); $this->assertNull(token_get_record($token)); diff --git a/tests/Unit/EncryptedPayloadTest.php b/tests/Unit/EncryptedPayloadTest.php index b0a0a1c..e1097ec 100644 --- a/tests/Unit/EncryptedPayloadTest.php +++ b/tests/Unit/EncryptedPayloadTest.php @@ -16,10 +16,24 @@ final class EncryptedPayloadTest extends TestCase $json = '{"clientCode":"X","answers":[]}'; $env = qdb_sensitive_envelope($json, $token); $this->assertTrue($env['encrypted']); + $this->assertSame('A256GCM', $env['alg']); $plain = qdb_decrypt_sensitive_envelope($env, $token); $this->assertSame($json, $plain); } + public function testLegacyCbcEnvelopeStillDecrypts(): void + { + require_once dirname(__DIR__, 2) . '/lib/encrypted_payload.php'; + $token = bin2hex(random_bytes(32)); + $json = '{"legacy":true}'; + $key = hkdf_session_key_from_token($token); + $env = [ + 'encrypted' => true, + 'payload' => base64_encode(aes256_cbc_encrypt_bytes($json, $key)), + ]; + $this->assertSame($json, qdb_decrypt_sensitive_envelope($env, $token)); + } + public function testWrongTokenFailsDecrypt(): void { require_once dirname(__DIR__, 2) . '/lib/encrypted_payload.php';