setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $dbh->exec("PRAGMA key = " . $dbh->quote($passphrase)); $dbh->exec("PRAGMA cipher_memory_security = ON"); return $dbh; } /** Return a single scalar from "PRAGMA xyz;" */ function pragmaOne(PDO $dbh, string $pragma): ?string { $stmt = $dbh->query("PRAGMA $pragma;"); $row = $stmt ? $stmt->fetch(PDO::FETCH_NUM) : false; return $row && isset($row[0]) ? (string)$row[0] : null; } /** * cipher_integrity_check returns: * - zero rows => OK (externally consistent) * - one or more rows => errors found (or wrong key) */ function cipherIntegrityOk(PDO $dbh): bool { $stmt = $dbh->query("PRAGMA cipher_integrity_check;"); if (!$stmt) return false; // query failed -> not OK $row = $stmt->fetch(PDO::FETCH_NUM); return ($row === false); // no rows == OK } // ---------------------------- Main actions ---------------------------- function createAndPopulate(string $path, string $passphrase, string $marker): void { $dbh = pdoWithKey($path, $passphrase); // Basic SQLCipher sanity: cipher_version should be non-empty $cipherVersion = pragmaOne($dbh, "cipher_version"); check("cipher_version available", !empty($cipherVersion), $cipherVersion ?? "(empty)"); // Create table and insert two rows (one is the plaintext 'marker') $dbh->exec("CREATE TABLE IF NOT EXISTS users (name TEXT NOT NULL)"); $stmt = $dbh->prepare("INSERT INTO users(name) VALUES(:name)"); $stmt->execute([":name" => "mig5"]); $stmt->execute([":name" => $marker]); // Confirm we can read back with the correct key $names = $dbh->query("SELECT name FROM users ORDER BY rowid")->fetchAll(PDO::FETCH_COLUMN, 0); check("Read back inserted rows", in_array("mig5", $names, true) && in_array($marker, $names, true)); // Integrity check should be OK (i.e., returns no rows) $ok = cipherIntegrityOk($dbh); check("cipher_integrity_check", $ok, $ok ? "no rows (OK)" : "reported errors"); // Capture page_size (for entropy sampling) $pageSize = (int)(pragmaOne($dbh, "page_size") ?? 4096); $dbh = null; // close // Store page size to a sidecar file so we can read it after closing the DB file_put_contents($path . ".pagesize", (string)$pageSize); } function probeWrongKey(string $path, string $wrongKey): void { try { $dbh = pdoWithKey($path, $wrongKey); // With the wrong key, integrity should NOT be OK (either rows returned or errors) $okWrong = false; try { $okWrong = cipherIntegrityOk($dbh); // true means "no rows" => (unexpected) } catch (Throwable $e) { // Expected scenarios with wrong key can throw; treat as failure (which is good here) $okWrong = false; } if ($okWrong) { check("Wrong key probe (unexpectedly OK)", false); } else { check("Wrong key probe (integrity fails as expected)", true); } } catch (Throwable $e) { // Exception creating/using the handle is also acceptable evidence key is wrong check("Wrong key probe (exception on use)", true, get_class($e) . ": " . $e->getMessage()); } } function scanForPlaintextMarker(string $path, string $marker): void { $raw = file_get_contents($path); $found = ($raw !== false) && (strpos($raw, $marker) !== false); check("Plaintext marker NOT present in raw file", !$found, $found ? "marker leaked to disk" : null); } function headerIsNotPlainSQLite(string $path): void { $plain = hasPlainSQLiteHeader($path); check("Header is not plain 'SQLite format 3\\0'", !$plain, $plain ? "found plain SQLite header" : null); } function entropyCheck(string $path): void { $pageSizePath = $path . ".pagesize"; $pageSize = 4096; if (is_file($pageSizePath)) { $ps = (int)trim((string)file_get_contents($pageSizePath)); if ($ps > 0) $pageSize = $ps; } $H = firstPageEntropy($path, $pageSize); // Encrypted data typically ~7.8–8.0 bits/byte. Use a lenient threshold. $ok = $H >= 7.5; check("First-page entropy high (>= 7.5 bits/byte)", $ok, sprintf("H=%.3f (page=%d)", $H, $pageSize)); @unlink($pageSizePath); } // ---------------------------- Run ---------------------------- try { $dir = makePrivateTempDir(); $file = $dir . DIRECTORY_SEPARATOR . 'test.db'; $passphrase = "super-secret-passphrase-for-sqlcipher-pragma"; $wrongKey = "definitely-the-wrong-key"; $marker = "PLAINTEXT_MARKER_" . bin2hex(random_bytes(8)); echo "Creating DB at: $file\n"; createAndPopulate($file, $passphrase, $marker); headerIsNotPlainSQLite($file); scanForPlaintextMarker($file, $marker); probeWrongKey($file, $wrongKey); entropyCheck($file); echo "\nAll checks passed.\n"; } catch (Throwable $e) { fwrite(STDERR, "Error: " . get_class($e) . ": " . $e->getMessage() . "\n"); exit(1); } finally { // Cleanup if (isset($file) && is_file($file)) @unlink($file); if (isset($dir) && is_dir($dir)) @rmdir($dir); }