OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

vault-selftest.php

4 KB
#!/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);