Encrypting java JWEObject with RSA in python

Question:

I asked a while ago how to do something similar: Decrypting and encrypting java JWEObject with algorithm RSA-OAEP-256 on python
Now I have a different encryption key and that code is not working for me anymore.

I need to be able to encrypt my data: {"value": "Object Encryption"} with JWE using RSA.

I have this key id: a4d4039e-a8c7-4d06-98c8-2bda90ab169c
and this encryption key:

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9JJaeFiDdB+dGvi3jUzKUMU73kG6vvc/P+jwZXRKKpJSwf8PU4SapMyFPFFoHwca6Z8vZogF4ghEJ18JipNyF3BLnfCt1EHuZ15FG1Aywvpi+xw7F0UoJ9DWItBM1SodKXIh1be44/9SiLrpcyROKId349zWMOl3IVVxekLPKWTHsy2Iowp7JsjNEK3t9RdV+PAtUzp1ACyqHD/MDYSmpJuEOR9AbmBayaFIWVP+52q1ir7ea88zocmklDg0SGjiRNXq1tUAljWezpKstKQNz/IZN1kMLQ8SknrlpZL0vjjAnHFlgtLfcwPbESt76surRshfGwwfx8T9AOfXMgELNQIDAQAB

and I should get this:

eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoiYTRkNDAzOWUtYThjNy00ZDA2LTk4YzgtMmJkYTkwYWIxNjljIn0.2hGqQVSbgZ9-9Hiz8VZizORpWRR2yioHb8vK6R9tQCpxr0jxBGehNL0K36XfJWJC5KxcxDdD9byeI_YTtB_hYTgsuMTHS5p-4aJ4nLk43Ya5yR8p8nn4s11wbkfSj0jbqSFb_1IOCMgX0Xu8lmnVe7Tjc4vACwBoaM6VpudEsLHpyQ9OxNaa1apbRp-BX3DEVM3l7ltHhMIh_DCRWbC4-LbS51L4RqLWxmihqRA97FYX4HX38Vbt3O__2tq5KfSjq78UEOffEFe_CRg8mXZ1CHgyH4YPMNmjS-jAI4m07Coja4zLXgv7ctFaFQePISLaZLgdp3a0a-Sht5cwwZfAhg.mc7_YA9mg3l7VV5B.ZOnYjkiXx1YSxDIILjcHUXluwW8jqsSO5NuIkto.9KtJGJRS9QevrqZPYYlcTQ

That’s the java code I’m trying to rewrite in python:

  private RSAPublicKey getObjectEncryptionKey()
      throws NoSuchAlgorithmException, InvalidKeySpecException {
    logger.debug("Getting object encryption key");
    if (Objects.isNull(objectEncryptionKey)) {
      objectEncryptionKey = getActiveKey(Algorithm.RSA);
    }
    byte[] encryptionKey = base64Decode(String.valueOf(objectEncryptionKey.getEncryptionKeyValue()).getBytes());

    KeyFactory keyFactory = getInstance(Algorithm.RSA.name());
    return (RSAPublicKey) keyFactory.generatePublic(new X509EncodedKeySpec(encryptionKey));
  }


  public String encryptObject(Object object) {
    logger.debug("Encrypting object with keyId: {}", getObjectEncryptionKeyId());
    JsonWebEncryption encryptedObject = getJWEObject(object);

    try {
      return encryptedObject.getCompactSerialization();
    } catch (JoseException e) {
      throw new CryptoException("Could not encrypt object/event", e);
    }
  }

  private JsonWebEncryption getJWEObject(Object object) {
    JsonWebEncryption jwe = new JsonWebEncryption();

    try {
      jwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.RSA_OAEP_256);
      jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_256_GCM);
      jwe.setKey(getObjectEncryptionKey());
      jwe.setKeyIdHeaderValue(getObjectEncryptionKeyId());
    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
      throw new CryptoException("Could not create JsonWebEncryption", e);
    }
  }

How is it different from my previous question and what is the correct way to do it in python?

I tried doing something like that:

def grouper(iterable, n, fillvalue=''):
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)


def decryption_key_to_pem(decryption_key: str) -> bytes:
    pem = ['-----BEGIN PRIVATE KEY-----']
    for group in grouper(decryption_key, 64):
        pem.append(''.join(group))
    pem.append('-----END PRIVATE KEY-----')
    return str.encode('n'.join(pem))


jwk.JWK.from_pem(decryption_key_to_pem(key))

but I get this exception:

ValueError: ('Could not deserialize key data. The data may be in an incorrect format, it may be encrypted with an unsupported algorithm, or it may be an unsupported key type (e.g. EC curves with explicit parameters).', [_OpenSSLErrorWithText(code=503841036, lib=60, reason=524556, reason_text=b'error:1E08010C:DECODER routines::unsupported'), _OpenSSLErrorWithText(code=109052072, lib=13, reason=168, reason_text=b'error:068000A8:asn1 encoding routines::wrong tag'), _OpenSSLErrorWithText(code=109576458, lib=13, reason=524554, reason_text=b'error:0688010A:asn1 encoding routines::nested asn1 error'), _OpenSSLErrorWithText(code=109576458, lib=13, reason=524554, reason_text=b'error:0688010A:asn1 encoding routines::nested asn1 error')])

Also tried something like:

def get_jwe_key(data, encryption_key, encryption_key_id):
    jwe = jwcrypto.jwe.JWE()
    jwe.plaintext = json.dumps(data).encode('utf-8')
    jwe.alg = 'RSA-OAEP-256'
    jwe.enc = 'A256GCM'
    jwe.recipient = encryption_key
    jwe.header = {'kid': encryption_key_id}
    return jwe


jwe_key = get_jwe_key(decrypted_data, key, key_id)
jwe_key.serialize()

and I get: jwcrypto.common.InvalidJWEOperation: No available ciphertext

Asked By: Ema Il

||

Answers:

In the JWCrypto documentation you can find examples for the encryption with JWCrypto. You basically only need to insert your values:

from jwcrypto import jwk, jwe
import json

spki_pem = '''-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9JJaeFiDdB+dGvi3jUzK
UMU73kG6vvc/P+jwZXRKKpJSwf8PU4SapMyFPFFoHwca6Z8vZogF4ghEJ18JipNy
F3BLnfCt1EHuZ15FG1Aywvpi+xw7F0UoJ9DWItBM1SodKXIh1be44/9SiLrpcyRO
KId349zWMOl3IVVxekLPKWTHsy2Iowp7JsjNEK3t9RdV+PAtUzp1ACyqHD/MDYSm
pJuEOR9AbmBayaFIWVP+52q1ir7ea88zocmklDg0SGjiRNXq1tUAljWezpKstKQN
z/IZN1kMLQ8SknrlpZL0vjjAnHFlgtLfcwPbESt76surRshfGwwfx8T9AOfXMgEL
NQIDAQAB
-----END PUBLIC KEY-----'''
data = {"value": "Object Encryption"}

public_key = jwk.JWK.from_pem(spki_pem.encode('utf-8'))
payload = json.dumps(data).encode('utf-8')
protected_header = {
    "alg": "RSA-OAEP-256",
    "enc": "A256GCM",
    "kid": "a4d4039e-a8c7-4d06-98c8-2bda90ab169c",
}
jwetoken = jwe.JWE(payload, recipient=public_key, protected=protected_header)
jewcompact = jwetoken.serialize(True)

print(jewcompact)

Possible output:

eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoiYTRkNDAzOWUtYThjNy00ZDA2LTk4YzgtMmJkYTkwYWIxNjljIn0.DstnMY6WkdCGA1NC0S4JIU3CNVFuUNQSghQyqkh8RpmUyBvnEelvkmWTTf3AApj4jNflnYL_bp8vbeu8PO6CyF1Pi_gUZ1vE1PHHcLD8VZ2eMiIdG08Qq9L7uDlqTIaM6qX0n_uctsm4Y0rflWXrSbr5iwXHxgOQ5XDgLCgg870tObS3RbK2RzrjYRnQs-_hK4R8LRJgCEKeV__fzmU6nx5jA4qXXs_U3y9Uxs3_4OE-xSelPT_yY5xCMs8fAHvaua92mrRCMSu9cp9iAYW8qu3bYdUFjnWqifOQIUB2HljqMH85tCxS02tBuVPs52b8pgNUckqa_v43BvxTbnwuJg.RWtp5j38l8mz_qoJ.2VBsxt1zyk5rAmSvg3k0eMLzIAPo9ttn-7dLB2nP.k4k9ZnCRRA1im2sbvMLlbQ

An encrypted token consists of 5 parts, the header, the encrypted key, the IV/nonce, the actual ciphertext and the tag, each separated by . and Base64url encoded. Since AES generates a different key and IV/nonce for each encryption and RSA encryption is non-deterministic, a different encrypted token is generated for each encryption except for the first part. So you should not expect that the code generates the same encrypted token that you posted.
The generated token can be decrypted using the private key associated with the posted public key and the decryption code of the linked post. This is also the right way for a test.


Edit:
Regarding the first part of your question from the comment: If the header

eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoiYTRkNDAzOWUtYThjNy00ZDA2LTk4YzgtMmJkYTkwYWIxNjljIn0

of the encrypted token is Base64url encoded, you get:

{"alg":"RSA-OAEP-256","enc":"A256GCM","kid":"a4d4039e-a8c7-4d06-98c8-2bda90ab169c"}

Here A256GCM as enc means that the plaintext is encrypted with AES-256 in GCM mode (symmetric encryption), with a random key being generated for the AES encryption.
RSA-OAEP-256 as alg means that this AES key is encrypted with RSA and with OAEP/SHA256 as padding (asymmetric encryption).
Bear in mind that RSA encryption always uses the public key (of the recipient).

So both cryptography types (symmetric and asymmetric) are involved, which is also called hybrid encryption.
The private RSA key does not come into play at all during encryption, but only during decryption, as you can verify in the linked post. There, the encrypted token is decrypted with the private RSA key (which must be associated with the public key that was used for encryption), meaning more precisely that the symmetric AES key is decrypted with RSA (using the private RSA key), which is then used to decrypt the actual ciphertext.

Regarding the second part: The format and type of the key can be determined with an ASN.1 parser, e.g. https://lapo.it/asn1js/.
The key you posted is an ASN.1/DER encoded public RSA key in X.509/SPKI format that is Base64 encoded. A public RSA key essentially contains modulus and public exponent. A private key also contains the private fields and also uses other formats.

Answered By: Topaco
Categories: questions Tags: , , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.