Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
use OpenNIT\Core\Database;
use OpenNIT\Core\Session;
use OpenNIT\Vault\VaultCrypto;
use OpenNIT\Vault\VaultManager;
/**
* Hält fest, dass der Vault-Master-Key die Sitzungsdatei nie im Klartext
* erreicht: Er liegt dort verschlüsselt, und der Schlüssel dazu stammt aus
* einem Begleit-Cookie. Wer nur die Sitzungsdatei hat, kommt nicht an den
* Tresor.
*/
return static function (): void {
/**
* Sitzung ohne PHP-Sitzungsverwaltung: hält Werte und Cookie im Speicher.
*/
$sitzung = static function (): Session {
return new class (['path' => '/', 'domain' => '', 'secure' => true, 'samesite' => 'Lax']) extends Session {
/** @var array<string,mixed> */
public array $werte = [];
/** @var array<string,string> */
public array $cookies = [];
public function __construct(private array $cfg) {}
public function set(string $key, mixed $value): void { $this->werte[$key] = $value; }
public function get(string $key, mixed $default = null): mixed { return $this->werte[$key] ?? $default; }
public function has(string $key): bool { return isset($this->werte[$key]); }
public function remove(string $key): void { unset($this->werte[$key]); }
public function begleitGeheimnis(string $name, bool $anlegen = false, int $laenge = 32): ?string
{
if (isset($this->cookies[$name])) {
return base64_decode(strtr($this->cookies[$name], '-_', '+/'), true) ?: null;
}
if (!$anlegen) {
return null;
}
$bytes = random_bytes($laenge);
$this->cookies[$name] = rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
return $bytes;
}
public function begleitGeheimnisLoeschen(string $name): void { unset($this->cookies[$name]); }
};
};
/** Datenbank-Doppel: liefert für Leerlauf-Abfragen nichts Brauchbares. */
$db = static function (): Database {
return new class extends Database {
public function __construct() {}
public function fetchOne(string $sql, array $params = []): ?array { return null; }
public function fetchAll(string $sql, array $params = []): array { return []; }
};
};
/** Ruft eine private Methode des VaultManagers auf. */
$ruf = static function (VaultManager $vm, string $methode, mixed ...$args): mixed {
$m = new ReflectionMethod(VaultManager::class, $methode);
$m->setAccessible(true);
return $m->invoke($vm, ...$args);
};
$vmkSchluessel = 'vault_vmk_7';
// ----------------------------------------------------------------
// Ablage
// ----------------------------------------------------------------
test('Der VMK liegt nicht im Klartext in der Sitzung', function () use ($sitzung, $db, $ruf, $vmkSchluessel) {
$s = $sitzung();
$vm = new VaultManager($db(), $s, 'app-key');
$vmk = VaultCrypto::generateKey();
$ruf($vm, 'writeSessionVmk', 7, $vmk);
$abgelegt = (string)$s->werte[$vmkSchluessel];
assertFalse(str_contains($abgelegt, base64_encode($vmk)));
assertFalse(str_contains((string)base64_decode($abgelegt, true), $vmk));
});
test('Mit dem Begleit-Cookie kommt derselbe VMK zurück', function () use ($sitzung, $db, $ruf) {
$s = $sitzung();
$vm = new VaultManager($db(), $s, 'app-key');
$vmk = VaultCrypto::generateKey();
$ruf($vm, 'writeSessionVmk', 7, $vmk);
assertSame($vmk, $ruf($vm, 'readSessionVmk', 7));
});
test('Ohne Begleit-Cookie bleibt der Tresor verschlossen', function () use ($sitzung, $db, $ruf) {
$s = $sitzung();
$vm = new VaultManager($db(), $s, 'app-key');
$vmk = VaultCrypto::generateKey();
$ruf($vm, 'writeSessionVmk', 7, $vmk);
// Genau der Fall „jemand hat nur die Sitzungsdatei".
$s->cookies = [];
assertSame(null, $ruf($vm, 'readSessionVmk', 7));
assertFalse($vm->isUnlocked(7));
});
test('Ein fremdes Begleit-Cookie öffnet den Tresor nicht', function () use ($sitzung, $db, $ruf) {
$s = $sitzung();
$vm = new VaultManager($db(), $s, 'app-key');
$vmk = VaultCrypto::generateKey();
$ruf($vm, 'writeSessionVmk', 7, $vmk);
foreach (array_keys($s->cookies) as $name) {
$s->cookies[$name] = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
}
assertSame(null, $ruf($vm, 'readSessionVmk', 7));
});
test('Jede Ablage benutzt eine eigene Nonce', function () use ($sitzung, $db, $ruf, $vmkSchluessel) {
$s = $sitzung();
$vm = new VaultManager($db(), $s, 'app-key');
$vmk = VaultCrypto::generateKey();
$ruf($vm, 'writeSessionVmk', 7, $vmk);
$erste = (string)$s->werte[$vmkSchluessel];
$ruf($vm, 'writeSessionVmk', 7, $vmk);
assertFalse($erste === (string)$s->werte[$vmkSchluessel]);
});
// ----------------------------------------------------------------
// Übergang und Sperren
// ----------------------------------------------------------------
test('Eine Sitzung von vor der Umstellung wird gelesen und aufgerüstet', function () use ($sitzung, $db, $ruf, $vmkSchluessel) {
$s = $sitzung();
$vm = new VaultManager($db(), $s, 'app-key');
$vmk = VaultCrypto::generateKey();
// Alte Form: der VMK selbst, nur base64-kodiert.
$s->werte[$vmkSchluessel] = base64_encode($vmk);
assertSame($vmk, $ruf($vm, 'readSessionVmk', 7));
// Danach liegt er in der neuen Form da.
assertFalse(base64_encode($vmk) === (string)$s->werte[$vmkSchluessel]);
assertSame($vmk, $ruf($vm, 'readSessionVmk', 7));
});
test('Sperren entfernt Chiffrat und Begleit-Cookie', function () use ($sitzung, $db, $ruf, $vmkSchluessel) {
$s = $sitzung();
$vm = new VaultManager($db(), $s, 'app-key');
$ruf($vm, 'writeSessionVmk', 7, VaultCrypto::generateKey());
assertTrue(isset($s->werte[$vmkSchluessel]));
assertTrue($s->cookies !== []);
$vm->lock(7);
assertFalse(isset($s->werte[$vmkSchluessel]));
assertSame([], $s->cookies);
});
test('Ohne Ablage liefert das Lesen nichts', function () use ($sitzung, $db, $ruf) {
$s = $sitzung();
$vm = new VaultManager($db(), $s, 'app-key');
assertSame(null, $ruf($vm, 'readSessionVmk', 7));
});
// ----------------------------------------------------------------
// Das Begleit-Cookie selbst
// ----------------------------------------------------------------
test('Das Begleit-Cookie endet mit dem Browser und ist nicht per Skript lesbar', function () {
$quelle = (string)file_get_contents(dirname(__DIR__) . '/src/Core/Session.php');
$anfang = strpos($quelle, 'public function begleitGeheimnis(');
assertTrue($anfang !== false);
$rumpf = substr($quelle, $anfang, 1400);
assertTrue(str_contains($rumpf, "'expires' => 0"));
assertTrue(str_contains($rumpf, "'httponly' => true"));
// Ohne die Möglichkeit, das Cookie zu senden, darf kein Geheimnis entstehen.
assertTrue(str_contains($rumpf, 'headers_sent()'));
});
};