Verschlüsselung mit dem AES-Algorithmus über das Kontextmenü des Explorers

Verschlüsselung per Kontextmenü

Entwickeln Sie eine ATL/COM-Anwendung, die sich in das Kontextmenü des Explorers integriert und sichere Verschlüsselung von Dateien anbietet.

Dominik Reichl

CryptoCtx Screenshot Sicher kennen Sie Anwendungen, die Funktionen über das Kontextmenü des Windows Explorers bereitstellen, wie z.B. das von WinZip. WinZip bietet dem Benutzer u.a. an, ausgewählte Dateien zu komprimieren/archivieren oder mit E-Mail zu verschicken. Wir wollen eine Anwendung schreiben, die sich in das Kontextmenü des Explorers integriert und der Benutzer dann bequem darüber Dateien verschlüsseln und entschlüsseln kann. Wir entwickeln keinen eigenen neuen, vielleicht unsicheren Verschlüsselungs-Algorithmus, sondern verwenden den sicheren AES-Algorithmus Rijndael. Rijndael wurde bereits von vielen Kryptologen genauestens analysiert und es wurden keine nennenswerten Schwächen gefunden.

Starten Sie also Microsoft Visual C++ und erstellen Sie ein neues Projekt. Wählen Sie aus der Liste den "ATL-COM-Anwendungs-Assistent" aus. Das Projekt soll CryptoCtx (für CryptoContext) heißen. Im nächsten Dialog belassen Sie alles so wie es bereits eingestellt ist: wir wollen eine DLL erstellen und brauchen die MFC nicht. Nachdem Sie das Projekt generieren haben lassen, müssen Sie ein neues ATL-Objekt erstellen. Bis jetzt haben Sie nur ein leeres ATL-Projekt das noch überhaupt nichts tun würde. Klicken Sie deshalb in der Classview-Ansicht rechts auf "CryptoCtx Klassen". Wählen Sie "Neues ATL-Objekt". Wir brauchen ein "simples Objekt". Da dieses standardmäßig schon ausgewählt ist können Sie in diesem Dialog einfach auf "Weiter" klicken. Im nächsten Dialog geben Sie unter "Kurzbezeichnung" (das Feld hat einen dickeren Rahmen als die anderen Eingabefelder) "CryptoShlExt" ein. Die anderen Eingabefelder werden automatisch ausgefüllt sodass Sie danach auf OK klicken können. Damit haben Sie schon den Grundstein für die Kontextmenü-Erweiterung gelegt. Über diese Klasse wird später das ganze Message-Handling und das Parsen der Daten aus dem Explorer laufen.

Initialisierung

Sobald der Benutzer im Explorer auf eine oder mehrere Dateien rechtsklickt wird unsere ATL-COM-DLL geladen und initialisiert. Schon zu diesem Zeitpunkt sind uns die Dateien bekannt! Der Explorer startet die DLL dann, wenn der Benutzer Dateien ausgewählt und die rechte Maustaste gedrückt hat, aber noch kein Befehl ausgewählt wurde!

Wir brauchen nun also eine Funktion, die Explorer aufrufen kann, wenn der Benutzer den Rechtsklick ausführt. Diese Funktion soll dann alle Dateinamen überprüfen, ob wir sie verwenden können, und sie dann gegebenfalls in eine String-Liste speichern.

Fügen Sie deshalb in der Headerdatei "CryptoShlExt" gleich ganz oben in der Deklaration der Klasse eine neue Zeile ein:

public IShellExtInit

Ein Stück weiter unten bei der COM-MAP fügen Sie unten ebenfalls eine neue Zeile ein:

COM_INTERFACE_ENTRY(IShellExtInit)

In der Klassendeklaration müssen Sie noch im public-Bereich

STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);

einfügen. Dies ist die Deklaration der Initialisierungsfunktion, die Sie gleich in die Cpp-Datei schreiben werden. Im protected-Bereich der Klasse müssen Sie noch string_list m_lsFiles; einfügen. Diese STL-String-Liste wird später die Dateinamen beinhalten, die der Benutzer ausgewählt hat. Zuletzt müssen Sie noch ganz oben die Headerdatei shlobj.h mit #include <shlobj.h> einbinden. So, das wars fürs erste in der Headerdatei. Wechseln Sie nun zur Cpp-Datei "CryptoShlExt.cpp".

Die Initialisierungsfunktion sieht wie folgt aus:

HRESULT CCryptoShlExt::Initialize(LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT pDO, HKEY hProgID).

Als erstes müssen Sie aus pDO ein FORMATETC und ein STGMEDIUM holen. Aus dem STGMEDIUM können Sie dann ein HDROP bekommen. Mit der Funkion DragQueryFile bekommen Sie dann aus dem HDROP die Anzahl der ausgewählten Dateien und können die einzelnen Dateinamen ermitteln. Als erstes rufen Sie DragQueryFile mit den Parametern hDrop, 0xFFFFFFFF, NULL, 0 auf. DragQueryFile gibt dann die Anzahl der ausgewählten Dateien zurück. Danach rufen Sie DragQueryFile in einer Schleife auf, so oft wie die Anzahl der Dateien. Als Parameter dienen aber jetzt hDrop, uFile, szFile und MAX_PATH. uFile gibt die Nummer der Datei an, deren Dateinamen in der Variable szFile die maximal MAX_PATH Zeichen beinhalten kann gespeichert werden soll. Nachdem Sie nun den Dateinamen in der temporären Variable szPath haben müssen wir sie jetzt in die STL-Stringliste einfügen. Dazu reicht ein sehr einfaches Kommando: m_lsFiles.push_back(szFile). Und schon ist szFile der Liste hinzugefügt. Wie Sie wieder an die in der Liste gespeicherten Einträge kommen erfahren Sie später. Nun sind wir in der Initialize-Funktion auch schon fast fertig. Zum Schluss müssen Sie noch das STGMEDIUM wieder freigeben (mit GlobalUnlock und ReleaseStgMedium). Wie das alles nun im einzelnen geht und wie die API-Aufrufe aussehen, entnehmen Sie bitte dem Beispielprojekt auf der Heft-CD.

Kontextmenüerweiterung

Jetzt sind wir also an dem Punkt angelangt, an dem wir unsere eigenen Einträge in das Kontextmenü des Explorers einfügen müssen. Wie schon vorhin erwähnt, wird unsere DLL ja schon vor dem Auswählen eines Befehls aufgerufen. Windows bietet dem Programmierer die Möglichkeit, unterschiedliche Kontextmenü-Einträge ins Kontextmenü einzufügen, meistens von den Endungen der Dateien abhängig. Auch hier wieder Beispiel WinZip: wenn Sie auf eine Zip-Datei rechtsklicken werden andere Befehle im Menü erscheinen, als wenn Sie auf eine Txt-Datei rechtsklicken. Wenn wir Dateien verschlüsseln wollen, müssen wir dem Benutzer die Möglichkeit bieten, die Datei direkt zu verschlüsseln, d.h. keine neue Datei zu erstellen, sondern die alte praktisch überschreiben. Deshalb können wir auch zum Zeitpunkt, an dem der Explorer uns fragt was er ins Kontextmenü für Befehle einbauen soll, nicht sagen, ob die Datei verschlüsselt ist oder nicht. Deshalb fügen wir einfach immer beide Befehle hinzu: einen zum Verschlüsseln und einen zum Entschlüsseln.

Gehen Sie also wieder in die Headerdatei um dort in der Klassendeklaration public IContextMenu und in der COM-MAP COM_INTERFACE_ENTRY(IContextMenu) hinzuzufügen. Im public-Bereich fügen Sie STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT, UINT, UINT); ein.

In der Cpp-Datei fügen Sie die neue Funktion QueryContextMenu, die sie geradeeben in der Headerdatei deklariert haben hinzu: HRESULT CCryptoShlExt::QueryContextMenu(HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags). Wenn in uFlags das Flag CMF_DEFAULTONLY gesetzt ist sollten Sie gar nichts tun und sofort die Kontrolle wieder an den Explorer zurückgeben. Wie Sie sehen können übergibt uns der Explorer ein HMENU als Parameter. Also ist es jetzt ein leichtes mit der API-Funktion InsertMenu(...) unsere Einträge dem Menü hinzuzufügen. uidFirstCmd gibt an an welcher Position wir den Eintrag hinzufügen sollen. Wir brauchen zwei Einträge (Verschlüsseln, Entschlüsseln) deshalb erhöhen Sie einfach nach dem ersten Aufruf von InsertMenu diese Variable um eins, damit die zweite InsertMenu-Funktion unseren zweiten Eintrag richtig einfügen kann.

Jetzt zu den Icons neben den Einträgen. Erstellen Sie als erstes zwei neue Bitmaps mit dem Ressourceneditor: IDB_SHL_ENCRYPT und IDB_SHL_DECRYPT. Jetzt brauchen wir Handles zu den Bitmaps. Deklarieren Sie deshalb im protected-Bereich der Klassendeklaration in der Headerdatei zwei HBITMAPS: m_hEncryptBmp und m_hDecryptBmp. Im Konstruktor der Klasse in der Cpp-Datei laden Sie die Bitmaps mit LoadBitmap. LoadBitmap gibt als Ergebnis ein Handle zur geladenen Bitmap zurück. LoadBitmap braucht allerdings als ersten Parameter ein InstanceHandle, von dem es die Ressource die im zweiten Parameter angegeben wird laden kann. Dieses InstanceHandle bekommen Sie mit _Module.GetModuleInstance(). Falls Sie sich wundern wo _Module deklariert ist, schauen Sie mal in die StdAfx.h.

Geladen haben wir die Bitmaps jetzt also. Nun müssen wir sie in das Kontextmenü einfügen. Gehen Sie deshalb jetzt wieder zur QueryContextMenu-Funktion und nutzen Sie die SetMenuItemBitmaps-API-Funktion um die Bitmaps dem Kontextmenü hinzuzufügen. Zuguterletzt müssen wir dem Explorer noch mitteilen, wieviele Menüeinträge wir hinzugefügt haben. Dies erfolgt über die Funktionsrückgabe

return MAKE_HRESULT (SEVERITY_SUCCESS, FACILITY_NULL, 2);

Der letzte Parameter sollte die Anzahl der hinzugefügten Einträge angeben.

Statuszeile

Wenn Sie im Explorer auf einen Eintrag im Kontextmenü zeigen wird unten in der Statuszeile eine Kurzbeschreibung des Befehls angezeigt. WinZip zeigt zum Beispiel "Dateien zu Archiv hinzufügen" an. Wir wollen auch eine Beschreibung zu unseren Kryptofunktionen anzeigen lassen: "Verschlüsselt eine oder mehrere ausgewählten Dateien".

Um dies für unser Programm einzubauen rufen Sie wieder die Headerdatei auf und ergänzen Sie

STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);

Oben bei den Publics müssen Sie dieses mal nichts weiter tun, GetCommandString gehört zu IContextMenu. In der Cpp-Datei fügen Sie die Funktion HRESULT CCryptoShlExt::GetCommandString (UINT uCmdID, UINT uFlags, UINT* puReserved, LPSTR szName, UINT cchMax) hinzu. Explorer ruft diese Funktion auf um die Texte für die Statuszeile zu bekommen. Prüfen Sie als erstes ob in uFlags das Flag GCS_HELPTEXT gesetzt ist. Wenn dieses Flag gesetzt ist möchte der Explorer die Beschreibung gerne in szName geliefert bekommen. uCmdID ist die Nummer des Befehles im Menü, zu dem Explorer die Beschreibung möchte. Falls in uFlags allerdings das Flag GCS_VERB gesetzt ist müssen Sie ein Verb in szName zurückgeben (in unserem Fall ist das "EncryptSvr" und "DecryptSvr"). So, das wars auch schon mit der Statuszeile.

Befehl ausführen

Der Benutzer klickt jetzt also auf eines unserer Menüpunkte im Kontextmenü. Wir brauchen noch eine Funktion, die der Windows Explorer im Falle des Anklickens eines unserer Einträge ausführen kann. Diese Funktion heißt InvokeCommand. Fügen Sie also in der Headerdatei

STDMETHOD (InvokeCommand) (LPCMINVOKECOMMANDINFO);

hinzu. In der Cpp-Datei schreiben Sie die Funktion HRESULT CCryptoShlExt::InvokeCommand(LPCMINVOKECOMMANDINFO pInfo). Prüfen Sie zunächst ob das höherwertige Wort (HIWORD) von pInfo->lpVerb nicht null ist. Falls es ungleich null ist beenden Sie sofort und kehren mit return E_INVALIDARG; zurück. Falls das höherwertige lpVerb-Wort jedoch null ist, schauen Sie sich das niederwertige Wort von lpVerb an. Ist dieses null, dann wurde der erste Eintrag = Verschlüsseln gewählt. Ist es eins, dann wurde der zweite Eintrag = Entschlüsseln gewählt. Im Übersichtlichkeit zu bewahren schreiben Sie nun noch eine neue Funktion DoWork(UINT nMode) die abhängig von nMode entweder verschlüsselt oder entschlüsselt.

Rijndael

Lange Zeit war des DES-Algorithmus Standard. Allerdings ist die Schlüssellänge für heutige Verhältnisse relativ klein. Wenn ihr Nachbar ein kleines Netzwerk aufbauen könnte, könnte er eine mit DES verschlüsselte Nachricht innerhalb von ein paar Tagen knacken. Besonders Regierungen und Firmen verlangten also nach einem neuen Standard. So gab es eine Konferenz, auf der zunächst von 15 vorgeschlagenen Algorithmen 5 ausgewählt wurden: Serpent, RC6, Twofish, Rijndael und Mars. Schließlich gewann der Rijndael-Algorithmus und ist deshalb der heutige Advanced Encryption Standard (AES).

Rijndael's Design ist sehr stark an dem von Square, einem von den gleichen Kryptologen früher entwickelten Algorithmus, angelehnt. Der Rijndael-Cipher akzeptiert 128-, 192- und 256-bit Schlüssel (Vergleich: DES mit 56-bit). Die zu verschlüsselnden Blöcke können ebenfalls eine Größe von 128-, 192- oder 256-bit haben. Alle 9 möglichen Kombinationen zwischen Schlüssellänge und Blockgröße sind erlaubt. Auf http://www.esat.kuleuven.ac.be/~rijmen/rijndael/index.html, der offiziellen Seite des Rijndael-Ciphers finden Sie mehrere Implementierungen bzw. Quellcodes in allen möglichen Sprachen. Wir verwenden die dort downloadbare C++-Klasse von Szymon Stefanek. Auf der Seite http://www.rijndael.com, einer Fanpage zum Rijndael-Cipher, finden Sie noch mehr Implementierungen.

Die Rijndael-Klasse, die wir verwenden, ist sehr einfach zu handhaben. Einfach die Klasse deklarieren, mit dem Schlüssel initialisieren und dann Datenblöcke verschlüsseln:

CRijndael oRijndael;
oRijndael.init(CRijndael::CBC, CRijndael::Encrypt,
    uKey, CRijndael::Key32Bytes);
oRijndael.blockEncrypt(chDataIn, SIZE_IN_BYTES*8, chDataOut);

Die Dateien verschlüsseln wir im CBC-Modus. CBC ist ein Blockcipher-Modus, bei dem jeder vorhergehende Block mit dem aktuellen geXORt wird. Dadurch wird der Eingabeblock in den Cipher zufällig gemacht und es kann außerdem mehr als eine Nachricht mit dem gleichen Schlüssel verschlüsselt werden. Es ist auch ein wenig schwieriger den verschlüsselten Text zu manipulieren. Synchronisierungsfehler sind allerdings nicht behebbar und ein Fehler im Ciphertext wirkt sich auf den kompletten aktuellen Block und auf den nächsten folgenden Block mit 1 Bit aus.

SHA-1

Wir wollen den Rijndael-Cipher mit einer Blockgröße von 256 bit und einer Schlüssellänge von 256 bit verwenden. Nun müssen wir also einen 256 bit langen Schlüssel aus dem vom Benutzer eingegebenen Passwort oder Schlüsselsatz ableiten. Was wäre da besser geeignet als eine kryptografische Hash-Funktion?

SHA-1 (Secure Hash Algorithm) ist ein kryptografischer Einweg-Hash-Algorithmus. Er wurde von der NIST mit der NSA (National Security Agency) entwickelt und basiert auf dem von Ronald Rivest entwickelten MD4, dem Vorgänger vom bekannten MD5-Hash-Algorithmus. SHA-1 produziert allerdings einen Hash der Länge 160 bit, MD5 produziert 'nur' 128 bit. Es wurden einige Schwächen in der Kompressionsfunktion von MD5 festgestellt, die aber keinen Einfluss auf die Sicherheit des Algorithmus haben sollen... Wir verwenden den SHA-1-Algorithmus, der wegen seinen 160-bit auch sicherer gegen Brute-Force-Attacken ist als MD5.

SHA-1 ist eine Einwegfunktion, d.h. aus dem Hash-Wert läßt sich nicht der Original-Text ableiten.

Dialoge

CryptoCtx Screenshot Was wir nun noch brauchen sind drei Dialoge: einen der das Passwort für das Verschlüsseln fragt, einen der das Passwort für das Entschlüsseln fragt und einen Statusdialog. Dialoge legen Sie an indem Sie unter "Einfügen" "Neues ATL-Objekt" wählen und dann in der Gruppe "Miscellaneous" "Dialog" wählen. Im folgenden Dialog geben Sie wie bei der Erstellung des Haupt-ATL-Objekts eine Kurzbezeichnung an, der Rest wird wieder automatisch ausgefüllt. Die ersten beiden Dialoge für das Ver- und Entschlüsseln sollen modal sein, deshalb werden Sie die Dialoge später mit der Memberfunktion DoModal aufrufen. Im Gegensatz dazu müssen Sie den Statusdialog mit Create erstellen und später mit DestroyWindow wieder verschwinden lassen.

Der Verschlüsselungsdialog unterscheidet sich von dem Entschlüsselungsdialog ein wenig: zum Verschlüsseln müssen Sie das Passwort zweimal eingeben, zum Entschlüsseln nur einmal.

STL-Liste

Ganz am Anfang haben wir in einer STL-Liste die Dateinamen abgelegt. Nun müssen wir wieder an diese Dateinamen ran. Dazu müssen Sie zwei Zeiger vom Typ string_list::const_iterator deklarieren: sl_iterator und sl_iteratorEnd. sl_iterator setzen Sie auf m_lsFiles.begin(). Die begin-Funktion gibt den Anfang der Liste zurück. Entsprechend setzen Sie sl_iteratorEnd auf m_lsFiles.end(). In einer Schleife inkrementieren Sie sl_iterator mit ++ so lange bis es gleich dem sl_iteratorEnd ist.

Um nun den aktuellen Eintrag in der Stringliste als LPSTR zu bekommen verwenden Sie folgenden Befehl: sl_iterator->c_str(). Diese Funktion gibt einen Zeiger auf einen C-ähnlichen String (deshalb c_str) an der Position von sl_iterator in der Liste m_lsFiles zurück.

Damit string_list funktioniert müssen Sie in der StdAfx.h folgende Zeile hinzufügen:

typedef std::list<std::basic_string<TCHAR> > string_list;

Verschlüsselungsprozess

CryptoCtx Screenshot Die DoWork-Funktion muss Folgendes der Reihe nach tun: zunächst muss der entsprechende Dialog zur Passworteingabe aufgerufen werden, dann muss aus dem eingegebenen Passwort ein 256-bit langer Schlüssel generiert werden. Dazu hashen wir das eingegebene Benutzerpasswort mit einem vordefinierten String. Der Hash dieser zwei Eingaben bildet die ersten 160 bit des Gesamtschlüssels. Dann hashen Sie das Benutzerpasswort nochmals, diesmal aber mit einem anderen vordefinierten String. Von dem resultierenden 160 bit Hash verwenden Sie nur die ersten 96 bit. Den Rest (64 bit) werfen wir einfach weg. Der Witz bei der ganzen Sache ist folgender: die letzten 96 bit können nicht von den ersten 160 bit abgeleitet werden! Wer das tun wollte müsste SHA-1 umkehren, und das ist bekanntlicherweise nur theoretisch möglich. In der Praxis ist es unmöglich, da es einfach viel zu viele Möglichkeiten gibt, die man ausprobieren müsste. SHA-1 heißt nicht umsonst Einweg-Funktion.

So bekommen wir also einen sicheren 256-bit Schlüssel. Nun müssen wir die Rijndael-Klasse initialisieren. Wir verwenden wie oben schon erwähnt den CBC Modus. Dieser Modus ist wie man weiß der beste zum Verschlüsseln von Dateien. Verschlüsseln läuft nun folgendermaßen ab: es werden 12000 Bytes (Sie können diese Zahl auch ändern, sie muss nur durch 32 dividierbar sein). Dieser Puffer wird dann im RAM verschlüsselt und in die Ausgabedatei geschrieben. Nun haben wir noch ein allerletztes Problem zu bewältigen: was tun wenn die Datei eine ungerade Anzahl Bytes hat? Wir könnten den verbleibenden Rest des Puffers auffüllen um einen Block richtiger Größe zu bekommen ("padden"), doch dann wäre die verschlüsselte Datei um ein paar Bytes größer als die Originaldatei. In manchen Fällen ist das absolut undenkbar und unmöglich.

Die Lösung ist folgende: wir generieren ein neues, ebenfalls 256-bit langes, Passwort um einen neuen Rijndael zu initialisieren. Dieser Rijndael verschlüsselt dann ein aus Null-Bytes bestehendes Array. Die ersten Bytes dieses Arrays werden dann mit dem Rest der Quelldatei geXORt. Auch hier gilt wieder: wer diese Schlüsselbytes aus den vorhergehenden Bytes erschließen wollte müsste SHA-1 umkehren, was unmöglich ist.




Copyright © 2003-2010 Dominik Reichl, [Imprint] [Disclaimer] [Acknowledgements]