latchset / jwcrypto

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

The decrypted "payload" is not decrypted #360

Closed AndreiCravtov closed 1 month ago

AndreiCravtov commented 2 months 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 2 months 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 2 months 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.

AndreiCravtov commented 1 month ago

Sorry for wasting your time, there was no issue. I just saw:

eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIn0.... <- ciphertext and eyJhbGciOiJFUzI1NiJ9.... <- plaintext

and the eyJhbGciOiJ... prefix in both short-circuited my brain