Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
use OpenNIT\Auth\TotpSecretStore;
use OpenNIT\Core\Database;
use OpenNIT\Security\CredentialCrypto;
/**
* Deckt die verschlüsselte Ablage der Zugangsdaten ab.
*
* TOTP-Geheimnis und Asset-Sync-Schlüssel lagen im Klartext in der Datenbank.
* Beide werden jetzt mit AES-256-GCM abgelegt; Bestandswerte bleiben lesbar und
* werden beim ersten Zugriff nachgezogen, damit eine laufende Instanz durch die
* Umstellung weder Anmeldungen noch den Agenten verliert.
*/
return static function (): void {
$_ENV['APP_KEY'] = 'test-schluessel-fuer-geheimnisse-32b';
/** Datenbank-Attrappe, die Schreibvorgänge mitschreibt. */
$db = static function (): Database {
return new class extends Database {
/** @var array<int,array{table:string,data:array}> */
public array $writes = [];
public function __construct()
{
parent::__construct([]);
}
public function update(string $table, array $data, string $where = '', array $params = []): int
{
$this->writes[] = ['table' => $table, 'data' => $data];
return 1;
}
};
};
// ----------------------------------------------------------------
// Ablageform
// ----------------------------------------------------------------
test('Das Geheimnis wird nicht im Klartext abgelegt', function () {
$abgelegt = TotpSecretStore::forStorage('JBSWY3DPEHPK3PXP');
assertTrue(CredentialCrypto::isEncrypted($abgelegt), 'Wert ist nicht verschlüsselt');
assertFalse(str_contains($abgelegt, 'JBSWY3DPEHPK3PXP'), 'Klartext steht im gespeicherten Wert');
});
test('Der abgelegte Wert lässt sich wieder lesen', function () use ($db) {
$datenbank = $db();
$abgelegt = TotpSecretStore::forStorage('JBSWY3DPEHPK3PXP');
assertSame('JBSWY3DPEHPK3PXP', TotpSecretStore::reveal($datenbank, 7, $abgelegt));
assertSame(0, count($datenbank->writes), 'Ein bereits verschlüsselter Wert wird erneut geschrieben');
});
test('Zwei Ablagen desselben Geheimnisses unterscheiden sich', function () {
// Salt und Nonce werden je Vorgang neu gezogen; gleiche Geheimnisse
// sind im Datenbestand deshalb nicht aneinander zu erkennen.
assertTrue(
TotpSecretStore::forStorage('JBSWY3DPEHPK3PXP') !== TotpSecretStore::forStorage('JBSWY3DPEHPK3PXP'),
'Die Ablage ist über die Vorgänge hinweg gleich'
);
});
test('Der verschlüsselte Wert passt nicht mehr in die frühere Spaltenbreite', function () {
// Begründet die Verbreiterung in der Migration: VARCHAR(64) hätte den
// Wert abgeschnitten und damit den zweiten Faktor unbrauchbar gemacht.
assertTrue(
strlen(TotpSecretStore::forStorage('JBSWY3DPEHPK3PXP')) > 64,
'Die Spaltenbreite müsste nicht angepasst werden'
);
});
// ----------------------------------------------------------------
// Bestandswerte
// ----------------------------------------------------------------
test('Ein Klartext-Geheimnis bleibt gültig und wird nachgezogen', function () use ($db) {
$datenbank = $db();
assertSame('JBSWY3DPEHPK3PXP', TotpSecretStore::reveal($datenbank, 7, 'JBSWY3DPEHPK3PXP'));
assertSame(1, count($datenbank->writes), 'Der Bestandswert wurde nicht nachgezogen');
assertSame('users', $datenbank->writes[0]['table']);
assertTrue(
CredentialCrypto::isEncrypted((string)$datenbank->writes[0]['data']['totp_secret']),
'Nachgezogen wurde ein unverschlüsselter Wert'
);
});
test('Ein leeres Feld bleibt leer und löst keinen Schreibvorgang aus', function () use ($db) {
$datenbank = $db();
assertSame('', TotpSecretStore::reveal($datenbank, 7, null));
assertSame('', TotpSecretStore::reveal($datenbank, 7, ''));
assertSame(0, count($datenbank->writes));
});
};