I have a problem decrypting a generated JWK generated by an IBM product. In particular have a look at my example:
JWE: eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiQTI1NktXIiwiY3R5IjoiSldUIn0.5Dx7B_0XI8F2ZZzkHjiJkeNsw11LlOuMzln9Z6OuGCAMpLeCOXnnPw.VEV_6HmnlroYO483zJdHFw.jS97NRZaPQfO46J9UvG9YsQ0po2SnUJuCe7M9VNIghD8lyUgdqaGx6xXH6MnAD01VLbjYROwh0z8CFGQ5PbamoiNxzMGM3UHDqvKU4j1pdRkcyPZbyZ6oo-NtY5dlwT6FhMMgu3kk7JKaFKXz0mhyNnvx22QTHKWHpMReEuc4AwdeDBL47iX8kT9cyqBzlGWKl-jLvEM73gUzPLC8RxG9_mtyIzEqyiGWtbDavD4yqf7lgo39jBIvwBu-VDVW05A.o15bGBayvRp9Dgzlqd2WAw
JWK: { "alg": "A256KW", "kty": "oct", "use": "enc", "k": "hD-S5Ll-StGTM6K0N891J3KdAgLVdUNRuKCpiweXJh8", "kid": "test"}
Now, this is my Python3 code:
import base64
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.keywrap import aes_key_unwrap
encrypted_jwe = ('eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiQTI1NktXIiwiY3R5IjoiSldUIn0'
'.5Dx7B_0XI8F2ZZzkHjiJkeNsw11LlOuMzln9Z6OuGCAMpLeCOXnnPw.VEV_6HmnlroYO483zJdHFw'
'.jS97NRZaPQfO46J9UvG9YsQ0po2SnUJuCe7M9VNIghD8lyUgdqaGx6xXH6MnAD01VLbjYROwh0z8CFGQ5PbamoiNxzMGM3UHDqvKU4j1pdRkcyPZbyZ6oo-NtY5dlwT6FhMMgu3kk7JKaFKXz0mhyNnvx22QTHKWHpMReEuc4AwdeDBL47iX8kT9cyqBzlGWKl-jLvEM73gUzPLC8RxG9_mtyIzEqyiGWtbDavD4yqf7lgo39jBIvwBu-VDVW05A.o15bGBayvRp9Dgzlqd2WAw')
jwk = {
"kty": "oct",
"k": "hD-S5Ll-StGTM6K0N891J3KdAgLVdUNRuKCpiweXJh8"
}
parts = encrypted_jwe.split('.')
if len(parts) != 5:
print("invalid JWE")
exit(1)
header, encrypted_key, iv, ciphertext, tag = parts
cek_encrypted = base64.urlsafe_b64decode( encrypted_key + '=' * (-len(encrypted_key) % 4))
cek_key = base64.urlsafe_b64decode(jwk['k'] + '=' )
cek = aes_key_unwrap(cek_key, cek_encrypted)
print("decrypted CEK:", cek)
decoded_ciphertext = base64.urlsafe_b64decode(ciphertext + '=' * (-len(ciphertext) % 4))
decoded_iv = base64.urlsafe_b64decode(iv + '=' * (-len(iv) % 4))
cipher = Cipher(algorithms.AES(cek), modes.CBC(decoded_iv), backend=default_backend())
decryptor = cipher.decryptor()
decrypted_payload = decryptor.update(decoded_ciphertext) + decryptor.finalize()
print("decrypted Payload:", decrypted_payload)
Technically speaking i think I need to do the following steps:
However this code produces an error:
cryptography.hazmat.primitives.keywrap.InvalidUnwrap
What am I doing wrong?
I would need a baseline of a python script capable of doing this job.
Thank you
The JWE token cannot be decrypted with the JWK, the problem occurs when unwrapping the primary key. A likely explanation is that the JWE token and JWK do not match.
The posted Python code works for a valid JWE token and JWK, at least as far as unwrapping the primary key is concerned. The second part, namely the decryption of the payload fails because the primary key is used as the key for decryption. Also, the decrypted payload lacks unpadding.
The correct way would be to halve the primary key. The last 16 bytes are the key for decryption. The first 16 bytes are the key for authentication via HMAC/SHA256.
You write in the comments that you don't need authentication. But this is a security component that should not be ignored (especially since the authentication effort is minimal).
To demonstrate this, I use the following valid data:
JWK:
{'kty': 'oct', 'k': 'c4OqgE8K3OMf-9W0lxa8EGSY5eeebSnVKvfZu9AssAg'}
JWE token:
eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.5ij3S9PQkJ0YPfVfoZ9Mzlu0m6_xQJU6JJnljm0IZOFvEGIYhbWk9Q.iDQqf4q20QVCMPMS-g64kw.uKD4LKZosgXcmKWMZtI4JwpjjGZMa0qDo8jRZsLnSRkI02Fr3trUTJMo4isr0TouSVnrHezgIRU0_jF-KCCDUA.4AwFYtD0o8JXaA2_Ha0AOw
The header is base64url decoded:
{"alg":"A256KW","enc":"A128CBC-HS256"}
and applies the same alg
and enc
value as the JWE token posted in the question. I.e. the primary key is wrapped with A256KW, for the plaintext encryption A128CBC-HS256 was used.
The following Python code is based on the code you posted, with the addition of authentication:
import base64
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.keywrap import aes_key_unwrap
from cryptography.hazmat.primitives import hashes, hmac
from cryptography.hazmat.primitives import padding
def b64url_decode(b64data):
b64data = b64data.encode('ascii')
return base64.urlsafe_b64decode(b64data + b'=' * (-len(b64data) % 4))
# valid data
encrypted_jwe = 'eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.5ij3S9PQkJ0YPfVfoZ9Mzlu0m6_xQJU6JJnljm0IZOFvEGIYhbWk9Q.iDQqf4q20QVCMPMS-g64kw.uKD4LKZosgXcmKWMZtI4JwpjjGZMa0qDo8jRZsLnSRkI02Fr3trUTJMo4isr0TouSVnrHezgIRU0_jF-KCCDUA.4AwFYtD0o8JXaA2_Ha0AOw'
jwk = {'kty': 'oct', 'k': 'c4OqgE8K3OMf-9W0lxa8EGSY5eeebSnVKvfZu9AssAg'}
parts = encrypted_jwe.split('.')
if len(parts) != 5:
print("invalid JWE")
exit(1)
header, encrypted_key, iv, ciphertext, tag = parts
# unwrap primary key and split in authentication and encryption key
cek_encrypted = b64url_decode(encrypted_key)
cek_key = b64url_decode(jwk['k'])
cek = aes_key_unwrap(cek_key, cek_encrypted)
auth_key = cek[:16]
enc_key = cek[16:]
# authenticate
decoded_iv = b64url_decode(iv)
decoded_tag = b64url_decode(tag)
decoded_ciphertext = b64url_decode(ciphertext)
authData = header.encode('ascii') + decoded_iv + decoded_ciphertext + (len(header) * 8).to_bytes(8, 'big')
h = hmac.HMAC(auth_key, hashes.SHA256())
h.update(authData)
tag_calc = h.finalize()
auth_suceeded = tag_calc[:16] == decoded_tag # compare the first 16 bytes from the calculated tag
# on success, decrypt
if auth_suceeded:
cipher = Cipher(algorithms.AES(enc_key), modes.CBC(decoded_iv), backend=default_backend())
decryptor = cipher.decryptor()
decrypted_payload = decryptor.update(decoded_ciphertext) + decryptor.finalize()
unpadder = padding.PKCS7(128).unpadder()
decrypted_payload = unpadder.update(decrypted_payload) + unpadder.finalize() # unpad!
print("decrypted Payload:", decrypted_payload.decode()) # decrypted Payload: {"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
else:
print("decrypted failed")
However, it is much more efficient to use a JOSE implementation, e.g. JWCrypto:
from jwcrypto import jwk, jwe
encrypted_jwe = 'eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.5ij3S9PQkJ0YPfVfoZ9Mzlu0m6_xQJU6JJnljm0IZOFvEGIYhbWk9Q.iDQqf4q20QVCMPMS-g64kw.uKD4LKZosgXcmKWMZtI4JwpjjGZMa0qDo8jRZsLnSRkI02Fr3trUTJMo4isr0TouSVnrHezgIRU0_jF-KCCDUA.4AwFYtD0o8JXaA2_Ha0AOw'
jwkey = {'kty': 'oct', 'k': 'c4OqgE8K3OMf-9W0lxa8EGSY5eeebSnVKvfZu9AssAg'}
jwetoken = jwe.JWE()
jwetoken.deserialize(encrypted_jwe)
jwetoken.decrypt(jwk.JWK(**jwkey))
payload = jwetoken.payload
print("with JWCrypto:", payload.decode('utf8')) # with JWCrypto: {"sub": "1234567890", "name": "John Doe", "iat": 1516239022}