Quelltext des aktuellen Standes
#!/usr/bin/env php
<?php
/**
* Vault-Krypto-Selbsttest (ohne Datenbank).
*
* Prüft die sicherheitskritischen Bausteine des Passwort-Tresors:
* - AES-256-GCM Encrypt/Decrypt Round-Trip
* - Manipulationserkennung (GCM-Tag)
* - PBKDF2-Schlüsselableitung (Determinismus, falsches Passwort scheitert)
* - Passwortwechsel-Self-Heal über die APP_KEY-Recovery-Kopie
* - CSV-Import mit mehrzeiligen, gequoteten Feldern
*
* Verwendung: php bin/vault-selftest.php
* Exit-Code 0 = alle Tests bestanden, sonst 1.
*/
declare(strict_types=1);
$autoload = dirname(__DIR__) . '/vendor/autoload.php';
if (is_file($autoload)) {
require_once $autoload;
}
require_once dirname(__DIR__) . '/bootstrap.php';
foreach (['Vault/VaultCrypto.php', 'Vault/CsvHandler.php'] as $f) {
$p = dirname(__DIR__) . '/src/' . $f;
if (is_file($p)) {
require_once $p;
}
}
use OpenNIT\Vault\VaultCrypto;
use OpenNIT\Vault\CsvHandler;
$pass = 0;
$fail = 0;
function check(string $name, bool $ok): void {
global $pass, $fail;
echo ($ok ? " \033[32m✓\033[0m " : " \033[31m✗\033[0m ") . $name . PHP_EOL;
$ok ? $pass++ : $fail++;
}
echo "Vault-Krypto-Selbsttest\n=======================\n";
// 1) Round-Trip
$key = VaultCrypto::generateKey();
$enc = VaultCrypto::encrypt('geheim123', $key);
check('AES-256-GCM Round-Trip', VaultCrypto::decrypt($enc['ciphertext'], $enc['nonce'], $enc['tag'], $key) === 'geheim123');
// 2) Nonce ist je Aufruf zufällig
$e2 = VaultCrypto::encrypt('geheim123', $key);
check('Zufälliger Nonce pro Verschlüsselung', $enc['nonce'] !== $e2['nonce']);
// 3) Manipulationserkennung
$tampered = $enc['ciphertext'];
$tampered[0] = ($tampered[0] === "\x00") ? "\x01" : "\x00";
$caught = false;
try { VaultCrypto::decrypt($tampered, $enc['nonce'], $enc['tag'], $key); } catch (\Throwable) { $caught = true; }
check('Manipulierter Ciphertext wird abgelehnt', $caught);
// 4) Falscher Schlüssel scheitert
$caught = false;
try { VaultCrypto::decrypt($enc['ciphertext'], $enc['nonce'], $enc['tag'], VaultCrypto::generateKey()); } catch (\Throwable) { $caught = true; }
check('Falscher Schlüssel scheitert', $caught);
// 5) PBKDF2 deterministisch + falsches Passwort
$salt = VaultCrypto::generateSalt();
check('PBKDF2 deterministisch', VaultCrypto::deriveKey('pw', $salt) === VaultCrypto::deriveKey('pw', $salt));
check('PBKDF2 salt-abhängig', VaultCrypto::deriveKey('pw', $salt) !== VaultCrypto::deriveKey('pw', VaultCrypto::generateSalt()));
// 6) Self-Heal über APP_KEY-Recovery (Passwortwechsel-Szenario)
$APP = 'app-key-mit-sehr-hoher-entropie-0123456789';
$vmk = VaultCrypto::generateKey();
$s1 = VaultCrypto::generateSalt();
$uk1 = VaultCrypto::deriveKey('altesPW', $s1);
$wrap = VaultCrypto::encrypt($vmk, $uk1);
$rs = VaultCrypto::generateSalt();
$rec = VaultCrypto::encrypt($vmk, VaultCrypto::deriveAdminKey($APP, $rs));
// Admin-Reset: neues PW, Unlock mit altem Wrap schlägt fehl
$uk2 = VaultCrypto::deriveKey('neuesPW', $s1);
$failed = false;
try { VaultCrypto::decrypt($wrap['ciphertext'], $wrap['nonce'], $wrap['tag'], $uk2); } catch (\Throwable) { $failed = true; }
check('Unlock mit neuem PW scheitert (vor Self-Heal)', $failed);
$recovered = VaultCrypto::decrypt($rec['ciphertext'], $rec['nonce'], $rec['tag'], VaultCrypto::deriveAdminKey($APP, $rs));
check('Recovery via APP_KEY liefert VMK', $recovered === $vmk);
$reWrap = VaultCrypto::encrypt($recovered, $uk2);
check('Unlock nach Self-Heal mit neuem PW', VaultCrypto::decrypt($reWrap['ciphertext'], $reWrap['nonce'], $reWrap['tag'], VaultCrypto::deriveKey('neuesPW', $s1)) === $vmk);
$badRec = false;
try { VaultCrypto::decrypt($rec['ciphertext'], $rec['nonce'], $rec['tag'], VaultCrypto::deriveAdminKey('falsch', $rs)); } catch (\Throwable) { $badRec = true; }
check('Recovery mit falschem APP_KEY scheitert', $badRec);
// 7) CSV-Import mit mehrzeiliger Notiz
$csv = "Title,Username,Password,URL,Notes\n"
. "GitHub,alice,\"p\"\"w,1\",https://github.com,\"Zeile1\nZeile2\"\n"
. "Mail,bob,secret,https://mail.example.com,kurz\n";
$rows = CsvHandler::import($csv);
check('CSV: 2 Einträge', count($rows) === 2);
check('CSV: mehrzeilige Notiz erhalten', ($rows[0]['notes'] ?? '') === "Zeile1\nZeile2");
check('CSV: Passwort mit Quote/Komma', ($rows[0]['password'] ?? '') === 'p"w,1');
echo "\n" . ($fail === 0
? "\033[32mAlle {$pass} Tests bestanden.\033[0m\n"
: "\033[31m{$fail} von " . ($pass + $fail) . " Tests fehlgeschlagen.\033[0m\n");
exit($fail === 0 ? 0 : 1);