Python – PyCrypto: Encrypt an string twice using RSA and PKCS#1

aesencryptionpycryptopythonrsa

Hello everyone.

I was wondering if it's possible to do a double RSA/PKCS#1 encryption with PyCrypto.

I have a server that has its own RSA key (generated with the openssl command when said server is installed) and a client which can request the public part of the server's key. Also, that client can ask the server to generate another RSA key (or key-pair) for it. In that case, the server also keeps the private (or the "whole" RSA key) and sends the client the public part of its key.

I've been playing around with RSA/PKCS and AES encription. I have created a test Python file that works fine encrypting with only one RSA key. What it does is encrypting the data with the symmetric AES system (which uses a random key generated "on-the-fly"), cyphers the password used for AES using the RSA/PKCS#1 system and puts it in the result to be sent:

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Interesting links: 
# 1> http://stackoverflow.com/a/9039039/289011
# 2> http://eli.thegreenplace.net/2010/06/25/aes-encryption-of-files-in-python-with-pycrypto/

from Crypto.PublicKey import RSA
import base64
import os
from Crypto.Cipher import AES
import Crypto.Util.number
import random
import struct
import cStringIO
from Crypto.Cipher import PKCS1_OAEP

def encrypt(string):
    #Begin RSA Part to get a cypher that uses the server's public key
    externKeyFilename="/home/borrajax/rsaKeys/server-key.pub"
    externKeyFile = open(externKeyFilename, "r")
    rsaKey= RSA.importKey(externKeyFile, passphrase="F00bAr")
    pkcs1Encryptor=PKCS1_OAEP.new(rsaKey)
    #End RSA Part

    #Begin AES Part
    iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16))
    thisMessagePassword = os.urandom(16)
    aesEncryptor = AES.new(thisMessagePassword, AES.MODE_CBC, iv)
    chunksize=64*1024
    #End AES Part

    #Begin RSA Encription of the AES Key
    rsaEncryptedPassword = pkcs1Encryptor.encrypt(thisMessagePassword)

    retvalTmp = cStringIO.StringIO()
    retvalTmp.write(struct.pack('<Q', len(string)))
    retvalTmp.write(struct.pack('<Q', len(rsaEncryptedPassword)))
    retvalTmp.write(rsaEncryptedPassword)
    retvalTmp.write(iv)
    while len(string) > 0:
        chunk = string[0:chunksize]
        string = string[chunksize:]
        if len(chunk) % 16 != 0:
            chunk += ' ' * (16 - len(chunk) % 16)
        retvalTmp.write(aesEncryptor.encrypt(chunk))
    return retvalTmp.getvalue()

def decrypt(string):
    stringAsBuffer = cStringIO.StringIO(string)
    retval = str()
    chunksize=64*1024

    externKeyFilename="/home/borrajax/rsaKeys/server-key.pem"
    externKey = open(externKeyFilename, "r")
    rsaKey = RSA.importKey(externKey, passphrase="F00bAr")
    pkcs1Decryptor=PKCS1_OAEP.new(rsaKey)


    origsize = struct.unpack('<Q', stringAsBuffer.read(struct.calcsize('Q')))[0]
    rsaEncryptedPasswordLength = long(struct.unpack('<Q', stringAsBuffer.read(struct.calcsize('Q')))[0])
    rsaEncryptedPassword = stringAsBuffer.read(rsaEncryptedPasswordLength)
    thisMessagePassword = pkcs1Decryptor.decrypt(rsaEncryptedPassword)
    iv = stringAsBuffer.read(16)
    decryptor = AES.new(thisMessagePassword, AES.MODE_CBC, iv)
    while True:
        chunk = stringAsBuffer.read(chunksize)
        if len(chunk) == 0:
            break
        retval += decryptor.decrypt(chunk)
    return retval



if __name__ == "__main__":
    encryptedThingy=encrypt(base64.b64encode("Toñóooooañjfl凯兰;kañañfjaafafs凱蘭pingüiñoo你好to金玉Toñóooooañjfl凯兰;kañañfjaafafs凱蘭pingüiñoo你好to金玉Toñóooooañjfl凯兰;kañañfjaafafs凱蘭pingüiñoo你好to金玉Toñóooooañjfl凯兰;kañañfjaafafs凱蘭pingüiñoo你好to金玉Toñóooooañjfl凯兰;kañañfjaafafs凱蘭pingüiñoo你好to金玉"))
    print "Decrypted thingy: %s" % base64.b64decode(decrypt(encryptedThingy))

As you can see, the AES password is encrypted with the server's RSA key. Now, I'd like to be extra paranoid, and encrypt that already encrypted password with the Client's public key, so the "encrypt" method would be something like:

def encrypt(string):
    #Begin RSA Part to get a cypher that uses the server's public key
    externServerKeyFilename="/home/borrajax/rsaKeys/server-key.pub"
    externServerKeyFile = open(externServerKeyFilename, "r")
    rsaServerKey= RSA.importKey(externServerKeyFile, passphrase="F00bAr")
    pkcs1ServerEncryptor=PKCS1_OAEP.new(rsaServerKey)
    #End RSA Part

    #Begin RSA Part to get a cypher that uses the client's public key
    externClientKeyFilename="/home/borrajax/rsaKeys/client-key.pub"
    externClientKeyFile = open(externClientKeyFilename, "r")
    rsaClientKey= RSA.importKey(externClientKeyFile, passphrase="F00bAr")
    pkcs1ClientEncryptor=PKCS1_OAEP.new(rsaClientKey)
    #End RSA Part


    #Begin AES Part
    iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16))
    thisMessagePassword = os.urandom(16)
    aesEncryptor = AES.new(thisMessagePassword, AES.MODE_CBC, iv)
    chunksize=64*1024
    #End AES Part

    #Begin RSA Encription of the AES Key
    rsaEncryptedPasswordWithServer = pkcs1ServerEncryptor.encrypt(thisMessagePassword)
    rsaEncryptedPasswordWithServerAndClient = pkcs1ClientEncryptor.encrypt(rsaEncryptedPasswordWithServer) #Katacrasssshh here!! 

    retvalTmp = cStringIO.StringIO()
    retvalTmp.write(struct.pack('<Q', len(string)))
    retvalTmp.write(struct.pack('<Q', len(rsaEncryptedPasswordWithServerAndClient)))
    #...Probably some yadda yadda here with key lengths and stuff so it would help re-build the keys in the server's side...
    retvalTmp.write(rsaEncryptedPasswordWithServerAndClient)
    retvalTmp.write(iv)
    while len(string) > 0:
        chunk = string[0:chunksize]
        string = string[chunksize:]
        if len(chunk) % 16 != 0:
            chunk += ' ' * (16 - len(chunk) % 16)
        retvalTmp.write(aesEncryptor.encrypt(chunk))
    return retvalTmp.getvalue()

But when I try to re-encrypt the key, I get a ValueError("Plaintext too large") exception. Which makes sense (at least makes sense to someone who barely knows anything about encryption) because PKCS adds a padding so when I encrypt the "thisMessagePassword" with the server's public key, I get a 256 bytes string, which is too long for the second PKCS encryptor (I've been doing some "manual testing" and the limit seems to be 214 bytes… I mean… that is the last value that doesn't throw an exception).

I am aware that is probably a weird construct and that it would probably make more sense use the server's public key for encryption and signing with the client's key, but I'm just trying to play a bit with encryption things and try to understand how they work and why. That's why any hint will be appreciated.

Thank you in advance!

Best Solution

The documentation of PKCS1OAEP.encrypt says the following about its input:

message (string) - The message to encrypt, also known as plaintext. It can be of variable length, but not longer than the RSA modulus (in bytes) minus 2, minus twice the hash output size.

SHA-1 (the default hash function) has a 160 bit digest, that is 20 bytes. The limitation that you see sounds about right: 256 = 214 + 2 + 2*20.

Beside that, the extra step you plan to add does not add much value. If you want the client to prove to the server it's really him, and not somebody else, you should provide the client with the private key and have the server to keep the public half. After the encryption step, the client could sign the whole package (wrapped AES key + encrypted data) with PKCS#1 PSS and send the signature along. The server will verify the origin with the client's public key, then decrypt the key using its own private key, and finally decrypt the data with AES.

Related Question