Search code examples
pythoncryptographyrsapublic-keysailpoint

Encrypt a password using a public key + RSA in Python. What am I doing wrong?


I'm accessing an API that returns the following public key:

"publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArZj/8FWa9e2PmHIBzdMwA/Wo5HYyOHBOxORU5bVBOsb8ZJekhgNWplZxskpuMx1GC9m0WTvCHK+lLmlxKyOomu85q7MxocM8n7iF8Cc0Qrgjushut35FM1bT36em46eCCuO4WqG9/GhCsUeLTsQFBTUxF2Zk6++EJcmBgwU1yNvFZNUScfTmNSMpOcnWlGgt0GpOCdsx8GECOgZhwkJFDnUa01k4BeHYDJEufgNkq4lXh8wxep03S6RyZIAye9zDTaGhGvA5+loQq8bBWCbBzNTJWNhn1kpsnPQJHFcugLMYUyglzxk6phy1Et/s1ANH8H8jdRojhoJEVjg7+Y0JwwIDAQAB"

I need to use this public key to encrypt a password and send it to another endpoint in Base64 format.

What I need to do is exactly what this website does: https://www.devglan.com/online-tools/rsa-encryption-decryption

I've followed several tutorials, asked gpt chat for help and tested it in several ways but I can't.

When I encrypt the password using the website above and call the API directly through Postman it works, but when I encrypt it via Python it doesn't work. It says the encrypted password was not recognized.

My code:

@staticmethod
def get_pem_format(publickey: str):
    lenkey = int(len(publickey) / 64)

    key = ""
    for x in range(0, lenkey+1):
        startpos = x*64
        endpos = startpos + 64
        keylen = str(publickey[startpos:endpos])
        key += f"{keylen}\n" if len(keylen) == 64 else keylen

    key = f"-----BEGIN PUBLIC KEY-----\n{key}\n-----END PUBLIC KEY-----"

    return key

def get_rsa_credentials(self, sourceid, newpass):
    userkeyinfo = self._query_password_info(sourceid)

    # Save public key
    with open('/tmp/public_key.pem', 'w') as f:
        f.write(self.get_pem_format(userkeyinfo["publicKey"]))

    # Get public key
    with open('/tmp/public_key.pem', 'rb') as f:
        publickey = RSA.importKey(f.read())

    cipher = PKCS1_OAEP.new(publickey)
    encryptedpass = cipher.encrypt(newpass.encode())
    base64encryptedpass = base64.b64encode(encryptedpass).decode()

    return base64encryptedpass, userkeyinfo["publicKeyId"]
    
newpass = "Oliveir4souz@"
sourceid = "2c9180878168627f018192ff06f66ccb"
ecryptedpass, publickeyid = self.get_rsa_credentials(sourceid, newpass)

The _query_password_info method is where I call the api and get the public key.

And I created this method get_pem_format that generates the file in pem format, because all the libraries I found only carry the key of a file.

The code above does not generate any errors. But the encrypted value is not valid. But when I use the website as in the image below, it works perfectly in the API call.

enter image description here

I cannot understand what I am doing wrong.


Solution

  • The problem is caused by different paddings: With RSA (just like with RSA/ECB/PKCS1Padding) in the Cipher Type field, the website applies PKCS#1 v1.5 padding. Since the ciphertext generated with the website can be successfully decrypted by the endpoint, the endpoint obviously uses PKCS#1 v1.5 padding as well.

    However, in the Python code, cipher = PKCS1_OAEP.new(publickey) specifies OAEP as padding, which is why the Python code on the one hand and the website (or the endpoint) on the other hand are incompatible. To fix the problem, cipher = PKCS1_v1_5.new(publickey) must be used in the Python code so that PKCS#1 v1.5 padding is applied there as well. With this change, the ciphertext generated with the Python code can be successfully decrypted using the website with RSA in the Cipher Type field (and should also be successfully decrypted by the endpoint).

    For completeness: PKCS1_OAEP() applies OAEP as padding using the default SHA-1 for content digest and MGF1 digest, which is equivalent to the RSA/ECB/OAEPWithSHA-1AndMGF1Padding option of the website.


    As side note: PyCryptdome supports import and export of keys in different formats and encodings. This way you can convert your Base64 encoded ASN.1/DER encoded key (in X.509/SPKI format) into a PEM encoded key as follows:

    key = RSA.import_key(base64.b64decode(publickey)).exportKey(format='PEM').decode('utf8') 
    

    where publickey is: MIIBIj....


    Regarding the posted screenshot on encryption with the website: Keep in mind that RSA encryptions (both with OAEP and PKCS#1 v1.5) are non-deterministic, i.e. encryptions with the same key and plaintext generate different ciphertexts. Therefore, it is not an indication of an error if tests with identical key and plaintext result in different ciphertexts.