OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

SecretsAtRestTest.php

4 KB
<?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));
    });
};