Search code examples
bouncycastlepycryptoopenpgppython-cryptography

Need public/private RSA keys for encrypting in Java and decrypting in Python


We have one system written in Java that will write encrypted files that need to be decrypted by a Python system. I am trying to figure out what kind of keys I need that can be used by both Java and Python API's, and how to generate them. The plan is to use the public key in Java to encrypt the file, and the private key in Python to decrypt it.

I tried generating RSA keys with gpg --generate-key and in an armor file get a file that looks like:

-----BEGIN PGP PRIVATE KEY BLOCK-----
... encoded key ...
-----END PGP PRIVATE KEY BLOCK-----

and create a public key from that which looks like:

-----BEGIN PGP PUBLIC KEY BLOCK-----
... encoded key ...
-----END PGP PUBLIC KEY BLOCK-----

I can parse the public key file with Bouncy Castle in Java with PGPUtil.getDecoderStream(), getting a PGPPublicKeyRingCollection and a PGPPublicKey which can be converted to a java.security.PublicKey.

On the Python side I have tried using both the cryptography.hazmat and PyCrypto api's but can't figure out how to import the private key file. When I try

from Crypto.PublicKey import RSA

RSA.importKey(open('/path/to/private/key/file').read())

I get RSA key format is not supported.

I have been reading up on the different types of keys and algorithms but I thought that an ASCII file holding a key like this should work but there is obviously something I'm missing.

I also tried going the other way and generating a new key using PyCrypto with something like:

from Crypto.PublicKey import RSA

key = RSA.generate(2048)
f = open('/tmp/private.pem','wb')
f.write(key.exportKey('PEM'))
f.close()

f = open('/tmp/public.pem','wb')
f.write(key.publickey().exportKey('PEM'))
f.close

And then reading it via Bouncy Castle's API like this:

PemReader reader = new PemReader(new FileReader("/tmp/public.pem"));
Object publicKey = RSAPublicKey.getInstance(reader.readPemObject().getContent());

But that gives me:

java.lang.IllegalArgumentException: illegal object in getInstance: org.bouncycastle.asn1.DLSequence

    at org.bouncycastle.asn1.ASN1Integer.getInstance(Unknown Source)
    at org.bouncycastle.asn1.pkcs.RSAPublicKey.<init>(Unknown Source)

Bouncy Castle provides two RSAPublicKey classes, I tried them both but got the same result.

It doesn't seem like it should be this hard so I am trying to figure out what I'm missing. Thanks for any help.


Solution

  • I ended up figuring this out, wanted to document this for anyone running into the same issue.

    To start with as the President mentioned, PGP keys are not universally supported in programmatic crypto API's and so probably aren't a great choice. The most widely used seem to be RSA keys such as those written by OpenSSL, this article gives a good explanation.

    From there once you have your keys you need to figure out which API's to use in Java and Python. As noted above it is possible to simply load a key with plain Java API's. On the Python side there is cryptography which seems to be relatively low level, PyCrypto which is higher level but stale since 2014, and PyCryptodome which is a fork of PyCrypto that is more up to date. For my solution I chose PyCryptodome.

    Then it is important to realize that the algorithm i.e. RSA is just one of many factors for the encryption, there is also the hash algorithm, padding, etc. Here is an excerpt from the java doc on com.sun.crypto.provider.RSACipher:

    /**
     * RSA cipher implementation. Supports RSA en/decryption and signing/verifying
     * using both PKCS#1 v1.5 and OAEP (v2.2) paddings and without padding (raw RSA).
     * Note that raw RSA is supported mostly for completeness and should only be
     * used in rare cases.
     *
     * Objects should be instantiated by calling Cipher.getInstance() using the
     * following algorithm names:
     *  . "RSA/ECB/PKCS1Padding" (or "RSA") for PKCS#1 v1.5 padding.
     *  . "RSA/ECB/OAEPwith<hash>andMGF1Padding" (or "RSA/ECB/OAEPPadding") for
     *    PKCS#1 v2.2 padding.
     *  . "RSA/ECB/NoPadding" for rsa RSA.
     * ...
    

    In my case, the Java toolkit I was using was creating the cipher with Cipher.getInstance("RSA") (YMMV), and based on that and the comments above I knew which Python module I would need, in my case the PKCS1_v1_5 module in PyCryptodome.

    This led to this Python solution, which I've paraphrased to omit some details specific to my case but should give you enough to develop your own solution.

    import base64
    from Crypto.PublicKey import RSA
    from Crypto.Cipher import AES, PKCS1_v1_5
    
    # The public key is not needed for this POC but this demonstrates how to load it
    pub_key = RSA.importKey(open('openssl-public.pem').read())
    priv_key = RSA.importKey(open('openssl-private.pem').read())
    
    # The public key extracted from the private key should match the imported public key,
    # could implement that as a double check
    # priv_key.publickey().export_key()
    
    # Need to use the PKCS1_v1_5 module to match "PKCS#1 v1.5" in the Java RSA class
    cipher_rsa = PKCS1_v1_5.new(priv_key)
    meta = # get the content key x-amz-key, IV x-amz-iv and the unencrypted content length x-amz-unencrypted-content-length
    
    # Base64 decode the iv and key
    iv = base64.b64decode(meta['x-amz-iv'])
    key = base64.b64decode(meta['x-amz-key'])
    
    # Decrypt the key
    decrypted_key = cipher_rsa.decrypt(key, 'An error has occurred')
    
    # Create an AES cipher using the content key and IV.  This must match
    # how the data was encoded
    cipher_aes = AES.new(decrypted_key, AES.MODE_CBC, iv)
    
    encryptedFile = # get the encrypted file
    # Need to read the encrypted file as binary 'rb'
    # The decrypted file may be padded
    length = meta['x-amz-unencrypted-content-length']
    decryptedContent = cipher_aes.decrypt(open(encryptedFile,mode='rb').read())[:length]