Der Schutz personenbezogener Daten durch Verschlüsselung auf Anwendungsebene ist leichter umzusetzen, als man denken mag. Dieser Artikel skizziert Ansätze für das Verschlüsseln und Pseudonymisieren auf der Anwendungsebene.
Die DSGVO ermutigt uns, den Schutz personenbezogener Daten möglichst früh in unsere Anwendungen einzubauen:
Companies/organisations are encouraged to implement technical and organisational measures, at the earliest stages of the design of the processing operations, in such a way that safeguards privacy and data protection principles right from the start (‘data protection by design’).
Data protection by design meint dabei Pseudonymisierung (wo möglich) und Verschlüsselung von Daten.
Wenn man also z.B. Kundendaten in einer Datenbank speichert, sollte man sie da, wo man sie nicht pseudonymisieren kann, verschlüsseln (encryption at rest). Dabei ausschließlich auf die Verschlüsselung des Dateisystems, auf dem die Datendateien liegen, zu setzen, erzeugt eine trügerische Sicherheit: Hat ein Angreifer Zugriff auf das eingehängte Dateisystem oder die Abfrage-Schnittstelle, z.B. durch gestohlene Passwörter oder SQL-Injection, sind die Daten nicht mehr durch diese Form der Verschlüsselung geschützt.
Es ist also deutlich wirksamer, personenbezogene Daten auf der Anwendungsebene zu verschüsseln (application layer encryption at rest), als nur auf der Datenbank-Ebene. Natürlich braucht man dann ein sicheres Schlüssel-Management, das ist jedoch nicht gegenstand dieses Beitrags.
Wenn man in der glücklichen Lage ist, mit Spring zu arbeiten, ist dies einfacher möglich, als man annehmen mag. Das github-Projekt https://github.com/neuland/persistent-privacy skizziert anhand dreier Beispiele, wie man diese Aufgabe für die eigene Business-Logik transparent umsetzen kann:
Alle Beispiele verwenden die folgende zu persistierende Entity:
class Customer {
@Pseudonymized
private String emailAddress;
private String encodedPassword;
@PersonalData
private String firstName;
@PersonalData
private String lastName;
}
Die Besonderheit sind hier die Annotationen @Pseudonymized
und @PersonalData
. Die Auswirkungen auf Serialisierung und Persistierung zeige ich in den folgenden Abschnitten.
Jackson
Die einfachste Art, personenbezogene Daten verschlüsselt zu serialisieren, ist, im ObjectMapper
das PersonalDataEncryptionModule zu registrieren:
objectMapper.registerModule(
new PersonalDataEncryptionModule(tinkCryptoService)
);
Wenn mann dann eine Customer-Instanz zu JSON serialisiert, sieht das wie folgt aus:
{
"encodedPassword": "$2y$12$xrh9tvl5WHna89.Od21EfuLanukZFYszmpuyNJwNTdmfAmHdQZW4W",
"emailAddress": "a81f0dc77d88cc35d51f03595d993607ed14285dbe8010fdba9b90df7a992844",
"$personal_data": {
"data": "ASYgjLi9JXi72eHZcpuk+sRSuz4WtK8HsUuhKIYuS...",
"keyRef": "639667384"
}
}
Alle Attribute, die als @PersonalData
annotiert sind, sind verschlüsselt im Element $personal_data
abgelegt. Das Passwort muss nicht verschlüsselt werden, da es ja bereits gehashed bzw. bcrypted ist. Für jemanden, der noch unsichere Hashes verwendet, kann das natürlich der Ausweg für Helden sein ;-)
Die @Pseudonymized
-Attribute sind im JSON pseudonymisiert (für das Beispiel einfach als SHA-3-Hash) abgelegt. Sie sind ebenfalls verschlüsselt in $personal_data
abgelegt, damit sie beim Laden wieder hergestellt werden können. Die Idee dahinter ist, dass man so einen Kunden über seine E-Mail-Adresse finden kann, ohne sie im Klartext ablegen zu müssen.
Beim Deserialisieren wird das synthetische Element $personal_data
transparent in die ursprünglichen Attribute entschlüsselt.
Mongodb
Die Verschlüsselung personenbezogener Daten funktioniert bei der MongoDB ähnlich wie bei Jackson. Die schützenswerten Einträge werden aus dem BSON-Dokument entfernt, verschlüsselt, und in ein synthetisches Element geschrieben.
Diese Aufgabe übernimmt der PrivacyProtectionListener
, der einfach per Spring-Configuration injiziert wird:
@Bean
public ApplicationListener<MongoMappingEvent<?>> privacyProtectionListener() {
CryptoService cryptoService = new ExampleCryptoService(keyRepository);
ObjectMapper personalDataObjectMapper = new ObjectMapper();
return new PrivacyProtectionListener(cryptoService, personalDataObjectMapper);
}
Wenn man nun einen Beispiel-Customer persistiert, wird folgendes Dokument in die Mongo-DB geschrieben:
{
"_id": ObjectId("5e3185f90a37746efb7bef8c"),
"_class": "de.neuland.persistentprivacy.mongodb.example.Customer",
"_personal_data": {
"data": "CFUiChpIDUTr54zjQ39WTmWd3mY6Qx1X2S...",
"iv": "x4uhguByoBTC3fxU",
keyRef": "key"
},
email": "484c01bc9dedf9d4d59e7d7dbec53377732af0a7099d4e64b16b89882dd97d1e
}
JPA/Hibernate
Der skizzierte Ansatz funktioniert mit Hibernate als Provider für JPA. Einstiegspunkt ist der InterceptorInjector
:
@Component
public class InterceptorInjector implements HibernatePropertiesCustomizer {
@Autowired
private CryptoInterceptor cryptoInterceptor;
@Override
public void customize(Map<String, Object> hibernateProperties) {
hibernateProperties.put("hibernate.ejb.interceptor", cryptoInterceptor);
}
}
Dieser injiziert einen CryptoInterceptor
in Hibernate. Der CryptoInterceptor verschlüsselt die Daten bevor sie in die DB geschrieben werden. In der DB sieht das dann wie folgt aus:
ID | 3
EMAIL | _crypt:key:Ftfzkhh0mT5NOEIt:nTpxxW1RGuBfJ9aIw3xT4iWm9mpOkN4JR5l0WlU5bbNlTw==
FIRST_NAME | _crypt:key:UY3i46Q+ChRdkHt/:MXHEYgLFoog9+X0b0U4eNQyMMuQ=
LAST_NAME | _crypt:key:QxFxK7W+rPD3Rvpm:JVl+lmHjnoe3D8gRiuW7vV75S1LmGBMRRzk=
Dieser Ansatz hat jedoch einige Nachteile:
Eine schönere Alternative ist die Verwendung einer PostgreSQL-Datenbank und das Speichern personenbezogener Daten in einer JSONB-Spalte. Dort kann man dann die Verschlüsselung während der Serialisierung anwenden. Aber das ist eine Geschichte für einen anderen Tag, und sie soll an einem anderen Tag erzählt werden.