Mit Passwort MussNichtGanzSoLangeSicherSein...
Dieses Dokument ist ein Jupyter Notebook, d.h. eine interaktive Python-Umgebung.
Die Anforderungen an eine kryptographische Hashfunktion sind
Weitere wünschenswerte Effekte:
Sichere Hashfunktionen in der Praxis: SHA2 (sha256
, sha384
, sha512
), SHA3
Unsichere Hashfunktionen in der Praxis: SHA1, MD5
# Commitment, Erster Ansatz:
from hashlib import sha256
my_action = b"Schere"
commitment = sha256(my_action).hexdigest()
print(commitment)
# Was ist das problem?
e6532e1b964c2ef602ae65967f297f80fe2214ef0fbc16e6928ebca0ce741a95
# Nicht deterministisch:
from hashlib import sha256
my_action = b"Stein - TODO: gib hier deine unvorhersehbare Daten ein!"
commitment = sha256(my_action).hexdigest()
print(commitment)
e7f1b9a9e6c94f09311bc75c4b79fe58419044d3822ee538049c1414203f6989
# Nicht deterministisch, automatisch:
from hashlib import sha256
from random import SystemRandom
rng = SystemRandom()
my_action = b"Schere"
message = my_action + b" - " + rng.randbytes(16)
commitment = sha256(message).hexdigest()
print(commitment)
print(message)
# Bob: TODO: ausfüllen
# message = ...
# commitment = "..."
print(sha256(message).hexdigest() == commitment)
Message Authentication Codes:
Alice und Bob haben gemeinsames geheimnis k
Alice schickt Nachricht + sha3(k + Nachricht)
Achtung: bei sha2 HMAC verwenden.
Beispiel: Zwei Faktor Authentifizierung wie Google Authenticator oder TANs
Speichern von Passwörtern (Historisch?)
hash(passwort) ist problematisch, als Alternative -> z.B. "Argon2"
bit: "binary digit" eine Ziffer 0 oder 1
Byte: 8 bit, d.h. Zahlen 0...255, häufig hexadezimal dargestellt: 2A = 2 * 16 + 10 = 42
Darstellung von Text als Bytes: viele möglichkeiten, z.B. UTF8:
Jedes Zeichen hat eine Nummer (-> Unicode, -> ASCII). Die Zeichennummern haben Darstellungen als ein oder mehrere Bytes. Darstellungen von mehreren Zeichen werden aneinander gehängt.
Hallöchen -> [72, 97, 108, 108, 246, 99, 104, 101, 110] -> [72, 97, 108, 108, 195, 182, 99, 104, 101, 110]
Hex: 48616c6cc3b66368656e
"Hallöchen".encode(), "Hallöchen".encode().hex()
n = 1027 # = 4 * 256 + 3
print(n.to_bytes(2, "big").hex())
print(n.to_bytes(2, "little"))
Verschlüsseln(Klartext, Schlüssel) -> Ciphertext
Entschlüsseln(Ciphertext, Schlüssel) -> Klartext
Ohne Schlüssel soll der Ciphertext von zufälligen Daten ununterscheidbar sein
Wenn man den Schlüssel mehrfach verwenden will:
Die Kenntnis von Klartext und Ciphertext soll nichts preisgeben
m
z.B. in bits darstellen, (m[0]
, m[1]
, ...), k
mindestens so lang wie die Nachricht, zufälligc
definiert als c[i] = m[i] + k[i]
(Stellenweise Addieren ohne Übertrag)Nachricht Hallo -> 48616c6c6f <-> 01001000 01100001 01101100 01101100011 01111
Schlüssel Perdox -> 506572646f78 <-> 01010000 01100101 01110010 01100100011 0111101111000
Ciphertext 00011000 00000100 00011110 00001000000 00000
1 8 0 4 1 E 0 8 0 0
Nachricht Hallo -> 48616c6c6f <-> 01001000 01100001 01101100 01101100 01101111
Zufälliger Schlüssel 11100000 11010010 10100101 01100000 01110010
10101000 10110011 11001001 00001100 00011101
A 8 B 3 C 9 0 C 1 D
Sicher, wenn man jeden Schlüssel nur ein einziges mal verwendet.
# One-Time-Pad in python
key_bytes = b"Perdox"
# Aufgabe: Nutze zufällige bytes stattdessen - siehe irgendwo oben :)
# key_bytes = bytes.fromhex("....")
print(key_bytes)
print(key_bytes.hex())
b'Perdox' 506572646f78
msg = "Hallo"
msg_bytes = msg.encode() # Macht string "Hallo" zu bytes.
n = len(msg_bytes)
ciphertext = bytearray(n) # Für Ergebnis: so viele bytes wie msg_bytes lang ist anlegen
for i in range(n):
# Wir arbeiten hier mit bytes statt mit bits, d.h.
# irgendwie müssen wir 8 bit-additionen auf einmal durchführen.
# das macht der ^ operator.
ciphertext[i] = msg_bytes[i] ^ key_bytes[i]
print(ciphertext)
print(ciphertext.hex())
# Entschlüsseln geht genauso wie verschlüsseln - probiert es aus!
bytearray(b'FLAG{oh no, you stole my deepest secret!}') 464c41477b6f68206e6f2c20796f752073746f6c65206d79206465657065737420736563726574217d
msg_bytes = bytearray(b'\x18\x04\x1e\x08\x00')
# Known plaintext attack gegen OTP
# ciphertext_1 und ciphertext_2 wurden mit dem selben Schlüssel erzeugt.
plaintext_1 = b"As OTP is proven to be secure, I will always use it!"
ciphertext_1 = bytes.fromhex("e0c141a1276dee44b7b0ee8147e3238011526aaa56cf971518e22401bef70ab0620dfd3958333916931b261d42bb1018dc3ce002")
ciphertext_2 = bytes.fromhex("e7fe20a90852a60daaffb2d351fa33ce42526ae6518ada1f5de53416abbe598d6209f13646762c5b99")
key_bytes = b'\xa1\xb2a\xees=\xce-\xc4\x90\x9e\xf3(\x95F\xee1&\x05\x8a4\xaa\xb7f}\x81Qs\xdb\xdb*\xf9Bz\x94U4\x13Xz\xe4z_nb\xcec}\xfcU\x94#'
msg_bytes = ciphertext_2
ciphertext = (nonce, Schlüsselstrom xor klartext)
Gängige sichere Stromchiffren sind z.B. ChaCha20 und AES im Counter-Mode
from Crypto.Cipher import AES
key = b"ist 16bytes lang" # oder 32 bytes
cipher = AES.new(key, mode=AES.MODE_CTR) #
nonce = cipher.nonce # zufällig. kann auch mit nonce= beim Erstellen angegeben werden
ciphertext = cipher.encrypt(b"der Klartext kann im Counter-Mode jede Laenge haben.")
print(nonce.hex(), ciphertext.hex())
cipher = AES.new(key, mode=AES.MODE_CTR, nonce=nonce)
pt = cipher.decrypt(ciphertext)
print(pt)
Bild aus Wikipedia
Wenn man statt normal zu rechnen, immer nur die Reste nach der division durch $n$ betrachtet, funktionieren die meisten bekannten Rechengesetze weiter:
(3 + 10 + 15) % 11 == (2 + 15) % 11 == (2 + 4) % 11 == 6
True
(9 * 11) % 11
# Potenzen von 2, mod 11:
for i in range(13):
print(i, 2**i, 2**i % 11, pow(2, i, 11))
0 1 1 1 1 2 2 2 2 4 4 4 3 8 8 8 4 16 5 5 5 32 10 10 6 64 9 9 7 128 7 7 8 256 3 3 9 512 6 6 10 1024 1 1 11 2048 2 2 12 4096 4 4
pow(a,b,n)
# DH in Python
from random import SystemRandom
p = 26620000968396558157058486122762022047658163186550617910789713670105584707837203686745958338704438582689048773144369335500395009787020830018463829975518131094597143856888552926514964146211312961812930093381831783656958664347685794138490752357760389677704342344983908376788668312350379037227937065593445519887213372732655128534070179543933664406307677407706816421792940468094821877210944618013484146000483753306052420260783034722557870719509408486061019840009710023976464989087815331876396002336659605610248016717945748351067194176593112926527800868468153526684544644879979343120714474757093198048023322841938394145463
# p ist eine "safe prime, d.h. (p-1)/2 ist auch prim", generiert mit `openssl dhparam 2048`
# Alternativ: p = 11
g = 2
rng = SystemRandom() # Kryptographischer Zufallszahlengenerator des Betriebssystems
my_secret = rng.randrange(p-1)
# TODO
my_public = 1234
print("SECRET:", my_secret)
print()
print("public", my_public)
# Verteile "public" über den chat
# Hier den empfangenen Wert einfügen
their_public = 1234
shared = pow(their_public, my_secret, p) # shared == g ** (my_secret * their_secret) % p
# Key derivation: hier gibt es für solche anwendungen optimierte Funktionen, wir nehmen
# jetzt einfach sha256.
shared_bytes = shared.to_bytes(2048//8, "big")
from hashlib import sha256
key = sha256(shared_bytes).digest()
# Jetzt habt ihr in "key" einen gemeinsamen schlüssel. Mit dem AES-Beispiel von oben könnt ihr jetzt nachrichten austauschen.
Jetzt müsste man nur noch wissen, dass auf der anderen Seite die richtige Person / Maschine antwortet...
Wenn die Geheimnisse für den Diffie-Hellman-Schlüsselaustausch immer frisch generiert werden ("Ephemeral Diffie Hellman"), brauchen wir eine andere Möglichkeit, einem Benutzer eine beweisbare Identität zuzuordnen...
(nicht verbreitet, weil mit (EC)DSA eine effizientere Alternative existiert)
Signieren einer Nachricht m:
h = hash(m)
, interpretiert als ZahlSignatur besteht aus $r$ und $s$
Verifizieren:
h = hash(m)
, interpretiert als Zahl$A^r \cdot r^s = g^{a \cdot r + k k^{-1} (h - a\cdot r)} = g^h$
Wie kann ich mit einer kleinen Webseite wie https://perdox-ev.de/ kommunizieren, ohne dass ich deren Publickey kenne?
Webseite sendet eine Zertifikatskette:
S/MIME: Das gleiche für Email - da nicht Interaktiv, keine Forward Secrecy
Wenn Zeit ist