Sensible Daten mit Git Smudge und Clean filtern

Wie man in Git Passwörter herausfiltert

Sensible Daten mit Git Smudge und Clean filtern

Heute schauen wir uns an, wie man sensiblen Daten wie Passwörter, API-Schlüssel, Server-Adressen usw. aus seinem Quellcode herausfiltert und mit Platzhaltern ersetzt, BEVOR sie im Git Repository landen – und auch wie man die Platzhalter automatisch wieder durch die richtigen Daten befüllt, wenn der Quellcode aus Git mit einem Pull abgerufen wird.

HINWEIS: Dieser Artikel ist kein Git Anfänger-Guide. Er setzt vorraus, dass du die Basics von Git kennst, wie man mit Remote Repositories arbeitet und wie man mit dem Terminal bzw. der Shell arbeitet.

Folgende bekannte Situation: Du hast das nächste großartige Arduino- oder ESP32-Projekt (oder jede beliebige andere Programmiersprache) erstellt. Nun möchtest du es mit Git in die Versionskontrolle geben und auch auf den GitHub-Server laden – vielleicht zu Sicherungszwecken oder um es öffentlich zu machen und anderen Maker-Kollegen zur Verfügung zu stellen (sehr löblich!). Oder vielleicht arbeitest du auch im Team an einem Projekt, dann ist Git immer eine sinnvolle Sache (ich weiß gar nicht, wie man früher im Team ohne Git zusammen programmiert hat 🤔).

Gerade bei ESP32- und Arduino-Projekten baut man oft eine WLAN Funktion ein und hat daher sensible Daten wie z.B. das WLAN-Passwort und SSID im Quellcode stehen.

Damit diese sensiblen Daten nicht im Repository auf dem GitHub Server landen, müsste man nun vor jedem Stagen diese sensiblen Daten per Hand mit Platzhaltern austauschen. Dann nach dem Pushen wieder auf die richtigen Zugangsdaten zurücksetzen, damit du an deinem Projekt weiter arebiten kannst. Und dann vor dem nächsten Stagen wieder daran denken die Platzhalter einzubauen … usw. Ziemlich nervig! Ziemlich fehleranfällig!

Diese Anleitung löst dieses Problem, in dem über sog. Git Smudge und Clean Filter das Ganze automatisiert wird.

ACHTUNG: Git vergisst nie! Git speichert eine vollständige Historie aller Dateien, die in Git eingecheckt werden. Daher müssen alle sensiblen Daten bereits vor dem allerersten Commit ersetzt werden. Andernfalls sind diese Daten für immer im Repository gespeichert.

Selbst wenn du die sensiblen Daten in einem späteren Commit mal mit Platzhaltern ersetzt hast, bleibt das Problem bestehen. Mit Git kannst du in der Zeit zurückreisen, bis zum allerersten Commit. Aus diesem Grund solltest du dich von Anfang an um sensiblen Daten in deinem Quellcode kümmern, wenn diese auf einen Server wie GitHub gepusht werden sollen – insbesondere wenn das Projekt öffentlich zugänglich sein wird.

TIPP: Alles zu spät und deine sensiblen Daten sind bereits im Repo gelandet?

Es ist möglich, sensible Daten aus dem gesamten Git-Verlauf zu entfernen. Falls du das tun musst, wirf z.B. einen Blick auf BFG Repo-Cleaner von rtyley.

Wie kann man Passwörter mit Git Clean und Smudge Filtern suchen und ersetzen?

Bei diesem Ansatz werden wir die weniger bekannte Datei .gitattributes und einen sog. Clean und Smudge-Filter verwenden.

Manche verwenden dafür auch einen Git pre-commit Hook, aber dieser Ansatz funktioniert nur in eine Richtung, bevor man die Dateien zu GitHub pusht. Der pre-commit Hook wird aber nicht getriggert, wenn man das Repository pullt.

Dieser Clean- und Smudge-Ansatz funktioniert jedoch in beide Richtungen: Wenn wir eine Datei stagen, lassen wir einen Filter laufen, der unsere sensiblen Daten sucht und durch Platzhalter ersetzt (clean). Nun können die Dateien aus dem Staging-Bereich committed und ins entfernte Repository gepusht werden. Und wenn wir das Repo von GitHub pullen, können wir denselben Filter in umgekehrter Richtung anwenden, um die Platzhalter automatisch durch unsere sensiblen Daten zu ersetzen (smudge) – noice! Also los!

Was macht die .gitattributes Datei?

Die Datei .gitattributes ist eine versteckte Konfigurationsdatei. Sie kann im Stammverzeichnis deines Projekts abgelegt werden. Sie kann auch mit ins Repo eingecheckt werden, da sie keine sensiblen Daten enthält.

Es macht oft sogar Sinn sie mit ins Repo einzuchecken, da so Probleme mit der Zeilenkodierung gelöst werden können, falls du in einem Team arbeitest und die Teamkollegen verschiedene Computersysteme wie macOS, UNIX und Windows verwenden.

Außerdem kannst du damit steuern, wie binäre Dateien oder wie Dateien in einem Diff-Tool behandelt werden sollen. Oder du kannst GitHub Hinweise geben, wie deine Dateien dort interpretiert werden sollen. So können die Dateien auf der GitHub-Website richtig angezeigt und kategorisiert werden.

Und: hier sagen wir Git, wie es unseren Clean- und Smudge-Filter anwenden soll.

Für ein Arduino-Projekt kannst du eine .gitattributes Datei wie im folgenden Snippet verwenden. Kopiere sie einfach in das Stammverzeichnis deines Projekts, dorthin wo sich auch der .git-Ordner befindet:

 Set default behavior to automatically normalize line endings.
* text=auto

# Force batch scripts to always use CRLF line endings so that if a repo is accessed
# in Windows via a file share from Linux, the scripts will work.
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
*.{ics,[iI][cC][sS]} text eol=crlf

# Force bash scripts to always use LF line endings so that if a repo is accessed
# in Unix via a file share from Windows, the scripts will work.
*.sh text eol=lf

# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary

# Sources
*.c     text diff=cpp   linguist-language=Arduino   filter=replaceStrings
*.cc    text diff=cpp   linguist-language=Arduino   filter=replaceStrings
*.cxx   text diff=cpp   linguist-language=Arduino   filter=replaceStrings
*.cpp   text diff=cpp   linguist-language=Arduino   filter=replaceStrings
*.c++   text diff=cpp   linguist-language=Arduino   filter=replaceStrings
*.hpp   text diff=cpp   linguist-language=Arduino   filter=replaceStrings
*.h     text diff=cpp   linguist-language=Arduino   filter=replaceStrings
*.h++   text diff=cpp   linguist-language=Arduino   filter=replaceStrings
*.hh    text diff=cpp   linguist-language=Arduino   filter=replaceStrings
*.ino   text diff=cpp   linguist-language=Arduino   filter=replaceStrings

Werfen wir einen Blick auf diese .gitattributes Datei.

Der wichtigste Teil ist dieser: *.cpp text diff=cpp linguist-language=Arduino filter=replaceStrings

Hier teilen wir Git folgendes mit:

  • *.cpp für jede Datei, die mit .cpp endet
  • behandle sie wie eine Textdatei (im Gegensatz zu einer Binärdatei wie Bilder, Schriftarten usw.)
  • bei Verwendung eines Diff-Tools behandle diese Datei als eine cpp (C++)-Datei (für eine .cpp-Datei ist das offensichtlich, aber z.B. für eine Arduino .ino-Datei ist es das nicht)
  • linguist-language=Arduino weist die GitHub-Website an, diese Datei als Teil eines Arduino-Projekts zu behandeln. Es gibt viele Projekte, die C++-Dateien verwenden, aber nichts mit Arduino zu tun haben. Dies ist also nur eine optionale Information, um die Anzeige auf der GitHub-Website zu verbessern.
  • filter=replaceStrings hier weisen wir Git an, eine Filteraktion namens replaceStrings für jede *.cpp-Datei durchzuführen, wenn sie zum Staging hinzugefügt wird. replaceStrings ist in diesem Fall unser Clean und Smudge Filter Aktion.

Die minimalste .gittatributes Datei für ein kleines Arduino-Projekt könnte demnach wie folgt aussehen:

*.cpp filter=replaceStrings
*.h filter=replaceStrings
*.ino filter=replaceStrings

Das sollte die meisten Dateien für ein kleines Arduino-Projekt abdecken. .ino-Dateien, falls du die Arduino IDE verwendest. Und .cpp und .h falls du zusätzliche Bibliotheken und/oder PlatformIO verwendest.

Arduino IDE vs. PlatformIO

Apropos PlatformIO: hier habe ich einen fantastischen Artikel mit Video dazu geschrieben. In dem Artikel vergleiche ich PlatformIO zur Arduino IDE und gebe Tipps zum Einstieg in PlatformIO und Visual Studio Code.

Suchen und Ersetzen per Shell-Skript im Git Repo

Als nächstes erstellen wir ein Shell-Skript, welches das Suchen und Ersetzen übernimmt. Ich werde dieses Skript in einem Ordner namens scripts in meinem Home-Ordner erstellen. Ich nenne das Skript git-replace-strings-filter.sh. Dabei gehe ich folgendermaßen vor:

  • Öffne ein Terminal und wechsle mit cd ~ in deinen Home Ordner.
  • Erstelle einen Ordner namens scripts mit: mkdir scripts
  • Wechsle in den neuen Ordner mit cd scripts
  • Erstelle eine neue Datei mit touch git-replace-strings-filter.sh
  • Mache die neue Skriptdatei ausführbar mit chmod +x git-replace-strings-filter.sh

Bearbeite die neue Datei und füge das Snippet aus den nächsten Abschnitten ein, je nachdem, welches Shell-System du verwendest. Du kannst vim, nano oder auch code verwenden, um die Datei in VS Code zu öffnen (wenn du den Terminal-Befehl von VS Code hinzugefügt hast). Oder öffne die Datei einfach manuell in dem Editor deiner Wahl.

TIPP: So fügst du den Terminal-Befehl code für Visual Studio Code hinzu

Öffne Visual Studio Code und öffne die Befehlspalette (STRG+Umschalt+P unter Windows oder CMD+Umschalt+P unter macOS) und beginne Shell Command zu tippen. Es werden zwei Optionen angezeigt: eine zum Installieren des Befehls „code“ in PATH und eine zum Deinstallieren des Befehls „code“ aus PATH.

Wähle den Befehl ‚code‘ in PATH installieren. Starte dein Terminal neu. Jetzt solltest du in der Lage sein, den Code-Befehl zu verwenden. Ein paar Beispiele:

code . öffnet das aktuelle Verzeichnis in VS Code
code -n . öffnet das aktuelle Verzeichnis in einem neuen Fenster in VS Code
code git-replace-strings-filter.sh öffnet nur diese eine Datei in VS Code 

Verwende code --help oder schaue in die Dokumentation für weitere Hilfen: The Visual Studio Code command-line interface.

Bash Version 4+ suchen und ersetzen Skript

Diese Version funktioniert nur für bash --version >= 4. Sie ist also nicht für macOS geeignet. Bash hat die Lizenz vor vielen Jahren auf GPL v3 geändert und Apple akzeptiert diese Lizenz nicht. Aus diesem Grund liefert Apple nur eine veraltete Bash-Versionen der GPL v2 Ära mit macOS aus. Und das ist auch der Grund, warum Apple letztlich auf Z Shell (zsh) als neues Standard-Shell-System umgestiegen ist. Weiter unten im Artikel findet du eine Version für ZSH (Z Shell).

#!/bin/bash

declare -A mapArr

mapArr["qwerty123!"]="YOUR-PASSWORD"
mapArr["mySecretWiFiName"]="YOUR-SSID"
mapArr["my.scret.server"]="YOUR-SERVER"
mapArr["myAPIkey13e#45*+"]="YOUR-API-KEY"

sedcmd="sed"
if [[ "$1" == "clean" ]]; then
  for key in ${!mapArr[@]}; do
    sedcmd+=" -e \"s/${key}/${mapArr[${key}]}/g\""
  done  
elif [[ "$1" == "smudge" ]]; then
  for key in ${!mapArr[@]}; do
    sedcmd+=" -e \"s/${mapArr[${key}]}/${key}/g\""
  done  
else  
  echo "use smudge/clean as the first argument"
  exit 1
fi

eval $sedcmd

ZSH (Z Shell) Version suchen und ersetzen Skript

Seit macOS 10.15 Catalina hat Z-Shell (zsh) die Bourne-again-Shell (Bash) als Standard-System-Shell abgelöst. Wenn du macOS nutzt, kannst du also das folgende Skript verwenden. Oder du musst Bash auf eine Version >= 4 aktualisieren (z.B. mit home-brew). Dann kannst du die obige Bash-Version verwenden.

#!/bin/zsh

declare -A mapArr

mapArr[qwerty123!]=TEST-REPLACED-PASSWORD
mapArr[mySecretWiFiName]=TEST-REPLACED-SSID
mapArr[my.secret.server]=TEST-REPLACED-SERVER
mapArr[myAPIkey13e#45*+]=TEST-REPLACED-KEY

sedcmd="sed"
if [[ "$1" == "clean" ]]; then
  for key value in ${(kv)mapArr}; do
    sedcmd+=" -e 's/${key:q}/${value}/g'"
  done  
elif [[ "$1" == "smudge" ]]; then
  for key value in ${(kv)mapArr}; do
    sedcmd+=" -e 's/${value}/${key:q}/g'"
  done  
else  
  echo "use smudge/clean as the first argument"
  exit 1
fi

eval $sedcmd

Wenn du wissen willst, wie dieses Skript genau funktioniert, recherchiere über den sed Befehl und wie man assoziative Arrays in zsh und/oder bash benutzt.

Konfigurieren der Texte, die gesucht und ersetzt werden sollen

Wie du siehst, werden die zu suchenden und zu ersetzenden Texte in diesem Skript konfiguriert. Du kannst beliebig viele Strings über das mapArr-Array anlegen.

In dem obigen Shell-Skript sind vier Strings angelegt, die ersetzt werden sollen. Schauen wir uns den ersten String an: mapArr["qwerty123!"]="YOUR-PASSWORD"
Das bedeutet, dass das Skript nach der Zeichenkette „qwerty123!“ im Code sucht und sie durch „YOUR-PASSWORD“ ersetzt, bevor die Datei dem Staging-Bereich hinzugefügt wird.

Hinzufügen des Git-Filtertreibers

Der letzte Schritt besteht darin, der Git-Konfiguration einen Filtertreiber hinzuzufügen. Klingt kompliziert, ist es aber nicht.

Ein Filtertreiber ist in diesem Fall nur eine Pfadangabe zu unserem git-replace-strings-filter.sh Skript, das im Falle einer Smudge-and-Clean-Situation ausgeführt werden soll.

Außerdem kann man den Treiber der globalen Git-Konfiguration hinzufügen, wenn man das --global Flag verwendet. In dem Fall wird der Filter auf jedes Git-Projekt mit dem aktuellen User und der entsprechenden .gitattributes-Datei angewendet.

TIPP: Wo ist meine globale .gitconfig Datei?

Die globale .gitconfig Datei sollte in deinem Home-Verzeichis liegen: ~/.gitconfig

Wenn du es nicht global anlegen willst, kannst du den Flag --global weglassen. Dann sollte eine lokale .gitconfig-Datei im Stammverzeichnis deines Projekts erstellt werden.

Ich bevorzuge die globale Version, da ich die Filterung immer noch mit der Datei .gitattributes steuern kann. Wenn ich keine Filterung möchte, lasse ich die .gitattributes Datei einfach weg, oder entferne filter=replaceStrings in der Datei.

Erstelle den Filtertreiber global, indem du die beiden folgenden Befehle im Terminal eingibst:

git config --global filter.replaceStrings.smudge "~/scripts/git-replace-strings-filter.sh smudge"
git config --global filter.replaceStrings.clean "~/scripts/git-replace-strings-filter.sh clean"

Und das war’s!

Jedes Mal wenn nun einen Datei ins Staging geschoben wird (git add), schaut Git zunächst in die .gittatributes Datei, ob es dort einen Eintrag zu der Datei-Endung gibt. Falls ja, und falls dort die Angabe filter=replaceStrings zu finden ist, wird für die Datei das Suchen- und Ersetzen Skript durchlaufen, welches wir mit dem Filtertreiber angegeben haben. Jeder Eintrag in mapArr wird dann gesucht und ersetzt und erst danach wird die Datei ins Staging geschoben.

Wie kann man prüfen, ob der Clean And Smudge Filter funktioniert?

Ich empfehle dir zunächst zu prüfen, ob der Filter wirklich funktioniert, bevor du die Datei in dein externes Remote Repository überträgst.

Erstelle zum Testen einfach einen neuen Ordner und wechsle im Terminal dort hinein. Initialisiere in diesem neuen Ordner ein lokales Git-Repository: git init -b main

Erstelle eine neue Textdatei in diesem Repository. Füge einen kurzen Test-Code hinzu, der auch die Strings enthält, die du ersetzen willst. Passend zu diesem Artikel und dem bisherigen Code-Beispielen wäre z.B. folgendes Snippet. Nenne die Datei test.cpp:

#include <superfoo.library>
#include "bar.h"

/*** SOME LONGER COMMENTS *************************/
/**************************************************/
/**************************************************/

#define WIFI_SSID "mySecretWiFiName"
#define WIFI_PASS "qwerty123!"

#define DATA_PIN 4

// a function call
start_attack("my.secret.server", "myAPIkey13e#45*+");

/*** SOME MORE COMMENTS ***************************/
/**************************************************/

Speichere die Datei.

Vergiss nicht, die .gitattributes-Datei ebenfalls in dieses Verzeichnis zu kopieren.

Jetzt kannst du die Datei mit git add . hinzfügen.

Damit werden alle neuen Dateien in den Staging-Bereich kopiert. Die Datei .gitattributes wird also ebenfalls hinzugefügt, was kein Problem darstellt. Du kannst auch git add text.cpp verwenden. Dann wird wirklich nur diese eine Datei zum Staging hinzugefügt.

Gib git status ein, um eine Liste aller Dateien zu erhalten, die im Staging sind. test.cpp und .gitattributes sollten hier in grün aufgelistet sein.

Verwende git status -v, um den Inhalt der Dateien im Staging anzuzeigen. Hier solltest du jetzt sehen, dass alle sensiblen Daten durch den Clear-Filter ersetzt worden sind.

Das bedeutet, dass sich die Datei in deinem Arbeitsbereich jetzt von der Datei im Staging-Bereich unterscheidet. Wenn du die Arbeitsdatei in deinem Code Editor öffnest, siehst du immer noch die sensiblen Daten. Nur bei den Dateien im Staging-Bereich wurden die sensiblen Daten ersetzt.

Wenn du deine zu suchenden Texte ändern und den Filter erneut testen möchtest, verwende den Befehl git reset, um alle Dateien im Staging wieder zu entfernen und zum letzten Stand zurückzukehren. Führe dann deine Änderungen durch und starte die Überprüfung mit git add . und git status -v erneut.

Mögliche Fallstricke

Wenn du die Bash- und die ZSH-Version vergleichst, siehst du, dass das assoziative Array mapArr leicht unterschiedlich ist. In der Bash-Version habe ich Anführungszeichen verwendet, in der ZSH-Version nicht.

Die ZSH-Version ohne Anführungszeichen ist universeller. Wenn du Anführungszeichen verwendest, gehören die Anführungszeichen mit zum suchenden Text.

Wenn du also mapArr["qwerty123!"]="YOUR-PASSWORD" verwendest, wird z.B. 'qwerty123' nicht im Code gefunden und ersetzt, weil es nur einfache Anführungszeichen sind. Besser ist also die ZSH-Version, da hier die Art der Anführungszeichen unerheblich ist.

Wenn ein Key im Assoziativen-Array Sonderzeichen enthält, funktioniert es leider nicht ohne Anführungszeichen, solange man keine speziellen Qualifier hinzufügt.

Im ZSH-Skript habe ich den :q-Qualifier wie in ${key:q} verwendet. Der erlaubt es, Sonderzeichen in den Keys des assoziativen Arrays zu verwenden.

Leider habe ich nicht herausgefunden, wie man das mit Bash macht. Wenn du weisst, wie das geht, schicke mir doch eine Nachricht oder schreibe es in die Kommentare.

Hilfreiche Links, die ich für die Recherche verwendet habe

Da ich im Netz keine funktionierende Lösung für macOS und ZSH gefunden habe, habe ich etwas recherchiert und konnte daraus das obige ZSH-Shell-Skript erstellen. Die folgenden Links waren für mich dabei sehr hilfreich, um die Technik dahinter zu verstehen – insbesondere der Artikel von Tomer Figenblat des RedHat Developer Teams. Das obige Bash-Skript basiert auf seinem Artikel, den du in der Links-Sektion unten findest.

Links

Associative array in Bash
Associative arrays in zsh – Scripting OS X
https://itnext.io/upgrading-bash-on-macos-7138bd1066ba
Secure data in Git with the clean/smudge filter | Red Hat Developer
How to use sed to find and replace text in files in Linux / Unix shell – nixCraft