Meine eBooks
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 Codecode -n .
öffnet das aktuelle Verzeichnis in einem neuen Fenster in VS Codecode 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