Von C nach Java, Teil 4: Datenkompression und Verschlüsselung

Seite 2: gzip und C

Inhaltsverzeichnis

In C gibt es leider keine Implementierung des Algorithmus, und auch die Betriebssysteme Windows und Unix bieten keine Systembibliothek hierfür an. Glücklicherweise gibt es seit vielen Jahren die zlib-Bibliothek für C und C++, die entsprechend gzip Funktionen für das Komprimieren und Dekomprimieren von Speicherblöcken und Dateien umfasst. Diese Bibliothek ist frei für Windows und Unix erhältlich und lässt sich vergleichsweise einfach in eigene Programme implementieren.

Es wird grob unterschieden zwischen dem Komprimieren von Dateien beziehungsweise Datenströmen und dem von Speicherblöcken, das im Verlauf dieses Beitrags zum gleichzeitigen Verschlüsseln benötigt wird (die darin verwendete Methode zur Verschlüsselung kann nur für Speicherblöcke erfolgen, da sie für Datenströme leider nicht verfügbar ist).

Nun sei ein kleines Beispiel gezeigt, das eine oder mehrere in der Kommandozeile übergebene Datei(en) komprimiert. Es erzeugt eine Zieldatei mit der Endung .gz, während die Kompressionsrate auf maximal ("9") gesetzt wird.

#include <stdio.h>
#include <errno.h>
#include <zlib.h>

#define BUFLEN 1024*1024

int main(int argc, char *argv[]) {
int argcnt=1;
char fileName[280];
unsigned char *buffer;

if (argc < 2) {
fprintf(stderr,"usage: %s file ...\n", argv[0]);
exit(1);
}
if ((buffer=malloc(BUFLEN))==NULL) {
fprintf(stderr,"unable to allocate buffer: malloc()
failed with %d: %s\n", errno, strerror(errno));
exit(errno);
}
while (--argc) {
gzFile gzfp;
FILE *ifp;
int rb;

strcpy(fileName, argv[argcnt++]);
if ((ifp=fopen(fileName,"rb"))==NULL) {
fprintf(stderr,"cannot open \"%s\": fopen() returned %d:
%s\n", fileName, errno, strerror(errno));
exit(errno);
}
strcat(fileName,".gz");
if ((gzfp=gzopen(fileName,"wb9"))==NULL) {
fprintf(stderr,"cannot open \"%s\": gzopen() returned %d:
%s\n", fileName, errno, strerror(errno));
exit(errno);
}
while ((rb=fread(buffer, 1, BUFLEN, ifp))>0) {
if (gzwrite(gzfp, buffer, rb)<rb) {
fprintf(stderr,"cannot write to gzipped file:
disk full?\n");
break;
}
}
fclose(ifp);
gzclose(gzfp);
}
}

Vorausgesetzt, dass die benötigten Include-Dateien (zlib.h und zconf.h) im Verzeichnis include und in der Bibliothek zdll.lib liegen, würde der Quelltext sowohl unter Windows als auch unter Unix mit dem GNU-C-Compiler wie folgt übersetzt:

$ gcc -Iinclude -o miniComp.exe miniComp.c lib/zdll.lib

Für die Ausführung müssen die Shared-Library zlib1.dll unter Windows oder zlib.a unter Unix im aktuellen Verzeichnis beziehungsweise unter Windows in einem zur Umgebungsvariablen PATH gehörenden Verzeichnis liegen. Unter Unix ist der LD_LIBRARY_PATH entsprechend zu setzen.

Zum Dekomprimieren wären die Funktionen gzopen (diesmal mit der Option rb) und gzread() zu verwenden. An der Stelle bleibt es dem Leser überlassen, das Programm um die Option "Dekomprimierung" zu erweitern. Die gut dokumentierte Header-Datei zlib.h gibt hier genügend Informationen.

In Java ist gzip eingebaut, man braucht keine zusätzlichen jar-Archive, was ungemein praktisch ist. Das obige Programm würde in Java folgendermaßen aussehen:

import java.io.*;
import java.util.zip.*;

public class MiniComp {
final static int BUFLEN=1024*1024;

public MiniComp(String[] args) throws Exception {
yte[] buffer=new byte[BUFLEN];
for (String s: args) {
FileInputStream fis=new FileInputStream(s);
GZIPOutputStream gos=new
GZIPOutputStream(new FileOutputStream(s+".gz"));
int rb;

while ((rb=fis.read(buffer, 0, BUFLEN))>0) {
gos.write(buffer, 0, rb);
}
fis.close();
gos.close();
}
}

public static void main(String[] args) {
if (args.length>0) {
try {
new MiniComp(args);
} catch(Exception e) {
System.err.println("Exception caught: "+e.getMessage());
System.exit(1);
}
} else {
System.err.println("usage: java "+MiniComp.class.getName()+"
file ...");
}
}
}

Nun sollen die komprimierten Daten zusätzlich verschlüsselt werden, um sie beispielsweise vor einem Zugriff zu schützen. Das ist in C und Java selbstverständlich möglich, jedoch mit unterschiedlichen Ausprägungen und damit verbundenen Aufwänden. Eine Verschlüsselung soll später mit einer standardisierten, professionellen Methode erfolgen, bei der auch bei Kenntnis des Programmiercodes ohne das Passwort keine Entschlüsselung möglich ist.

Zu Beginn sei eine verblüffend einfache Methode zur Verschlüsselung vorgestellt, die jedoch bei Kenntnis des Codes das Erraten des Passworts relativ leicht ermöglicht. Sie beruht darauf, dass Zufallszahlengeneratoren in C und in Java keine wirklichen Zufallszahlen, sondern immer die gleiche Folge an Zahlen liefern. Abhängig vom sogenannten Seed, dem Startwert dieser Reihe, wird eben eine immer gleiche Folge an Zahlen generiert. Nun lässt sich das vom Anwender vergebene Passwort zur Berechnung des Startwerts für den Zufallsgenerator heranziehen, und jedes Byte aus der Quelldatei wird mit der nächsten Zufallszahl XOR verknüpft. Damit ist der Algorithmus fertig beschrieben, und das Ergebnis kann sich sehen lassen: Durch die vielen unterschiedlichen Zufallszahlen ergibt sich ein Bitmuster, das überhaupt keinen Hinweis mehr auf die Ursprungsdaten zulässt.

Einen Haken gibt es jedoch. Der Algorithmus des Zufallszahlengenerators muss beim Kodieren und Dekodieren der gleiche sein. Lässt man das Programm unter Windows mit Microsofts C-Compiler übersetzen und verwendet man das ausführbare Programm zum Verschlüsseln, wird es mit einem unter Linux und dem GNU-C Compiler übersetzten Programm mit hoher Wahrscheinlichkeit nicht wieder zu entschlüsseln sein. Abhilfe schafft etwa eine eigene Routine für das Generieren von Zufallszahlen. Sie zu verwenden ist zudem sinnvoll, wenn auch das später vorgestellte Java-Beispiel zum Ver- und Entschlüsseln von Daten zum Einsatz kommen soll.

Ein kleines C-Programm (Listing 1) soll nun den Einsatz dieser Methode veranschaulichen. Der Aufbau ist nahezu identisch mit dem ersten kleinen Kompressionsprogramm. Statt einer komprimierten wird nun eine mit der einfachen Methode verschlüsselte Zieldatei erzeugt. Die Dateinamenerweiterung ist .crypt. Das Programm eignet sich sowohl zum Ver- als auch zum Entschlüsseln. Um Letzteres zu bewerkstelligen, ist das Programm einfach noch einmal aufzurufen, wobei die verschlüsselte Datei nunmehr Input ist. Der wiederholte Vorgang stellt durch die XOR-Operation die ursprüngliche Version wieder her.

Ein Leichtes ist es nun, das Programm aus Listing 1 nach Java zu portieren (siehe Listing 2). Das Vorhaben vereinfacht zusätzlich der Umstand, dass die String-Klasse bereits über eine Methode zur Bildung eines Hash-Codes verfügt.