summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSybren St?vel <sybren@stuvel.eu>2008-04-23 13:22:44 +0200
committerSybren St?vel <sybren@stuvel.eu>2008-04-23 13:22:44 +0200
commit8ad45cc66d1c4b01e20ef73d13cd4fdaec5d5cc6 (patch)
treefe2c987747df861bdf4b62ae1aa47271a9fed267
downloadrsa-8ad45cc66d1c4b01e20ef73d13cd4fdaec5d5cc6.tar.gz
Initial checkin of version 1.1
-rw-r--r--.hgignore5
-rw-r--r--rsa-test.py38
-rw-r--r--rsa/__init__.py448
-rwxr-xr-xrunme.py122
-rwxr-xr-xsetup.py26
5 files changed, 639 insertions, 0 deletions
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000..42b6988
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,5 @@
+\.pyc$
+\..*\.swp$
+^build/
+^dist/
+^rsa.egg-info/
diff --git a/rsa-test.py b/rsa-test.py
new file mode 100644
index 0000000..319c138
--- /dev/null
+++ b/rsa-test.py
@@ -0,0 +1,38 @@
+import rsa
+
+(pub, priv) = rsa.gen_pubpriv_keys(64)
+
+print "Testing integer operations:"
+
+message = 42
+print "\tMessage: %d" % message
+
+encrypted = rsa.encrypt_int(message, pub['e'], pub['n'])
+print "\tEncrypted: %d" % encrypted
+
+decrypted = rsa.decrypt_int(encrypted, priv['d'], pub['n'])
+print "\tDecrypted: %d" % decrypted
+
+signed = rsa.sign_int(message,priv['d'], pub['n'])
+print "\tSigned: %d" % signed
+
+verified = rsa.verify_int(signed, pub['e'],pub['n'])
+print "\tVerified: %d" % verified
+
+
+print "Testing string operations:"
+
+message = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+print "\tMessage: %s" % message
+
+encrypted = rsa.encrypt(message, pub)
+print "\tEncrypted: %s" % encrypted
+
+decrypted = rsa.decrypt(encrypted, priv)
+print "\tDecrypted: %s" % decrypted
+
+signed = rsa.sign(message,priv)
+print "\tSigned: %s" % signed
+
+verified = rsa.verify(signed, pub)
+print "\tVerified: %s" % verified
diff --git a/rsa/__init__.py b/rsa/__init__.py
new file mode 100644
index 0000000..996a7e2
--- /dev/null
+++ b/rsa/__init__.py
@@ -0,0 +1,448 @@
+"""RSA module
+
+Module for calculating large primes, and RSA encryption, decryption,
+signing and verification. Includes generating public and private keys.
+"""
+
+__author__ = "Sybren Stuvel, Marloes de Boer and Ivo Tamboer"
+__date__ = "2008-04-23"
+
+# NOTE: Python's modulo can return negative numbers. We compensate for
+# this behaviour using the abs() function
+
+import math
+import sys
+import random # For picking semi-random numbers
+import types
+from cPickle import dumps, loads
+import base64
+import zlib
+
+RANDOM_DEV="/dev/urandom"
+has_broken_randint = False
+
+try:
+ random.randint(1, 10000000000000000000000000L)
+except:
+ has_broken_randint = True
+ print "This system's random.randint() can't handle large numbers"
+ print "Random integers will all be read from %s" % RANDOM_DEV
+
+
+def log(x, base = 10):
+ return math.log(x) / math.log(base)
+
+def gcd(p, q):
+ """Returns the greatest common divisor of p and q
+
+
+ >>> gcd(42, 6)
+ 6
+ """
+ if p<q: return gcd(q, p)
+ if q == 0: return p
+ return gcd(q, abs(p%q))
+
+def bytes2int(bytes):
+ """Converts a list of bytes or a string to an integer
+
+ >>> (128*256 + 64)*256 + + 15
+ 8405007
+ >>> l = [128, 64, 15]
+ >>> bytes2int(l)
+ 8405007
+ """
+
+ if not (type(bytes) is types.ListType or type(bytes) is types.StringType):
+ raise TypeError("You must pass a string or a list")
+
+ # Convert byte stream to integer
+ integer = 0
+ for byte in bytes:
+ integer *= 256
+ if type(byte) is types.StringType: byte = ord(byte)
+ integer += byte
+
+ return integer
+
+def int2bytes(number):
+ """Converts a number to a string of bytes
+
+ >>> bytes2int(int2bytes(123456789))
+ 123456789
+ """
+
+ if not (type(number) is types.LongType or type(number) is types.IntType):
+ raise TypeError("You must pass a long or an int")
+
+ string = ""
+
+ while number > 0:
+ string = "%s%s" % (chr(number & 0xFF), string)
+ number /= 256
+
+ return string
+
+def fast_exponentiation(a, p, n):
+ """Calculates r = a^p mod n
+ """
+ result = a % n
+ remainders = []
+ while p != 1:
+ remainders.append(p & 1)
+ p = p >> 1
+ while remainders:
+ rem = remainders.pop()
+ result = ((a ** rem) * result ** 2) % n
+ return result
+
+def read_random_int(nbits):
+ """Reads a random integer from RANDOM_DEV of approximately nbits
+ bits rounded up to whole bytes"""
+
+ nbytes = ceil(nbits/8)
+
+ # Read 'nbits' bits of random data
+ fd = open(RANDOM_DEV)
+ randomdata = fd.read(nbytes)
+ fd.close()
+
+ if len(randomdata) != nbytes:
+ raise Exception("Unable to read enough random bytes")
+
+ return bytes2int(randomdata)
+
+def ceil(x):
+ """Returns int(math.ceil(x))"""
+
+ return int(math.ceil(x))
+
+def randint(minvalue, maxvalue):
+ """Returns a random integer x with minvalue <= x <= maxvalue"""
+
+ if not has_broken_randint:
+ return random.randint(minvalue, maxvalue)
+
+ # Safety - get a lot of random data even if the range is fairly
+ # small
+ min_nbits = 32
+
+ # The range of the random numbers we need to generate
+ range = maxvalue - minvalue
+
+ # Which is this number of bytes
+ rangebytes = ceil(log(range, 2) / 8)
+
+ # Convert to bits, but make sure it's always at least min_nbits*2
+ rangebits = max(rangebytes * 8, min_nbits * 2)
+
+ # Take a random number of bits between min_nbits and rangebits
+ nbits = random.randint(min_nbits, rangebits)
+
+ return (read_random_int(nbits) % range) + minvalue
+
+def fermat_little_theorem(p):
+ """Returns 1 if p may be prime, and something else if p definitely
+ is not prime"""
+
+ a = randint(1, p-1)
+ return fast_exponentiation(a, p-1, p)
+
+def jacobi(a, b):
+ """Calculates the value of the Jacobi symbol (a/b)
+ """
+
+ if a % b == 0:
+ return 0
+ result = 1
+ while a > 1:
+ if a & 1:
+ if ((a-1)*(b-1) >> 2) & 1:
+ result = -result
+ b, a = a, b % a
+ else:
+ if ((b ** 2 - 1) >> 3) & 1:
+ result = -result
+ a = a >> 1
+ return result
+
+def jacobi_witness(x, n):
+ """Returns False if n is an Euler pseudo-prime with base x, and
+ True otherwise.
+ """
+
+ j = jacobi(x, n) % n
+ f = fast_exponentiation(x, (n-1)/2, n)
+
+ if j == f: return False
+ return True
+
+def randomized_primality_testing(n, k):
+ """Calculates whether n is composite (which is always correct) or
+ prime (which is incorrect with error probability 2**-k)
+
+ Returns False if the number if composite, and True if it's
+ probably prime.
+ """
+
+ q = 0.5 # Property of the jacobi_witness function
+
+ # t = int(math.ceil(k / log(1/q, 2)))
+ t = ceil(k / log(1/q, 2))
+ for i in range(t+1):
+ x = randint(1, n-1)
+ if jacobi_witness(x, n): return False
+
+ return True
+
+def is_prime(number):
+ """Returns True if the number is prime, and False otherwise.
+
+ >>> is_prime(42)
+ 0
+ >>> is_prime(41)
+ 1
+ """
+
+ """
+ if not fermat_little_theorem(number) == 1:
+ # Not prime, according to Fermat's little theorem
+ return False
+ """
+
+ if randomized_primality_testing(number, 5):
+ # Prime, according to Jacobi
+ return True
+
+ # Not prime
+ return False
+
+
+def getprime(nbits):
+ """Returns a prime number of max. 'math.ceil(nbits/8)*8' bits. In
+ other words: nbits is rounded up to whole bytes.
+
+ >>> p = getprime(8)
+ >>> is_prime(p-1)
+ 0
+ >>> is_prime(p)
+ 1
+ >>> is_prime(p+1)
+ 0
+ """
+
+ nbytes = int(math.ceil(nbits/8))
+
+ while True:
+ integer = read_random_int(nbits)
+
+ # Make sure it's odd
+ integer |= 1
+
+ # Test for primeness
+ if is_prime(integer): break
+
+ # Retry if not prime
+
+ return integer
+
+def are_relatively_prime(a, b):
+ """Returns True if a and b are relatively prime, and False if they
+ are not.
+
+ >>> are_relatively_prime(2, 3)
+ 1
+ >>> are_relatively_prime(2, 4)
+ 0
+ """
+
+ d = gcd(a, b)
+ return (d == 1)
+
+def find_p_q(nbits):
+ """Returns a tuple of two different primes of nbits bits"""
+
+ p = getprime(nbits)
+ while True:
+ q = getprime(nbits)
+ if not q == p: break
+
+ return (p, q)
+
+def extended_euclid_gcd(a, b):
+ """Returns a tuple (d, i, j) such that d = gcd(a, b) = ia + jb
+ """
+
+ if b == 0:
+ return (a, 1, 0)
+
+ q = abs(a % b)
+ r = long(a / b)
+ (d, k, l) = extended_euclid_gcd(b, q)
+
+ return (d, l, k - l*r)
+
+# Main function: calculate encryption and decryption keys
+def calculate_keys(p, q, nbits):
+ """Calculates an encryption and a decryption key for p and q, and
+ returns them as a tuple (e, d)"""
+
+ n = p * q
+ phi_n = (p-1) * (q-1)
+
+ while True:
+ # Make sure e has enough bits so we ensure "wrapping" through
+ # modulo n
+ e = getprime(max(8, nbits/2))
+ if are_relatively_prime(e, n) and are_relatively_prime(e, phi_n): break
+
+ (d, i, j) = extended_euclid_gcd(e, phi_n)
+
+ if not d == 1:
+ raise Exception("e (%d) and phi_n (%d) are not relatively prime" % (e, phi_n))
+
+ if not (e * i) % phi_n == 1:
+ raise Exception("e (%d) and i (%d) are not mult. inv. modulo phi_n (%d)" % (e, i, phi_n))
+
+ return (e, i)
+
+
+def gen_keys(nbits):
+ """Generate RSA keys of nbits bits. Returns (p, q, e, d).
+ """
+
+ while True:
+ (p, q) = find_p_q(nbits)
+ (e, d) = calculate_keys(p, q, nbits)
+
+ # For some reason, d is sometimes negative. We don't know how
+ # to fix it (yet), so we keep trying until everything is shiny
+ if d > 0: break
+
+ return (p, q, e, d)
+
+def gen_pubpriv_keys(nbits):
+ """Generates public and private keys, and returns them as (pub,
+ priv).
+
+ The public key consists of a dict {e: ..., , n: ....). The private
+ key consists of a dict {d: ...., p: ...., q: ....).
+ """
+
+ (p, q, e, d) = gen_keys(nbits)
+
+ return ( {'e': e, 'n': p*q}, {'d': d, 'p': p, 'q': q} )
+
+def encrypt_int(message, ekey, n):
+ """Encrypts a message using encryption key 'ekey', working modulo
+ n"""
+
+ if type(message) is types.IntType:
+ return encrypt_int(long(message), ekey, n)
+
+ if not type(message) is types.LongType:
+ raise TypeError("You must pass a long or an int")
+
+ if math.floor(log(message, 2)) > math.floor(log(n, 2)):
+ raise OverflowError("The message is too long")
+
+ return fast_exponentiation(message, ekey, n)
+
+def decrypt_int(cyphertext, dkey, n):
+ """Decrypts a cypher text using the decryption key 'dkey', working
+ modulo n"""
+
+ return encrypt_int(cyphertext, dkey, n)
+
+def sign_int(message, dkey, n):
+ """Signs 'message' using key 'dkey', working modulo n"""
+
+ return decrypt_int(message, dkey, n)
+
+def verify_int(signed, ekey, n):
+ """verifies 'signed' using key 'ekey', working modulo n"""
+
+ return encrypt_int(signed, ekey, n)
+
+def picklechops(chops):
+ """Pickles and base64encodes it's argument chops"""
+
+ value = zlib.compress(dumps(chops))
+ encoded = base64.encodestring(value)
+ return encoded.strip()
+
+def unpicklechops(string):
+ """base64decodes and unpickes it's argument string into chops"""
+
+ return loads(zlib.decompress(base64.decodestring(string)))
+
+def chopstring(message, key, n, funcref):
+ """Splits 'message' into chops that are at most as long as n,
+ converts these into integers, and calls funcref(integer, key, n)
+ for each chop.
+
+ Used by 'encrypt' and 'sign'.
+ """
+
+ msglen = len(message)
+ mbits = msglen * 8
+ nbits = int(math.floor(log(n, 2)))
+ nbytes = nbits / 8
+ blocks = msglen / nbytes
+
+ if msglen % nbytes > 0:
+ blocks += 1
+
+ cypher = []
+
+ for bindex in range(blocks):
+ offset = bindex * nbytes
+ block = message[offset:offset+nbytes]
+ value = bytes2int(block)
+ cypher.append(funcref(value, key, n))
+
+ return picklechops(cypher)
+
+def gluechops(chops, key, n, funcref):
+ """Glues chops back together into a string. calls
+ funcref(integer, key, n) for each chop.
+
+ Used by 'decrypt' and 'verify'.
+ """
+ message = ""
+
+ chops = unpicklechops(chops)
+
+ for cpart in chops:
+ mpart = funcref(cpart, key, n)
+ message += int2bytes(mpart)
+
+ return message
+
+def encrypt(message, key):
+ """Encrypts a string 'message' with the public key 'key'"""
+
+ return chopstring(message, key['e'], key['n'], encrypt_int)
+
+def sign(message, key):
+ """Signs a string 'message' with the private key 'key'"""
+
+ return chopstring(message, key['d'], key['p']*key['q'], decrypt_int)
+
+def decrypt(cypher, key):
+ """Decrypts a cypher with the private key 'key'"""
+
+ return gluechops(cypher, key['d'], key['p']*key['q'], decrypt_int)
+
+def verify(cypher, key):
+ """Verifies a cypher with the public key 'key'"""
+
+ return gluechops(cypher, key['e'], key['n'], encrypt_int)
+
+# Do doctest if we're not imported
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
+
+__all__ = ["gen_pubpriv_keys", "encrypt", "decrypt", "sign", "verify"]
+
diff --git a/runme.py b/runme.py
new file mode 100755
index 0000000..9218666
--- /dev/null
+++ b/runme.py
@@ -0,0 +1,122 @@
+#!/usr/bin/python
+
+import sys
+import rsa
+
+pub = {'e': 248780186015280096941266517855109524861L, 'n': 289829846799217715870297548823528254304373554012264300246542488949110436220426067982155063515456468780177806574004719751854439098845391023946149242816599L}
+
+print "Running rsa.verify(verslag, pub)..."
+
+verslag = """
+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
+"""
+
+verslag = rsa.verify(verslag, pub)
+
+print "Decryption done, press enter to read"
+sys.stdin.readline()
+print verslag
+
+print "Generating public & private keypair for demonstrational purposes..."
+(pub, priv) = rsa.gen_pubpriv_keys(256)
+
+print
+print "Public:"
+print "\te: %d" % pub['e']
+print "\tn: %d" % pub['n']
+print
+
+print "Private:"
+print "\td: %d" % priv['d']
+print "\tp: %d" % priv['p']
+print "\tq: %d" % priv['q']
+
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..f704dd1
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from setuptools import setup
+
+setup(name='rsa',
+ version='1.1',
+ description='Pure-Python RSA implementation',
+ author=u'Sybren Stüvel'.encode('utf-8'),
+ author_email='sybren@stuvel.eu',
+ maintainer=u'Sybren Stüvel'.encode('utf-8'),
+ maintainer_email='sybren@stuvel.eu',
+ url='http://www.stuvel.eu/rsa',
+ packages=['rsa'],
+ license='GPL',
+ classifiers=[
+ 'Development Status :: 5 - Beta',
+ 'Intended Audience :: End Users/Desktop',
+ 'License :: OSI Approved :: GNU General Public License (GPL)',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Multimedia :: Graphics :: Capture :: Digital Camera',
+ 'Topic :: Artistic Software',
+ 'Natural Language :: English',
+ ],
+)