Шифрование java JWEObject с помощью RSA в python

Я спрашивал некоторое время назад, как сделать что-то подобное: Расшифровка и шифрование java JWEObject с алгоритмом RSA-OAEP-256 на python Теперь у меня другой ключ шифрования, и этот код больше не работает для меня.

Мне нужно иметь возможность шифровать свои данные: {"value": "Object Encryption"} с помощью JWE с использованием RSA.

У меня есть этот идентификатор ключа: a4d4039e-a8c7-4d06-98c8-2bda90ab169c и этот ключ шифрования:

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

и я должен получить это:

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

Это java-код, который я пытаюсь переписать на 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);
    }
  }

Чем это отличается от моего предыдущего вопроса и как правильно это сделать в python?

Я пытался сделать что-то вроде этого:

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))

но я получаю это исключение:

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')])

Также пробовал что-то вроде:

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()

и я получаю: jwcrypto.common.InvalidJWEOperation: No available ciphertext

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
0
69
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В документации JWCrypto вы можете найти примеры шифрования с помощью JWCrypto. В основном вам нужно только вставить свои значения:

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)

Возможный вывод:

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

Зашифрованный токен состоит из 5 частей: заголовка, зашифрованного ключа, IV/одноразового номера, фактического зашифрованного текста и тега, каждая из которых разделена . и закодирована в Base64url. Поскольку AES генерирует разные ключи и IV/nonce для каждого шифрования, а шифрование RSA является недетерминированным, для каждого шифрования, кроме первой части, создается отдельный зашифрованный токен. Так что не стоит ожидать, что код сгенерирует тот же зашифрованный токен, который вы разместили.
Сгенерированный токен можно расшифровать с помощью закрытого ключа, связанного с опубликованным открытым ключом, и кода дешифрования связанного сообщения. Это также правильный путь для теста.


Обновлено:
По поводу первой части вашего вопроса из комментария: Если заголовок

eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoiYTRkNDAzOWUtYThjNy00ZDA2LTk4YzgtMmJkYTkwYWIxNjljIn0

зашифрованного токена кодируется Base64url, вы получаете:

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

Здесь A256GCM как enc означает, что открытый текст зашифрован с помощью AES-256 в режиме GCM (симметричное шифрование), при этом для шифрования AES генерируется случайный ключ. RSA-OAEP-256 в качестве алгоритма означает, что этот ключ AES зашифрован с помощью RSA и OAEP/SHA256 в качестве дополнения (асимметричное шифрование).
Имейте в виду, что шифрование RSA всегда использует открытый ключ (получателя).

Таким образом, задействованы оба типа криптографии (симметричная и асимметричная), что также называется гибридным шифрованием. Закрытый ключ RSA вообще не используется во время шифрования, а только во время дешифрования, как вы можете убедиться в связанном посте. Там зашифрованный токен расшифровывается с помощью закрытого ключа RSA (который должен быть связан с открытым ключом, который использовался для шифрования), то есть, точнее, симметричный ключ AES расшифровывается с помощью RSA (с использованием закрытого ключа RSA), что затем используется для расшифровки фактического зашифрованного текста.

Что касается второй части: формат и тип ключа можно определить с помощью синтаксического анализатора ASN.1, например. https://lapo.it/asn1js/.
Опубликованный вами ключ представляет собой открытый ключ RSA в кодировке ASN.1/DER в формате X.509/SPKI, закодированный в Base64. Открытый ключ RSA по существу содержит модуль и открытый показатель степени. Закрытый ключ также содержит закрытые поля и использует другие форматы.

Спасибо за подробный ответ! У меня все еще есть один вопрос, и я думаю, что это причина, которая помешала мне использовать пример в документации JWCrypto в первую очередь. Мой вопрос: разве код, который я отправил, не выполняет асимметричное шифрование, в то время как ваш код симметричен, поскольку нет закрытого ключа? или я пропустил, где в игру вступил закрытый ключ. И второй вопрос: как вы узнали, что ключ, который я отправил, является открытым ключом?

Ema Il 31.01.2023 11:38

@EmaIl - зашифрованный токен содержит в заголовке информацию о том, как он был зашифрован, поэтому помимо алгоритмов также указывается, какие именно ключи для чего используются. Пожалуйста, смотрите раздел редактирования моего ответа для более подробного объяснения.

Topaco 31.01.2023 14:42

Большое спасибо за удивительный ответ! теперь намного понятнее!!

Ema Il 31.01.2023 16:42

@Topcao - я только что снова прочитал ваш отредактированный ответ, и мне в голову пришел еще один вопрос. Вы написали, что закрытый ключ при шифровании вообще не привязывается, только при расшифровке. но позже, когда вы объяснили, как его расшифровать, вы написали, что используете «закрытый ключ RSA (который должен быть связан с открытым ключом, который использовался для шифрования)». Итак, мой вопрос: как закрытый ключ может быть связан с открытым ключом, если мы вообще не упоминаем закрытый ключ во время шифрования?

Ema Il 01.02.2023 11:45

@EmaIl — Помните Распределение ключей RSA: прежде чем может быть выполнено шифрование с помощью RSA, сторона дешифрования генерирует пару ключей (priv, pub) и отправляет pub шифрующей стороне. Только после этого распределения ключей выполняется шифрование и дешифрование. Шифрующая сторона шифрует с помощью pub, дешифрующая сторона расшифровывает с помощью priv. Отсюда тривиально следует, что priv и pub связаны.

Topaco 01.02.2023 14:05

Другие вопросы по теме