29.01.2020
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.
[
{
"attrs": [],
"content": "Die DSGVO ermutigt uns, den Schutz personenbezogener Daten m\u00f6glichst fr\u00fch in unsere Anwendungen einzubauen:",
"id": "_I9SvvxCX2",
"type": "paragraph"
},
{
"attrs": [],
"content": "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 (\u2018data protection by design\u2019).",
"id": "_lwfeq8cc2",
"type": "blockquote"
},
{
"attrs": [],
"content": "Data protection by design meint dabei Pseudonymisierung (wo m\u00f6glich) und Verschl\u00fcsselung von Daten.",
"id": "_t2hQ3aAM8",
"type": "paragraph"
},
{
"attrs": [],
"content": "Wenn man also z.B. Kundendaten in einer Datenbank speichert, sollte man sie da, wo man sie nicht pseudonymisieren kann, verschl\u00fcsseln (encryption at rest). Dabei ausschlie\u00dflich auf die Verschl\u00fcsselung des Dateisystems, auf dem die Datendateien liegen, zu setzen, erzeugt eine tr\u00fcgerische Sicherheit: Hat ein Angreifer Zugriff auf das eingeh\u00e4ngte Dateisystem oder die Abfrage-Schnittstelle, z.B. durch gestohlene Passw\u00f6rter oder SQL-Injection, sind die Daten nicht mehr durch diese Form der Verschl\u00fcsselung gesch\u00fctzt.",
"id": "_hgcVKMSxp",
"type": "paragraph"
},
{
"attrs": [],
"content": "Es ist also deutlich wirksamer, personenbezogene Daten auf der Anwendungsebene zu versch\u00fcsseln (application layer encryption at rest), als nur auf der Datenbank-Ebene. Nat\u00fcrlich braucht man dann ein sicheres Schl\u00fcssel-Management, das ist jedoch nicht gegenstand dieses Beitrags.",
"id": "_OdyyLFu2J",
"type": "paragraph"
},
{
"attrs": [],
"content": "Wenn man in der gl\u00fccklichen Lage ist, mit Spring zu arbeiten, ist dies einfacher m\u00f6glich, als man annehmen mag. Das github-Projekt https://github.com/neuland/persistent-privacy skizziert anhand dreier Beispiele, wie man diese Aufgabe f\u00fcr die eigene Business-Logik transparent umsetzen kann:",
"id": "_qrD0Wd80I",
"type": "paragraph"
},
{
"attrs": [],
"content": "Jackson: > privacyProtectionListener() {\n CryptoService cryptoService = new ExampleCryptoService(keyRepository);\n ObjectMapper personalDataObjectMapper = new ObjectMapper();\n return new PrivacyProtectionListener(cryptoService, personalDataObjectMapper);\n}",
"id": "_zfq3p8u7l",
"type": "code"
},
{
"attrs": [],
"content": "Wenn man nun einen Beispiel-Customer persistiert, wird folgendes Dokument in die Mongo-DB geschrieben:",
"id": "_a93U4sZhp",
"type": "paragraph"
},
{
"attrs": {
"language": "json"
},
"content": "{\n \"_id\": ObjectId(\"5e3185f90a37746efb7bef8c\"),\n \"_class\": \"de.neuland.persistentprivacy.mongodb.example.Customer\",\n \"_personal_data\": {\n \"data\": \"CFUiChpIDUTr54zjQ39WTmWd3mY6Qx1X2S...\",\n \"iv\": \"x4uhguByoBTC3fxU\",\n keyRef\": \"key\"\n },\n email\": \"484c01bc9dedf9d4d59e7d7dbec53377732af0a7099d4e64b16b89882dd97d1e\n}",
"id": "_fwnwo78ls",
"type": "code"
},
{
"attrs": [],
"content": "JPA/Hibernate",
"id": "_6oiwhnrk6",
"type": "h2"
},
{
"attrs": [],
"content": "Der skizzierte Ansatz funktioniert mit Hibernate als Provider f\u00fcr JPA. Einstiegspunkt ist der hibernateProperties) {\n hibernateProperties.put(\"hibernate.ejb.interceptor\", cryptoInterceptor);\n }\n}",
"id": "_0sgozfacc",
"type": "code"
},
{
"attrs": [],
"content": "Dieser injiziert einen
jackson-module/
",
"id": "_gntbm8jcq",
"type": "ul"
},
{
"attrs": [],
"content": "Mongo-DB: mongo-adapter/
",
"id": "_y24leginf",
"type": "ul"
},
{
"attrs": [],
"content": "JPA mit Hibernate: jpa-hibernate-adapter/
",
"id": "_myoz3y09u",
"type": "ul"
},
{
"attrs": [],
"content": "Alle Beispiele verwenden die folgende zu persistierende Entity:",
"id": "_lqe6FBpqC",
"type": "paragraph"
},
{
"attrs": {
"language": "java"
},
"content": "class Customer {\n @Pseudonymized\n private String emailAddress;\n private String encodedPassword;\n @PersonalData\n private String firstName;\n @PersonalData\n private String lastName;\n}",
"id": "_x027jmdn5",
"type": "code"
},
{
"attrs": [],
"content": "Die Besonderheit sind hier die Annotationen @Pseudonymized
und @PersonalData
. Die Auswirkungen auf Serialisierung und Persistierung zeige ich in den folgenden Abschnitten.",
"id": "_dicT76oJl",
"type": "paragraph"
},
{
"attrs": [],
"content": "Jackson",
"id": "_8v6nmfdac",
"type": "h2"
},
{
"attrs": [],
"content": "Die einfachste Art, personenbezogene Daten verschl\u00fcsselt zu serialisieren, ist, im ObjectMapper
das PersonalDataEncryptionModule zu registrieren:",
"id": "_ozYY54O5c",
"type": "paragraph"
},
{
"attrs": {
"language": "java"
},
"content": "objectMapper.registerModule(\n new PersonalDataEncryptionModule(tinkCryptoService)\n);",
"id": "_n3a78xjbm",
"type": "code"
},
{
"attrs": [],
"content": "Wenn mann dann eine Customer-Instanz zu JSON serialisiert, sieht das wie folgt aus:",
"id": "_0N1gmOToc",
"type": "paragraph"
},
{
"attrs": {
"language": "json"
},
"content": "{\n \"encodedPassword\": \"$2y$12$xrh9tvl5WHna89.Od21EfuLanukZFYszmpuyNJwNTdmfAmHdQZW4W\",\n \"emailAddress\": \"a81f0dc77d88cc35d51f03595d993607ed14285dbe8010fdba9b90df7a992844\",\n \"$personal_data\": {\n \"data\": \"ASYgjLi9JXi72eHZcpuk+sRSuz4WtK8HsUuhKIYuS...\",\n \"keyRef\": \"639667384\"\n }\n}",
"id": "_xhkkcuazs",
"type": "code"
},
{
"attrs": [],
"content": "Alle Attribute, die als @PersonalData
annotiert sind, sind verschl\u00fcsselt im Element $personal_data
abgelegt. Das Passwort muss nicht verschl\u00fcsselt werden, da es ja bereits gehashed bzw. bcrypted ist. F\u00fcr jemanden, der noch unsichere Hashes verwendet, kann das nat\u00fcrlich der Ausweg f\u00fcr Helden sein ;-)",
"id": "_XXwRH8lps",
"type": "paragraph"
},
{
"attrs": [],
"content": "Die @Pseudonymized
-Attribute sind im JSON pseudonymisiert (f\u00fcr das Beispiel einfach als SHA-3-Hash) abgelegt. Sie sind ebenfalls verschl\u00fcsselt in $personal_data
abgelegt, damit sie beim Laden wieder hergestellt werden k\u00f6nnen. Die Idee dahinter ist, dass man so einen Kunden \u00fcber seine E-Mail-Adresse finden kann, ohne sie im Klartext ablegen zu m\u00fcssen.",
"id": "_QvzREQ9os",
"type": "paragraph"
},
{
"attrs": [],
"content": "Beim Deserialisieren wird das synthetische Element $personal_data
transparent in die urspr\u00fcnglichen Attribute entschl\u00fcsselt.",
"id": "_Mu1DchX8T",
"type": "paragraph"
},
{
"attrs": [],
"content": "Mongodb",
"id": "_hnlkmn5jj",
"type": "h2"
},
{
"attrs": [],
"content": "Die Verschl\u00fcsselung personenbezogener Daten funktioniert bei der MongoDB \u00e4hnlich wie bei Jackson. Die sch\u00fctzenswerten Eintr\u00e4ge werden aus dem BSON-Dokument entfernt, verschl\u00fcsselt, und in ein synthetisches Element geschrieben.",
"id": "_WGRuad0aZ",
"type": "paragraph"
},
{
"attrs": [],
"content": "Diese Aufgabe \u00fcbernimmt der PrivacyProtectionListener
, der einfach per Spring-Configuration injiziert wird:",
"id": "_XIADoVOX2",
"type": "paragraph"
},
{
"attrs": {
"language": "java"
},
"content": "@Bean\npublic ApplicationListenerInterceptorInjector
:",
"id": "_j0L4vkj4n",
"type": "paragraph"
},
{
"attrs": {
"language": "java"
},
"content": "@Component\npublic class InterceptorInjector implements HibernatePropertiesCustomizer {\n @Autowired\n private CryptoInterceptor cryptoInterceptor;\n @Override\n public void customize(MapCryptoInterceptor
in Hibernate. Der CryptoInterceptor verschl\u00fcsselt die Daten bevor sie in die DB geschrieben werden. In der DB sieht das dann wie folgt aus:",
"id": "_gctawXE7m",
"type": "paragraph"
},
{
"attrs": [],
"content": "ID | 3\nEMAIL | _crypt:key:Ftfzkhh0mT5NOEIt:nTpxxW1RGuBfJ9aIw3xT4iWm9mpOkN4JR5l0WlU5bbNlTw==\nFIRST_NAME | _crypt:key:UY3i46Q+ChRdkHt/:MXHEYgLFoog9+X0b0U4eNQyMMuQ=\nLAST_NAME | _crypt:key:QxFxK7W+rPD3Rvpm:JVl+lmHjnoe3D8gRiuW7vV75S1LmGBMRRzk=",
"id": "_qleug7y7d",
"type": "code"
},
{
"attrs": [],
"content": "Dieser Ansatz hat jedoch einige Nachteile:",
"id": "_LBVei0qgo",
"type": "paragraph"
},
{
"attrs": [],
"content": "Da durch den CryptoInterceptor auch der Wert im Speicher verschl\u00fcsselt wird, m\u00fcssen diese nach dem Flush wieder entschl\u00fcsselt werden. Der Overhead ist hier also gr\u00f6\u00dfer als bei den anderen L\u00f6sungen.",
"id": "_08ia5xnql",
"type": "ul"
},
{
"attrs": [],
"content": "Diese Methode funktioniert nur f\u00fcr String-Attribute.",
"id": "_znynafcy3",
"type": "ul"
},
{
"attrs": [],
"content": "Pseudonymisieren ist hier nicht m\u00f6glich, da es keinen Platz zum Ablegen des Pseudonyms und des verschl\u00fcsselten Wertes gibt.",
"id": "_zw6o3zr0i",
"type": "ul"
},
{
"attrs": [],
"content": "Der verschl\u00fcsselte Wert ist l\u00e4nger als der Klartext, wird also unter Umst\u00e4nden nicht in die Spalte passen.",
"id": "_k1c5kr327",
"type": "ul"
},
{
"attrs": [],
"content": "Eine sch\u00f6nere Alternative ist die Verwendung einer PostgreSQL-Datenbank und das Speichern personenbezogener Daten in einer JSONB-Spalte. Dort kann man dann die Verschl\u00fcsselung w\u00e4hrend der Serialisierung anwenden. Aber das ist eine Geschichte f\u00fcr einen anderen Tag, und sie soll an einem anderen Tag erz\u00e4hlt werden.",
"id": "_BUJX0ulZn",
"type": "paragraph"
}
]