OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

VaultCryptoTest.php

7 KB
<?php

declare(strict_types=1);

use OpenNIT\Vault\VaultCrypto;

/**
 * Deckt die Verschlüsselung des Passwort-Tresors ab.
 *
 * Der Tresor ist die Stelle, an der eine Schwäche unmittelbar Zugangsdaten
 * preisgibt. Geprüft werden deshalb die Eigenschaften, auf denen alles Weitere
 * aufbaut: Der Rundlauf erhält den Inhalt, jede Veränderung am Chiffrat wird
 * erkannt, und weder Nonce noch abgeleiteter Schlüssel wiederholen sich.
 */
return static function (): void {

    $schluessel = str_repeat("\x2a", 32);

    // ----------------------------------------------------------------
    // Rundlauf
    // ----------------------------------------------------------------

    test('Ein verschlüsselter Wert kommt unverändert zurück', function () use ($schluessel) {
        foreach (['geheim', '', 'Umlaute äöü und ein € Zeichen', str_repeat('x', 5000)] as $klartext) {
            $enc = VaultCrypto::encrypt($klartext, $schluessel);
            assertSame(
                $klartext,
                VaultCrypto::decrypt($enc['ciphertext'], $enc['nonce'], $enc['tag'], $schluessel)
            );
        }
    });

    test('Binärdaten überstehen den Rundlauf', function () use ($schluessel) {
        $klartext = random_bytes(256);
        $enc = VaultCrypto::encrypt($klartext, $schluessel);
        assertSame(
            $klartext,
            VaultCrypto::decrypt($enc['ciphertext'], $enc['nonce'], $enc['tag'], $schluessel)
        );
    });

    // ----------------------------------------------------------------
    // Erkennung von Veränderungen
    // ----------------------------------------------------------------

    /** Schlägt das Entschlüsseln fehl? */
    $scheitert = static function (callable $aktion): bool {
        try {
            $aktion();
            return false;
        } catch (\Throwable) {
            return true;
        }
    };

    test('Ein verändertes Chiffrat wird nicht entschlüsselt', function () use ($schluessel, $scheitert) {
        $enc = VaultCrypto::encrypt('geheim', $schluessel);
        $verdreht = $enc['ciphertext'];
        $verdreht[0] = chr(ord($verdreht[0]) ^ 0x01);

        assertTrue($scheitert(
            fn() => VaultCrypto::decrypt($verdreht, $enc['nonce'], $enc['tag'], $schluessel)
        ));
    });

    test('Ein vertauschtes Prüfzeichen wird erkannt', function () use ($schluessel, $scheitert) {
        // Ohne geprüftes Tag ließe sich der Klartext gezielt verändern.
        $enc = VaultCrypto::encrypt('geheim', $schluessel);
        $tag = $enc['tag'];
        $tag[0] = chr(ord($tag[0]) ^ 0x01);

        assertTrue($scheitert(
            fn() => VaultCrypto::decrypt($enc['ciphertext'], $enc['nonce'], $tag, $schluessel)
        ));
    });

    test('Ein falscher Schlüssel liefert keinen Klartext', function () use ($schluessel, $scheitert) {
        $enc = VaultCrypto::encrypt('geheim', $schluessel);

        assertTrue($scheitert(
            fn() => VaultCrypto::decrypt($enc['ciphertext'], $enc['nonce'], $enc['tag'], str_repeat("\x2b", 32))
        ));
    });

    test('Ein Schlüssel falscher Länge wird abgewiesen', function () use ($scheitert) {
        // Fail-closed: lieber ein Fehler als eine Verschlüsselung mit einem
        // stillschweigend aufgefüllten Schlüssel.
        assertTrue($scheitert(fn() => VaultCrypto::encrypt('geheim', 'zu kurz')));
        assertTrue($scheitert(fn() => VaultCrypto::encrypt('geheim', str_repeat('x', 33))));
    });

    // ----------------------------------------------------------------
    // Einmaligkeit
    // ----------------------------------------------------------------

    test('Derselbe Klartext ergibt nie dasselbe Chiffrat', function () use ($schluessel) {
        // Gleiche Passwörter im Tresor dürfen im Datenbestand nicht als gleich
        // erkennbar sein.
        $a = VaultCrypto::encrypt('geheim', $schluessel);
        $b = VaultCrypto::encrypt('geheim', $schluessel);

        assertTrue($a['nonce'] !== $b['nonce'], 'Die Nonce wiederholt sich');
        assertTrue($a['ciphertext'] !== $b['ciphertext'], 'Das Chiffrat wiederholt sich');
    });

    test('Die Nonce ist bei jedem Vorgang neu', function () use ($schluessel) {
        // Eine wiederverwendete Nonce hebt bei AES-GCM den Schutz auf.
        $nonces = [];
        for ($i = 0; $i < 50; $i++) {
            $nonces[] = VaultCrypto::encrypt('geheim', $schluessel)['nonce'];
        }
        assertSame(50, count(array_unique($nonces)));
        assertSame(12, strlen($nonces[0]));
    });

    // ----------------------------------------------------------------
    // Schlüsselableitung
    // ----------------------------------------------------------------

    test('Dasselbe Passwort mit demselben Salt ergibt denselben Schlüssel', function () {
        // Voraussetzung dafür, dass ein Tresor nach dem Anmelden wieder aufgeht.
        $salt = str_repeat("\x07", 32);
        assertSame(
            VaultCrypto::deriveKey('Passwort', $salt),
            VaultCrypto::deriveKey('Passwort', $salt)
        );
    });

    test('Ein anderes Salt ergibt einen anderen Schlüssel', function () {
        // Damit sind gleiche Passwörter verschiedener Benutzer nicht gleich
        // abgeleitet und eine vorberechnete Tabelle nutzlos.
        assertTrue(
            VaultCrypto::deriveKey('Passwort', str_repeat("\x07", 32))
            !== VaultCrypto::deriveKey('Passwort', str_repeat("\x08", 32))
        );
    });

    test('Ein anderes Passwort ergibt einen anderen Schlüssel', function () {
        $salt = str_repeat("\x07", 32);
        assertTrue(VaultCrypto::deriveKey('Passwort', $salt) !== VaultCrypto::deriveKey('Passwort ', $salt));
    });

    test('Der abgeleitete Schlüssel hat die erwartete Länge', function () {
        assertSame(32, strlen(VaultCrypto::deriveKey('Passwort', str_repeat("\x07", 32))));
        assertSame(32, strlen(VaultCrypto::deriveAdminKey('anwendungsschluessel', str_repeat("\x07", 32))));
        assertSame(32, strlen(VaultCrypto::generateKey()));
        assertSame(32, strlen(VaultCrypto::generateSalt()));
    });

    test('Erzeugte Schlüssel und Salts wiederholen sich nicht', function () {
        $werte = [];
        for ($i = 0; $i < 30; $i++) {
            $werte[] = VaultCrypto::generateKey();
            $werte[] = VaultCrypto::generateSalt();
        }
        assertSame(60, count(array_unique($werte)));
    });

    test('Die Ableitung aus dem Anwendungsschlüssel ist von der aus dem Passwort verschieden', function () {
        // Beide Wege sichern denselben Tresorschlüssel; sie dürfen nicht
        // ineinander übergehen.
        $salt = str_repeat("\x07", 32);
        assertTrue(
            VaultCrypto::deriveKey('geheim', $salt) !== VaultCrypto::deriveAdminKey('geheim', $salt)
        );
    });
};