1

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.

4

2 回答 2

2

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]
于 2020-04-15T15:40:51.337 回答
0

Unfortunately, in public key cryptography software there are many different formats for the same basic thing. Most packages try to support the most popular ones. One of the more ubiquitous formats for public keys is the SubjectPublicKeyInfo or SPKI format. As defined in RFC 5280, the format is a the DER encoding of the SubjectPublicKeyInfo Asn.1 structure, a binary encoding which is inconvenient for some applications. By employing base64 encoding and wrapping with header and footer lines we arrive at the so-called "PEM" encoding of public keys. It is this format the Pycryptodome produces. It can be relatively easily handled in Java by using the Bouncycastle (BC) provider and pkix libraries. It is only a little more difficult to handle it with just the standard Java SE classes.

Here are a few code snippets showing how to parse PEM SPKI data to produces public key objects in Java.

Using Java SE, BC prov, and BC pkix:

File pemPubFile = new File("/tmp/public.pem");
PEMParser pemParser = new PEMParser(new FileReader(pemPubFile));
SubjectPublicKeyInfo spki = (SubjectPublicKeyInfo) pemParser.readObject();
PublicKey publicKey = new JcaPEMKeyConverter().getPublicKey(spki);
System.out.println(publicKey);

Using only Java SE:

List < String > pemLines = Files.readAllLines(pemPubFile.toPath());
String b64Data = pemLines.stream().reduce("", new BinaryOperator < String > () {@Override
    public String apply(String accum, String element) {
        return accum.concat(element.trim());
    }
});

// Delete the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY-----
b64Data = b64Data.replace("-----BEGIN PUBLIC KEY-----", "");
b64Data = b64Data.replace("-----END PUBLIC KEY-----", "");
byte[] der = Base64.getDecoder().decode(b64Data);
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(der));
System.out.println(publicKey);
于 2020-04-10T17:49:41.023 回答