Blog

IT Security Summit 2020
Das große Trainingsevent für IT Security
23. - 25. November 2020 | München
10
Aug

Rubinrote Sicherheit
Teil 1: Symmetrische Verschlüsselung

Sicherheit geht vor – das gilt auch in Ruby. Das Bauchgefühl kann jedoch trügen, wenn es um sichere Lösungen geht. Darum lohnt es sich, die zur Verfügung stehenden Optionen einmal genauer unter die Lupe zu nehmen. Von Martin Boßlet

Die Standard-Library in Ruby bietet mit dem OpenSSL-Modul ein leistungsfähiges Interface zum Verschlüsseln. Wie der Name schon andeutet, handelt es sich hierbei um einen Wrapper der nativen OpenSSL Library, garniert mit dem syntaktischen Zucker, den die Programmiersprache von Haus aus mitbringt.
Auf OpenSSL als De-facto-Standard bei Kryptographie im Open-Source-Bereich zu setzen, ist natürlich naheliegend – es birgt aber auch einige Gefahren. OpenSSL versteht sich als ein Werkzeugkasten für kryptographische Algorithmen und legt seinen Fokus auf Vollständigkeit, was aber zur Folge hat, dass es eine Vielzahl von Optionen mit sich bringt, die nach aktuellsten Kriterien leider nicht immer alle als sicher gelten. Man ist als Entwickler also dazu verdammt, unter der Vielzahl der Optionen diejenigen herauszupicken, die für den jeweiligen Anwendungsfall passen und dabei so sicher wie möglich sind.
Dies ist natürlich nicht immer einfach und deshalb möchte ich Ihnen konkret zum Thema Verschlüsselung einen Überblick verschaffen, was es aus historischer Sicht an Möglichkeiten gab und gibt, was die Nachteile sind, wie sich die Dinge weiterentwickelt haben und wie Stand heute eine moderne Verschlüsselung aussieht, die Sie dann auch in Ihren Projekten einsetzen können und sollten.
Es gibt zwei prominente Ansätze, um symmetrische Verschlüsselung zu betreiben: den Stream Cipher [1] und den Block Cipher [2]. Beides sind Varianten, die mit unterschiedlichen Ansätzen sichere Verschlüsselungsverfahren liefern können. Wir konzentrieren uns vorerst auf die Stream Cipher, die in Ihrer Konstruktion einfacher sind, bevor wir uns den komplexeren Block Ciphern widmen.

Was bedeutet „sicher“ im Kontext von Verschlüsselung?
Wer kennt das nicht? Der Chef verlangt, dass wir aufgrund von erhöhten Sicherheitsanforderungen bestimmte Daten verschlüsseln sollen, damit das Ganze „sicher“ wird. Alle Beteiligten nicken dann eifrig, aber bei den Armen, die das dann ausbaden – sprich implementieren – sollen, macht sich schnell ein ungutes Gefühl breit, weil sich eigentlich keiner so sicher ist, was „sicher“ in diesem Zusammenhang wirklich bedeutet. Tatsächlich ist es schwierig, das allumfassend für Software zu definieren, da es natürlich vom Kontext, den Anforderungen und den möglichen Risiken abhängt. Für die Theorie ist aber ein Bauchgefühl keine gute Grundlage, denn die theoretische Erforschung und Bewertung von Kryptographie kann nur gelingen, wenn man auf einem soliden Fundament aufbaut.
Da wir dazu erzogen werden, die Schlüssel beim Verschlüsseln geheim zu halten, ist man dazu verleitet, den Sicherheitsbegriff vor allem an diesen festzumachen. Man muss sich aber immer vor Augen halten, dass die Schlüssel immer nur ein Werkzeug und ein Mittel zum Zweck sind, denn in Wahrheit hat die oberste Priorität immer die Nachricht selbst. Sie ist es, die wir in erster Linie schützen wollen, nicht die Schlüssel. Diese helfen uns lediglich auf dem Weg dahin. Von dieser Prämisse ausgehend definiert man Sicherheit in der Verschlüsselung so, dass ein Verfahren dann als sicher gilt, wenn man aus alleiniger Kenntnis des Ciphertexts keinerlei Informationen, nicht mal partielle, über den zugrunde liegenden Plaintext gewinnen kann [3]. Umgekehrt bedeutet dies, dass man also noch nicht einmal zwangsläufig den Ciphertext entschlüsseln muss, sondern jeder noch so winzige Informationsgewinn über den Plaintext bereits ausreicht, um ein Verfahren zu brechen. Salopp formuliert: Die Kenntnis des Ciphertexts bringt einem keine zusätzlichen Informationen über den Plaintext. Man ist mit oder ohne genau so schlau wie vorher. Der Ausschluss des Informationsgewinns hat allerdings eine Ausnahme: Man lässt die Kenntnis über die Länge des Plaintexts zu, d. h. die einzige Information, die man einem potenziellen Angreifer zugesteht, ist, dass er herausfinden kann, wie lange der ursprüngliche Plaintext ist. Dies ist in erster Linie eine technische Einschränkung, da es mit erheblichem Aufwand verbunden ist, die Länge des Plaintexts zu verschleiern. Wenn ein Verfahren diese Anforderungen erfüllt, spricht man davon, dass die Verschlüsselung Perfect Secrecy [4] genügt.

Ausführen der Codebeispiele:

Sollten Sie Ruby noch nicht installiert haben, können Sie dies über www.ruby-lang.org/de vornehmen oder mit einem Tool wie www.rvm.io oder www.github.com/rbenv/rbenv.

Damit Sie auch die neueren Algorithmen wie ChaCha20/Poly1305 nutzen können, sollten Sie auf Ihrem System OpenSSL in einer Version >= 1.1.0 installiert haben.

Danach können Sie die Codebeispiele einfach ausführen, indem Sie das Git Repository unter www.github.com/emboss/stream-ciphers klonen, in das Verzeichnis code wechseln und dort die Beispiele ausführen mit ruby <beispiel.rb>

Das One-Time Pad
Ein solches, bereits seit Langem bekanntes Verfahren ist das One-Time Pad [5]. Seine Konstruktion ist geradezu simpel. Hat man eine Nachricht m der Länge n, so benötigt man einen zufällig generierten Schlüssel ebenfalls der Länge n. Hat also m z. B. 100 Bytes Länge, so generiert man 100 zufällige Bytes als Schlüssel. Die Verschlüsselung c ist dann definiert als:

Wobei „⊕“ XOR, Bit für Bit bzw. Byte für Byte, bedeutet. Die entsprechende Entschlüsselung ist ähnlich einfach definiert.

m := c ⊕ k

Im Grunde genommen ist es dieselbe Operation, es wird in beiden Fällen ein XOR mit dem Schlüssel durchgeführt. Dass dies tatsächlich das gewünschte Ergebnis, nämlich die ursprüngliche Nachricht, liefert, sieht man, wenn man c ersetzt:

c ⊕ k = m ⊕ k ⊕ k = m ⊕ (k ⊕ k) = m ⊕ 0 = m

Man nutzt aus, dass XOR assoziativ ist und dass „XOR mit sich selbst“ lauter Nullen ergibt und XOR mit lauter Nullen keine Änderung herbeiführt. Es kommt zum Schluss also tatsächlich wieder die ursprüngliche Nachricht zum Vorschein.
Das One-Time Pad lässt sich einfach in Ruby [6] nachbauen, wie Listing 1 zeigt.

  • Listing 1
require 'securerandom'

require_relative 'string_refinements'

using StringRefinements

module OneTimePad

  def encrypt(data, pad)

    data.xor(pad)

  end

  alias_method :decrypt, :encrypt

  module_function :encrypt, :decrypt

end

data = "Attack at dawn"

pad = SecureRandom.bytes(data.size)

encrypted = OneTimePad.encrypt(data, pad)

# Zufällig, aber immer gleich lang wie die ursprüngliche Nachricht

p encrypted

decrypted = OneTimePad.decrypt(encrypted, pad)

puts decrypted

puts data == decrypted

Statt echtem Zufall, den wir streng genommen für das One-Time Pad voraussetzen, verwenden wir SecureRandom, den kryptographisch sicheren Zufallsgenerator in Ruby. Sie haben sich vielleicht schon gewundert, woher die xor-Methode bei der Klasse String kommt, diese gibt es nämlich nicht in der Standardimplementierung. Wir nutzen hier Refinements, um der Klasse diese Funktionalität temporär zu verleihen [7] (Listing 2).

  • Listing 2
module StringRefinements

  refine String do

    def xor(other)

      raise "Length mismatch" unless bytesize == other.bytesize

      each_byte.zip(other.each_byte).map do |(a, b)|

        a ^ b

      end.map(&:chr).join("")

    end

  end

end

Dass das One-Time Pad trotz seiner simplen Konstruktion Perfect Secrecy erfüllt, ist insofern erstaunlich, als das alle modernen Verschlüsselungsalgorithmen nicht schaffen. Wieso nutzen wir dann nicht alle einfach das One-Time Pad? Einer der Hauptgründe ist der extreme Aufwand, der dadurch entsteht, dass der Schlüssel immer so lang sein muss wie die Nachricht selbst. Denn zu einer symmetrischen Verschlüsselung gehören ja in der Regel zwei Parteien oder gar mehr, und nur eine Partei kann den Schlüssel erzeugen, danach muss er mit den restlichen Parteien ausgetauscht werden. Was sich so einfach anhört, ist in Wahrheit ein großes Problem, so groß sogar, dass es einen völlig neuen Zweig der Kryptographie, die asymmetrische Kryptographie, hervorgebracht hat.
Perfect Secrecy setzt leider voraus, dass die Schlüssel mindestens so lang wie die zu verschlüsselnde Nachricht sein müssen. In dieser Hinsicht ist das One-Time Pad also bereits optimal, da hier der Schlüssel genauso lang wie die Nachricht ist. Möchte man aber kürzere Schlüssel verwenden, muss man sich von Perfect Secrecy als Sicherheitsmodell verabschieden. Dass dies in der Praxis gar nicht so gravierend ist, werden wir gleich sehen.
Verschärft wird dieser Umstand aber dadurch, dass man mit einem Schlüssel des One-Time Pad immer nur genau einziges Mal verschlüsseln darf. Mehrmaliges Verschlüsseln mit demselben Schlüssel macht das Verfahren angreifbar. Schauen wir uns dazu das Beispiel in Listing 3 an [8].

  • Listing 3
data1 = "Attack at dawn"
data2 = " " * data1.size

data3 = "AAAxxxBBByyy"
data4 = "CCCxxxDDDabc"

pad = SecureRandom.bytes(data1.size)

encrypted1 = OneTimePad.encrypt(data1, pad)
encrypted2 = OneTimePad.encrypt(data2, pad)

p encrypted1 # Zufällige Bytes
p encrypted2 # Zufällige Bytes

xor_encrypted = encrypted1.xor(encrypted2)
p xor_encrypted # "aTTACK\x00AT\x00DAWN"

pad = SecureRandom.bytes(data3.size)

encrypted1 = OneTimePad.encrypt(data3, pad)
encrypted2 = OneTimePad.encrypt(data4, pad)

xor_encrypted = encrypted1.xor(encrypted2)
p xor_encrypted # "\x02\x02\x02\x00\x00\x00\x06\x06\x06\x18\e\x1A"

Sie sehen, dass die einzelnen Verschlüsselungen an sich zwar komplett willkürlich sind, wenn man sie aber per XOR miteinander verknüpft, das Ergebnis deutlich anders aussieht. Im ersten Fall kann man sogar den ursprünglichen Plaintext erkennen, allerdings mit umgekehrter Groß-/Kleinschreibung. Im zweiten Fall sind zumindest Muster zu erkennen. Was ist hier passiert? Nehmen wir an, ein Angreifer kann zwei Ciphertexte abgreifen, die mit demselben Schlüssel erzeugt wurden:

c1 := m1 ⊕ k c2 := m2 ⊕ k

Wenn er jetzt die beiden Ciphertexte mit XOR verbindet, erhält er Folgendes:

c1 ⊕ c2 = (m1 ⊕ k) ⊕ (m2 ⊕ k) = m1 ⊕ m2 ⊕ k ⊕ k = m1 ⊕ m2

Der Schlüssel „kürzt“ sich raus und es bleibt m1 ⊕ m2 zurück. Dieses Ergebnis ist zwar in erster Linie binärer Datensalat, aber wie wir in unserem Beispiel gesehen haben, eben doch nicht nur. Überall dort, wo in der einen Nachricht ein Leerzeichen war, taucht der Buchstabe aus der anderen in umgekehrter Groß-/Kleinschreibung auf. Dort, wo identische Buchstaben waren, taucht ein Null-Byte auf. Die Muster beim zweiten Beispiel tauchen, wenn auch in abgewandelter Form, erneut im Ergebnis auf. Diejenigen unter Ihnen in meiner Altersklasse werden sich schnell an das Glücksrad erinnert fühlen. Zurecht! Denn tatsächlich lässt sich mit genügend Aufwand die Lösung finden, d. h. die ursprünglichen Plaintexte lassen sich aus diesem XOR-Ergebnis wiedergewinnen.
Welche Schlüsse ziehen wir daraus? Obwohl Perfect Secrecy eine mächtige Grundlage für die Theorie liefert, deckt sie noch nicht alle Aspekte ab, die wir uns für eine zeitgemäße Verschlüsselung wünschen. Da die Schlüssel immer so groß sein müssen wie die zu verschlüsselnde Nachricht selbst und dazu noch nicht einmal wiederverwendet werden dürfen, haben sich die Kryptographen auf die Suche nach Alternativen gemacht.

Die Geburtsstunde der Stream Cipher

Die Idee war, die Vorzüge des One-Time Pad soweit es geht zu bewahren, und die Nachteile möglichst auszubessern. Das Ziel soll sein, möglichst kurze Schlüssel fester Größe zu etablieren. Je kleiner die Schlüssel, um so einfacher sind sie zu verwalten und umso breitflächiger können sie eingesetzt werden. Da das One-Time Pad aber zwingend Schlüssel variabler Größe voraussetzt, muss es einen Weg geben, sozusagen auf Knopfdruck aus einem relativ kurzen Schlüssel einen Schlüssel passender Länge zu erzeugen, und dies möglichst, ohne allzu viel Einbußen bei den Sicherheitsgarantien hinzunehmen. Die Lösung: Man nutzt den tatsächlichen Schlüssel als Startwert (Seed) für einen Zufallsgenerator, der einem dann auf Zuruf Schlüssel der gewünschten Länge generiert. Statt echten Zufallbytes wie bei dem One-Time Pad verwendet man pseudozufällige Bytes, die aus einem geheimen Seed heraus erzeugt werden. Solange Angreifer den Seed nicht kennen, erscheinen ihnen die generierten Bytes wie tatsächlich zufällig generierte Bytes. Weiterhin ist es ihnen allein durch die Observation der bis dato generierten Bytes nicht möglich, Rückschlüsse auf zukünftig generierte Bytes zu ziehen. Zufallszahlengeneratoren, die dies leisten können, bezeichnet man als kryptographisch sichere Pseudozufallszahlengeneratoren. Mit Hilfe eines solchen lässt sich die One-Time-Pad-Konstruktion umformulieren zu:

c := m ⊕ G(k)

Wobei G der kryptographisch sichere Pseudozufallszahlengenerator ist und k der geheim zu haltende Startwert – der eigentliche Schlüssel. Beim Entschlüsseln nutzen wir den Trick aus, dass es sich bei dem Pseudozufallszahlengenerator, wie der Name schon andeutet, eben nicht um echten Zufall handelt, sondern letztendlich um eine ganz normale deterministische Funktion, die als Eingabewert den geheimen Schlüssel hat. Jeder, der den geheimen Schlüssel kennt, kann die exakt gleiche Sequenz von Zufallszahlen erzeugen. Somit ist sichergestellt, dass beim Entschlüsseln der Empfänger die gleiche Sequenz erzeugt und die XOR-Operation wieder den ursprünglichen Plaintext zum Vorschein bringt:

m := c ⊕ G(k)

Wie sicher ist das nun? Perfect Secrecy kann nicht erfüllt werden, da der Schlüssel in der Regel kürzer ist als die zu verschlüsselnde Nachricht. Einem Angreifer bleibt immer der Brute-Force-Angriff, mit dem einfach alle möglichen Schlüssel der Reihe nach durchprobiert werden, bis der richtige gefunden ist. Daher wählt man die Schlüssellänge zumindest so groß, dass ein solcher Brute-Force-Angriff selbst mit modernster Hardware und erheblichem finanziellem Aufwand schlicht nicht möglich ist. Man verabschiedet sich von dem Absolutheitsanspruch, dass man Angriffe kategorisch ausschließt und nimmt in Kauf, dass es in der Theorie zwar valide Angriffe gibt, diese in der Praxis aber einfach nicht durchführbar sind. Das wird als Computational Security bezeichnet [9]. Als Sicherheitsmodell, das diese theoretischen Angriffe in Kauf nimmt, hat man Semantic Security [10] definiert, was grob gesagt Perfect Secrecy einfach übernimmt, allerdings gesteht man einem Angreifer zu, dass Informationen über den Plaintext gewonnen werden können, jedoch nicht mit effizient berechenbaren Verfahren. Was sich erst einmal nach einer erheblichen Einschränkung anhören mag, ist tatsächlich gar nicht so schlimm, denn die Schranken, die man für Berechenbarkeit anlegt, sind so hoch (und mit Puffer) gewählt, dass sie zum heutigen Zeitpunkt schlicht als unüberwindbar gelten.
Die obige Konstruktion wird als Stream Cipher bezeichnet und es kann gezeigt werden, dass Stream Ciphers Semantic Security erfüllen, vorausgesetzt der zugrunde liegende Zufallszahlengenerator ist kryptographisch sicher. Wir können uns mit recht einfachen Mitteln selbst einen Stream Cipher basteln [11], wie in Listing 4 zu sehen ist.

  • Listing 4
class StreamCipher

attr_reader :key, :rng

def initialize(key)
@rng = RNG.new(key)
end

def encrypt(data)
pad = rng.bytes(data.size)
data.xor(pad)
end
alias_method :decrypt, :encrypt

end

key = "\x00" * 16
data = "Attack at dawn"
puts "Data length: #{data.size}"

encrypter = StreamCipher.new(key)
encrypted = encrypter.encrypt(data)

p encrypted

decrypter = StreamCipher.new(key)
decrypted = decrypter.decrypt(encrypted)

puts decrypted
puts data == decrypted

Eine Warnung gleich vorweg: Die eigens gebastelten Stream Ciphers und Zufallszahlengeneratoren dienen nur der Veranschaulichung, Sie sollten sie niemals in einem Produktionsumfeld verwenden.
Streng genommen ist der Code identisch zu unserer One-Time-Pad-Implementierung, denn in Wahrheit war unser One-Time Pad bereits ein Stream Cipher, da wir bereits mit SecureRandom mangels Alternative in Form eines echten Zufallsgenerators stattdessen einen kryptographisch sicheren Pseudozufallszahlengenerator verwendet haben. Um den Unterschied zum One-Time Pad aber noch einmal deutlich hervorzuheben, benutzen wir einen ebenso simplen wie kruden Ersatz, der uns dazu dient, die Funktionsweise eines Pseudozufallszahlengenerators zu veranschaulichen [12] (Listing 5).

  • Listing 5
require 'openssl'

class RNG
def initialize(seed)
   @digest = OpenSSL::Digest::SHA256.new
   @state = @digest.digest(seed)
end

def bytes(n)
   result = String.new
   while result.size < n
      result += @state

     @state = @digest.digest(@state)
   end
   result[0...n]
end
end

Wir nutzen den Seed und strecken ihn wie benötigt mit einer kryptographischen Hash-Funktion, in diesem Fall SHA-256. Man kann sich den Hash als eine Art Nudelholz vorstellen, das eine Handvoll echten Zufall nach Belieben in die Breite walzt, indem man die Hash-Funktion iterativ wieder und wieder auf die Zwischenergebnisse anwendet. In dieser Form war z. B. der Linux-interne sichere Zufallszahlengenerator implementiert [13], bevor SHA-1 als Hash-Funktion durch den Stream Cipher ChaCha20 ersetzt wurde [14], den wir auch noch kennenlernen werden. Man nutzt hierbei aus, dass sowohl der Output einer kryptographischen Hash-Funktion als auch der einer sicheren Verschlüsselung komplett willkürlich ist. Man spricht davon, dass der Output ununterscheidbar von echt zufälligem Output ist. Diese Eigenschaft werden wir gleich näher betrachten, da sie auch bei sicherer Verschlüsselung eine große Rolle spielt.

Wenn Sie unser Stream-Cipher-Beispiel mehrfach ausführen, werden Sie feststellen, dass das Ergebnis der Verschlüsselung immer das Gleiche ist:

"v3|\x9E\x94\x1A\xBD\xB4\xE3\xBE\xAC\x14\xA2\x02"

Das Problem deterministischer Verschlüsselung
Bei gleichem Schlüssel und gleichem Plaintext ist der daraus resultierende Ciphertext immer gleich. Das ist nicht etwa unserem „Spiel-Cipher“ geschuldet, auch seriöse Stream Ciphers haben mit diesem Problem zu kämpfen. Beispielsweise RC4, der mittlerweile nicht mehr eingesetzt werden sollte [15] (Listing 6).

  • Listing 6
require 'openssl'

data = "Attack at dawn"
puts "Data length: #{data.size}" # => 14

encryptor = OpenSSL::Cipher.new("rc4")
key = encryptor.key = "\x00" * encryptor.key_len
encryptor.encrypt
encrypted = encryptor.update(data)
last = encryptor.final

p encrypted # => "\x9Fl\xFD \xC0\\}[\xFE&z\x06 \x00"
puts encrypted.size # => 14

decryptor = OpenSSL::Cipher.new("rc4")
decryptor.key = key
decryptor.decrypt
decrypted = decryptor.update(encrypted)

puts decrypted # => Attack at dawn
puts data == decrypted # => true

# "decrypt" = "encrypt"
encryptor = OpenSSL::Cipher.new("rc4")
encryptor.key = key
encryptor.decrypt
encrypted = encryptor.update(data)

p encrypted # => "\x9Fl\xFD \xC0\\}[\xFE&z\x06 \x00"

decryptor = OpenSSL::Cipher.new("rc4")
decryptor.key = key
decryptor.encrypt
decrypted = decryptor.update(encrypted)

puts decrypted # => Attack at dawn
puts data == decrypted # => true

puts "RC4 key length: #{encryptor.key_len}" # => 16
puts "RC4 iv length: #{encryptor.iv_len}" # => 0

Wir verwenden RC4 mit einem Schlüssel, der aus lauter Null-Bytes besteht. Praktischerweise bietet Ruby OpenSSL mit Cipher#key_len die Möglichkeit, an einer bestehenden Cipher-Instanz deren Schlüssellänge konkret abzufragen. Wir sehen, dass der RC4-Schlüssel 16 Bytes, also 128 Bit, lang ist. Nach dem ersten Ver-/Entschlüsseln spielen wir denselben Vorgang erneut durch, vertauschen aber die Rollen der Ver- und Entschlüsselung. Im ersten Durchgang „entschlüsseln“ wir die Daten, um sie danach zu „verschlüsseln“. Das dient als Beweis dafür, dass die grundsätzliche Unterscheidung zwischen beidem bei Stream Ciphers keine Rolle spielt. Die Operationen beim Ver- sowie beim Entschlüsseln sind immer gleich. Probieren Sie es gerne aus, es klappt in allen möglichen Kombinationen, selbst decrypt/decrypt und encrypt/encrypt – Sie erhalten stets das exakt gleiche Resultat für den Ciphertext und am Ende immer den korrekten Plaintext zurück.
Dabei wird der erzeugte Ciphertext stets wie folgt aussehen:

"\x9Fl\xFD \xC0\\}[\xFE&z\x06 \x00"

Das gleiche Phänomen also wie bei unserem selbstgebastelten Stream Cipher. Der Grund ist auch hier der gleiche: Der Pseudozufallsgenerator der Stream Ciphers ist am Ende doch nur eine deterministische Funktion, die sich dadurch auszeichnet, bei gleichen Eingaben gleiche Ergebnisse zu liefern.
Dies ist aber ein großes Problem, denn wie wir im nächsten Teil sehen werden, ist es bei der Mehrfachverschlüsselung mit demselben Schlüssel essenziell, dass die Verschlüsselung nichtdeterministisch arbeitet. Bevor wir uns diesem Punkt weiter widmen, schauen wir uns nochmal das Problem des One-Time Pad an: Was passiert, wenn man den Schlüssel wiederverwendet? Dieses Problem überträgt sich eins zu eins auf die Stream Ciphers [16] in Listing 7.

  • Listing 7
require 'openssl'

require_relative 'string_refinements'

using StringRefinements

data1 = "Attack at dawn"
data2 = " " * data1.size

data3 = "AAAxxxBBByyy"
data4 = "CCCxxxDDDabc"

def encrypt(data)
  key = "\x00" * 16
  cipher = OpenSSL::Cipher.new("rc4")
  cipher.key = key
  cipher.encrypt
  cipher.update(data)
end

encrypted1 = encrypt(data1)
encrypted2 = encrypt(data2)
encrypted3 = encrypt(data3)
encrypted4 = encrypt(data4)

p encrypted1.xor(encrypted2) # => "aTTACK\x00AT\x00DAWN"
p encrypted3.xor(encrypted4) # => "\x02\x02\x02\x00\x00\x00\x06\x06\x06\x18\e\x1A"

Auch hier können wir Teile der Plaintexte wiedererkennen und es werden Muster der zugrunde liegenden Plaintexte deutlich. Wenn Sie sich an unsere Definition von Sicherheit zurückerinnern, ist diese eindeutig verletzt. Denn es werden hier sehr wohl Informationen über die Plaintexte gewonnen, und dieser Informationsgewinn geschah in absolut vertretbarer Zeit. Heißt das, Stream-Cipher-Konstruktionen wie etwa RC4 nicht sicher sind?
Ja und nein. Die Stream Cipher-Konstruktion erfüllt Semantic Security. Allerdings berücksichtigt man dabei nur eine einzige Verschlüsselung. Die Mehrfachverschlüsselung mit dem gleichen Schlüssel wird hier noch nicht betrachtet. Um diese zu erlauben, müssen wir unsere Konstruktion erst noch um ein wichtiges Element erweitern. Entsprechend werden wir dann auch unser Sicherheitsmodell erweitern, sodass es explizit Mehrfachverschlüsselung berücksichtigt.
Damit endet der erste Teil und wir werden uns weiter mit der Lösung dieses Problems im zweiten Teil der Artikelserie beschäftigen.

Martin Boßlet ist selbständiger Berater mit den Schwerpunkten Sicherheit, Web- und Unternehmensanwendungen. Zudem vermittelt er regelmäßig sein Wissen in Seminaren als Trainer für Kryptographie, Programmiersprachen und verschiedene Themen aus dem Bereich der Webentwicklung. Seine besondere Leidenschaft gilt der Programmiersprache Ruby, wo er Mitglied im Core-Team ist und die Kryptographiebibliothek betreut.

Links & Literatur

[1] https://en.wikipedia.org/wiki/Stream_cipher

[2] https://en.wikipedia.org/wiki/Block_cipher

[3] https://en.wikipedia.org/wiki/Semantic_security

[4] https://en.wikipedia.org/w/index.php?title=Information-theoretic_security

[5] https://en.wikipedia.org/wiki/One-time_pad

[6] https://github.com/emboss/stream-ciphers/blob/master/code/onetimepad.rb

[7] https://github.com/emboss/stream-ciphers/blob/master/code/string_refinements.rb

[8] https://github.com/emboss/stream-ciphers/blob/master/code/onetimepadkeyreuse.rb

[9] http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.156.4530&rep=rep1&type=pdf

[10] https://en.wikipedia.org/wiki/Semantic_security

[11] https://github.com/emboss/stream-ciphers/blob/master/code/deterministicstreamcipher.rb

[12] https://github.com/emboss/stream-ciphers/blob/master/code/rng.rb

[13] https://github.com/torvalds/linux/blob/e192be9d9a30555aae2ca1dc3aad37cba484cd4a/drivers/char/random.c#L78

[14] https://github.com/torvalds/linux/commit/e192be9d9a30555aae2ca1dc3aad37cba484cd4a

[15] https://github.com/emboss/stream-ciphers/blob/master/code/rc4.rb

[16] https://github.com/emboss/stream-ciphers/blob/master/code/rc4keyreuse.rb

Alle News zum IT Security Summit