|
| 1 | +# YForm Encryption |
| 2 | + |
| 3 | +**REDAXO-Addon zur transparenten Feldverschlüsselung in YForm-Tabellen.** |
| 4 | + |
| 5 | +Verschlüsselt sensible YForm-Felder serverseitig mit libsodium (XSalsa20-Poly1305). Die Daten werden verschlüsselt in der Datenbank gespeichert und im Backend automatisch entschlüsselt angezeigt. Der Schlüssel liegt außerhalb des Webroots. |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## Voraussetzungen |
| 10 | + |
| 11 | +- REDAXO ≥ 5.18 |
| 12 | +- PHP ≥ 8.1 mit aktivierter `sodium`-Extension |
| 13 | +- YForm ≥ 5.0 |
| 14 | +- **Inkompatibel mit dem Addon `yform_export`** (eigener Export ist integriert) |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## Installation |
| 19 | + |
| 20 | +1. Addon über den REDAXO-Installer installieren oder manuell in `redaxo/src/addons/yform_encryption/` ablegen. |
| 21 | +2. Im REDAXO-Backend unter **YForm Encryption → Einstellungen** den Schlüsselpfad konfigurieren (Verzeichnis **außerhalb** des Webroots empfohlen). |
| 22 | +3. Schlüssel generieren lassen oder vorhandenen Schlüssel hinterlegen. |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## Konfiguration |
| 27 | + |
| 28 | +| Einstellung | Beschreibung | |
| 29 | +|---|---| |
| 30 | +| Schlüsselpfad | Absoluter Pfad zur Schlüsseldatei, idealerweise außerhalb des Webroots | |
| 31 | +| Schlüssel generieren | Neuen libsodium-Schlüssel erzeugen und speichern | |
| 32 | + |
| 33 | +--- |
| 34 | + |
| 35 | +## Schlüssel als Umgebungsvariable (empfohlen) |
| 36 | + |
| 37 | +Statt den Schlüssel in einer Datei zu speichern, kann er als Systemumgebungsvariable `YFORM_ENCRYPTION_KEY` hinterlegt werden. Das Addon prüft diese Variable **zuerst** – vor dem Dateipfad. |
| 38 | + |
| 39 | +Der Wert muss der **Base64-kodierte** 32-Byte-Schlüssel sein (wird im Backend unter „Einstellungen → Schlüssel anzeigen" angezeigt). |
| 40 | + |
| 41 | +### Plesk |
| 42 | + |
| 43 | +1. Plesk → Domain → **Apache & nginx-Einstellungen** |
| 44 | +2. Im Feld **„Zusätzliche Apache-Direktiven"** (HTTP oder HTTPS): |
| 45 | + ```apache |
| 46 | + SetEnv YFORM_ENCRYPTION_KEY "IhrBase64SchluesselHier==" |
| 47 | + ``` |
| 48 | +3. **Speichern** – Plesk schreibt die Direktive in die vHost-Konfiguration. |
| 49 | + |
| 50 | +Alternativ über die Plesk-Erweiterung **„Node.js/PHP Environment Variables"** oder direkt in der `php.ini` der Domain: |
| 51 | +```ini |
| 52 | +; Plesk → PHP-Einstellungen → Zusätzliche Direktiven |
| 53 | +env[YFORM_ENCRYPTION_KEY] = "IhrBase64SchluesselHier==" |
| 54 | +``` |
| 55 | + |
| 56 | +--- |
| 57 | + |
| 58 | +### Docker / docker-compose |
| 59 | + |
| 60 | +In `docker-compose.yml`: |
| 61 | +```yaml |
| 62 | +services: |
| 63 | + web: |
| 64 | + image: your-redaxo-image |
| 65 | + environment: |
| 66 | + YFORM_ENCRYPTION_KEY: "IhrBase64SchluesselHier==" |
| 67 | +``` |
| 68 | +
|
| 69 | +Oder mit einer `.env`-Datei (niemals ins Repository einchecken!): |
| 70 | +```dotenv |
| 71 | +# .env |
| 72 | +YFORM_ENCRYPTION_KEY=IhrBase64SchluesselHier== |
| 73 | +``` |
| 74 | +```yaml |
| 75 | +# docker-compose.yml |
| 76 | +services: |
| 77 | + web: |
| 78 | + env_file: |
| 79 | + - .env |
| 80 | +``` |
| 81 | + |
| 82 | +Beim direkten `docker run`: |
| 83 | +```bash |
| 84 | +docker run -e YFORM_ENCRYPTION_KEY="IhrBase64SchluesselHier==" your-redaxo-image |
| 85 | +``` |
| 86 | + |
| 87 | +--- |
| 88 | + |
| 89 | +### Apache (ohne Plesk) |
| 90 | + |
| 91 | +In der **VirtualHost-Konfiguration** oder `.htaccess`: |
| 92 | +```apache |
| 93 | +# /etc/apache2/sites-available/ihre-domain.conf ODER .htaccess |
| 94 | +SetEnv YFORM_ENCRYPTION_KEY "IhrBase64SchluesselHier==" |
| 95 | +``` |
| 96 | + |
| 97 | +> `.htaccess` ist weniger sicher als die VirtualHost-Konfiguration, da sie im Webroot liegen kann. Den `.htaccess`-Eintrag unbedingt **außerhalb des öffentlich zugänglichen Verzeichnisses** setzen oder per `Deny from all` schützen. |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +### Nginx + PHP-FPM |
| 102 | + |
| 103 | +Nginx selbst unterstützt `SetEnv` nicht – die Variable muss im **PHP-FPM-Pool** gesetzt werden: |
| 104 | + |
| 105 | +```ini |
| 106 | +# /etc/php/8.x/fpm/pool.d/ihre-domain.conf |
| 107 | +env[YFORM_ENCRYPTION_KEY] = "IhrBase64SchluesselHier==" |
| 108 | +``` |
| 109 | + |
| 110 | +Nach dem Bearbeiten PHP-FPM neu starten: |
| 111 | +```bash |
| 112 | +systemctl restart php8.x-fpm |
| 113 | +``` |
| 114 | + |
| 115 | +--- |
| 116 | + |
| 117 | +### Linux-Server (systemd / global) |
| 118 | + |
| 119 | +Für systemd-verwaltete PHP-FPM-Dienste in der Override-Konfiguration: |
| 120 | +```bash |
| 121 | +systemctl edit php8.x-fpm |
| 122 | +``` |
| 123 | +```ini |
| 124 | +[Service] |
| 125 | +Environment="YFORM_ENCRYPTION_KEY=IhrBase64SchluesselHier==" |
| 126 | +``` |
| 127 | + |
| 128 | +Global für alle Prozesse (weniger empfohlen): |
| 129 | +```bash |
| 130 | +# /etc/environment |
| 131 | +YFORM_ENCRYPTION_KEY="IhrBase64SchluesselHier==" |
| 132 | +``` |
| 133 | + |
| 134 | +--- |
| 135 | + |
| 136 | +### Sicherheitshinweis zu Umgebungsvariablen |
| 137 | + |
| 138 | +- `.env`-Dateien **niemals** in Git einchecken – `.gitignore`-Eintrag prüfen. |
| 139 | +- Berechtigungen von Konfigurationsdateien mit dem Schlüssel auf `640` oder `600` setzen. |
| 140 | +- In Shared-Hosting-Umgebungen lieber die **Schlüsseldatei außerhalb des Webroots** verwenden, da Umgebungsvariablen u.U. durch `phpinfo()` sichtbar werden. |
| 141 | + |
| 142 | +--- |
| 143 | + |
| 144 | +## Felder verschlüsseln |
| 145 | + |
| 146 | +Unter **YForm Encryption → Feldzuordnung** können pro YForm-Tabelle einzelne Felder zur Verschlüsselung markiert werden. |
| 147 | + |
| 148 | +Folgende YForm-Feldtypen können verschlüsselt werden: |
| 149 | + |
| 150 | +| Feldtyp | Beschreibung | Typischer Anwendungsfall | |
| 151 | +|---|---|---| |
| 152 | +| `text` | Einzeiliges Textfeld | Name, Vorname, Ausweisnummer, Tokens | |
| 153 | +| `textarea` | Mehrzeiliges Textfeld | Notizen, Anamnese, Freitexte mit sensiblem Inhalt | |
| 154 | +| `email` | E-Mail-Adresse | Kontaktdaten mit erhöhtem Schutzbedarf | |
| 155 | +| `phone` | Telefonnummer | Mobil-/Festnetznummern | |
| 156 | +| `url` | URL-Feld | Interne Links, Token-URLs | |
| 157 | +| `ip` | IP-Adresse | Logging, DSGVO-relevante Netzwerkdaten | |
| 158 | +| `fields_iban` | IBAN (fields-Addon) | Bankverbindungen – **besonders schützenswert** | |
| 159 | +| `fields_inline` | Inline-Gruppe (fields-Addon) | Kombinierte Felder z.B. Adresse + IBAN | |
| 160 | + |
| 161 | +**Nicht verschlüsselbar** (und auch nicht sinnvoll): |
| 162 | +- Auswahlfelder (`select`, `choice`, `checkbox`, `radio`) – zu wenige diskrete Werte, Verschlüsselung bringt keinen Sicherheitsgewinn |
| 163 | +- Relationsfelder (`be_relation`, `relation`) – nur IDs, kein personenbezogener Inhalt |
| 164 | +- Zahlenfelder (`integer`, `float`) – Datenbanktyp inkompatibel mit Ciphertext-String |
| 165 | +- Datumsfelder (`date`, `datetime`) – oft für Sortierung/Filterung nötig, Verschlüsselung würde Abfragen komplett blockieren |
| 166 | +- `be_media`, `upload` – Dateiname/Pfad, Dateiinhalt selbst liegt im Dateisystem |
| 167 | + |
| 168 | +Nach dem Speichern der Feldzuordnung werden **neue Einträge** automatisch verschlüsselt gespeichert. Bestehende Daten können über die **Migration**-Funktion nachträglich verschlüsselt werden. |
| 169 | + |
| 170 | +--- |
| 171 | + |
| 172 | +## Export (CSV / Excel) |
| 173 | + |
| 174 | +Da das Addon mit `yform_export` inkompatibel ist, bringt es einen eigenen Exporter mit: |
| 175 | + |
| 176 | +- In der YForm-Datenliste erscheinen **CSV**- und **Excel**-Buttons. |
| 177 | +- Die Daten werden beim Export automatisch entschlüsselt. |
| 178 | +- Die Excel-Datei enthält Metadaten (Autor = eingeloggter User, Exportzeitpunkt, Domain). |
| 179 | + |
| 180 | +--- |
| 181 | + |
| 182 | +## ⚠️ Einschränkung: YForm-Suche |
| 183 | + |
| 184 | +**Die integrierte YForm-Datenlisten-Suche kann verschlüsselte Felder nicht durchsuchen.** |
| 185 | + |
| 186 | +Der Grund: Die Suche arbeitet mit SQL-`LIKE`-Abfragen direkt auf der Datenbank. Verschlüsselte Werte liegen als Ciphertext vor – ein Klartext-Suchbegriff kann dort keine Treffer finden. |
| 187 | + |
| 188 | +**Betroffen sind alle als verschlüsselt markierten Felder.** Nicht-verschlüsselte Felder derselben Tabelle (z.B. `id`, Datumsfelder, Status-Felder) sind weiterhin normal durchsuchbar. |
| 189 | + |
| 190 | +**Workaround**: Den **CSV- oder Excel-Export** nutzen und lokal in der Tabellenkalkulation suchen – die exportierten Daten sind vollständig entschlüsselt. |
| 191 | + |
| 192 | +> **Empfehlung**: Nur wirklich sensible Felder verschlüsseln (IBAN, Ausweisdaten, Gesundheitsdaten, Tokens). Felder, nach denen häufig gesucht wird (z.B. Name, E-Mail), nur verschlüsseln wenn ein erhöhtes Breach-Risiko besteht – dann ist der Komfortverlust durch die eingeschränkte Suche bewusst in Kauf zu nehmen. |
| 193 | + |
| 194 | +--- |
| 195 | + |
| 196 | +## Autorisierung für Bulk-Operationen (SessionGuard) |
| 197 | + |
| 198 | +Massenoperationen wie das **nachträgliche Ver- oder Entschlüsseln bestehender Datensätze** (Migration) sind durch einen zusätzlichen Authentifizierungsschritt gesichert – den **SessionGuard**. |
| 199 | + |
| 200 | +**Ablauf:** |
| 201 | +1. Unter **YForm Encryption → Feldzuordnung** eine Bulk-Aktion auslösen (z.B. „Alle bestehenden Datensätze verschlüsseln"). |
| 202 | +2. Das System fordert eine **erneute Eingabe** von REDAXO-Benutzername und Passwort. |
| 203 | +3. Nach erfolgreicher Eingabe ist die Session für **30 Minuten** freigeschaltet. |
| 204 | +4. Weitere Bulk-Aktionen innerhalb dieses Zeitfensters erfordern keine erneute Eingabe. |
| 205 | +5. Nach Ablauf des Timeouts (oder manueller Sperrung) muss erneut authentifiziert werden. |
| 206 | + |
| 207 | +> Der Timeout ist unter **YForm Encryption → Einstellungen** konfigurierbar. |
| 208 | + |
| 209 | +**Warum dieser zusätzliche Schutz?** |
| 210 | +Bulk-Operationen schreiben alle Datensätze einer Tabelle um. Ein versehentlicher Klick – oder ein kompromittiertes Admin-Konto das gerade in der Session läuft – könnte sonst alle verschlüsselten Daten im Klartext in die DB schreiben. Die erneute Passwortabfrage stellt sicher, dass die Aktion bewusst von einer autorisierten Person ausgelöst wird. |
| 211 | + |
| 212 | +--- |
| 213 | + |
| 214 | +## Sicherheitshinweise |
| 215 | + |
| 216 | +- Den Schlüssel **regelmäßig sichern** – ohne Schlüssel sind verschlüsselte Daten unwiederbringlich verloren. |
| 217 | +- Den Schlüssel **niemals** im Webroot ablegen. |
| 218 | +- Den Schlüssel **nicht** in der Versionsverwaltung tracken. |
| 219 | +- Bei einem Server-Umzug den Schlüssel separat übertragen. |
| 220 | + |
| 221 | +--- |
| 222 | + |
| 223 | +## API-Referenz |
| 224 | + |
| 225 | +Alle Klassen liegen im Namespace `FriendsOfREDAXO\YFormEncryption\` und werden von REDAXO automatisch geladen. |
| 226 | + |
| 227 | +--- |
| 228 | + |
| 229 | +### `Helper` – Einstiegspunkt für externe Nutzung |
| 230 | + |
| 231 | +Die einfachste Klasse für den Zugriff aus Modulen, Addons oder Templates. |
| 232 | + |
| 233 | +```php |
| 234 | +use FriendsOfREDAXO\YFormEncryption\Helper; |
| 235 | +``` |
| 236 | + |
| 237 | +| Methode | Rückgabe | Beschreibung | |
| 238 | +|---|---|---| |
| 239 | +| `Helper::getDecryptedRow(string $tableName, int $id)` | `?array` | Einzelnen Datensatz laden und alle verschlüsselten Felder entschlüsseln | |
| 240 | +| `Helper::getDecryptedTable(string $tableName, string $where, string $orderBy, int $limit, int $offset)` | `array` | Mehrere Datensätze laden und entschlüsseln | |
| 241 | +| `Helper::decryptValue(string $value)` | `string` | Einzelnen Wert entschlüsseln (gibt Klartext zurück, auch wenn nicht verschlüsselt) | |
| 242 | +| `Helper::encryptValue(string $value)` | `string` | Einzelnen Wert verschlüsseln | |
| 243 | +| `Helper::isEncrypted(string $value)` | `bool` | Prüfen ob ein Wert verschlüsselt ist | |
| 244 | +| `Helper::getEncryptedFieldsForTable(string $tableName)` | `array` | Liste der verschlüsselten Feldnamen für eine Tabelle | |
| 245 | + |
| 246 | +**Beispiel:** |
| 247 | +```php |
| 248 | +$row = Helper::getDecryptedRow('rex_my_table', 42); |
| 249 | +echo $row['iban']; // entschlüsselt |
| 250 | +
|
| 251 | +$rows = Helper::getDecryptedTable('rex_my_table', 'status = 1', 'id DESC', 50); |
| 252 | +``` |
| 253 | + |
| 254 | +--- |
| 255 | + |
| 256 | +### `EncryptionService` – Kern-Verschlüsselung |
| 257 | + |
| 258 | +```php |
| 259 | +use FriendsOfREDAXO\YFormEncryption\EncryptionService; |
| 260 | +$enc = EncryptionService::getInstance(); |
| 261 | +``` |
| 262 | + |
| 263 | +| Methode | Rückgabe | Beschreibung | |
| 264 | +|---|---|---| |
| 265 | +| `getInstance()` | `self` | Singleton-Instanz | |
| 266 | +| `encrypt(string $plaintext)` | `string` | Text verschlüsseln (mit Prefix) | |
| 267 | +| `decrypt(string $encrypted)` | `string` | Text entschlüsseln | |
| 268 | +| `decryptSafe(string $value)` | `string` | Entschlüsseln ohne Exception – gibt Originalwert bei Fehler zurück | |
| 269 | +| `isEncrypted(string $value)` | `bool` | Präfix-Prüfung | |
| 270 | +| `encryptFields(array $data, array $fields)` | `array` | Mehrere Felder eines Datensatzes verschlüsseln | |
| 271 | +| `decryptFields(array $data, array $fields)` | `array` | Mehrere Felder eines Datensatzes entschlüsseln | |
| 272 | +| `getPrefix()` | `string` | Verschlüsselungs-Präfix (statisch) | |
| 273 | + |
| 274 | +--- |
| 275 | + |
| 276 | +### `FieldMapper` – Feldzuordnungen |
| 277 | + |
| 278 | +```php |
| 279 | +use FriendsOfREDAXO\YFormEncryption\FieldMapper; |
| 280 | +$mapper = FieldMapper::getInstance(); |
| 281 | +``` |
| 282 | + |
| 283 | +| Methode | Rückgabe | Beschreibung | |
| 284 | +|---|---|---| |
| 285 | +| `getInstance()` | `self` | Singleton-Instanz | |
| 286 | +| `getEncryptedFields(string $tableName)` | `array` | Alle verschlüsselten Feldnamen einer Tabelle | |
| 287 | +| `hasEncryptedFields(string $tableName)` | `bool` | Hat die Tabelle verschlüsselte Felder? | |
| 288 | +| `isFieldEncrypted(string $tableName, string $fieldName)` | `bool` | Ist ein bestimmtes Feld verschlüsselt? | |
| 289 | +| `addField(string $tableName, string $fieldName)` | `void` | Feld zur Verschlüsselung hinzufügen | |
| 290 | +| `removeField(string $tableName, string $fieldName)` | `void` | Feld aus der Verschlüsselung entfernen (Mapping löschen) | |
| 291 | +| `getAllMappings()` | `array` | Alle Zuordnungen als `['table' => ['field1', 'field2']]` | |
| 292 | +| `getAvailableTablesAndFields()` | `array` | Alle YForm-Tabellen mit verschlüsselbaren Feldern | |
| 293 | + |
| 294 | +--- |
| 295 | + |
| 296 | +### `KeyManager` – Schlüsselverwaltung |
| 297 | + |
| 298 | +```php |
| 299 | +use FriendsOfREDAXO\YFormEncryption\KeyManager; |
| 300 | +$km = KeyManager::getInstance(); |
| 301 | +``` |
| 302 | + |
| 303 | +| Methode | Rückgabe | Beschreibung | |
| 304 | +|---|---|---| |
| 305 | +| `getInstance()` | `self` | Singleton-Instanz | |
| 306 | +| `hasKey()` | `bool` | Ist ein Schlüssel verfügbar? | |
| 307 | +| `getKey()` | `string` | Rohen Schlüssel-Binärstring liefern | |
| 308 | +| `getKeySource()` | `string` | Herkunft des Schlüssels: `environment`, `file`, `data_dir` | |
| 309 | +| `getKeyFilePath()` | `string` | Pfad zur konfigurierten Schlüsseldatei | |
| 310 | +| `generateKey(string $location)` | `string` | Neuen Schlüssel erzeugen (`'file'` oder `'data_dir'`) | |
| 311 | + |
| 312 | +**Schlüsselpriorität**: `YFORM_ENCRYPTION_KEY` (Env) → konfigurierter Dateipfad → `data/`-Verzeichnis |
| 313 | + |
| 314 | +--- |
| 315 | + |
| 316 | +### `SessionGuard` – Bulk-Autorisierung |
| 317 | + |
| 318 | +```php |
| 319 | +use FriendsOfREDAXO\YFormEncryption\SessionGuard; |
| 320 | +$guard = SessionGuard::getInstance(); |
| 321 | +``` |
| 322 | + |
| 323 | +| Methode | Rückgabe | Beschreibung | |
| 324 | +|---|---|---| |
| 325 | +| `getInstance()` | `self` | Singleton-Instanz | |
| 326 | +| `isUnlocked()` | `bool` | Ist die Session aktuell entsperrt? | |
| 327 | +| `authenticate(string $login, string $password)` | `bool` | Authentifizieren und Session entsperren | |
| 328 | +| `unlock()` | `void` | Session manuell entsperren (ohne Passwort) | |
| 329 | +| `lock()` | `void` | Session sofort sperren | |
| 330 | +| `getRemainingTime()` | `int` | Verbleibende Sekunden bis zum Timeout | |
| 331 | +| `getTimeout()` | `int` | Konfigurierten Timeout in Sekunden liefern | |
| 332 | + |
| 333 | +--- |
| 334 | + |
| 335 | +### `ColumnMigrator` – Spaltentyp-Migration |
| 336 | + |
| 337 | +```php |
| 338 | +use FriendsOfREDAXO\YFormEncryption\ColumnMigrator; |
| 339 | +``` |
| 340 | + |
| 341 | +| Methode | Rückgabe | Beschreibung | |
| 342 | +|---|---|---| |
| 343 | +| `ColumnMigrator::checkColumns(string $tableName, array $fields)` | `array` | Prüfen welche Spalten auf `TEXT` erweitert werden müssen | |
| 344 | +| `ColumnMigrator::migrateColumns(string $tableName, array $fields)` | `int` | Spalten auf `TEXT` migrieren, gibt Anzahl geänderter Spalten zurück | |
| 345 | +| `ColumnMigrator::getWarnings()` | `array` | Warnungen der letzten Migration abrufen | |
| 346 | + |
| 347 | +> Ciphertexte sind länger als Klartexte – `VARCHAR`-Spalten werden bei Bedarf automatisch auf `TEXT` erweitert. |
| 348 | + |
| 349 | +--- |
| 350 | + |
| 351 | +## Lizenz |
| 352 | + |
| 353 | +MIT – siehe [LICENSE.md](LICENSE.md) |
| 354 | + |
| 355 | +**Autor**: [Thomas Skerbis](https://github.com/skerbis) / [FriendsOfREDAXO](https://github.com/FriendsOfREDAXO) |
| 356 | +**Support**: https://github.com/FriendsOfREDAXO/yform_encryption |
0 commit comments