- FinTS-Abruf für Comdirect/DKB/Sparkassen via nemiah/php-fints (4.1) inkl. photoTAN-Eingabe-Modal und Multi-Konto-Auswahl - Neue Tabelle bank_sources (CSV/API/FinTS) ersetzt bank_import_formats und bank_connections, Backfill in Migration 068 - Symmetrische Verschlüsselung sensibler Felder (PayPal-Signature, FinTS-PIN) mit AES-256-GCM, APP_KEY in .env - Routes /bank/fints* → /bank/direkt*; "+ Neue Bank…"-Dialog erlaubt freie API/FinTS-Anbieter - Scripts/encrypt_bank_secrets.php als idempotenter Migrations-Helfer - Neue Tests 36 (Crypto) und 37 (BankSource)
75 lines
3.3 KiB
PHP
75 lines
3.3 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Steuerkram\Config;
|
||
use Steuerkram\Services\Crypto;
|
||
|
||
/**
|
||
* Test 36: Crypto – symmetrische Verschlüsselung für sensible Felder.
|
||
*/
|
||
class CryptoTest extends TestCase
|
||
{
|
||
protected string $testName = 'Test 36: Crypto – AES-256-GCM Roundtrip und Idempotenz';
|
||
|
||
public function run(): void
|
||
{
|
||
// Test-Key setzen und Config neu laden, damit Config::get('app_key') greift.
|
||
$oldKey = $_ENV['APP_KEY'] ?? null;
|
||
$_ENV['APP_KEY'] = str_repeat('a1', 32); // 64 Hex-Zeichen
|
||
Config::load();
|
||
|
||
try {
|
||
// 1. Roundtrip
|
||
$plain = 'streng-geheimes-passwort!';
|
||
$cipher = Crypto::encrypt($plain);
|
||
$this->assert($cipher !== $plain, 'Verschlüsselter Wert unterscheidet sich vom Klartext');
|
||
$this->assert(str_starts_with($cipher, 'enc:v1:'), 'Verschlüsselter Wert hat Magic-Prefix enc:v1:');
|
||
$this->assertEquals($plain, Crypto::decrypt($cipher), 'Entschlüsselung liefert Originaltext');
|
||
|
||
// 2. Leerer String
|
||
$this->assertEquals('', Crypto::encrypt(''), 'Leerer Eingabewert wird leer zurückgegeben (encrypt)');
|
||
$this->assertEquals('', Crypto::decrypt(''), 'Leerer Eingabewert wird leer zurückgegeben (decrypt)');
|
||
|
||
// 3. Idempotenz: bereits verschlüsselt → unverändert
|
||
$cipher2 = Crypto::encrypt($cipher);
|
||
$this->assertEquals($cipher, $cipher2, 'encrypt() auf bereits verschlüsselten Wert ist idempotent');
|
||
|
||
// 4. Migration: Klartext-Wert ohne Prefix → unverändert durch decrypt()
|
||
$this->assertEquals('roher-text', Crypto::decrypt('roher-text'), 'decrypt() reicht Klartext (ohne Prefix) unverändert durch');
|
||
|
||
// 5. Zwei Verschlüsselungen desselben Klartextes ergeben verschiedene Cipher (zufälliges IV)
|
||
$a = Crypto::encrypt('hallo');
|
||
$b = Crypto::encrypt('hallo');
|
||
$this->assert($a !== $b, 'Zwei encrypt()-Aufrufe ergeben verschiedene Cipher (IV ist zufällig)');
|
||
$this->assertEquals('hallo', Crypto::decrypt($a), 'Cipher A entschlüsselt korrekt');
|
||
$this->assertEquals('hallo', Crypto::decrypt($b), 'Cipher B entschlüsselt korrekt');
|
||
|
||
// 6. Falscher Key → Exception
|
||
$_ENV['APP_KEY'] = str_repeat('b2', 32);
|
||
Config::load();
|
||
$threw = false;
|
||
try {
|
||
Crypto::decrypt($cipher);
|
||
} catch (\RuntimeException) {
|
||
$threw = true;
|
||
}
|
||
$this->assert($threw, 'decrypt() mit falschem Schlüssel wirft RuntimeException');
|
||
|
||
// 7. isEncrypted-Helper
|
||
$_ENV['APP_KEY'] = str_repeat('a1', 32);
|
||
Config::load();
|
||
$this->assert(Crypto::isEncrypted($cipher), 'isEncrypted erkennt verschlüsselten Wert');
|
||
$this->assert(!Crypto::isEncrypted('plain'), 'isEncrypted erkennt Klartext nicht als verschlüsselt');
|
||
$this->assert(!Crypto::isEncrypted(''), 'isEncrypted ist false für leeren String');
|
||
} finally {
|
||
// Original-Key wiederherstellen
|
||
if ($oldKey === null) {
|
||
unset($_ENV['APP_KEY']);
|
||
} else {
|
||
$_ENV['APP_KEY'] = $oldKey;
|
||
}
|
||
Config::load();
|
||
}
|
||
}
|
||
}
|