0
votes

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!

3

3 Answers

3
votes

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.

1
votes

I'm not recommending you do this, or suggesting it makes any sense, but if you just want to play with it here is what you can do.

  1. Make sure the modulus of the first RSA key you apply is less than the the modulus of the second RSA key you apply.
  2. Perform the first RSA encryption using the smaller modulus and proper PKCS#1 padding
  3. Perform the second RSA encryption using the larger modulus and no padding.

On decrypt you must reverse the order of these operations of course.

1
votes

So, what you are doing seems to make little sense. You want to securely send a message from a server to a client?

The code you have attempts to encrypt a message under the severs public key, then under the client's public key. The client won't be able to read this because he should never ever have the server's private key (Which is needed to read the message encrypted under the servers public key). Or put another way, if the server and client both have the same private key, then you should just use AES. Why are you doing this ?

Really, you probably ought to just send a message to the client using ssl/tls/https ,since writing cryptographic code is problematic and you have made at least two bad errors in your code along with the one you want fixed.

  1. Your IV needs to be securely random. The python random call is not, which is why you use os.random(16) for the key. you should do so for the IV as well

  2. You need to use an hmac to authenticate the encrypted data and key the hmac with a separate random key. Then using the same key on the other end, regenerate the hmac over the same inputs, and compare the two. If you don't do this, someone could tamper with your data and use errors in the crypto libraries to read it.

  3. The problem you posted: Note as I said above you should not be doing this at all because it's nonsensical. You need to encrypted rsaEncryptedPasswordWithServer with AES under a new key(and use HMAC as per 2 above) and then encrypt the new key with the clients public key.