Das Wort der Pseud­ony­mi­sie­rung hat Kon­junk­tur – spä­tes­tens seit der euro­päi­schen Daten­schutz­grund­ver­ord­nung (GDPR). Gemeint ist eine ein­fa­che Tren­nung von Iden­ti­fi­ka­ti­ons­merk­ma­len und Daten. „Ein­fach“, weil das Ziel klar ist. Die prak­ti­sche Umset­zung kann jedoch stark vari­ie­ren und im schlimms­ten Fall den Daten­fluss ein­schrän­ken. Dabei müs­sen auch sen­si­ble Daten inner­halb von Geschäfts­pro­zes­sen zur rich­ti­gen Zeit, am rich­ti­gen Ort für die rich­ti­gen Per­so­nen ver­füg­bar sein. Das erfor­dert einen klu­gen Umgang mit den recht­li­chen Anfor­de­run­gen. Mit prä­zi­sen und fle­xi­blen Demas­kie­rungs­stan­dards kön­nen Daten­schutz und Daten­fluss zusam­men­ar­bei­ten. Der fol­gende Blog stellt dafür die dyna­mi­sche Mas­kie­rung mit Apa­che Ran­ger in Hive vor.

Zwei Eigen­schaf­ten der Maskierung

Eine Mas­kie­rung muss zunächst zwei Eigen­schaf­ten erfül­len. Zum einen soll sie die Iden­ti­tät der Per­son unkennt­lich machen. In man­chen Fäl­len reicht dafür eine Tren­nung von Name und Daten, in ande­ren Fäl­len müs­sen wei­tere Iden­ti­fi­ka­ti­ons­merk­male mas­kiert wer­den (z. B. durch höher­stu­fige Aggre­ga­tion). Die zweite Eigen­schaft ist die Leser­lich­keit – beson­ders wich­tig für die interne und externe Ergeb­nis­kom­mu­ni­ka­tion. Das Data­ware­house-Sys­tem im Hadoop Öko­sys­tem Apa­che Hive sorgt mit nati­ven Mas­kie­rungs­funk­tio­nen wie z.B. einem SHA265-Hash­ing für eine sichere Mas­kie­rung mit gerin­ger Kol­li­si­ons­wahr­schein­lich­keit. Zwei unter­schied­li­che Ein­ga­be­werte wer­den sehr wahr­schein­lich auf ein­deu­tige Hash­werte abge­bil­det. Es liegt jedoch in der Natur der Sache, dass die­ser Hash nicht son­der­lich leser­lich erscheint, son­dern lang­ket­tig und dif­fus. Zur Unkennt­lich­keit trägt bei, dass bei der in Hive imple­men­tier­ten Funk­tion auch leere Strings und Null Values gehasht wer­den. In der Pra­xis soll aber eine Report­ing­ta­belle auf einem Blick Auf­schluss über einen Sach­ver­halt bie­ten und nicht mit Hash­wer­ten über­frach­tet wer­den. Ein nai­ver Ansatz die­ses Pro­blem zu lösen ist eine ein­fa­che Sub­sti­tu­tion mit Lookup-Tabel­len: ersetze Mül­ler durch Schmidt. Diese Trans­for­ma­tion sichert jedoch keine ein­deu­tige Rück­füh­rung der Namen. Es muss also ein Mit­tel­weg zwi­schen rei­nem Hash und rei­nem Namen gefun­den wer­den, der beide Vor­teile, näm­lich sicher und leser­lich, ver­eint. Indi­vi­dua­li­sierte For­men der Mas­kie­rung kön­nen im Hadoop Öko­sys­tem bequem mit User Defi­ned Func­tions (UDFs) in Apa­che Hive und Apa­che Ran­ger Poli­cies imple­men­tiert werden.

Poli­cies fle­xi­bel fest­le­gen – Apa­che Ran­ger Hive Plugin

Der Zugriff auf sen­si­ble Daten­be­stände lässt sich in Form von soge­nann­ten Poli­cies ver­wal­ten und bezieht sich auf Benut­zer, Grup­pen oder fest­ge­legte Bedin­gun­gen (z.B. Benut­zer­merk­male). Für gewöhn­lich sind Berech­ti­gun­gen an die phy­si­schen Zugriffs­ob­jekte (z.B. DB, Tabel­len etc.) gekop­pelt und set­zen vor­aus, dass der Admi­nis­tra­tor die Spei­cher­orte kennt. Ran­ger ermög­licht mit sei­nen Tag-based Poli­cies zusätz­lich die Zugriffs­ver­gabe auf Grund­lage von Meta­da­ten aus Apa­che Atlas. Sobald ein Tag mit einem Daten­ob­jekt asso­zi­iert ist, kann der Admi­nis­tra­tor Berech­ti­gun­gen fle­xi­bel über den Daten­in­halt steu­ern. Fle­xi­bel ist auch die Seg­men­tie­rung sen­si­bler Daten­be­stände, die mit Apa­che Ran­ger sowohl zei­len- als auch spal­ten­ba­siert erfol­gen kann. In der Pra­xis erweist sich diese Funk­tio­na­li­tät als beson­ders nütz­lich, bei­spiels­weise dür­fen Län­der­ge­sell­schaf­ten oft­mals nur die Kun­den ihres eige­nen Lan­des ein­se­hen, daher müs­sen die rest­li­chen Kun­den­zei­len ver­bor­gen wer­den. In ande­ren Fäl­len sind sen­si­ble Merk­male wie zum Bei­spiel die Kre­dit­kar­ten­num­mer nur für die auto­ri­sierte Bank­ab­tei­lung von Inter­esse, wodurch die Merk­mals­spalte für andere Abtei­lun­gen aus­ge­schlos­sen wer­den soll. Ran­ger kann bei­des, Zei­len fil­tern und seg­men­tie­ren und Spal­ten ausschließen.

Daten­schutz erfor­dert jedoch nicht per se eine Beschnei­dung des ursprüng­li­chen Daten­sat­zes, son­dern kann auch in Form einer benut­zer­de­fi­nier­ten Daten­an­zeige gere­gelt wer­den. Apa­che Ran­ger stellt dafür die dyna­mi­sche Mas­kie­rung zur Ver­fü­gung. Dyna­misch bedeu­tet, dass die Mas­kie­rung der Daten on-demand erfolgt. Wäh­rend eine per­sis­tente Mas­kie­rung eine Kopie der Daten erstel­len, schreibt Apa­che Ran­ger Daten­an­fra­gen dyna­misch um und mas­kiert Daten „on-the-fly“. Die fest­ge­setz­ten, akti­ven Poli­cies wer­den bei jeder Anfrage im Hin­ter­grund der vor­ge­ge­be­nen Rei­hen­folge nach eva­lu­iert und ent­schei­den somit, wie Daten­be­stände für den jewei­li­gen Nut­zer ein­seh­bar sind. Dafür sind weder eine Ände­rung der Anwen­dung oder des Hive-Layer, noch ein Daten­fluss außer­halb Hive erfor­der­lich. Ein gro­ßer Vor­teil der dyna­mi­schen Mas­kie­rung ist somit die Ein­spa­rung von ETL-Pro­zes­sen und eine Mini­mie­rung der Daten­red­un­danz. Um die Daten­an­fra­gen zu doku­men­tie­ren, wird für jede Anwen­dung einer Policy ein eige­ner Audit-log-Ein­trag erstellt, der in einer inter­ak­ti­ven Ansicht in der Admin-Kon­sole auf­ge­ru­fen wer­den kann.

Die Art der Mas­kie­rung kann in einem Opti­ons­menü fest­ge­legt wer­den. Stan­dard­mas­kie­run­gen sind z.B. eine par­ti­elle Mas­kie­rung wie die Anzeige der ers­ten oder letz­ten 4 Zei­chen,  ein Hash- oder Null­wert, die Reduk­tion des Geburts­da­tums auf die Jah­res­zahl. Dar­über hin­aus kön­nen auch indi­vi­du­elle Mas­kie­rungs­trans­for­ma­tio­nen defi­niert wer­den. Wie an obi­ger Stelle schon für den Daten­zu­griff erwähnt, kann auch die Mas­kie­rung sowohl tag-basiert als auch zei­len- oder spal­ten­weise fest­ge­legt wer­den, wodurch Zeile und Spalte eine eigene Mas­kie­rungs-Policy erhal­ten. Die prak­ti­sche Umset­zung der Mas­kie­rung mit Lookup-Tabel­len soll anhand eines kur­zen Bei­spiels ver­an­schau­licht werden.

Mas­kie­rung mit Lookup-Tabellen

Da die Anony­mi­sie­rung mit Lookup-Tabel­len in Hive nicht imple­men­tiert ist, müs­sen benut­zer­de­fi­nierte Funk­tio­nen (UDF) geschrie­ben wer­den. Fol­gende Code-Snip­pets zei­gen, wie solch eine UDF aus­se­hen könnte:

private void readLookupFile(String lookupFile) throws HiveException {
		try {
			Configuration conf = new Configuration();
			Path filePath = new Path(lookupFile);

			FileSystem fs = FileSystem.get(filePath.toUri(), conf);
			FSDataInputStream inputFile = fs.open(filePath);

			initializeLookup(inputFile);
		} catch (Exception e) {
			throw new HiveException(e + ": when attempting to 
                            access: " + lookupFile);
		}
	}

protected void intializeLookup(InputStream inputStream) throws IOException {
		//lookupMap variable is initialized in the UDF Class
        lookupMap = new HashMap<Integer, String>();
		
		BufferedReader buffIn = new BufferedReader(new 
                    InputStreamReader(inputStream));
		String line;
		int lineNr = 0;
		while ((line = buffIn.readLine()) != null) {
            lookup.put(lineNr, line);
			lineNr++;
		}
	}

Die Lookup-Tabelle (CSV-Datei mit einer Spalte Lookup-Wer­ten) wird in der Regel im HDFS abge­legt und mit Hilfe der beschrie­be­nen Uti­lity Funk­tion in eine Hash­Map über­führt. Die Hash­Map ermög­licht ein per­for­ma­tes Lookup inner­halb der UDF Aufrufe.

Ein bekann­tes Pro­blem bei der Mas­kie­rung mit Loo­kups ist, dass die Lookup-Tabelle deut­lich weni­ger Uni­que Values als die Ursprungs­ta­belle hat. In der fol­gen­den Funk­tion wer­den daher an den Namen, der über einen ers­ten Hash aus der Lookup Tabelle extra­hiert wird, noch die ers­ten 5 Zei­chen des SHA-256-Hex Hashs des Ursprungs­wer­tes ange­han­gen. Bei­spiel­weise kann mit dem Lookup der Name Mül­ler durch den Namen Meier x1y2z ersetzt wer­den. Ein ande­rer Name wie z.B. Schulze (der im Lookup auch den Namen Meier zuge­wie­sen bekäme) würde dann aber Meier a4b5c hei­ßen. So bleibt trotzt les­ba­rem Lookup die Ein­deu­tig­keit der Ursprungs­werte erhal­ten. Das Ergeb­nis ist eine Mas­kie­rung mit ein­deu­ti­ger Wertzuweisung.

public Object evaluate(DeferredObject[] args) throws HiveException {
    // when value is null return null
    if(args[0].get() == null) return null;
    
    // when value is only blanks return only blanks
    String col = (String) stringConverter.convert(args[0].get());
    if (col.trim().length() == 0) return returnHelper.setReturnValue(col);
    
    // only initialize the lookup HashMap if not yet done
    if (lookup == null) {
    	if (args.length > 1) {
    		initHdfsLookup((String)fileInspector
                .getPrimitiveJavaObject(args[1].get()));
    	}
    	//check if lookup is filled
    	if(lookup.size() == 0) throw new HiveException("Lookup File is empty 
            or was in the wrong format");
    }
    		
    // perform lookup
    String inputString = (String) stringConverter.convert(args[0].get());
    String val = null;
    
    // use the hashCode for lookup
    int hash = inputString.hashCode();
    int lookupRow = Math.abs(hash%lookup.size());
    
    try {
    	// add part of sha1base64 as suffix to anonymized value to have (limited)
           uniqueness
    	String sha1base64 = new Sha1Base64().evaluate(inputString);
    	
    	val = lookup.get(lookupRow) +"_"+sha1base64.substring(0,5);
    	return returnHelper.setReturnValue(val);
    }catch(NoSuchAlgorithmException ex) {
    	ex.printStackTrace();
    }
    
    val = "lookup failed";
    return returnHelper.setReturnValue(val);
}

Der Funk­ti­ons­auf­ruf in Hive kann fol­gen­der­ma­ßen aussehen:

Select hash_lookup(name_column, “/hdfs/path/to/name_lookup.csv”) from table_name;

Es gibt jedoch eini­ges zu beachten:

  • Die Lookup-CSV sollte nur vom Hive-User les­bar sein, da dar­über eine Rück­füh­rung der Namen bzw. ein manu­el­les Bil­den des Hash-Wer­tes ermög­licht wird. (Hive-Kon­fi­gu­ra­tion muss dafür ange­passt sein)
  • Die Lookup-CSV darf keine ein­deu­ti­gen Namen ent­hal­ten, son­dern nur Namen die häu­fig vorkommen
  • Da nur ein Teil des Hashs als Suf­fix ver­wen­det wird, besteht in der Theo­rie die Mög­lich­keit, dass unter­schied­li­che Namen den­sel­ben Lookup und Hash wert bekom­men. Diese Chance ist aber sehr gering und in den meis­ten Fäl­len vernachlässigbar

 
Die Funk­tion ist fle­xi­bel und kann zur Mas­kie­rung mit Lookup-Tabel­len für ver­schie­dene fach­li­che Daten­ty­pen genutzt wer­den. Sie hat eine ver­gleich­bare Per­for­mance bei dyna­mi­scher Mas­kie­rung mit unmas­kier­ten Daten­auf­ru­fen und ist auch im Zusam­men­hang mit tag­ba­sier­ten poli­cies sehr gut ein­setz­bar. Hier­bei kön­nen je nach Tag-Zuwei­sung andere Lookup-Dateien genutzt werden.

Fazit

Ins­ge­samt bie­tet die dyna­mi­sche Mas­kie­rung mit Apa­che Ran­ger Poli­cies eine Reihe von Vor­tei­len. Sie löst das Pro­blem der unle­ser­li­chen Hash­werte und der unge­nauen Namens­sub­sti­tu­tion durch eine Aus­wahl an Mas­kie­rungs­mög­lich­kei­ten und ver­bin­det die Vor­teile bei­der Ansätze. Sie ver­braucht kei­nen zusätz­li­chen Spei­cher durch lokale Kopien und kann durch Meta­da­ten fle­xi­bel gesteu­ert wer­den. Die Daten­an­sicht von Benut­zern und Grup­pen kann sowohl für Zei­len- als auch Spal­ten ein­ge­schränkt wer­den und bie­tet dadurch eine anpas­sungs­fä­hige Seg­men­tie­rung. Trotz der vie­len Vor­teile lei­det die dyna­mi­sche Mas­kie­rung im Ver­gleich mit der hash­ba­sier­ten Hive-Mas­kie­rung an kei­nem deut­li­chen Per­for­mance­ver­lust und ist somit den ande­ren Alter­na­ti­ven vorzuziehen.