Comretix Blog

Wir bieten aktuelle Informationen über uns und aus der IT Welt.

Secure Coding: Sicheres Passwort-Handling in Java – von String zu char[]

Anzeige

In der Programmiersprache Java spielt String als eine unveränderliche (immutable) Klasse eine zentrale Rolle in der Speicherverwaltung sowie bei der Lebensdauer von Zeichenketten. Anknüpfend an die beiden vorangegangenen Beiträge Sicherere Passwörter mit Salt, Pepper und Hashing und Sichere Datenhaltung in der JVM – Risiken und Best Practices erläutert dieser Artikel nochmals detailliert die sich daraus für Entwicklerinnen und Entwickler ergebenden Konsequenzen beim sicheren Programmieren.

Aufgrund der Unveränderlichkeit der Klasse erzeugt jeder Versuch, einen String zu modifizieren, in Wirklichkeit eine neue Instanz, anstatt den ursprünglichen Wert zu verändern. Dieses Design führt dazu, dass String-Objekte effizient im String Pool verwaltet werden, einem speziellen Bereich im Heap-Speicher, in dem String-Literale gespeichert und wiederverwendet werden.

Ein neu deklariertes String-Literal wird standardmäßig im String Pool abgelegt. Falls bereits eine identische Zeichenkette existiert, wird die Referenz auf das existierende Objekt zurückgegeben, anstatt eine neue Instanz zu erzeugen. Diese Speicheroptimierung reduziert Redundanzen und minimiert den Speicherverbrauch.

Wird jedoch ein String-Objekt explizit mit new String("...") erzeugt, so wird ein separater Speicherbereich auf dem Heap angelegt, der unabhängig vom String Pool ist. Dies kann zu unerwartetem Speicherverbrauch führen, wenn viele solcher Objekte erzeugt werden. Die Garbage Collection entfernt nicht referenzierte String-Instanzen aus dem Heap, während der String Pool von der internen JVM-Optimierung abhängt, insbesondere durch die Methode intern().

Die Methode intern()

Die Methode intern() dient dazu, ein String-Objekt explizit in den String Pool zu überführen. Falls die Zeichenkette bereits im Pool existiert, wird die Referenz auf das existierende Objekt zurückgegeben. Falls nicht, wird die Zeichenkette in den Pool aufgenommen. Diese Methode kann die Speicherverwaltung effizienter gestalten, indem sie redundante Objekte vermeidet. Allerdings sollte intern() mit Bedacht verwendet werden, da übermäßiger Gebrauch zu einem überfüllten String Pool führt, was potenzielle Performance-Einbußen nach sich ziehen kann.

Codebeispiel:

String s1 = new String("Hello"); String s2 = s1.intern(); String s3 = "Hello"; // false, da s1 ein Heap-Objekt ist System.out.println(s1 == s2); // true, da s2 und s3 auf den gleichen Pool-Eintrag verweisen System.out.println(s2 == s3);

Da String unveränderlich ist und jede Änderung eine neue Instanz erzeugt, können StringBuilder und StringBuffer als effiziente Alternativen verwendet werden. Beide Klassen ermöglichen veränderliche (mutable) Zeichenketten, wodurch sich Speicherallokationen und Garbage Collection minimieren lassen.

StringBuilder bietet eine nicht synchronisierte, performante Variante für String-Manipulationen in einem einzelnen Thread.

StringBuffer ist synchronisiert und für Multi-Threading-Szenarien konzipiert, jedoch aufgrund der Synchronisation langsamer als StringBuilder.

Beide Klassen operieren intern mit einem dynamisch erweiterbaren char[], wodurch Änderungen direkt auf dem bestehenden Speicherbereich durchgeführt werden können, ohne neue Objekte zu erzeugen.

Codebeispiel:

StringBuilder sb = new StringBuilder("Hello"); // Kein neues Objekt wird erstellt, der Inhalt wird modifiziert sb.append(" World"); System.out.println(sb.toString()); // "Hello World"

Gibt es einen "Builder", der ein char[] zurückgibt?

Es gibt keine Standardlösung, wie StringBuilder direkt ein char[] zurückgibt. Alternativ lässt sich allerdings die Klasse CharArrayWriter aus dem Paket java.io nutzen, falls eine effiziente Verarbeitung von Zeichenarrays erforderlich ist:

CharArrayWriter in der Übersicht

Die Klasse CharArrayWriter ist die spezialisierte Implementierung eines Writer-Streams, die intern mit einem dynamisch wachsenden char[]-Array arbeitet. Sie liefert damit eine effiziente Möglichkeit, Zeichenfolgen zu manipulieren und als char[] weiterzugeben, ohne einen String-Puffer oder StringBuilder zu verwenden.

Da CharArrayWriter direkt auf einem char[]-Array operiert, kann die Klasse besonders nützlich sein, wenn Zeichenfolgen verarbeitet werden müssen, ohne die Speicher- und Performance-Nachteile einer String-Objekt-Erstellung in Kauf nehmen zu wollen. Dies ist insbesondere in sicherheitskritischen Anwendungen relevant, da der Inhalt eines char[] explizit überschrieben werden kann, während String-Objekte unveränderlich sind und der Garbage Collection überlassen werden müssen.

Funktionsweise und Verhalten im Speicher

Intern verwaltet CharArrayWriter ein char[], das dynamisch erweitert wird, sobald mehr Zeichen hinzugefügt werden. Dies ähnelt dem Verhalten von StringBuilder, mit dem Unterschied, dass CharArrayWriter als Writer fungiert und gezielt der Ein- und Ausgabe von Zeichenströmen dient.

Sobald der Puffer seine Kapazität überschreitet, wird er automatisch vergrößert – ein Verhalten, das an die ArrayList erinnert. Die gespeicherten Daten bleiben bis zur expliziten Freigabe (reset()) oder bis zur Objektdestruktion im Speicher erhalten. Da CharArrayWriter keinen expliziten Speicherfreigabemechanismus benötigt (kein close() erforderlich), eignet sich die Klasse gut für kurzfristige Pufferspeicherung in der Ein- und Ausgabe.

CharArrayWriter ist somit eine effiziente Alternative zu StringBuilder, wenn ein char[] benötigt wird. Der speziell implementierte Writer-Stream eignet sich besonders gut für Szenarien, in denen Zeichenströme verarbeitet und als Arrays weitergegeben werden müssen, ohne die Nachteile eines unveränderlichen String-Objekts in Kauf nehmen zu müssen. Seine Fähigkeit, den Speicher gezielt zu überschreiben, macht ihn ideal für sicherheitskritische Anwendungen, in denen sensible Daten verwaltet werden.

Wir benötigen einen CharArrayString, der die grundlegenden Funktionen eines String-Objekts bietet, jedoch ausschließlich mit einem char[] arbeitet. Dabei wird sichergestellt, dass Änderungen am Inhalt tatsächlich das zugrunde liegende char[] beeinflussen, ohne auf die Unveränderlichkeit eines String angewiesen zu sein.

Besonders wichtig ist, dass sensible Daten durch Überschreiben des char[]-Arrays sicher gelöscht werden können. Die Klasse zeichnet sich demnach durch ihre konsequente Arbeit mit char[]-Arrays aus. Sie stellt eine Reihe von Methoden zur Verfügung, die eine effiziente Manipulation und Abfrage von Zeichenfolgen ermöglichen, darunter length(), charAt(), substring() und append(). Ein wesentliches Merkmal dieser Implementierung ist die Möglichkeit zur expliziten Speicherlöschung mittels der clear()-Methode, da auf diese Weise sensible Daten direkt aus dem Speicher entfernt werden können. Im Gegensatz zur Standardklasse String wird bewusst auf eine toString()-Implementierung verzichtet, um zu verhindern, dass unbeabsichtigt neue String-Instanzen im Speicher entstehen, die durch die JVM nicht unmittelbar entfernt werden können.

Implementierung der Klasse CharArrayString

import java.io.CharArrayWriter; import java.util.Arrays; public class CharArrayString { private CharArrayWriter writer; // Konstruktoren public CharArrayString() { this.writer = new CharArrayWriter(); } public CharArrayString(char[] chars) { this.writer = new CharArrayWriter(); this.writer.write(chars, 0, chars.length); } public CharArrayString(String str) { this.writer = new CharArrayWriter(); this.writer.write(str, 0, str.length()); } // Gibt die Länge der Zeichenkette zurück public int length() { return writer.size(); } // Gibt ein Zeichen an einer bestimmten Position zurück public char charAt(int index) { if (index < 0 || index >= writer.size()) { throw new IndexOutOfBoundsException("Index " + index + " out of bounds"); } return writer.toCharArray()[index]; } // Gibt ein Teilstück des CharArrayString zurück public CharArrayString substring(int start, int end) { if (start < 0 || end > writer.size() || start > end) { throw new IndexOutOfBoundsException("Invalid substring range"); } return new CharArrayString(Arrays.copyOfRange(writer.toCharArray(), start, end)); } // Fügt Zeichen oder Strings hinzu public void append(char c) { writer.write(c); } public void append(char[] chars) { writer.write(chars, 0, chars.length); } public void append(String str) { writer.write(str, 0, str.length()); } // Konvertiert in ein char-Array public char[] toCharArray() { return writer.toCharArray(); } // Löscht den Speicher (z. B. für sensible Daten) public void clear() { char[] array = writer.toCharArray(); Arrays.fill(array, '\0'); // Überschreibt den Speicherbereich writer.reset(); // Leert den Writer-Puffer } // Vergleicht zwei `CharArrayString`-Objekte @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; CharArrayString other = (CharArrayString) obj; return Arrays.equals(this.toCharArray(), other.toCharArray()); } // Hashcode-Berechnung auf Basis des char-Arrays @Override public int hashCode() { return Arrays.hashCode(writer.toCharArray()); } // Debug-Ausgabe, aber ohne String-Pool-Effekte public void print() { System.out.println(writer.toCharArray()); } }

Beispielhafte Nutzung:

class CharArrayStringTest { @Test void length() { CharArrayString cas = new CharArrayString(); cas.append('a'); assertEquals(1, cas.length()); cas.append('b'); assertEquals(2, cas.length()); } @Test void charAt() { CharArrayString cas = new CharArrayString(); cas.append(new char[]{'a','b','c','d',}); assertEquals('c',cas.charAt(2)); } @Test void substring() { CharArrayString cas = new CharArrayString(); cas.append(new char[]{'a','b','c','d',}); assertArrayEquals(new char[]{'b','c'},cas.substring(1,3).toCharArray()); } @Test void toCharArray() { CharArrayString cas = new CharArrayString(); char[] input = {'a', 'b', 'c', 'd',}; cas.append(input); assertArrayEquals(input,cas.toCharArray()); } @Test void clear() { CharArrayString cas = new CharArrayString(); char[] input = {'a', 'b', 'c', 'd',}; cas.append(input); assertEquals(4,cas.length()); cas.clear(); assertEquals(0, cas.length()); } @Test void testEquals() { char[] input = {'a', 'b', 'c', 'd',}; CharArrayString casA = new CharArrayString(input); CharArrayString casB = new CharArrayString(input); assertTrue(casA.equals(casB)); } }

Diese CharArrayString-Klasse ist eine mögliche Alternative zu String, wenn direkt mit char[] gearbeitet werden soll. Sie vermeidet unnötige Speicherallokationen und ermöglicht das explizite Löschen sensibler Daten, was sie besonders für sicherheitskritische Anwendungen geeignet macht. Aber in diesem Fall sind die Daten immer noch im Klartext im Speicher vorhanden. Welche weiteren Maßnahmen lassen sich sinnvoll umsetzen?

SecretKeySpec ist eine Klasse innerhalb der Java Cryptography Architecture (JCA), die zur Repräsentation und Handhabung geheimer Schlüssel in symmetrischen Verschlüsselungsverfahren dient. Diese Klasse implementiert das SecretKey-Interface und ermöglicht die Konstruktion eines kryptografischen Schlüssels aus einem bestehenden Byte-Array. Dadurch wird eine direkte Interaktion mit Algorithmen wie AES (Advanced Encryption Standard), DES (Data Encryption Standard) oder HMAC (Hash-based Message Authentication Code) ermöglicht.

Die Architektur von SecretKeySpec ist darauf ausgelegt, eine kompakte und effiziente Repräsentation eines Schlüssels bereitzustellen, ohne dass dieser durch zusätzliche Schlüsselgenerierungsmechanismen verarbeitet werden muss. Stattdessen lässt sich ein bereits vorhandenes Byte-Array in eine Instanz von SecretKeySpec umwandeln, wodurch der Schlüssel unmittelbar für kryptografische Operationen einsetzbar ist. Dies geschieht unabhängig von einem KeyGenerator oder einer dedizierten Schlüsselerzeugung durch eine Key Management Infrastructure (KMI).

Eine zentrale Eigenschaft von SecretKeySpec ist, dass es keine zusätzlichen Metadaten über den Schlüssel speichert, außer der eigentlichen Byte-Repräsentation und dem zugehörigen Algorithmus. Das bedeutet, dass ein mit SecretKeySpec erstellter Schlüssel keinerlei Informationen über dessen Ursprung oder sichere Generierung enthält und daher in sicherheitskritischen Anwendungen häufig mit zusätzlichen Maßnahmen wie Schlüsselableitungsfunktionen (Key Derivation Functions – KDFs) oder Hardware-Sicherheitsmodulen (HSMs) kombiniert wird.

In der Praxis dient SecretKeySpec primär als Übergangsrepräsentation für symmetrische Schlüssel innerhalb von kryptografischen APIs und wird insbesondere in Verbindung mit der Cipher-Klasse verwendet, um Daten zu verschlüsseln oder zu entschlüsseln. Seine Verwendung ermöglicht eine direkte und effiziente Integration mit standardisierten Sicherheitsalgorithmen, wobei die Handhabung des zugrundeliegenden Schlüssels durch klare und sichere Mechanismen geregelt wird.

Das Verwenden von SecretKeySpec zur temporären Speicherung eines Login-Passworts während eines Authentifizierungsprozesses bietet eine praktikable Möglichkeit, sensible Informationen in einer kontrollierten und speichersicheren Weise zu verwalten. Während des Logins wird das Passwort typischerweise entweder zur direkten Authentifizierung gegen eine hinterlegte Referenz (z. B. einen mit Hash und Salt versehenen Wert in einer Datenbank) oder zur Ableitung eines kryptografischen Schlüssels für weitere Sicherheitsmaßnahmen genutzt.

Durch die Zwischenspeicherung des Passworts in einer SecretKeySpec-Instanz bleibt der Wert innerhalb einer klar definierten, manipulierbaren Speicherstruktur erhalten.

Ein Destroyable ist ein Interface innerhalb der Java Security API, das von Klassen implementiert werden kann, deren Instanzen sensible Daten enthalten und daher explizit aus dem Speicher entfernt werden sollen. Es definiert eine standardisierte Methode zur sicheren Löschung (destroy()), um zu gewährleisten, dass vertrauliche Informationen, wie kryptografische Schlüssel, Authentifizierungsdaten oder geheime Tokens, nicht länger im Speicher verbleiben, als es unbedingt erforderlich ist.

Ein wesentliches Merkmal von Destroyable ist die Möglichkeit, nach der Löschung den Zustand des Objekts zu überprüfen, indem die Methode isDestroyed() aufgerufen wird. Dies erlaubt eine zuverlässige Kontrolle darüber, ob eine Instanz erfolgreich entfernt wurde. Implementierende Klassen müssen sicherstellen, dass nach dem Aufruf von destroy() keine sensiblen Daten mehr rekonstruierbar sind und dass jegliche weitere Nutzung der Instanz entweder zu einer Exception führt oder definierte Verhaltenseinschränkungen aufweist.

Das Destroyable-Interface wird insbesondere für sicherheitskritische Objekte wie Private Keys (PrivateKey), Passwörter (PasswordCallback) oder kryptografische Schlüssel (SecretKey) verwendet. Es bietet eine einheitliche API für das Management sicherheitsrelevanter Ressourcen und gewährleistet, dass Implementierungen die Notwendigkeit der kontrollierten Speicherbereinigung berücksichtigen.

Das Zusammenspiel von Destroyable und SecretKeySpec ergibt sich aus der hierarchischen Vererbung innerhalb der Java Cryptography Architecture (JCA). Da SecretKeySpec das SecretKey-Interface implementiert und SecretKey wiederum Destroyable erweitert, erhält jede Instanz von SecretKeySpec die Fähigkeit, explizit zerstörbar zu sein.

Durch diese Implementierung kann ein SecretKeySpec-Objekt, das sensible kryptografische Schlüssel repräsentiert, mittels der Methode destroy() aus dem Speicher entfernt werden. Nach dem Aufruf von destroy() befindet sich das Objekt in einem irreversiblen Zustand, sodass darauf nicht mehr zugegriffen werden kann. Die Methode isDestroyed() ermöglicht es anschließend, den Zustand des Objekts zu überprüfen, um sicherzustellen, dass das Schlüsselmaterial erfolgreich gelöscht wurde.

Trotz der Eigenschaften von Destroyable bleibt die konkrete Speicherbereinigung von der jeweiligen SecretKeySpec-Implementierung und dem JVM-Speicherverwaltungssystem abhängig. In sicherheitskritischen Anwendungen kann es daher erforderlich sein, dass zusätzlich zur expliziten Zerstörung eine manuelle Speicherbereinigung durchgeführt wird, um zu gewährleisten, dass keine sensiblen Daten im Heap verbleiben.

Die maximale Größe des Inhalts eines SecretKeySpec-Objekts wird durch den zugrunde liegenden kryptografischen Algorithmus definiert, für den das Schlüsselmaterial verwendet wird. Da SecretKeySpec primär zur Speicherung symmetrischer Schlüssel dient, richtet sich die erlaubte Größe nach den Schlüssellängen der jeweiligen Algorithmen. Beispielsweise unterstützt der AES-Algorithmus Schlüsselgrößen von 128 Bit (16 Byte), 192 Bit (24 Byte) und 256 Bit (32 Byte), während andere Algorithmen wie DES oder Triple-DES spezifische Längenbegrenzungen aufweisen.

Da SecretKeySpec weder für die persistente Speicherung großer Datenmengen noch für effiziente Datenverarbeitung ausgelegt ist, eignet es sich nicht unmittelbar zur Handhabung von Inhalten, die über die typische Schlüsselgröße hinausgehen. Große binäre Daten wie Bilder oder Videos sollten daher nicht in SecretKeySpec verwaltet werden.

Sobald eine Datenmenge die für einen symmetrischen Schlüssel typische Größe überschreitet – insbesondere, wenn sie mehrere Kilobyte oder gar Megabyte erreicht –, sollte eine alternative Speicher- und Sicherheitsstrategie gewählt werden. In solchen Fällen empfiehlt es sich, die betreffenden Daten unabhängig vom kryptografischen Schlüssel in einer separaten Speicherstruktur zu verwalten. Dabei können diese Inhalte mit einem zuvor in SecretKeySpec gehaltenen Schlüssel verschlüsselt werden, bevor sie im Arbeitsspeicher oder persistenten Speicher abgelegt werden. Dies ermöglicht eine effiziente Trennung von Schlüsselverwaltung und Datenhaltung, wodurch sowohl die Sicherheit als auch die Performance der Verarbeitung gewährleistet bleibt.

(Ursprünglich geschrieben von Heise)
×
Stay Informed

When you subscribe to the blog, we will send you an e-mail when there are new updates on the site so you wouldn't miss them.

Größter Krypto-Coup aller Zeiten: Bybit verliert 1...
OpenH264: Sicherheitslücke in Ciscos Video-Codec g...
 

Kommentare

Derzeit gibt es keine Kommentare. Schreibe den ersten Kommentar!
Bereits registriert? Hier einloggen
Freitag, 31. Oktober 2025

Die Comretix GmbH ist ein IT Systemhaus aus Tuttlingen. Seit über 30 Jahren betreuen wir unsere Kunden in Baden-Württemberg, der Schweiz und im gesamten Bundesgebiet mit Leidenschaft, Fairness und Loyalität. Wir bauen auf eine zuverlässige Partnerschaft mit unseren Lieferanten und Kunden. Unseren Mitarbeitern stehen wir auf Augenhöhe gegenüber.

Comretix GmbH Logo