Quelltext des aktuellen Standes
<?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)
);
});
};