summaryrefslogtreecommitdiff
path: root/rsa/pkcs1.py
diff options
context:
space:
mode:
Diffstat (limited to 'rsa/pkcs1.py')
-rw-r--r--rsa/pkcs1.py146
1 files changed, 146 insertions, 0 deletions
diff --git a/rsa/pkcs1.py b/rsa/pkcs1.py
new file mode 100644
index 0000000..58aa312
--- /dev/null
+++ b/rsa/pkcs1.py
@@ -0,0 +1,146 @@
+'''Functions for PKCS1 version 1.5
+
+This module implements certain functionality from PKCS1 version 1.5. For a
+very clear example, read http://www.di-mgt.com.au/rsa_alg.html#pkcs1schemes
+
+'''
+
+import os
+
+from rsa import common, transform, core
+
+def _pad_for_encryption(message, target_length):
+ r'''Pads the message for encryption, returning the padded message.
+
+ @return: 00 02 RANDOM_DATA 00 MESSAGE
+
+ >>> block = _pad_for_encryption('hello', 16)
+ >>> len(block)
+ 16
+ >>> block[0:2]
+ '\x00\x02'
+ >>> block[-6:]
+ '\x00hello'
+
+ '''
+
+ max_msglength = target_length - 11
+ msglength = len(message)
+
+ if msglength > max_msglength:
+ raise OverflowError('%i bytes needed for message, but there is only'
+ ' space for %i' % (msglength, max_msglength))
+
+ # Get random padding
+ padding = ''
+ padding_length = target_length - msglength - 3
+
+ # We remove 0-bytes, so we'll end up with less padding than we've asked for,
+ # so keep adding data until we're at the correct length.
+ while len(padding) < padding_length:
+ needed_bytes = padding_length - len(padding)
+
+ # Always read at least 8 bytes more than we need, and trim off the rest
+ # after removing the 0-bytes. This increases the chance of getting
+ # enough bytes, especially when needed_bytes is small
+ new_padding = os.urandom(needed_bytes + 5)
+ new_padding = new_padding.replace('\x00', '')
+ padding = padding + new_padding[:needed_bytes]
+
+ assert len(padding) == padding_length
+
+ return ''.join(['\x00\x02',
+ padding,
+ '\x00',
+ message])
+
+
+def encrypt(message, pub_key):
+ '''Encrypts the given message using PKCS1 v1.5
+
+ @param message: the message to encrypt. Must be a byte string no longer than
+ ``k-11`` bytes, where ``k`` is the number of bytes needed to encode
+ the ``n`` component of the public key.
+ @param pub_key: the public key to encrypt with.
+
+ @raise OverflowError: when the message is too large to fit in the padded
+ block.
+
+ >>> from rsa import keygen, common
+ >>> (pub_key, priv_key) = keygen.newkeys(256)
+ >>> message = 'hello'
+ >>> crypto = encrypt(message, pub_key)
+
+ The crypto text should be just as long as the public key 'n' component:
+ >>> len(crypto) == common.byte_size(pub_key['n'])
+ True
+
+ '''
+
+ keylength = common.byte_size(pub_key['n'])
+ padded = _pad_for_encryption(message, keylength)
+
+ payload = transform.bytes2int(padded)
+ encrypted = core.encrypt_int(payload, pub_key['e'], pub_key['n'])
+ block = transform.int2bytes(encrypted, keylength)
+
+ return block
+
+def decrypt(crypto, priv_key):
+ r'''Decrypts the given message using PKCS1 v1.5
+
+ The decryption is considered 'failed' when the resulting cleartext doesn't
+ start with the bytes 00 02, or when the 00 byte between the padding and
+ the message cannot be found.
+
+ @param crypto: the crypto text as returned by ``encrypt(message, pub_key)``
+ @param priv_key: the private key to decrypt with.
+
+ @raise ValueError: when the decryption fails. No details are given as to why
+ the code thinks the decryption fails, as this would leak information
+ about the private key.
+
+ >>> from rsa import keygen, common
+ >>> (pub_key, priv_key) = keygen.newkeys(256)
+
+ It works with strings:
+ >>> decrypt(encrypt('hello', pub_key), priv_key)
+ 'hello'
+
+ And with binary data:
+ >>> decrypt(encrypt('\x00\x00\x00\x00\x01', pub_key), priv_key)
+ '\x00\x00\x00\x00\x01'
+
+ '''
+
+ blocksize = common.byte_size(priv_key['n'])
+ encrypted = transform.bytes2int(crypto)
+ decrypted = core.decrypt_int(encrypted, priv_key['d'], priv_key['n'])
+ cleartext = transform.int2bytes(decrypted, blocksize)
+
+ # If we can't find the cleartext marker, decryption failed.
+ if cleartext[0:2] != '\x00\x02':
+ raise ValueError('Decryption failed')
+
+ # Find the 00 separator between the padding and the message
+ try:
+ sep_idx = cleartext.index('\x00', 2)
+ except ValueError:
+ raise ValueError('Decryption failed')
+
+ return cleartext[sep_idx+1:]
+
+
+if __name__ == '__main__':
+ print 'Running doctests 1000x or until failure'
+ import doctest
+
+ for count in range(1000):
+ (failures, tests) = doctest.testmod()
+ if failures:
+ break
+
+ if count and count % 100 == 0:
+ print '%i times' % count
+
+ print 'Doctests done'