Blockchain – eine Einführung

Das Konzept Blockchain ist grundlegender Bestandteil der verschiedenen Kryptobezahlmethoden wie Bitcoin und Ethereum. Was hat es aber mit der Technologie Blockchain auf sich und wozu kann man dieses Konzept noch anwenden? Grundlegend kann man sagen, dass Blockchain wie die Datenstruktur der rückwärtsverketteten Liste aufgebaut ist. Jedes Element der Liste zeigt auf den Vorgänger. Was macht die Blockchain aber nun so besonders?

Blockchain erweitert das Konzept der Liste um verschiedene Rahmenbedingungen. Eine dieser Rahmenbedingungen ist, sicherzustellen, dass kein Element der Liste verändert oder entfernt werden kann. Das lässt sich recht einfach bewerkstelligen. Hierzu verwendet man eine Hashfunktion. Wir codieren über einen Hashalgorithmus den Inhalt eines jeden Elements in der Liste zu einem Hash. Es gibt mittlerweile ein breites Angebot an Hashfunktionen, von denen ein aktueller Standard SHA-512 ist. In nahezu jeder Programmiersprache ist dieser Hashalgorithmus in der Standardbibliothek bereits implementiert und man kann ihn einfach verwenden. Im Konkreten bedeutet es, dass über die gesamten Daten eines Blockes der Hash SHA‑512 gebildet wird. Dieser Hash ist stets eineindeutig und kommt nicht noch einmal vor. Somit dient der Hash als Identifier (ID), um einen Block zu finden. Ein Eintrag in dem Block ist der Verweis auf seine Vorgänger. Dieser Verweis ist der Hashwert des Vorgängers, also seine ID. Bei der Implementierung einer Blockchain ist es essenziell, dass der Hashwert des Vorgängers Bestandteil der Berechnung des Hashwertes des aktuellen Blockes ist. Dieses Detail sorgt dafür, dass die in der Blockchain geänderten Elemente nur sehr aufwendig geändert werden können. Man muss sozusagen für das Element, das man manipulieren möchte, alle nachfolgenden Elemente ebenfalls ändern. Bei der umfangreichen Blockchain mit sehr vielen Blöcken bedeutet so ein Vorhaben einen enormen Rechenaufwand, der nur sehr schwer bis überhaupt nicht zu leisten ist.

Durch diese Verkettung erhalten wir eine lückenlose Transaktionshistorie. Dieser Umstand erklärt auch, weswegen Kryptobezahlmethoden nicht anonym sind. Auch wenn der Aufwand, einen Transaktionsteilnehmer eindeutig zu identifizieren, enorm werden kann. Nutzt dieser Transaktionsteilnehmer zudem noch verschiedene Verschleierungsmöglichkeiten mit unterschiedlichen und nicht durch weitere Transaktionen verknüpften Wallets, steigt der Aufwand exponentiell an.

Natürlich hat der gerade beschriebene Mechanismus noch erhebliche Schwachstellen. Denn Transaktionen, also das Hinzufügen neuer Blöcke, können erst als verifiziert und sicher eingestuft werden, wenn genügend Nachfolger in der Blockchain hinzugekommen sind, die sicherstellen, dass Änderungen wieder schwerer zu bewerkstelligen sind. Für Bitcoin und Co. gilt eine Transaktion als sicher, wenn fünf Nachfolgetransaktionen sind.

Damit es nun nicht nur eine Instanz gibt, die die Transaktionshistorie, also alle Blöcke der Blockchain, aufbewahrt, kommt ein dezentraler Ansatz ins Spiel. Es gibt also keinen zentralen Server, der als Vermittler agiert. Ein solcher zentraler Server könnte durch den Betreiber manipuliert werden. Man hätte so die Möglichkeit, mit genügend Rechenleistung auch sehr umfangreiche Blockchains neu aufzubauen. Im Kontext von Kryptowährungen spricht man hier von einer Reorganisation der Chain. Dies ist auch die Kritik an vielen Kryptowährungen. Außer Bitcoin existiert keine andere dezentrale und unabhängige Kryptowährung. Wenn die Blockchain mit allen ihren enthaltenen Elementen öffentlich gemacht wird und jeder Nutzer eine eigene Instanz dieser Blockchain lokal auf seinem Rechner besitzt und dort Elemente hinzufügen kann, die dann mit allen anderen Instanzen der Blockchain synchronisiert werden, haben wir einen dezentralen Ansatz.

Die Technologie zur dezentralen Kommunikation ohne einen Vermittler nennt sich Peer to Peer (P2P). P2P Netzwerke sind besonders in ihren Entstehungsphasen, also wenn nur wenige Teilnehmer im Netzwerk sind, schwach und angreifbar. Man könnte einfach mit sehr viel Rechenleistung eine große Anzahl von sogenannten Zomi Peers erstellen, die das Verhalten des Netzes beeinflussen. Gerade in Zeiten, in denen das Cloud-Computing mit AWS und der Google Cloud Platform als Anbieter, für vergleichsweise wenig Geld schier endlose Ressourcen bereitstellen kann, ist dies ein nicht zu unterschätzendes Problem. Besonders wenn es einen sehr hohen finanziellen Anreiz für Betrüger gibt, darf man diesen Punkt auf keinen Fall vernachlässigen.

Auch bei P2P gibt es verschiedene Konzepte, die miteinander konkurrieren. Um eine stabile und sichere Blockchain zu implementieren, ist es notwendig, nur auf Lösungen zurückzugreifen, die ohne unterstützende Backbone Server auskommen. Es soll vermieden werden, dass sich eine Master Chain etablieren kann. Daher müssen Fragen geklärt werden, wie sich die einzelnen Peers finden können und mit welchem Protokoll sie ihre Daten synchronisieren. Als Protokoll verstehen wir ein Regelwerk, also ein festes Schema, wie die Interaktion zwischen den Peers geregelt wird. Da bereits dieser Punkt sehr umfangreich ist, verweise ich zum Einstieg in das Thema auf meinen Vortrag 2022.

Eine weitere Eigenschaft von Blockchain‑Blöcken ist, dass sich diese problemlos und mit wenig Mühe auf Gültigkeit hin verifizieren lassen. Hierzu muss lediglich über die gesamten Inhalte eines Blocks der SHA-512 Hash erzeugt werden. Stimmt dieser mit der ID überein, ist der Block gültig. Aber auch zeitrelevante beziehungsweise zeitkritische Transaktionen, wie sie bei Bezahlsystemen relevant sind, lassen sich mit wenig Aufwand erstellen. Dazu werden auch keine komplexen Zeitserver als Vermittler benötigt. Jeder Block wird um einen Zeitstempel erweitert. Dieser Zeitstempel muss allerdings den Ort, an dem er erstellt wird, berücksichtigen, also die Zeitzone mit angeben. Um den Standort der Transaktionsteilnehmer zu verschleiern, können auch alle Uhrzeiten der verschiedenen Zeitzonen auf die aktuelle UTC 0 umgerechnet werden.

Um sicherzustellen, dass die Uhrzeit auch korrekt auf dem System eingestellt ist, kann man einen Zeitserver für die aktuelle Uhrzeit beim Start der Software abfragen und bei Abweichungen einen Hinweis zur Korrektur ausgeben.

Natürlich sind zeitkritische Transaktionen mit einer Reihe von Problemen behaftet. Es muss sichergestellt werden, dass eine Transaktion in einem festgelegten Zeitfenster durchgeführt wurde. Ein Problem, mit dem sich sogenannte Echtzeitsysteme auseinandersetzen müssen. Aber auch das Double Spending Problem muss verhindert werden. Also, dass derselbe Betrag zweimal an unterschiedliche Empfänger gesendet werden kann. Was in einem dezentralisierten Netzwerk die Bestätigung der Transaktion von mehreren Teilnehmern erfordert. Aber auch klassische Race Conditions können ein Problem darstellen. Race Conditions lassen sich durch die Anwendung des Immutable-Entwurfsmusters auf die Block-Elemente beherrschen.

Damit die Blockchain nun nicht durch Spam-Attacken gestört wird, benötigen wir eine Lösung, die das Erstellen eines einzelnen Blocks in der Masse teuer macht. Das erreichen wir, indem wir die Rechenleistung einbeziehen. Hierzu muss der Teilnehmer, der einen Block erstellt, ein Puzzle lösen, das eine ‚gewisse‘ Rechenzeit beansprucht. Wenn ein Spammer das Netzwerk mit vielen Blöcken fluten möchte, steigt seine Rechenleistung exorbitant an und es ist ihm unmöglich, in kurzer Zeit beliebig viele Blöcke zu erzeugen. Dieses kryptografische Puzzle nennt man Nonce, was für „number used only once“ steht. Der Mechanismus Nonce in der Blockchain wird auch oft als Proof of Work (POW) bezeichnet und dient bei Bitcoin zur Verifizierung der Blöcke durch die Miner.

Die Nonce ist eine (pseudo) zufällige Zahl, zu der ein Hash generiert werden muss. Dieser Hash muss dann ‚bestimmte‘ Kriterien erfüllen. Das können zum Beispiel zwei oder drei führende Nullen im Hash sein. Damit man nun keinen beliebigen Hash in den Block einfügt, wird die Zufallszahl, die das Rätsel löst, direkt abgespeichert. Eine bereits genutzte Nounce darf kein weiteres Mal verwendet werden, da sonst das Rätsel umgangen werden kann. Erzeugt man nun aus der Nonce den Hash, muss dieser die Vorgaben z. B. führende Nullen erfüllen, um akzeptiert zu werden.

Da je mehr Blöcke in einer Blockchain enthalten sind, es immer schwieriger wird, eine gültige Nonce zu finden, ist es notwendig, die Regeln für so eine Nonce zyklisch, beispielsweise alle 2048 Blöcke, zu verändern. Das bedeute aber auch, dass die Regeln für eine gültige Nonce ebenfalls den entsprechenden Blöcken zugeordnet werden müssen. Ein solches Regelwerk für die Nonce lässt sich problemlos über eine RegEx formulieren.

Mittlerweile haben wir auch eine beachtliche Menge an Regelwerk für eine Blockchain kennengelernt. Sodass es nun an der Zeit ist, sich ein wenig Gedanken über die Performance zu machen. Würden wir alle einzelnen Blöcke der Blockchain einfach in einer Liste abspeichern, würde uns recht bald der Arbeitsspeicher ausgehen. Jetzt könnte man die Blöcke auch in einer lokalen Datenbank speichern, was auch bei einer Embedded-Lösung wie SQLlite durchaus negative Auswirkungen auf die Geschwindigkeit der Blockchain haben würde. Eine einfache Lösung wäre hier, die Blockchain in gleichlange Teile zu zerlegen, sogenannte Chunks. Ein Chunk hätte sozusagen eine feste Länge von 2048 validen Blöcken und der erste Block eines neuen Chunk zeigt auf den letzten Block des vorhergehenden Chunk. In jedem Chunk kann man auch eine zentrale Regel für die Nonce hinterlegen und mögliche Metadaten wie beispielsweise Min- und Max-Zeitstempel abspeichern.

Wenn wir unseren aktuellen Stand des Blockchain-Regelwerks kurz rekapitulieren, kommen wir aktuell auf drei verschiedene Ebenen. Die größte Ebene ist die Blockchain selbst, welche grundlegende Metainformationen und Konfigurationen enthält. Solche Konfigurationen sind etwa der verwendete Hash Algorithmus. Die zweite Ebene stellen die sogenannten Chunks dar, die eine festgelegte Menge an Block-Elementen enthalten. Wie bereits erwähnt enthalten die Chunks auch Metadaten und Konfigurationen. Das kleinste Element der Blockchain ist der Block selbst, der aus einer ID, den beschriebenen Zusatzinformationen wie Zeitstempel und Nonce und dem Payload besteht. Der Payload ist eine allgemeine Bezeichnung für jedes beliebige Datenobjekt, das durch die Blockchain überprüfbar gemacht werden soll. Für Bitcoin und andere Kryptowährungen ist der Payload die Information, welcher Betrag von Wallet A (Quelle) an Wallet B (Ziel) transferiert wird.

Die Blockchain-Technologie eignet sich aber auch für viele andere Anwendungszenarien. So könnten beispielsweise die Hashwerte von Open-Source-Software Artefakten in einer Blockchain gespeichert werden. Damit könnte man auch Binärdateien aus unsicheren Quellen herunterladen und gegen die entsprechende Blockchain verifizieren. Das gleiche Prinzip wäre auf für die Signaturen von Antivirenprogrammen denkbar. Aber auch im behördlichen Umfeld könnten Anträge und andere Dokumente rechtssicher übermittelt werden. Die Blockchain würde hier als ‚Posteingangsstempel‘ funktionieren. Aber auch Buchhaltung inklusive aller Belege der Warenwirtschaft aus dem Einkauf und der verkauften Artikel und Dienstleistungen sind denkbare Szenarien.

Eine Erweiterung der Blockchain wäre je nach Anwendungsfall die eindeutige Signierung eines Blocks durch den Ersteller. Hierzu käme das klassische PKI Verfahren mit dem öffentlichen und privaten Schlüssel zum Einsatz. Der Unterzeichner speichert in Block seinen öffentlichen Schlüssel, erstellt mittels seines geheimen Schlüssels über den Payload eine Signatur, die als Signatur ebenfalls im Block mitgespeichert wird.

Bisher gibt es zwei frei verfügbare Implementierungen der Blockchain, BitcoinJ und für Ethereum Web3j. Natürlich ist es möglich, mit den gerade beschriebenen Prinzipien auch eine eigene allgemeingültige Blockchain-Implementierung zu erstellen. Hier liegen die Stolperfallen natürlich in den Details, von denen ich in diesem Artikel bereits einige angesprochen habe. Grundsätzlich ist Blockchain aber auch keine Raketentechnologie und für erfahrene Entwickler gut beherrschbar. Wer mit dem Gedanken spielt, sich an einer eigenen Implementierung zu versuchen, hat nun genügend Grundwissen, um sich mit dem notwendigen Detailwissen in den verschiedenen zum Einsatz kommenden Technologien tiefer vertraut zu machen.


Zähl auf mich – Java Enums

Eigentlich denkt man als erfahrener Entwickler oft, dass reine CORE Java Themen eher was für Anfänger sind. Das muss aber nicht unbedingt so sein. Ein Grund ist natürlich die Betriebsblindheit, die sich bei ‚Profis‘ mit der Zeit einstellt. Dieser Betriebsblindheit kann man nur entgegenwirken, wenn man sich gelegentlich auch mit vermeintlich vertrauten Themen erneut beschäftigt. Aufzählungen in Java sind leider zu Unrecht etwas unbeachtet und finden viel zu wenig Anwendung. Ein möglicher Grund für diese stiefmütterliche Behandlung können auch die vielen im Netz verfügbaren Tutorials zu Enums sein, die lediglich eine triviale Verwendung demonstrieren. Fangen wir zuallererst also damit an, wo Enums in Java ihre Verwendung finden.

Gehen wir einmal von ein paar einfachen Szenarien aus, die uns im Entwickleralltag begegnen. Stellen wir uns vor, wir müssen eine Hashfunktion implementieren, die mehrere Hash-Algorithmen wie MD5 und SHA-1 unterstützen kann. Als Einstiegspunkt haben wir auf Benutzerebene dann oft eine Funktion, die so oder ähnlich ausschauen kann: String hash(String text, String algorithm); – eine technisch korrekte Lösung.

Ein anderes Beispiel kann das Logging sein. Um einen Logeintrag zu schreiben, könnte die Methode wie folgt aussehen: void log(String message, String logLevel); und so lassen sich die Beispiele in diesem Stil beliebig fortführen.

Das Problem dieser Variante sind freie Parameter wie logLevel und algorithm, die für den korrekten Gebrauch nur eine sehr begrenzte Anzahl von Möglichkeiten gestatten. Das Risiko, dass Entwickler im Projektstress diesen Parameter falsch befüllen, ist recht hoch. Tippfehler, Variationen in Groß- und Kleinschreibung und so weiter sind mögliche Fehlerquellen. In der Java API gibt es durchaus einige solcher verbesserungswürdigen Designentscheidungen.

Um die Verwendung der API, also der nach außen genutzten Funktionen, zu verbessern, führen wie so oft viele Wege nach Rom. Ein Weg, der durchaus öfter zu sehen ist, ist die Verwendung von Konstanten. Um die Lösung ein wenig besser zu verdeutlichen, greife ich das Beispiel der Hash-Funktionalität auf. Es sollen die beiden Hash-Algorithmen MD5 und SHA-1 in der Klasse Hash unterstützt werden.

void Hash {
	
	public final static MD5 = "1";
	public final static SHA-1 = "2";

	/**
	 * Creates a hash from text by using the chosen algorithm.
	 */
	public String calculateHash(String text, String algorithm) {
		String hash = "";

		if(algorithm.equals(MD5)) {
			hash = MD5.hash(text);
		}
		if(algorithm.equals(SHA-1)) {
			hash = SHA-1.hash(text);
		}
		return hash;
	}
}

// Call
calculateHash("myHashValue", Hash.MD5);

In der Klasse Hash werden die beiden Konstanten MD5 und SHA-1 definiert, um anzuzeigen, welche Werte für den Parameter algorithem gültige Werte darstellen. Entwickler mit etwas Erfahrung erkennen diese Konstellation recht schnell und verwenden sie auch korrekt. Auch hier ist die Lösung technisch gesehen absolut korrekt. Aber auch wenn Dinge syntaktisch richtig sind, heißt es nicht, dass sie im semantischen Kontext bereits eine optimale Lösung darstellen. Die hier vorgestellte Strategie missachtet nämlich die Entwurfskriterien für objektorientierte Programmierung. Obwohl wir bereits durch die Konstanten gültige Eingabewerte für den Parameter algorithm bereitstellen, gibt es keinen Mechanismus, der die Verwendung der Konstanten erzwingt.

Nun könnte man argumentieren, das ist doch nicht wichtig, solange die Methode mit einem gültigen Parameter aufgerufen wird. Dem ist aber nicht so. Denn jeder Aufruf der Methode calculateHash ohne die Verwendung der Konstanten führt bei Refactorings zu möglichen Problemen. Will man aus Vereinheitlichung beispielweise den Wert “2“ in sha1 ändern, wird das überall da zu Fehlern führen, wo die Konstanten nicht zum Einsatz kommen. Wie können wir es also besser machen? Die bevorzugte Lösung sollte die Verwendung von Enums sein.

Alle in Java definierten Enums sind von der Klasse java.lang.Enum [2] abgeleitet. Eine Auswahl der Methoden ist:

  • name() gibt den Namen dieser Enum-Konstante genau so zurück, wie er in ihrer Enum-Deklaration angegeben ist.
  • ordinal() gibt die Ordnungszahl dieser Enum-Konstante zurück (ihre Position in der Enum-Deklaration, wobei der ersten Konstante die Ordnungszahl Null zugewiesen wird).
  • toString() gibt den Namen dieser Enum-Konstante so zurück, wie er in der Deklaration enthalten ist.

Enums gehören in Java von Anfang an zum Sprachkern und sind recht intuitiv zu benutzen. Für den Parameter algorithm erzeugen wir mit den beiden Werten MD5 und SHA1 die Enumklasse HashAlgorithm.

public enum HashAlgorithm {
MD5, SHA1
}

Dies ist die einfachste Variante, wie man ein Enum definieren kann. Eine einfache Aufzählung der gültigen Werte als Konstanten. In dieser Notation sind Konstantenname und Konstantenwert identisch. Wichtig beim Umgang mit Enums ist sich bewusst zu sein das die Namenkonventionen für Konstanten gelten.

Die Namen von als Klassenkonstanten deklarierten Variablen und von ANSI-Konstanten sollten ausschließlich in Großbuchstaben geschrieben und die Wörter durch Unterstriche („_“) getrennt werden. (ANSI-Konstanten sollten zur Vereinfachung der Fehlersuche vermieden werden.) Bsp.: static final int GET_THE_CPU = 1;

[1] Oracle Dokumentation

Diese Eigenschaft führt uns zum ersten Problem bei der vereinfachten Definition von Enums. Wenn wir aus irgendeinem Grund anstatt SHA-1 anstatt SHA1 definieren wollen, wird das zu einem Fehler führen, da “-“ nicht der Konvention für Konstanten entspricht. Daher findet sich für die Definition von Enums meist folgendes Konstrukt:

public enum HashAlgorithm {

    MD5("MD5"),
    SHA1("SHA-1");

    private String value;

    HashAlgorithm(final String value) {
        this.value = value;
    }

    public String getValue() {
        return this.value;
    }
}


In diesem Beispiel wird die Konstante SHA1 um einen String Wert “SHA-1“ erweitert. Um an diesen Wert zu gelangen, ist es notwendig, einen Zugriff dafür zu implementieren. Dieser Zugriff erfolgt über die Variable value, welche den zugehörigen Getter getValue() besitzt. Damit die Variable value auch befüllt wird, benötigen wir noch einen zugehörigen Konstruktor. Wie die verschiedenen Zugriffe auf den Eintrag SHA1 mit den entsprechenden Ausgaben zeigt der nachfolgende Testfall.

public class HashAlgorithmTest {

    @Test
    void enumNames() {
        assertEquals("SHA1",
                HashAlgorithm.SHA1.name());
    }

    @Test
    void enumToString() {
        assertEquals("SHA1",
                HashAlgorithm.SHA1.toString());
    }
    
    @Test
    void enumValues() {
        assertEquals("SHA-1",
                HashAlgorithm.SHA1.getValue());
    }


    @Test
    void enumOrdinals() {
        assertEquals(1,
                HashAlgorithm.SHA1.ordinal());
    }
}


Zuerst greifen wir auf den Namen der Konstante zu, der für unser Beispiel SHA1 lautet. Das gleiche Ergebnis erhalten wir mit der toString() Methode. Den Inhalt von SHA1, also den Wert SHA-1 erhalten wir über den Aufruf unserer definierten Methode getValue(). Zusätzlich kann noch die Position von SHA1 in der Enum abgefragt werden. Dies ist für unser Beispiel die zweite Stelle, an der SHA1 vorkommt, und hat dadurch den Wert 1. Schauen wir uns für die Implementierung der Hash-Klasse aus Listing 1 die Änderung für Enums an.

public String calculateHash(String text, HashAlgorithm algorithm) {
    String hash = "";
    switch (algorithm) {
        case MD5:
		hash = MD5.hash(text);
            break;
        case SHA1:
		hash = SHA1.hash(text);
            break;
    }
    return hash;
}

//CaLL
calculateHash(“text2hash“, HashAlgorithm.MD5);

Wir sehen in diesem Beispiel eine deutliche Vereinfachung der Implementierung und erreichen zudem eine sichere Verwendung für den Parameter algorithm, der nun durch die Enum bestimmt wird. Sicher gibt es einige Kritiker, die möglicherweise die Menge des zu schreibenden Codes bemängeln und darauf verweisen, dass dies viel kompakter möglich wäre. Solche Argumente kommen eher von Personen, die keine Erfahrung mit großen Projekten von mehreren Millionen Zeilen Quelltext konfrontiert wurden oder nicht in einem Team mit mehreren Programmierern zusammenarbeiten. Im objektorientierten Entwurf geht es nicht darum, möglichst jedes überflüssige Zeichen zu vermeiden, sondern lesbaren, fehlertoleranten, ausdruckstarken und leicht änderbaren Code zu schreiben. Alle diese Attribute haben wir in diesem Beispiel mit Enums abgedeckt.

Laut dem Tutorial von Oracle für Java Enums [3] sind noch viel komplexere Konstrukte möglich. Enums verhalten sich wie Java Klassen und können auch mit Logik angereichert werden. Die Spezifikation sieht hier vor allem die Methoden compare() und equals() vor, was Vergleichen und Sortieren ermöglicht. Für das Beispiel der Hashalgorithmen würde diese Funktionalität keinen Mehrwert bringen. Auch sollte man grundsätzlich davon Abstand nehmen, weitere Funktionalität beziehungsweise Logik in Enums unterzubringen, da diese ähnlich wie reine Datenklassen behandelt werden sollten.

Dass Enums durchaus ein wichtiges Thema in der Java Entwicklung darstellen, zeigt auch der Umstand, dass J. Bloch in seinem Buch “Effective Java“ dazu ein ganzes Kapitel mit knapp 30 Seiten gewidmet hat.

Damit haben wir ein ausführliches Beispiel über die korrekte Verwendung von Enums durchgearbeitet und vor allem auch erfahren, wo dieses Sprachkonstrukt in Java eine sinnvolle Anwendung findet.

Ressourcen

Abonnement / Subscription

[English] This content is only available to subscribers.

[Deutsch] Diese Inhalte sind nur für Abonnenten verfügbar.


PDF-Generierung mit TP-CORE

PDF ist vermutlich das wichtigste interoperable Dokumentenformat zwischen Betriebssystemen und Geräten wie Computer, Smartphone und Tablet. Die wichtigste Eigenschaft für PDF ist die Unveränderlichkeit der Dokumente. Natürlich sind auch hier Grenzen gesetzt und man kann heutzutage problemlos mit entsprechenden Programmen Seiten aus einem PDF Dokument entfernen oder hinzufügen. Dennoch lassen sich die Inhalte der einzelnen Seiten nicht ohne Weiteres verändern. Wo vor Jahren noch kostspielige Spezialsoftware wie beispielsweise Acrobat von Adobe nötig war, um mit PDF umzugehen, sind mittlerweile viele kostenfreie Lösungen auch für den kommerziellen Einsatz verfügbar. So beherrschen die gängigen Office Suiten den Export des Dokumentes als PDF.

Gerade im Geschäftsbereich ist PDF eine hervorragende Wahl, um Rechnungen oder Berichte zu generieren. Hier setzt auch dieser Artikel an. Ich beschreibe, wie einfache PDF Dokumente für die Rechnungslegung etc. genutzt werden können. Komplizierte Layouts, wie sie beispielsweise für Magazine und Zeitschriften in Verwendung kommen, sind nicht Teil dieses Artikels.

Technisch kommt die frei verfügbare Bibliothek openPDF zum Einsatz. Das Konzept ist, aus einem gültigen HTML Code, welcher in einem Template enthalten ist, eine PDF zu generieren. Da wir uns im JAVA Umfeld bewegen, ist die Template Engine der Wahl Velocity. Alle diese Abhängigkeiten nutzt TP-CORE in der Funktionalität PdfRenderer, welche als Fassade implementiert ist. Dies soll die Komplexität der PDF-Generierung in der Funktionalität kapseln und einen Austausch der Bibliothek ermöglichen.

Auch wenn ich nun kein Spezialist der PDF-Generierung bin, habe ich im Laufe der Jahre einige Erfahrungen speziell mit dieser Funktionalität in Bezug auf die Wartbarkeit von Softwareprojekten sammeln können. Dazu gibt es sogar einen Konferenzvortrag. Als ich vor sehr vielen Jahren den Entschluss gefasst hatte, PDF in TP-CORE zu unterstützen, gab es nur die iText-Bibliothek, die zu dieser Zeit mit der Version 5 frei verfügbar war. Ähnlich wie Linus Torvalds mit seinem ursprünglich genutzten Source Control-Management-System, erging es mir. Itext wurde kommerziell und ich brauchte eine neue Lösung. Nun, ich bin nicht Linus, der mal fix GIT aus dem Boden stampft, also habe ich nach einiger Wartezeit openPDF entdeckt. OpenPDF ist ein Fork von iText5. Jetzt musste ich meinen bestehenden Code entsprechend anpassen. Das erforderte einiges an Zeit, war aber dank meiner Kapselung eine überschaubare Aufgabe. In diesem Anpassungsprozess habe ich allerdings Probleme entdeckt, die mir in meiner kleinen Welt bereits das Leben schwer gemacht haben, also habe ich TP-CORE Version 3.0 veröffentlicht, um in der Funktionalität Stabilität zu erreichen. Wer also TP-CORE in einer 2.X Version verwendet, findet dort als PDF Lösung noch iText5. Das soll aber jetzt genug über die Entstehungsgeschichte sein. Kümmern wir uns darum, wie wir aktuell mit TP-CORE 3.1 PDF generieren können. Auch hier gilt: Mein größtes Ziel ist es, eine möglichst hohe Kompatibilität zu erreichen.

Bevor wir beginnen können, müssen wir TP-CORE als Abhängigkeit in unser Java Projekt einbinden. Das Beispiel demonstriert die Verwendung von Maven als Build Werkzeug, kann aber leicht auf Gradle umgestellt werden.

<dependency>
    <groupId>io.github.together.modules</groupId>
    <artifactId>core</artifactId>
    <version>3.1.0</version>
</dependency>

Um etwa eine Rechnung zu generieren, brauchen wir verschiedene Schritte. Zu erst benötigen wir ein HTML Template, das wir mit Velocity erzeugen. Das Template kann natürlich Platzhalter für Namen und Adresse enthalten, wodurch Serienbriefe und Batchverarbeitung möglich sind. Der folgende Code zeigt, wie wir damit umgehen können.

<h1>HTML to PDF Velocity Template</h1>

<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet.</p>

<p>Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.</p>

<h2>Lists</h2>
<ul>
    <li><b>bold</b></li>
    <li><i>italic</i></li>
    <li><u>underline</u></li>
</ul>

<ol>
    <li>Item</li>
    <li>Item</li>
</ol>

<h2>Table 100%</h2>
<table border="1" style="width: 100%; border: 1px solid black;">
    <tr>
        <td>Property 1</td>
        <td>$prop_01</td>
        <td>Lorem ipsum dolor sit amet,</td>
    </tr>
    <tr>
        <td>Property 2</td>
        <td>$prop_02</td>
        <td>Lorem ipsum dolor sit amet,</td>
    </tr>
    <tr>
        <td>Property 3</td>
        <td>$prop_03</td>
        <td>Lorem ipsum dolor sit amet,</td>
    </tr>
</table>

<img src="path/to/myImage.png" />

<h2>Links</h2>
<p>here we try a simple <a href="https://together-platform.org/tp-core/">link</a></p>

template.vm

In dem Template sind drei Properties enthalten: $prop_01, $prop_02 und $prop_03, die wir mit Werten füllen müssen. Das gelingt uns mit einer einfachen HashMap, die wir mit dem TemplateRenderer einfügen, wie das folgende Beispiel zeigt.

Map<String, String> properties = new HashMap<>();
properties.put("prop_01", "value 01");
properties.put("prop_02", "value 02");
properties.put("prop_03", "value 03");

TemplateRenderer templateEngine = new VelocityRenderer();
String directory = Constraints.SYSTEM_USER_HOME_DIR +/;
String htmlTemplate = templateEngine
                .loadContentByFileResource(directory, "template.vm", properties);

Der TemplateRenderer benötigt drei Argumente:

  • directory – Der vollständige Pfad vo das Template zu finden ist.
  • template – den Namen des Velocity-Templates
  • properties – Die HashMap mit den zu ersetzenden Variablen

Das Ergebnis ist ein gültiger HTML Code, aus dem wir nun über den PdfRenderer das PDF generieren können. Eine Besonderheit ist die Konstante SYSTEM_USER_HOME_DIR, welche auf das Nutzerverzeichnis des aktuell angemeldeten Benutzers zeigt und die Unterschiede einzelner Betriebssysteme wie Linux und Windows abfängt.

PdfRenderer pdf = new OpenPdfRenderer();
pdf.setTitle("Title of my own PDF");
pdf.setAuthor("Elmar Dott");
pdf.setSubject("A little description about the PDF document.");
pdf.setKeywords("pdf, html, openPDF");
pdf.renderDocumentFromHtml(directory + "myOwn.pdf", htmlTemplate);

Das Listing zum Erstellen der PDF ist überschaubar und recht selbsterklärend. Es ist natürlich möglich, das HTML hardcodiert zu übergeben. Die Nutzung des Templates erlaubt hier eine Trennung von Code und Design, was auch spätere Anpassungen flexibel erlaubt.

Das Standardformat ist A4 mit den Dimensionen size: 21cm 29.7cm; margin: 20mm 20mm; und ist als inline CSS definiert. Über die Methode setFormat(String format); lässt sich dieser Wert individuell festlegen. Der erste Wert steht hierbei für die Breite und der zweite Wert für die Höhe.

Der PDF Renderer hat auch die Möglichkeit, einzelne Seiten aus einem Dokument zu löschen.

PdfRenderer pdf = new OpenPdfRenderer();
PdfDocument original = pdf.loadDocument( new File("document.pdf") );
PdfDocument reduced = pdf.removePage(original, 1, 5);
pdf.writeDocument(reduced, "reduced.pdf");


Wir sehen also, dass bei der Implementierung der PDF Funktionalität sehr viel Wert auf eine einfache Handhabung gelegt wurde. Dennoch gibt es viele Möglichkeiten, individuelle PDF Dokumente zu erstellen. Daher hat die Lösung, über HTML das Layout zu definieren, einen besonderen Reiz. Trotz, dass der PdfRenderer mit einer sehr überschaubaren Funktionalität aufwartet, ist es sehr einfach möglich, durch den Vererbungsmechanismus von Java das Interface und die zugehörige Implementierung durch eigene Lösungen zu erweitern. Diese Möglichkeit besteht auch für alle anderen Implementierungen aus TP-CORE.

TP-CORE ist auch für den kommerziellen Gebrauch frei und hat dank der Apache 2 Lizenz keine Einschränkungen. Der Source Code ist unter https://git.elmar-dott.com zu finden und die Dokumentation inklusive Security und Testcoverage findet sich unter https://together-platform.org/tp-core/.

BugChaser – Die Grenze der Testabdeckung

Die mittlerweile im Software Engineering etablierten Paradigmen wie Test Driven Development (TDD) und Behavior Driven Development (BDD) mit entsprechend einfach zu bedienenden Werkzeugen haben eine neue pragmatische Sichtweise auf das Thema Software Tests eröffnet. Ein wichtiger Faktor in kommerziellen Softwareprojekten sind automatisierte Tests. Deshalb spricht man in diesem Kontext von einer erfolgreichen Teststrategie, wenn die Testausführung ohne menschliches Zutun vonstattengeht.

Testautomatisierung bildet die Grundlage, um Stabilität und Risikoreduzierung bei kritischen Arbeiten zu erreichen. Zu solchen kritischen Tätigkeiten zählen insbesondere das Refactoring, Maintenance und Fehlerkorrekturen. Allen diesen Aktivitäten obliegt eine Gemeinsamkeit: dass sich keine neuen Fehler in den Code einschleichen dürfen.

In dem Artikel „The Humble Programmer“ von 1972 stellte Edsger W. Dijkstra folgendes fest:

„Programmtests können ein sehr effektiver Weg sein, um das Vorhandensein von Fehlern aufzuzeigen, sind aber völlig unzureichend, um deren Abwesenheit nachzuweisen.“

Eine alleinige Automatisierung der Testausführung ist deshalb nicht ausreichend, um sicherzustellen, dass Änderungen der Codebasis keine unerwünschten Effekte auf bestehende Funktionen haben. Aus diesem Grund muss die Qualität der Testfälle bewertet werden. Hierzu gibt es bereits bewährte Werkzeuge.

Bevor wir tiefer in die Thematik einsteigen, wollen wir uns zuerst überlegen, was eigentlich automatisiertes Testen bedeutet. Diese Frage ist recht einfach zu beantworten. Nahezu jede Programmiersprache hat ein entsprechendes Unit Test Framework. Unit Tests rufen eine Methode mit verschiedenen Parametern auf und vergleichen den Rückgabewert mit einem Erwartungswert. Stimmen beide Werte überein, gilt der Test als bestanden. Zusätzlich kann noch überprüft werden, ob eine Ausnahme geworfen wurde.

Für den Fall, dass eine Methode keinen Rückgabewert hat oder keinen Fehler wirft, kann diese Methode nicht getestet werden. Auch als private gekennzeichnete Methoden oder innere Klassen, sind nicht ohne Weiteres zu testen, da sie nicht direkt aufgerufen werden können. Diese sind über öffentliche Methoden, welche die ‚versteckten‘ Methoden aufrufen, zu testen.

Im Umgang mit als private gekennzeichneten Methoden ist es keine Option, die dadurch abgebildete Funktionalität über Techniken wie die Verwendung der Reflection API zu erreichen und zu testen. Denn wir müssen uns bewusst machen, dass solche Methoden oft auch dazu verwendet werden, Fragmente zu kapseln, um Dopplungen zu vermeiden.

public boolean method() {
    boolean success = false;
    List collector = new ArryList();
    collector.add(1);
    collector.add(2);
    collector.add(3);
    
    sortAsc(collector);
    if(collector.getFirst().equals(1)) {
        success = true;
    }
    return success;
}

private void sortAsc(List collection) {
    collection.sort( 
            (a, b) -> { 
                return -1 * a.compareTo(b); 
            });
}

Um also effektiv automatisierte Tests schreiben zu können, ist es notwendig, einem gewissen Codingstil zu folgen. Das vorangegangene Listing 1 demonstriert auf einfache Weise, wie testbarer Code aussehen kann.

Da Entwickler für ihre eigene Implementierung auch die zugehörigen Komponententests schreiben, ist das Problem von schwer testbarem Code in Projekten, die einem testgetriebenen Ansatz folgen, weitgehend eliminiert. Die Motivation zu testen liegt nun beim Entwickler, da dieser mit diesem Paradigma feststellen kann, ob seine Implementierung sich wie gewünscht verhält. Dabei müssen wir uns aber fragen: Ist das bereits alles, was wir tun müssen, um gute und stabile Software zu entwickeln?

Wie wir uns bei solchen Fragen immer denken können, lautet die Antwort nein. Ein essenzielles Werkzeug, um die Qualität der Tests zu bewerten, ist das Erreichen einer möglichst hohen Testabdeckung. Dabei wird zwischen Branch und Line Coverage unterschieden. Um den Unterschied etwas besser zu verdeutlichen, schauen wir kurz auf den Pseudocode in Listing 2.

if( Expression-A OR Expression-B ) {
print(‘allow‘);
} else {
print(‘decline‘);
}

Unser Ziel ist es, nach Möglichkeit alle Zeilen zu durchlaufen. Dazu brauchen wir bereits zwei separate Testfälle. Einen für das Betreten des IF-Zweiges und einen für das Betreten des ELSE-Zweiges. Damit wir aber auch eine hundertprozentige Branch-Coverage erzielen, müssen wir alle Varianten des IF-Zweiges abdecken. Für das Beispiel heißt das: ein Test, der Expression-A true werden lässt, und ein weiterer Test, der Expression-B true werden lässt. Daraus ergeben sich insgesamt drei verschiedene Testfälle.

Der Screenshot aus dem Projekt TP-CORE zeigt, wie eine solche Testabdeckung in ‚echten‘ Projekten aussehen kann.

Natürlich ist dieses Beispiel sehr einfach und es gibt im wirklichen Leben oft Konstrukte, bei denen man trotz aller Bemühungen nicht alle Lines beziehungsweise Branches erreicht. Sehr typisch sind Exceptions aus Fremdbibliotheken, die zu fangen sind, aber unter normalen Umständen nicht provoziert werden können.

Aus diesem Grund versuchen wir zwar, eine möglichst hohe Testabdeckung zu erreichen, und streben natürlich die 100 % an, aber es gibt genügend Fälle, in denen dies nicht möglich ist. Eine Testabdeckung von 90% gelingt aber durchaus. Der Industriestandard für kommerzielle Projekte liegt bei 85 % Testabdeckung. Mit diesen Erkenntnissen können wir also sagen, dass die Testabdeckung mit der Testbarkeit einer Anwendung korreliert. Das bedeutet, die Testabdeckung ist ein geeignetes Maß für die Testbarkeit.

Hier muss man allerdings auch zugeben, dass die Metrik der Testabdeckung ihre Grenze hat. Reguläre Ausdrücke und Annotationen zur Datenvalidierung sind nur einige einfache Beispiele für eine nicht aussagefähige Testabdeckung.

Ohne allzu sehr in die Implementierungsdetails einzugehen, stellen wir uns vor, wir müssten einen regulären Ausdruck schreiben, um Eingaben auf ein korrektes 24 Stunden Format zu überprüfen. Haben wir das korrekte Intervall nicht im Auge, ist unser regulärer Ausdruck möglicherweise nicht korrekt. Das richtige Intervall für das 24-Stunden-Format lautet: 00:00 – 23:59. Beispiele für fehlerhafte Werte sind 24:00 oder 23:60. Ist uns dieser Fakt nicht bewusst, können trotz Testfällen Fehler in unserer Anwendung versteckt sein, bis sie verwendet werden un zu Fehlern führen.

Dies ist ein hervorragendes Beispiel für das Zitat von Dijkstra zu Beginn des Artikels. Zudem möchte ich noch ein weiteres Zitat aus einem Artikel anführen, an dem Christian Bird mitgewirkt hat. Der Artikel heißt „The Design of Bug Fixes“ und ist aus dem Jahr 2013.

„… In a few cases, participants were unable to think of alternative solutions …“

Hier ging es um die Fragestellung, ob eine Fehlerkorrektur immer die optimale Lösung darstellt. Abgesehen davon wäre zu klären, was in kommerziellen Softwareentwicklungsprojekten eine optimale Lösung darstellt. Sehr demonstrativ ist die Aussage, dass es Fälle gibt, in denen Entwickler nur einen Weg kennen beziehungsweise verstehen. Das spiegelt auch unser Beispiel der RegEx wider. Softwareentwicklung ist ein Denkprozess, der sich auch nicht beschleunigen lässt. Unser Denken wird durch unsere Vorstellungskraft bestimmt, die wiederum von unserer Erfahrung beeinflusst ist.

Dies zeigt uns bereits ein weiteres Beispiel für Fehlerquellen in Testfällen. Ein Klassiker sind z. B. inkorrekte Vergleiche in Collections. Es geht unter anderem um das Vergleichen von Arrays. Die Problematik, mit der wir hier zu kämpfen haben, ist das Thema, wie auf Variablen zugegriffen wird: Call by Value oder Call by Reference. Bei Arrays erfolgt der Zugriff über Call by Reference, also direkt auf die Speicherzelle. Weist man nun ein Array einer neuen Variable zu und vergleicht beide Variablen, sind diese immer gleich, da man das Array mit sich selbst vergleicht. Ein Beispiel für einen Testfall, der eigentlich keine Aussagekraft hat. Ist die Implementierung dennoch korrekt, wird dieser fehlerhafte Testfall nie ins Gewicht fallen.

Diese Erkenntnis zeigt uns, dass ein blindes Erreichen der Testabdeckung für die Qualität nicht zielführend ist. Natürlich ist es verständlich, dass im Management diese Metrik einen hohen Stellenwert hat. Wir haben aber auch nachweisen können, das man sich darauf alleine nicht verlassen darf. Wir sehen also, dass auch für Testfälle Codeinspektionen und Refactorings ein Bedarf besteht. Da man aus Zeitgründen nun nicht den ganzen Code von vorn bis hinten lesen und auch verstehen kann, ist es wichtig, auf problematische Bereiche zu konzentrieren. Wie kann man aber diese Problembereiche finden? Hier hilft uns eine vergleichsweise neue Technik. Die theoretischen Arbeiten dazu sind bereits etwas älter, es hat nur eine Weile gedauert, bis entsprechende Implementierungen verfügbar wurden.

Mutation Testing. Dies erlaubt, durch Veränderung des Originalcodes Testfälle zu finden, die trotz verschiedener Mutationen nicht fehlschlagen. Damit haben wir ein weiteres Werkzeug an der Hand, die Qualität der vorhandenen Tests zu bewerten. In diesem Artikel habe ich zeigen können, dass man sich nicht komplett auf die Testcoverage verlassen darf. Um dieses Problem zu lösen, können wir uns des Mutation Testing bedienen. Wie konkret Mutation Testing funktioniert, kann wiederum Thema eines eigenständigen Artikels sein und würde an dieser Stelle den Rahmen sprengen.

Kryptografie – mehr als nur Zufall

Im täglichen Sprachgebrauch benutzen wir das Wort Zufall recht unreflektiert. Sätze wie, „Ich bin zufällig hier vorbeigekommen.“ oder „Was für ein Zufall, dich hier zu treffen.“ kennt jeder von uns. Aber was möchten wir damit zum Ausdruck bringen? Eigentlich möchten wir damit sagen, dass wir die aktuelle Situation nicht erwartet haben.

Zufall ist eigentlich ein mathematischer Ausdruck, den wir in den täglichen Sprachgebrauch übernommen haben. Zufall meint etwas nicht Vorhersagbares. Also Dinge wie, an welcher Stelle sich zu einem exakten Moment ein beliebiges Elektron eines Atoms befindet. Welchen Weg ich zu einem bestimmten Ziel nehme, kann zwar beliebig sein, dennoch lassen sich über Wahrscheinlichkeiten Präferenzen ableiten, welche die Wahl dann durchaus vorhersagbar machen.

Umstände für ein solches Szenario können Entfernung, persönliche Befinden (Zeitdruck, Unwohlsein oder Langeweile) oder äußere Umstände (Wetter: Sonnenschein, Regen) sein. Habe ich Langeweile UND es scheint die Sonne, wähle ich für etwas Zerstreuung und Neugier eine unbekannte Strecke. Habe ich wenig Zeit UND es regnet, entscheide ich mich für den mir kürzesten bekannten Weg, oder eine Strecke, die möglichst überdacht ist. Daraus folgt: Je besser man die Gewohnheiten einer Person kennt, umso vorhersagbarer sind ihre Entscheidungen. Vorhersagbarkeit aber widerspricht dem Konzept Zufall.

Dass mathematische Begriffe, die sehr streng definiert sind, zeitweilig als Modeerscheinung in unseren täglichen Sprachgebrauch übernommen werden, ist keine neue Sache. Ein sehr populäres Beispiel, das bereits Joseph Weizenbaum angeführt hat, möchte ich hier kurz aufgreifen. Der Begriff Chaos. Eigentlich beschreibt Chaos im mathematischen, den Umstand, dass eine sehr kleine Änderung bei sehr langen Strecken das Ergebnis erheblich verfälscht, sodass es nicht einmal als Schätzung oder Näherung verwendet werden kann. Eine typische Anwendung ist die Astronomie. Richte ich einen Laserstrahl von der Erde auf den Mond, so verursacht bereits eine Abweichung im Winkel von wenigen Millimetern, dass der Laserstrahl kilometerweit am Mond vorbeigeht. Um solche Gegebenheiten populärwissenschaftlich einer breiten Masse zu erklären, verwendete man eine Assoziation, dass, wenn ein Schmetterling in Tokio mit den Flügeln schlägt, dies zu einem Sturm in Berlin führen kann. Leider gibt es nicht wenige Pseudowissenschaftler, die dieses Bild aufgreifen und ihrer Umwelt als Tatsache verkaufen. Das ist natürlich Unfug. Das Flügelschlagen eines Schmetterlings kann auf der anderen Seite des Globus keinen Sturm erzeugen. Denken wir nur daran, welche Auswirkungen das auf unsere Welt hätte, alleine die ganzen Vögel, die sich jeden Tag in die Luft schwingen.

„Warum ist die Ehe des Mathematikers gescheitert? Seine Frau war nicht berechenbar.“

Warum ist Zufall in der Mathematik aber eine so wichtige Sache? Im Konkreten geht es um das breite Thema Kryptografie. Wenn wir für die Verschlüsselung Kombinationen wählen, die man leicht erraten kann, ist der Schutz schnell dahin. Dazu ein kleines Beispiel.

Die Seiten des Internets sind statuslos. Das bedeutet, dass, nachdem eine Webseite aufgerufen wird und man auf einen Link klickt, um zur nächsten Seite zu gelangen, alle Informationen aus der vorangegangenen Seite verloren gegangen sind. Um dennoch Dinge wie in einem Onlineshop, einen Warenkorb und all die sonst noch notwendigen Funktionen zum Einkaufen bereitstellen zu können, gibt es die Möglichkeit, Daten auf dem Server in sogenannten Sessions zu speichern. Zu diesen Daten gehört oft auch das Login des Nutzers. Um die Sessions zu unterscheiden, haben diese eine Identifikation (ID). Der Programmierer legt nun fest, wie diese ID generiert wird. Eine Eigenschaft dieser IDs ist, dass sie eindeutig sein müssen, es darf also keine ID zweimal vorkommen.

Nun könnte man auf die Idee kommen, den Zeitstempel inklusive der Millisekunden zu nutzen, um daraus einen Hash zu generieren. Der Hash verhindert, dass man auf den ersten Blick erkennt, dass die ID aus einem Zeitstempel erstellt wird. Ein geduldiger Hacker hat dieses Geheimnis mit ein wenig Fleiß vergleichsweise schnell gelüftet. Hinzukommt noch die Wahrscheinlichkeit, dass zwei Nutzer zur gleichen Zeit eine Session erzeugen können, was zu einem Fehler führen würde.

Nun könnte man auf die Idee kommen, die SessionID aus verschiedenen Segmenten wie Zeitstempel + Benutzernamen und anderen Details zusammenzubauen. Obwohl steigende Komplexität einen gewissen Schutz bietet, ist dies keine wirkliche Sicherheit. Denn Profis haben Methoden mit überschaubarem Aufwand, diese ‚vermeidlichen‘ Geheimnisse zu erraten. Der einzig wirkliche Schutz ist die Verwendung von kryptografisch sicherem Zufall. Als ein Segment, das sich mit noch so viel Aufwand nicht erraten lässt.

Bevor ich aber verrate, wie wir dem Problem begegnen können, möchte ich den typischen Angriffsvektor und den damit erzeugten Schaden auf SessionIDs kurz besprechen. Wenn die SessionID durch einen Angreifer erraten wurde und diese Session noch aktiv ist, dann kann der Hacker diese Session in seinem Browser übernehmen. Das Ganze nennt sich Session Hijacking oder auch Session Riding. Der Angreifer, der eine aktive Session übernehmen konnte, ist als fremder Nutzer mit einem Profil, das ihm nicht gehört, bei einem Onlinedienst angemeldet. Damit kann er alle Aktionen durchführen, die ein legitimer Nutzer auch tun kann. Es wäre also möglich, in einem Onlineshop eine Bestellung auszulösen und die Ware an eine andere Adresse zu schicken. Ein Umstand, den es mit allen Mitteln zu verhindern gilt.

Nun gibt es verschiedene Strategien, die eingesetzt werden, um das Stehlen einer aktiven Session zu unterbinden. Jede einzelne dieser Strategien bietet schon einen ‚gewissen‘ Schutz, aber die volle Stärke wird erst durch die Kombination der verschiedenen Optionen erreicht, denn die Hacker rüsten ja auch stetig nach und suchen nach Möglichkeiten. Im Rahmen dieses kleinen Artikels betrachten wir ausschließlich den Aspekt, wie man eine kryptografisch sichere Session ID erzeugen kann.

So ziemlich alle gängigen Programmiersprachen haben eine Funktion random(), die eine zufällige Zahl erzeugt. Die Implementierung dieser Zufallszahl variiert. Leider sind diese generierten Zahlen für Angreifer gar nicht so zufällig, wie sie sein sollten. Deswegen gilt für Entwickler immer der Grundsatz, diese einfache Zufallsfunktion zu meiden. Stattdessen gibt es für Backendsprachen wie PHP und JAVA kryptografisch sichere Implementierungen für Zufallszahlen.

Für Java Programme kann man auf die Klasse java.security.SecureRandom zurückgreifen. Eine wichtige Funktion dieser Klasse ist die Möglichkeit, aus verschiedenen Kryptografie-Algorithmen [1] zu wählen. Zusätzlich lässt sich der Startwert über den sogenannten Seed. Um die Verwendung ein wenig zu demonstrieren, hier ein kleiner Codeausschnitt:

Abonnement / Subscription

[English] This content is only available to subscribers.

[Deutsch] Diese Inhalte sind nur für Abonnenten verfügbar.

Wir sehen, die Verwendung ist recht einfach und kann leicht angepasst werden. Für PHP ist es sogar noch einfacher, Zufall zu erzeugen. Dazu ruft man lediglich die Funktion random_int ( $min, $max ); [2] auf. Das Intervall kann optional angegeben werden.

Wir sehen also, dass die Annahme vieler Menschen, unsere Welt wäre in großen Teilen berechenbar, nicht ganz. Es gibt in vielen Bereichen der Naturwissenschaften Prozesse, die wir nicht berechnen können, Diese bilden dann wiederum die Grundlage, um ‚echten‘ Zufall zu erzeugen. Für Anwendungen, die einen sehr starken Schutz benötigen, greift man oft auf Hardware zurück. Das können etwa Geräte sein, die den radioaktiven Zerfall eines gering strahlenden Isotops messen.

Das Feld der Kryptografie und auch der Web-Application-Security sind natürlich noch viel umfangreicher. Dieser Artikel sollte mit einem recht einfachen Beispiel auf die Notwendigkeit dieser Thematik lenken. Dabei habe ich es vermieden, mit komplizierter Mathematik mögliche Interessenten zu verwirren und sie schlussendlich auch zu vergraulen.

Ressourcen

Abonnement / Subscription

[English] This content is only available to subscribers.

[Deutsch] Diese Inhalte sind nur für Abonnenten verfügbar.


Pfadfinder

Damit wir Konsolenprogramme systemweit direkt aufrufen können, ohne dass wir dazu den vollständigen Pfad angeben müssen, bedienen wir uns der sogenannten Pfadvariable. Wir speichern also in dieser Pfadvariable den gesamten Pfad inklusive des ausführbaren Programmes, der sogenannten Executable, um auf der Kommandozeile den Pfad inklusive der Executable nicht mehr mit angeben zu müssen. Übrigens leitet sich aus dem Wort executable die in Windows übliche Dateierweiterung exe ab. Hier haben wir auch einen signifikanten Unterschied zwischen den beiden Betriebssystemen Windows und Linux. Während Windows über die Dateiendung wie beispielsweise exe oder txt weiß, ob es sich um eine reine ASCII Textdatei oder um eine ausführbare Datei handelt, nutzt Linux die Metainformationen der Datei, um diese Unterscheidung zu machen. Deswegen ist es unter Linux eher unüblich, diese Dateiendungen txt und exe zu verwenden.

Typische Anwendungsfälle für das Setzen der Pfadvariable sind Programmiersprachen wie Java oder Werkzeuge wie das Buildwerkzeug Maven. Wenn wir zum Beispiel Maven von der offiziellen Homepage heruntergeladen haben, können wir das Programm an einer beliebigen Stelle auf unserem System entpacken. Unter Linux könnte der Ort /opt/maven und unter Microsoft Windows C:/Programme/Maven lauten. In diesem Installationsverzeichnis gibt es wiederum ein Unterverzeichnis /bin in dem die ausführbaren Programme liegen. Die Executable für Maven heißt mvn und um etwa die Version auszugeben, wäre unter Linux ohne den Eintrag in der Pfadvariablen das Kommando wie folgt: /opt/maven/bin/mvn -v. Also ein wenig lang, wie wir durchaus zugeben können. Der Eintrag des Installationsverzeichnisses von Maven in den Pfad verkürzt den gesamten Befehl auf mvn -v. Dieser Mechanismus gilt übrigens für alle Programme, die wir als Befehl in der Konsole verwenden.

Bevor ich dazu komme, wie die Pfadvariable unter Linux, als auch unter Windows angepasst werden kann, möchte ich noch ein weiteres Konzept, die Systemvariable, vorstellen. Systemvariablen sind globale Variablen, die uns in der Bash zur Verfügung stehen. Die Pfadvariable zählt auch als Systemvariable. Eine andere Systemvariable ist HOME, welche auf das Stammverzeichnis des angemeldeten Benutzers zeigt. Systemvariablen werden großgeschrieben und die Wörter mit einem Unterstrich getrennt. Für unser Beispiel mit dem Eintragen der Maven Executable in den Pfad, können wir auch eine eigene Systemvariable setzen. Hier gibt es für Maven die Konvention M2_HOME und für Java gilt JAVA_HOME. Als best practice bindet man das Installationsverzeichnis an eine Systemvariable und nutzt dann die selbst definierte Systemvariable, um den Pfad zu erweitern. Dieses Vorgehen ist recht typisch für Systemadministratoren, die ihre Serverinstallation mithilfe von Systemvariablen vereinfachen. Denn diese Systemvariablen sind global und können von Automatisierungsskripten auch ausgelesen werden.

Die Kommandozeile, oder auch Shell, Bash, Konsole und Terminal genannt, bietet mit echo eine einfache Möglichkeit, den Wert der Systemvariablen auszugeben. Am Beispiel der Pfadvariable sehen wir auch gleich den Unterschied zu Linux und Windows. Linux: echo $PATH Windows: echo %PATH%

ed@local:~$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/snap/bin:/home/ed/Programs/maven/bin:/home/ed/.local/share/gem//bin:/home/ed/.local/bin:/usr/share/openjfx/lib

Beginnen wir auch nun gleich mit der einfachsten Möglichkeit, die Pfadvariable zu setzen. Unter Linux müssen wir lediglich die versteckte Datei .bashrc editieren. Am Ende der Datei fügen wir folgende Zeilen hinzu und speichern den Inhalt.

export M2_HOME="/opt/maven"
export PATH=$PATH:$M2_HOME/bin

Wir binden an die Variable M2_HOME das Installationsverzeichnis. Anschließend erweitern wir die Pfadvariable um um die M2_HOME Systemvariable mit dem Zusatz zum Unterverzeichnis der ausführbaren Dateien. Dieses Vorgehen ist auch bei Windows Systemen üblich, da sich so der Installationspfad einer Anwendung schneller finden und auch anpassen lässt. Nach dem Ändern der Datei .bashrc muss das Terminal neu gestartet werden, damit die Änderungen wirksam werden. Dieses Vorgehen stellt sicher, dass auch nach einem Neustart des Rechners die Einträge nicht verloren gehen.

Unter Windows besteht die Herausforderung darin, lediglich die Eingabemaske zu finden, wo die Systemvariablen gesetzt werden können. In diesem Artikel beschränke ich mich auf die Variante für Windows 11. Es kann natürlich sein, dass sich bei einem künftigen Update der Weg, die Systemvariablen zu editieren, geändert hat. Zwischen den einzelnen Windows Versionen gibt es leichte Variationen. Die Einstellung gilt dann sowohl für die CMD als auch für die PowerShell. Der nachfolgende Screenshot zeigt, wie man in Windows 11 zu den Systemeinstellungen gelangt.

Dazu klicken wir auf einen leeren Bereich auf dem Desktop die rechte Maustaste und wählen den Eintrag System aus. Im Untermenü System – Über finden sich die Systemeinstellungen, die das Popup Systemproperties öffnen. In den Systemeinstellungen drücken wir den Knopf Umgebungsvariablen, um die finale Eingabemaske zu erhalten. Nach den entsprechenden Anpassungen muss ebenfalls die Konsole neu gestartet werden, um die Änderungen wirksam werden zu lassen.

In dieser kleinen Hilfe haben wir den Zweck von Systemvariablen kennengelernt und wie man diese dauerhaft unter Linux und Windows speichert. Den Erfolg unserer Anstrengungen können wir in der Shell anschließend zügig über echo mit der Ausgabe des Inhalts der Variablen kontrollieren. Und schon sind wir dem IT-Profi wieder einen Schritt nähergekommen.


Erfolgreiches Validieren von ISBN Nummern

Regelmäßig stehen Entwickler vor der Aufgabe, Nutzereingaben auf Korrektheit zu prüfen. Mittlerweile existiert eine erhebliche Anzahl an standardisierten Datenformaten, mit denen solche Validierungsaufgaben leicht zu meistern sind. Die International Standard Book Number oder kurz ISBN ist ein solches Datenformat. ISBN gibt es in zwei Ausführungen: in einer zehnstelligen und in einer 13-stelligen Variante. Von 1970 bis 2006 wurde die zehnstellige Version der ISBN verwendet (ISBN-10), die im Januar 2007 von der 13-stelligen Fassung abgelöst wurde (ISBN-13). Heutzutage ist es in vielen Verlagen verbreitete Praxis, für Titel beide Versionen der ISBN bereitzustellen. Dass sich anhand dieser Nummer Bücher eindeutig identifizieren lassen, ist allgemein bekannt. Das bedeutet natürlich auch, dass diese Nummern eindeutig sind. Es gibt also keine zwei unterschiedlichen Bücher mit gleicher ISBN (Bild 1).

Der theoretische Hintergrund, um festzustellen, ob eine Zahlenfolge korrekt ist stammt aus der Codierungstheorie. Wer sich also etwas ausführlicher mit dem mathematischen Hintergrund Fehler-erkennender und Fehler-korrigierender Codes beschäftigen möchte, dem Sei das Buch „Codierungstheorie“ von Ralph Hardo Schulz empfohlen [1]. Darin lernt man beispielsweise, wie die Fehlerkorrektur bei Comact Disks (CD) funktioniert. Aber keine Sorge, wir reduzieren in diesem kleinen Workshop die notwendige Mathematik auf ein Minimum.

Bei der ISBN handelt es sich um einen Fehler erkennenden Code. Wir können also den erkannten Fehler nicht automatisch wieder beheben. Wir wissen nur, dass etwas falsch ist, kennen aber nicht den konkreten Fehler. Gehen wir der Sache daher ein wenig auf den Grund.

Warum man sich bei ISBN-13 genau auf 13 Stellen geeinigt hat, bleibt Spekulation. Zumindest haben sich die Entwickler nicht von irgendwelchem Aberglauben beeindrucken lassen. Das große Geheimnis hinter der Validierung ist die Bestimmung der Restklassen [2]. Die Algorithmen für ISBN-10 und ISBN-13 sind recht ähnlich. Beginnen wir also mit dem älteren Standard, ISBN-10, der sich wie folgt errechnet:

1x1 + 2x2 + 3x3 + 4x4 + 5x5 + 6x6 + 7x7 + 8x8 + 9x9 + 10x10 = k modulo 11

Keine Sorge, um die oben stehende Formel zu verstehen, müssen Sie kein Raketeningenieur bei SpaceX sein. Wir heben den Schleier der Verwirrung anschaulich mit einem kleinen Beispiel für die ISBN 3836278340. Daraus ergibt sich folgende Rechnung:

(1*3) + (2*8) + (3*3) + (4*6) + (5*2) + (6*7) + (7*8) + (8*3) + (9*4) + (10*0) = 220
220 modulo 11 = 0

Die letzte Ziffer der ISBN ist die sogenannte Prüfziffer. In dem aufgeführten Beispiel lautet diese 0. Um diese Prüfziffer zu erhalten, multiplizieren wir jede Stelle mit ihrem Wert. Das heißt, an vierter Position steht eine 6, also rechnen wir 4 * 6. Das wiederholen wir mit allen Positionen und die einzelnen Ergebnisse addieren wir zusammen. So erhalten wir den Betrag 220. Die 220 wird mit der sogenannten Restwertoperation Modulo durch 11 geteilt. Da die 11 genau 20 mal in die 220 hineinpasst, bleibt ein Rest null. Das Ergebnis von 220 modulo 11 ist 0 und stimmt mit der Prüfziffer überein, was uns sagt das eine gültige ISBN-10 vorliegt.

Eine Besonderheit gibt es aber noch zu beachten. Bisweilen kommt es vor, dass die letzte Ziffer der ISBN mit X endet. In diesem Fall ist das X gegen 10 auszutauschen.

Wir sehen, der Algorithmus ist sehr einfach gehalten und kann leicht über eine einfache for-Schleife umgesetzt werden.

boolean success = false;
int[] isbn;
int sum = 0;

for(i=0; i<10; i++) {
    sum += i*isbn[i];
}

if(sum%11 == 0) {
    success = true;
}

Um den Algorithmus so einfach wie möglich zu halten, wird jede Stelle der ISBN-10-Nummer in einem Integer-Array gespeichert. Ausgehend von dieser Vorbereitung ist es nur noch nötig, das Array zu durchlaufen. Wenn dann die Überprüfung der Summe durch das Modulo 11 das Ergebnis 0 liefert, ist alles bestens.

Um die Funktion richtig zu testen, werden zwei Testfälle benötigt. Einerseits gilt es zu überprüfen ob eine ISBN korrekt erkannt wird. Der zweite Test überprüft die sogenannten false positives. Es wird also ein erwarteter Fehler mit einer falschen ISBN provoziert. Das lässt sich zügig bewerkstelligen, indem man von einer gültigen ISBN eine beliebige Stelle ändert.

Unser ISBN-10 Validator hat noch einen kleinen Schönheitsfehler. Ziffernfolgen, die kürzer oder länger als 10 sind, also dem erwarteten Format nicht entsprechen, könnten bereits vorher abgewiesen werden. Der Grund hierfür lässt sich in dem Beispiel erkennen: Die letzte Stelle der ISBN-10 ist eine 0 – somit ist das Zeichenergebnis auch 0. Wird die letzte Stelle also vergessen und eine Prüfung auf das korrekte Format fehlt, wird der Fehler nicht erkannt. Etwas das keine Auswirkung auf den Algorithmus hat, aber sehr hilfreich als Feedback bei Nutzereingaben ist, ist das Eingabefeld so lange auszugrauen und den Absenden-Button zu deaktivieren, bis das korrekte Format der ISBN eingegeben wurde.

Der Algorithmus für ISBN-13 ist ähnlich einfach aufgebaut.

x1 + 3x2 + x3 + 3x4 + x5 + 3x6 + x7 + 3x8 + x9 + 3x10 + x11 + 3x12 + x13 = k modulo 10

Analog wie bei ISBN-10 steht xn für den Zahlenwert an der entsprechenden Position in er ISBN-13. Auch hier werden die Teilergebnisse aufsummiert und durch ein Modulo geteilt. Der große Unterschied ist, dass hier nur die geraden Positionen, also die Stellen 2, 4, 6, 8, 10 und 12, mit 3 multipliziert werden und das Ergebnis dann mit Modulo 10 dividiert wird. Als Beispiel berechnen wir die ISBN-13: 9783836278348.

9 + (3*7) + 8 + (3*3) + 8 + (3*3) + 6 + (3*2) + 7 + (3*8) + 3 + (3*4) + 8 = 130
130 modulo 10 = 0

Auch für die ISBN-13 lässt sich der Algorithmus in einer einfachen for-Schleife umsetzen.

boolean success = false;
int[] isbn;
int sum = 0;

for(i=0; i<13; i++) {
    if(i%2 == 0) {
        sum += 3*isbn[i];
    } else {
        sum += isbn[i];
    }
}

if(sum%10 == 0) {
    success = true;
}

Die beiden Codebeispiele zu ISBN-10 und ISBN-13 unterscheiden sich vor allem in der if-Bedingung. Der Ausdruck i % 2 berechnet den Modulo-Wert 2 zur jeweiligen Iteration. Wenn an dieser Stelle der Wert 0 herauskommt, bedeutet das, dass es sich um eine gerade Zahl handelt. Der dazugehörige Wert muss dann mit 3 multipliziert werden.

Hier zeigt sich wie praktisch die Modulo-Operation % für das Programmieren sein kann. Um die Implementierung möglichst kompakt zu halten, kann anstatt der if-else-Bedingung auch der sogenannte Dreifach-Operator verwendet werden. Der Ausdruck sum += (i%2) ? isbn[i] : 3 * isbn[3] ist wesentlich kompakter, dafür aber auch schwerer zu verstehen.

Nachfolgend finden Sie eine vollständig implementierte Klasse zur Prüfung der ISBN in den Programmiersprachen: Java, PHP und C#.

Abonnement / Subscription

[English] This content is only available to subscribers.

[Deutsch] Diese Inhalte sind nur für Abonnenten verfügbar.

Die in den Beispielen vorgestellten Lösungen haben zwar alle denselben Kernansatz, unterscheiden sich aber nicht nur in syntaktischen Details. So bietet die Java-Version eine allumfassende Variante, die etwas generischer zwischen ISBN-10 und ISBN-13 unterscheidet. Das demonstriert zum einen, dass viele Wege nach Rom führen. Soll aber auch gerade weniger erfahrenen Entwicklern verschiedene Lösungsansätze zeigen und sie motivieren, eigene Anpassungen vorzunehmen. Um das Verständnis zu vereinfachen, wurde der Quelltext mit Kommentaren angereichert. Bei PHP, als untypisierte Sprache, entfällt insbesondere das Konvertieren des Strings in Nummern. Dafür wird eine RegEx genutzt, um sicherzustellen, dass die eingegebenen Zeichen typsicher sind.

Lessons Learned

Wie Sie sehen, handelt es sich bei der Überprüfung, ob eine ISBN korrekt ist, um keine Hexerei. Das Thema der Validierung von Benutzereingaben ist natürlich viel umfangreicher. Andere Beispiele sind Kreditkartennummern. Aber auch reguläre Ausdrücke leisten in diesem Zusammenhang wertvolle Dienste.

Ressourcen

  • [1] Ralph-Hardo Schulz, Codierungstheorie: Eine Einführung, 2003, ISBN 978-3-528-16419-5
  • [2] Begriff der Restklasse bei Wikipedia, https://de.wikipedia.org/wiki/Restklasse