latchset / jwcrypto

Implements JWK,JWS,JWE specifications using python-cryptography
GNU Lesser General Public License v3.0
432 stars 119 forks source link

The decrypted "payload" is not decrypted #360

Open AndreiCravtov opened 1 week ago

AndreiCravtov commented 1 week ago

I'm trying to decrypt a JWE with a symmetric key, withenc=A256GCM and alg=A256KW, following the documentation I get something like this:

# make JWK
aes_key_bytes = # raw bytes of the AES symmetric key
aes_key = JWK(
        kty="oct",
        key_ops="decrypt",
        alg="A256KW",
        k=base64url_encode(aes_key_bytes),
)

# decrypt JWE
jwe_compressed_str = # compressed JWE token string
jwe_token = JWE()
jwe_token.deserialize(jwe_compressed_str)
jwe_token.decrypt(aes_key)
print(jwe_token.payload) # output: the exact same string as contents of `jwe_compressed_str`, except its a b"string"

When I check decryptlog I see ['Success'] so its failing silently for some reason, so I have no idea whats going on. The specific payload and keys are for server-side validation of Google Play Integrity verdict tokens, and I'm porting the decryption logic from Rust code (which works), so the cryptographic tokens themselves aren't incorrect. Somewhere along the way either I made a mistake with using this API or there is a bug.

AndreiCravtov commented 1 week ago

Stepping through the code, the culprit of the unexpected behavior seems to be _AesGcm.decrypt in jwa.py:

    def decrypt(self, k, aad, iv, e, t):
        """ Decrypt according to the selected encryption and hashing
        functions.
        :param k: Encryption key
        :param aad: Additional Authenticated Data
        :param iv: Initialization Vector
        :param e: Ciphertext
        :param t: Authentication Tag

        Returns plaintext or raises an error
        """
        cipher = Cipher(algorithms.AES(k), modes.GCM(iv, t),
                        backend=self.backend)
        decryptor = cipher.decryptor()
        decryptor.authenticate_additional_data(aad)
        return decryptor.update(e) + decryptor.finalize()

When it returns, the calling block (which is JWE._unwrap_decrypt in jwe.py) receives the problematic "payload"

    def _unwrap_decrypt(self, alg, enc, key, enckey, header,
                        aad, iv, ciphertext, tag):
        cek = alg.unwrap(key, enc.wrap_key_size, enckey, header)
        data = enc.decrypt(cek, aad, iv, ciphertext, tag)
        self.decryptlog.append('Success')
        self.cek = cek
        return data

Here, data is precisely the contents of jwe_compressed_str except its a b"string"; if it doesn't actually decrypt anything it should surely at least raise an error?

simo5 commented 1 week ago

So what you are saying is that decryption is returning the actual ciphertext instead of the expected plaintext ? It is not entirely clear to me what problem you are identifying, can you write a simple reproducer? (Create a JWE with a random key, then decrypt it) Can you post the unprotected header of your JWE? (Just base64 decode the token string until the first dot). I'd like to see what algorithms the JWE is specifying.