Was du hier lernst
- Asymmetrische Verschlüsselung
- Modulare Arithmetik
- Square and Multiply
- Erweiterter euklidischer Algorithmus
- Miller-Rabin-Primzahltest
- Große Ganzzahlen in Python
- Text als Zahl (UTF-8)
So funktioniert RSA
Bei Caesar und Vigenère verschlüsselt und entschlüsselt man mit demselben Geheimnis. RSA trennt beides: Mit dem öffentlichen Schlüssel kann jeder eine Nachricht verschlüsseln, lesen kann sie nur, wer den privaten Schlüssel besitzt. Ron Rivest, Adi Shamir und Leonard Adleman haben das Verfahren 1977 vorgestellt. Seine Sicherheit beruht auf einer einfachen Asymmetrie: Zwei große Primzahlen zu multiplizieren ist leicht. Das Produkt wieder in seine Faktoren zu zerlegen, ist praktisch unmöglich.
1. Die Idee in kleinen Zahlen
Der erste Teil des Programms rechnet mit Zahlen, die man noch im Kopf überblickt. Aus den Primzahlen p = 61 und q = 53 entsteht der Modul n = 3233. Dazu gehört φ(n) = (p − 1)(q − 1) = 3120. Als öffentlichen Exponenten wählt man eine Zahl e, die mit 3120 keinen gemeinsamen Teiler hat, hier 17. Der private Exponent d ist die Zahl, für die e · d beim Teilen durch φ(n) den Rest 1 lässt. Das ist 2753, denn 17 · 2753 = 46801 = 15 · 3120 + 1.
Verschlüsseln heißt: die Nachricht m hoch e, modulo n. Aus 65 wird 65¹⁷ mod 3233 = 2790. Entschlüsseln geht genauso, nur mit d statt e: 2790²⁷⁵³ mod 3233 ergibt wieder 65.
Warum kommt die Nachricht zurück?
Hinter RSA steckt der Satz von Euler: Für Zahlen m, die mit n keinen gemeinsamen Teiler haben, gilt m^φ(n) ≡ 1 (mod n). Weil e · d = k · φ(n) + 1 ist, wird aus (m^e)^d = m^(k·φ(n)+1) wieder m. Wer n in p und q zerlegen kann, kennt φ(n) und damit auch d. Deshalb hängt alles daran, dass niemand n faktorisieren kann.
2. Schnelles Potenzieren: »square and multiply«
2790²⁷⁵³ ist eine Zahl mit rund 9500 Stellen, und im echten RSA haben die Exponenten über 300 Stellen. Direkt ausrechnen geht nicht. modpot aus dem Buch nutzt zwei Tricks. Ist der Exponent gerade, wird die Basis quadriert und der Exponent halbiert, denn x^(2k) = (x²)^k. Ist er ungerade, wandert ein Faktor ins Ergebnis. Nach jedem Schritt wird modulo n reduziert, so bleiben alle Zwischenwerte klein:
while y > 0:
if y % 2 == 1:
result *= x
result %= n
y -= 1
else:
x *= x
x %= n
y //= 2
Statt 2753 Multiplikationen braucht das nur 16 Schleifendurchläufe. Bei einem Exponenten mit 1024 Bit sind es etwa 1500 – der naive Weg bräuchte eine Anzahl von Multiplikationen, die selbst über 300 Stellen hat.
3. Das Inverse mit dem erweiterten Euklid
Wie findet man d? Der erweiterte euklidische Algorithmus xggt(a, b) berechnet nicht nur den größten gemeinsamen Teiler, sondern auch zwei Zahlen x und y mit x · a + y · b = ggT(a, b). Ist der ggT von e und φ(n) gleich 1, dann ist x · e um genau 1 größer als ein Vielfaches von φ(n), und x ist das gesuchte d. inverse_mod macht es mit (x + m) % m noch positiv. Hat e einen gemeinsamen Teiler mit φ(n), erhöht findMultiplicativeInverse den Wert von e so lange um 2, bis es passt.
4. Riesige Primzahlen finden: Miller-Rabin
Für echte Schlüssel braucht das Programm zwei Primzahlen mit je 512 Bit, also mit rund 155 Stellen. Ausprobieren, ob eine solche Zahl durch 2, 3, 5, … teilbar ist, würde bis zum Ende des Universums dauern. Der Test von Miller und Rabin geht einen anderen Weg. Für eine Primzahl n gilt a^(n−1) mod n = 1, egal welches a man nimmt. test(a, n) rechnet genau das aus und achtet zusätzlich auf verdächtige Wurzeln aus 1. Schlägt der Test an, ist n sicher keine Primzahl.
Eine zusammengesetzte Zahl übersteht einen Durchgang höchstens mit Wahrscheinlichkeit 1/4. MillerRabin wiederholt ihn mit 50 zufälligen Werten für a, und die Irrtumswahrscheinlichkeit sinkt unter 10⁻³⁰. randomPrime würfelt so lange ungerade Zahlen, bis eine den Test besteht. Im Schnitt ist das etwa jede 180. ungerade Zahl dieser Größe.
5. Text als Zahl
RSA verschlüsselt Zahlen, keine Buchstaben. strToNumber wandelt den Text deshalb zuerst mit UTF-8 in Bytes um. Diese Bytes liest es als Ziffern einer Zahl zur Basis 256, das erste Byte ist die Einerstelle, das zweite zählt 256-fach, und so weiter. numberToStr pflückt die Ziffern mit % 256 und // 256 wieder ab. Weil RSA modulo n rechnet, muss diese Zahl kleiner als n sein. Wie es sonst ausgeht, zeigt die Variante »Zu kleine Schlüssel«.
6. Große Zahlen sind in Python eingebaut
Für die Rechnungen braucht Python keine Zusatzbibliothek. Ganze Zahlen dürfen in Python beliebig groß sein, eine Zahl mit 309 Stellen ist ein ganz normales int. In Java bräuchte man dafür die Klasse BigInteger. Daran erinnert ein auskommentiertes Java-Fragment im Originalcode von rsa.py.
Nur zum Lernen
Dieses »Lehrbuch-RSA« zeigt das Prinzip, ist aber nicht für echte Geheimnisse gedacht: Es fehlt unter anderem ein Padding-Verfahren wie OAEP, das gleiche Nachrichten unterschiedlich aussehen lässt. Für echte Anwendungen nimmst du eine geprüfte Bibliothek.
Probier mal
Jede Variante ändert den Code oben. Ein Klick auf »Ausprobieren« übernimmt die Änderung in den Editor und startet das Programm. »Zurücksetzen« holt das Original zurück.
-
01Doppelt so lange Schlüssel
Mit 1024 Bit pro Primzahl hat der Modul n über 600 Stellen. Das entspricht einem RSA-Schlüssel mit 2048 Bit, wie er heute üblich ist. Die Primzahlsuche dauert jetzt spürbar länger, denn jeder Test rechnet mit doppelt so langen Zahlen und braucht doppelt so viele Schritte.
- bits = 512 # Länge + bits = 1024 # Länge -
02Zu kleine Schlüssel
Mit Primzahlen aus nur 24 Bit ist n kleiner als die Zahl, die für die Nachricht steht. RSA rechnet aber modulo n: Alles oberhalb von n geht verloren, und heraus kommt Zeichensalat. Die Nachricht muss kleiner als n sein. Deshalb zerlegt man lange Texte in Blöcke oder verschlüsselt mit RSA nur einen kurzen Schlüssel.
- bits = 512 # Länge + bits = 24 # Länge -
03Eine digitale Unterschrift
RSA funktioniert auch rückwärts. Wer mit dem privaten Schlüssel d »verschlüsselt«, erzeugt eine Unterschrift. Mit dem öffentlichen Schlüssel e kann jeder prüfen, dass sie echt ist, aber niemand kann sie fälschen. Genau so werden Software-Updates und Zertifikate signiert.
- print(" entschlüsselt:", plain) + print(" entschlüsselt:", plain) + + signature = modpot(number, d, n) + print() + print("4) Unterschrift mit d:", short(signature)) + print(" geprüft mit e: ", numberToStr(modpot(signature, e, n))) -
04Pythons eingebaute Abkürzungen
Python kann das alles schon selbst.
pow(x, y, n)rechnet schnelles Potenzieren modulo n, und seit Python 3.8 liefertpow(e, -1, phi)das modulare Inverse. Die Variante vergleicht beides mit den Funktionen aus dem Buch.- print(" entschlüsselt:", plain) + print(" entschlüsselt:", plain) + + phi = (p - 1) * (q - 1) + print() + print("4) pow(number, e, n) == modpot(number, e, n):", pow(number, e, n) == cipher) + print(" pow(e, -1, phi) == d:", pow(e, -1, phi) == d) -
05Deine eigene Nachricht
Ersetze den Text in
message. Umlaute und Emojis sind kein Problem, weilstrToNumberden Text zuerst in UTF-8-Bytes umwandelt. Nur zu lang darf er nicht werden: Bei 512 Bit pro Primzahl passen gut 120 Bytes in eine Zahl unterhalb von n.- message = "Coding for Fun mit Python – geheime Grüße!" + message = "Treffen um 12 Uhr am Brunnen 🐍"
Vom Buch in den Browser
Auf der Buch-CD verteilt sich RSA auf drei Python-2-Dateien. RSA.py enthält die Grundbausteine (modpot, xggt, Miller-Rabin), rsa/rsa.py Schlüsselberechnung und Umwandlung von Text in Zahlen, und rsa/rsaTest.py verschlüsselt mit zwei fest eingetragenen Primzahlen. Hier ist alles in einem Programm zusammengefasst. Neu sind randomPrime, die Hilfsfunktion short für lange Zahlen und das Mini-Beispiel am Anfang. Die Lehrfunktionen multi und pot sowie die unfertige isQualityPrime sind weggefallen.
Die wichtigste Änderung: / wird zu //. In Python 2 teilt / zwei ganze Zahlen ganzzahlig, in Python 3 entsteht immer eine Kommazahl. Bei Zahlen mit Hunderten Stellen ist das fatal: Eine Kommazahl hat nur etwa 16 gültige Stellen, und oberhalb von 10³⁰⁸ gibt es einen OverflowError. Deshalb steht jetzt überall die Ganzzahldivision // bzw. //=: in modpot (y //= 2), in toBinary und in numberToStr.
Außerdem:
printist eine Funktion,xrangeheißtrange.- Ein Fehler aus
RSA.pyist behoben. Dort steht intest()die Prüfungif d != 1: return Trueinnerhalb der Schleife statt danach. Dadurch meldet der Test schon nach dem ersten Schritt fast immer »zusammengesetzt«, auch für echte Primzahlen. Inrsa/rsa.pyist die Zeile richtig eingerückt. Die Portierung übernimmt diese Fassung. findMultiplicativeInversedarf e erhöhen, falls es einen gemeinsamen Teiler mit φ(n) hat. Das Original gab aber nur d zurück, das veränderte e ging verloren. Jetzt kommen beide zurück:e, d = findMultiplicativeInverse(p, q, e).strToNumberundnumberToStrarbeiteten in Python 2 mitord()undchr()auf Byte-Strings. In Python 3 sind Strings Unicode, deshalb geht es jetzt übertext.encode("utf-8")und einbytearray. So klappen auch Umlaute und Emojis.errors="replace"sorgt dafür, dass eine missglückte Entschlüsselung Zeichensalat zeigt, statt abzubrechen.- Statt der fest eingetragenen Primzahlen und des Exponenten
e = 1210475170735953725411ausrsaTest.pyerzeugt das Programm bei jedem Lauf neue Primzahlen und startet mit dem heute üblichene = 65537.
Original aus dem Buch ansehen RSA.py · Python 2
#coding: latin1
'''
Created on 05.12.2009
@author: Lars Heppert
'''
def multi(x, y):
'''Multiplizieren durch Addieren und Verdoppeln'''
result = 0
while y > 0:
if y % 2 == 1:
# bei ungerade Werten fuer die 2. Zahl wird
# die das Zwischenergebnis der 1. Zahl summiert
result += x
# die Reduzierung um 1 macht die 2. Zahl wieder gerade
# und verhindert einen Rest bei der Division durch 2,
# wobei es hier nur darum geht in den else-Zweig zu wechseln
y -= 1
else:
x *= 2
y /= 2
return result
print multi(31, 74)
def pot(x,y):
'''Potenzieren durch Quadrieren und Multiplizieren'''
result = 1
while y > 0:
if y % 2 == 1:
result *= x
y -= 1
else:
y /= 2
x *= x
return result
print pot(2, 4)
def modpot(x,y,n):
'''
Schnelles Potenzieren x^y (mod n)
nach der Methode "square and multiply".
Zwischenergebnisse werden modulo n reduziert
'''
result = 1
while y > 0:
if y % 2 == 1:
result *= x
result %= n
y -= 1
else:
x *= x
x %= n
y /= 2
return result
def xggt(a,b):
x1, x2, y1, y2 = 1, 0, 0, 1
while b:
q = a // b
x1, x2, y1, y2 = x2, x1 - q * x2, y2, y1 - q * y2
a, b = b , a % b
return a, x1, y1
print "xggt = ", xggt(23, 120)
import math
def isprime(n):
if n % 2 == 0 and not n == 2:
return (False)
else:
limit = int(math.sqrt(n)) + 1
for l in range(3, limit, 2):
if n % l == 0:
return (False)
return (True)
print isprime(23), "ist eine Primzahl"
import sys
import random
def toBinary(n):
"""
toBinary(n)
wandelt n in eine Liste, welche die Binärdarstellung
von n repräsentiert - diese Darstellung vereinfacht
die folgenden Schritte bzgl. der Primzahlprüfung
"""
r = []
while (n > 0):
r.append(n % 2)
n = n / 2
return r
def test(a, n):
"""
test(a, n)
überprüft, ob n (einfach) zusammengestezt ist,
also als Produkt anderer Zahlen entsteht
"""
b = toBinary(n - 1)
d = 1
for i in xrange(len(b) - 1, -1, -1):
x = d
d = (d * d) % n
if d == 1 and x != 1 and x != n - 1:
return True
if b[i] == 1:
d = (d * a) % n
if d != 1:
return True
return False
def MillerRabin(n, s = 50):
"""
MillerRabin(n, s = 1000)
überprüft, ob n prim ist
"""
for j in xrange(1, s + 1):
a = random.randint(1, n - 1)
if (test(a, n)):
return False # n is complex
return True # n is prime
Original aus dem Buch ansehen rsa/rsa.py · Python 2
'''
Created on 07.12.2009
@author: GWV7FD3
'''
def gcd(a, b):
while b > 0:
a, b = b, a % b
return a
print gcd(48, 16)
def extgcd(a, b):
u = t = 1
v = s = 0
while b > 0:
q = a // b
a, b = b, a - q * b
u, s = s, u - q * s
v, t = t, v - q * t
return a, u, v
print extgcd(48, 16)
def toBinary(n):
r = []
while (n > 0):
r.append(n % 2)
n = n / 2
return r
def test(a, n):
b = toBinary(n - 1)
d = 1
for i in xrange(len(b) - 1, -1, -1):
x = d
d = (d * d) % n
if d == 1 and x != 1 and x != n - 1:
return True
if b[i] == 1:
d = (d * a) % n
if d != 1:
return True
return False
import random
def MillerRabin(n, s = 50):
for j in xrange(1, s + 1):
a = random.randint(1, n - 1)
if (test(a, n)):
return False
return True
#bigNumbers = []
#for x in range(1000):
# bigNumbers.append(random.getrandbits(random.randint(4200, 4600)))
#
#x = 0
#for number in bigNumbers:
# x += 1
# isPrime = MillerRabin(number)
# print x
# if isPrime:
# print number, "is prime"
def extended_gcd(a, b):
x, last_x = 0, 1
y, last_y = 1, 0
while b:
quotient = a // b
a, b = b, a % b
x, last_x = last_x - quotient*x, x
y, last_y = last_y - quotient*y, y
return (last_x, last_y, a)
def inverse_mod(a, m):
x, q, gcd = extended_gcd(a, m)
if gcd == 1:
# x is the inverse, but we want to be sure a positive number is returned.
return (x + m) % m
else:
# if gcd != 1 then a and m are not coprime and the inverse does not exist.
return None
def findMultiplicativeInverse(p, q, e):
phi = (p - 1) * (q - 1)
while gcd(phi, e) > 1:
e += 2
d = inverse_mod(e, phi)
return d
def modPow(a, b, n):
z = 1
while b != 0:
while (b % 2) == 0:
b = b / 2
a = (a * a) % n
b = b - 1
z = (z * a) % n
return z
# private boolean isQualityPrime(BigInteger p) {
# BigInteger two = new BigInteger("2");
#
# int x = p.bitLength();
# BigInteger b = two.pow(x);
# while (b.compareTo(p) < 0) {
# x++;
# b = two.pow(x);
# }
# BigInteger s = two.pow(--x);
#
# boolean isEasyBiggerPrime = (b.subtract(BigInteger.ONE)).equals(p) ? true : false;
# boolean isEasySmallerPrime = (s.add(BigInteger.ONE).equals(p)) ? true : false;
#
# if (isEasyBiggerPrime || isEasySmallerPrime) {
# System.out.println("WARNING: Prime is near to 2^x!");
# System.out.println("p = " + p);
# }
#
# return !(isEasyBiggerPrime || isEasySmallerPrime);
# }
import math
def isQualityPrime(p):
bitLength = math.floor(math.log(p, 2))
powNum = 2**bitLength
while powNum < p:
bitLength += 1
powNum = 2**bitLength
isQualityPrime(3000)
def strToNumber(text):
x = 1
number = 0
for zeichen in text:
number += ord(zeichen)*x
x *= 256
return number
def numberToStr(number):
text = []
while number > 0:
zeichen = chr(number % 256)
number = number / 256
text.append(zeichen)
return "".join(text)
Original aus dem Buch ansehen rsa/rsaTest.py · Python 2
'''
Created on 09.12.2009
@author: GWV7FD3
'''
import rsa
q = 864006630282221617546607165505132768833442927403087253437971070579738052430368997434168187774249241951614756886898778257056066247808371060434399495780120519992351353974141025134838110883240469955211816819125032764542995315946722704756735945014229730497747034468419619537240011450094095310929293646545904289036986543253037388962051682151266850800780321722541
p = 203760457035785707274440765779841571954960905020216438093288697556051306412361010381948211478739481724728871392770550387381532382660420658266741318271699868487291176814463322229620222245833529237754749958472949097604722412462711929816226562448624147196460229785274621791738101798102616023157685119198323119793328819060209462062125952867409315980899161557
n = p * q
phi = (p - 1) * (q - 1)
e = 1210475170735953725411
d = rsa.findMultiplicativeInverse(p, q, e)
print d
#plain = 123456789
#cipher = rsa.modPow(plain, e, n)
#
#print cipher
#
#back = rsa.modPow(cipher, d, n)
#
#print back
text = "Dieser Text soll in eine Zahl umgewandelt werden"
number = rsa.strToNumber(text)
print number
print rsa.numberToStr(number)
cipher = rsa.modPow(number, e, n)
cipher = rsa.numberToStr(cipher)
print cipher
cipher = rsa.strToNumber(cipher)
cipher = rsa.modPow(cipher, d, n)
plain = rsa.numberToStr(cipher)
print plain