From e6a0b7cc39e2b0743e41ea0dc57b14230e072bdf Mon Sep 17 00:00:00 2001 From: Ridge Wendt Date: Wed, 2 Mar 2016 23:35:43 -0800 Subject: [PATCH 1/7] Added in a if-else statement to decide whether or not to use symmetric encryption/decryption(AESCBC) or to use Asymmetric encryption/decryption (RSA) --- jose.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/jose.py b/jose.py index c93089a..7282a58 100644 --- a/jose.py +++ b/jose.py @@ -168,8 +168,14 @@ def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP', encryption_key[:-hash_mod.digest_size/2], hash_mod) # cek encryption - (cipher, _), _ = JWA[alg] - encryption_key_ciphertext = cipher(encryption_key, jwk) + alg = header['alg'] + if (alg == "A128CBC" or alg == "A192CBC" or alg == "A256CBC"): + (cipher, _), _ = JWA[alg] + encryption_key_ciphertext = cipher(encryption_key, jwk, iv) + else: + (cipher, _), _ = JWA[alg] + encryption_key_ciphertext = cipher(encryption_key, jwk) + return JWE(*map(b64encode_url, (json_encode(header), @@ -203,9 +209,14 @@ def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None): header = json_decode(header) # decrypt cek - (_, decipher), _ = JWA[header['alg']] - encryption_key = decipher(encryption_key_ciphertext, jwk) - + alg = header['alg'] + if (alg == "A128CBC" or alg == "A192CBC" or alg == "A256CBC"): + (_, decipher), _ = JWA[header['alg']] + encryption_key = decipher(encryption_key_ciphertext, jwk, iv) + else: + (_, decipher), _ = JWA[header['alg']] + encryption_key = decipher(encryption_key_ciphertext, jwk) + # decrypt body ((_, decipher), _), ((hash_fn, _), mod) = JWA[header['enc']] From 5d85edbfed3fed31e283e4a9d5c6dff96aab48dd Mon Sep 17 00:00:00 2001 From: Ridge Wendt Date: Thu, 3 Mar 2016 01:08:10 -0800 Subject: [PATCH 2/7] Created a test for the symmetric crypto, more tests could be done --- tests.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 04a4f72..0f1e41b 100644 --- a/tests.py +++ b/tests.py @@ -11,7 +11,7 @@ from Crypto.Random import get_random_bytes import jose - +aes_128_key = "This is a key123" rsa_key = RSA.generate(2048) rsa_priv_key = { @@ -159,7 +159,25 @@ def test_jwe_add_header(self): jwt = jose.decrypt(jose.deserialize_compact(et), rsa_priv_key) self.assertEqual(jwt.header['foo'], add_header['foo']) + def test_jwe_symmetric(self): + + for (alg, jwk), enc in product(self.algs, self.encs): + jwe = jose.encrypt(claims, aes_128_key, alg="A128CBC") + + # make sure the body can't be loaded as json (should be encrypted) + try: + json.loads(jose.b64decode_url(jwe.ciphertext)) + self.fail() + except ValueError: + pass + + token = jose.serialize_compact(jwe) + jwt = jose.decrypt(jose.deserialize_compact(token), aes_128_key) + self.assertNotIn(jose._TEMP_VER_KEY, claims) + + self.assertEqual(jwt.claims, claims) + def test_jwe_adata(self): adata = '42' for (alg, jwk), enc in product(self.algs, self.encs): From 5aec61b6a8df4f03f597bdd8377f5c753421463f Mon Sep 17 00:00:00 2001 From: Scott Register Date: Fri, 4 Mar 2016 15:07:39 -0800 Subject: [PATCH 3/7] modified to support dir --- jose.py | 157 ++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 102 insertions(+), 55 deletions(-) diff --git a/jose.py b/jose.py index 7282a58..c3f3931 100644 --- a/jose.py +++ b/jose.py @@ -110,7 +110,8 @@ def deserialize_compact(jwt): def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP', - enc='A128CBC-HS256', rng=get_random_bytes, compression=None): + enc='A128CBC-HS256', rng=get_random_bytes, compression=None, + dir_key=None): """ Encrypts the given claims and produces a :class:`~jose.JWE` :param claims: A `dict` representing the claims for this @@ -155,27 +156,37 @@ def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP', (compress, _) = COMPRESSION[compression] except KeyError: raise Error( - 'Unsupported compression algorithm: {}'.format(compression)) - plaintext = compress(plaintext) - - # body encryption/hash - ((cipher, _), key_size), ((hash_fn, _), hash_mod) = JWA[enc] - iv = rng(AES.block_size) - encryption_key = rng(hash_mod.digest_size) - - ciphertext = cipher(plaintext, encryption_key[-hash_mod.digest_size/2:], iv) - hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata), - encryption_key[:-hash_mod.digest_size/2], hash_mod) - - # cek encryption + 'Unsupported compression algorithm: {}'.format(compression)) + plaintext = compress(plaintext) + alg = header['alg'] - if (alg == "A128CBC" or alg == "A192CBC" or alg == "A256CBC"): - (cipher, _), _ = JWA[alg] - encryption_key_ciphertext = cipher(encryption_key, jwk, iv) + if(alg == 'dir'): + # body encryption/hash + ((cipher, _), key_size), ((hash_fn, _), hash_mod) = JWA[enc] + iv = rng(AES.block_size) + # for Direct encryption, pre-shared symmetric key is used + + #CHECK SECOND VALUE + ciphertext = cipher(plaintext, dir_key, iv) + hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata), + dir_key, hash_mod) + + # cek encryption + encryption_key_ciphertext = '' + else: - (cipher, _), _ = JWA[alg] - encryption_key_ciphertext = cipher(encryption_key, jwk) + # body encryption/hash + ((cipher, _), key_size), ((hash_fn, _), hash_mod) = JWA[enc] + iv = rng(AES.block_size) + encryption_key = rng(hash_mod.digest_size) + ciphertext = cipher(plaintext, encryption_key[-hash_mod.digest_size/2:], iv) + hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata), + encryption_key[:-hash_mod.digest_size/2], hash_mod) + + # cek encryption + (cipher, _), _ = JWA[alg] + encryption_key_ciphertext = cipher(encryption_key, jwk) return JWE(*map(b64encode_url, (json_encode(header), @@ -184,8 +195,7 @@ def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP', ciphertext, auth_tag(hash)))) - -def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None): +def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None, dir_key=None): """ Decrypts a deserialized :class:`~jose.JWE` :param jwe: An instance of :class:`~jose.JWE` @@ -208,48 +218,81 @@ def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None): b64decode_url, jwe) header = json_decode(header) - # decrypt cek + + alg = header['alg'] - if (alg == "A128CBC" or alg == "A192CBC" or alg == "A256CBC"): - (_, decipher), _ = JWA[header['alg']] - encryption_key = decipher(encryption_key_ciphertext, jwk, iv) - else: + if(alg == 'dir'): + # decrypt body + ((_, decipher), _), ((hash_fn, _), mod) = JWA[header['enc']] + + version = header.get(_TEMP_VER_KEY) + if version: + plaintext = decipher(ciphertext, dir_key, iv) + hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata, version), + dir_key, mod=mod) + else: + plaintext = decipher(ciphertext, dir_key, iv) + hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata, version), + dir_key, mod=mod) + + if not const_compare(auth_tag(hash), tag): + raise Error('Mismatched authentication tags') + + if 'zip' in header: + try: + (_, decompress) = COMPRESSION[header['zip']] + except KeyError: + raise Error('Unsupported compression algorithm: {}'.format( + header['zip'])) + + plaintext = decompress(plaintext) + + claims = json_decode(plaintext) + try: + del claims[_TEMP_VER_KEY] + except KeyError: + # expected when decrypting legacy tokens + pass + + _validate(claims, validate_claims, expiry_seconds) + else: + # decrypt cek (_, decipher), _ = JWA[header['alg']] encryption_key = decipher(encryption_key_ciphertext, jwk) - - # decrypt body - ((_, decipher), _), ((hash_fn, _), mod) = JWA[header['enc']] - - version = header.get(_TEMP_VER_KEY) - if version: - plaintext = decipher(ciphertext, encryption_key[-mod.digest_size/2:], iv) - hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata, version), + + # decrypt body + ((_, decipher), _), ((hash_fn, _), mod) = JWA[header['enc']] + + version = header.get(_TEMP_VER_KEY) + if version: + plaintext = decipher(ciphertext, encryption_key[-mod.digest_size/2:], iv) + hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata, version), encryption_key[:-mod.digest_size/2], mod=mod) - else: - plaintext = decipher(ciphertext, encryption_key[:-mod.digest_size], iv) - hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata, version), - encryption_key[-mod.digest_size:], mod=mod) + else: + plaintext = decipher(ciphertext, encryption_key[:-mod.digest_size], iv) + hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata, version), + encryption_key[-mod.digest_size:], mod=mod) - if not const_compare(auth_tag(hash), tag): - raise Error('Mismatched authentication tags') + if not const_compare(auth_tag(hash), tag): + raise Error('Mismatched authentication tags') - if 'zip' in header: - try: - (_, decompress) = COMPRESSION[header['zip']] - except KeyError: - raise Error('Unsupported compression algorithm: {}'.format( - header['zip'])) + if 'zip' in header: + try: + (_, decompress) = COMPRESSION[header['zip']] + except KeyError: + raise Error('Unsupported compression algorithm: {}'.format( + header['zip'])) - plaintext = decompress(plaintext) + plaintext = decompress(plaintext) - claims = json_decode(plaintext) - try: - del claims[_TEMP_VER_KEY] - except KeyError: - # expected when decrypting legacy tokens - pass + claims = json_decode(plaintext) + try: + del claims[_TEMP_VER_KEY] + except KeyError: + # expected when decrypting legacy tokens + pass - _validate(claims, validate_claims, expiry_seconds) + _validate(claims, validate_claims, expiry_seconds) return JWT(header, claims) @@ -426,12 +469,14 @@ class _JWA(object): 'RS384': ((rsa_sign, rsa_verify), SHA384), 'RS512': ((rsa_sign, rsa_verify), SHA512), 'RSA-OAEP': ((encrypt_oaep, decrypt_oaep), 2048), - + + #'dir': ((_,_)_) 'A128CBC': ((encrypt_aescbc, decrypt_aescbc), 128), 'A192CBC': ((encrypt_aescbc, decrypt_aescbc), 192), 'A256CBC': ((encrypt_aescbc, decrypt_aescbc), 256), } + def __getitem__(self, key): """ Derive implementation(s) from key """ @@ -450,6 +495,8 @@ def _compound_from_key(self, key): try: enc, hash = key.split('-') + 'A192CBC': ((encrypt_aescbc, decrypt_aescbc), 192), + 'A256CBC': ((encrypt_aescbc, decrypt_aescbc), 256), return enc, hash except ValueError: pass From 68f0fef3d4fd24b790caaf6d8e4f648b3e329745 Mon Sep 17 00:00:00 2001 From: Ridge Wendt Date: Fri, 4 Mar 2016 15:29:16 -0800 Subject: [PATCH 4/7] Removed random strings --- jose.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/jose.py b/jose.py index c3f3931..eb0ce95 100644 --- a/jose.py +++ b/jose.py @@ -495,8 +495,6 @@ def _compound_from_key(self, key): try: enc, hash = key.split('-') - 'A192CBC': ((encrypt_aescbc, decrypt_aescbc), 192), - 'A256CBC': ((encrypt_aescbc, decrypt_aescbc), 256), return enc, hash except ValueError: pass From ed379a72d41434345841bb4c96a8fe0ea9e00121 Mon Sep 17 00:00:00 2001 From: Ridge Wendt Date: Fri, 4 Mar 2016 17:37:33 -0800 Subject: [PATCH 5/7] fixed jose.py --- jose.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jose.py b/jose.py index eb0ce95..63d9956 100644 --- a/jose.py +++ b/jose.py @@ -129,6 +129,7 @@ def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP', as output. :param compression: The compression algorithm to use. Currently supports `'DEF'`. + :param dir_key: Symmetric key to be used when alg = "dir" :rtype: :class:`~jose.JWE` :raises: :class:`~jose.Error` if there is an error producing the JWE """ From f375235b9f02b7cf8e195444db0a9f8cce29038c Mon Sep 17 00:00:00 2001 From: Ridge Wendt Date: Tue, 8 Mar 2016 22:22:31 -0800 Subject: [PATCH 6/7] Implemented Direct Symmetric Encryption. Added in a test as well --- jose.py | 72 ++++++++++++++++++++++++++------------------------------ tests.py | 43 ++++++++++++++++++++------------- 2 files changed, 59 insertions(+), 56 deletions(-) diff --git a/jose.py b/jose.py index 63d9956..3257681 100644 --- a/jose.py +++ b/jose.py @@ -108,10 +108,9 @@ def deserialize_compact(jwt): return token_type(*parts) - def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP', enc='A128CBC-HS256', rng=get_random_bytes, compression=None, - dir_key=None): + dir_key=""): """ Encrypts the given claims and produces a :class:`~jose.JWE` :param claims: A `dict` representing the claims for this @@ -129,7 +128,8 @@ def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP', as output. :param compression: The compression algorithm to use. Currently supports `'DEF'`. - :param dir_key: Symmetric key to be used when alg = "dir" + :param dir_key: A symmetric key to be used for Direct Ciphertext Encryption. + Defined in RFC 7518, Section 4.1 :rtype: :class:`~jose.JWE` :raises: :class:`~jose.Error` if there is an error producing the JWE """ @@ -157,8 +157,8 @@ def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP', (compress, _) = COMPRESSION[compression] except KeyError: raise Error( - 'Unsupported compression algorithm: {}'.format(compression)) - plaintext = compress(plaintext) + 'Unsupported compression algorithm: {}'.format(compression)) + plaintext = compress(plaintext) alg = header['alg'] if(alg == 'dir'): @@ -174,7 +174,7 @@ def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP', # cek encryption encryption_key_ciphertext = '' - + else: # body encryption/hash ((cipher, _), key_size), ((hash_fn, _), hash_mod) = JWA[enc] @@ -183,7 +183,7 @@ def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP', ciphertext = cipher(plaintext, encryption_key[-hash_mod.digest_size/2:], iv) hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata), - encryption_key[:-hash_mod.digest_size/2], hash_mod) + encryption_key[:-hash_mod.digest_size/2], hash_mod) # cek encryption (cipher, _), _ = JWA[alg] @@ -196,7 +196,7 @@ def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP', ciphertext, auth_tag(hash)))) -def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None, dir_key=None): +def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None, dir_key=""): """ Decrypts a deserialized :class:`~jose.JWE` :param jwe: An instance of :class:`~jose.JWE` @@ -210,6 +210,8 @@ def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None, dir_k :param expiry_seconds: An `int` containing the JWT expiry in seconds, used when evaluating the `iat` claim. Defaults to `None`, which disables `iat` claim validation. + :param dir_key: A symmetric key to be used for Direct Ciphertext Encryption. + Defined in RFC 7518, Section 4.1 :rtype: :class:`~jose.JWT` :raises: :class:`~jose.Expired` if the JWT has expired :raises: :class:`~jose.NotYetValid` if the JWT is not yet valid @@ -219,10 +221,8 @@ def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None, dir_k b64decode_url, jwe) header = json_decode(header) - - alg = header['alg'] - if(alg == 'dir'): + if(alg == 'dir'):#Use a shared symmetric key as the CEK # decrypt body ((_, decipher), _), ((hash_fn, _), mod) = JWA[header['enc']] @@ -235,17 +235,14 @@ def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None, dir_k plaintext = decipher(ciphertext, dir_key, iv) hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata, version), dir_key, mod=mod) - if not const_compare(auth_tag(hash), tag): raise Error('Mismatched authentication tags') - if 'zip' in header: try: (_, decompress) = COMPRESSION[header['zip']] except KeyError: raise Error('Unsupported compression algorithm: {}'.format( header['zip'])) - plaintext = decompress(plaintext) claims = json_decode(plaintext) @@ -255,7 +252,6 @@ def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None, dir_k # expected when decrypting legacy tokens pass - _validate(claims, validate_claims, expiry_seconds) else: # decrypt cek (_, decipher), _ = JWA[header['alg']] @@ -266,34 +262,34 @@ def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None, dir_k version = header.get(_TEMP_VER_KEY) if version: - plaintext = decipher(ciphertext, encryption_key[-mod.digest_size/2:], iv) - hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata, version), - encryption_key[:-mod.digest_size/2], mod=mod) + plaintext = decipher(ciphertext, encryption_key[-mod.digest_size/2:], iv) + hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata, version), + encryption_key[:-mod.digest_size/2], mod=mod) else: plaintext = decipher(ciphertext, encryption_key[:-mod.digest_size], iv) - hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata, version), - encryption_key[-mod.digest_size:], mod=mod) + hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata, version), + encryption_key[-mod.digest_size:], mod=mod) - if not const_compare(auth_tag(hash), tag): - raise Error('Mismatched authentication tags') + if not const_compare(auth_tag(hash), tag): + raise Error('Mismatched authentication tags') - if 'zip' in header: - try: - (_, decompress) = COMPRESSION[header['zip']] - except KeyError: - raise Error('Unsupported compression algorithm: {}'.format( - header['zip'])) + if 'zip' in header: + try: + (_, decompress) = COMPRESSION[header['zip']] + except KeyError: + raise Error('Unsupported compression algorithm: {}'.format( + header['zip'])) - plaintext = decompress(plaintext) + plaintext = decompress(plaintext) - claims = json_decode(plaintext) - try: - del claims[_TEMP_VER_KEY] - except KeyError: - # expected when decrypting legacy tokens - pass + claims = json_decode(plaintext) + try: + del claims[_TEMP_VER_KEY] + except KeyError: + # expected when decrypting legacy tokens + pass - _validate(claims, validate_claims, expiry_seconds) + _validate(claims, validate_claims, expiry_seconds) return JWT(header, claims) @@ -470,14 +466,12 @@ class _JWA(object): 'RS384': ((rsa_sign, rsa_verify), SHA384), 'RS512': ((rsa_sign, rsa_verify), SHA512), 'RSA-OAEP': ((encrypt_oaep, decrypt_oaep), 2048), - - #'dir': ((_,_)_) + 'A128CBC': ((encrypt_aescbc, decrypt_aescbc), 128), 'A192CBC': ((encrypt_aescbc, decrypt_aescbc), 192), 'A256CBC': ((encrypt_aescbc, decrypt_aescbc), 256), } - def __getitem__(self, key): """ Derive implementation(s) from key """ diff --git a/tests.py b/tests.py index 0f1e41b..c1ee5f3 100644 --- a/tests.py +++ b/tests.py @@ -159,25 +159,34 @@ def test_jwe_add_header(self): jwt = jose.decrypt(jose.deserialize_compact(et), rsa_priv_key) self.assertEqual(jwt.header['foo'], add_header['foo']) - def test_jwe_symmetric(self): - - for (alg, jwk), enc in product(self.algs, self.encs): - jwe = jose.encrypt(claims, aes_128_key, alg="A128CBC") - - # make sure the body can't be loaded as json (should be encrypted) - try: - json.loads(jose.b64decode_url(jwe.ciphertext)) - self.fail() - except ValueError: - pass + def test_jwe_direct_encryption(self): + symmetric_key = "tisasymmetrickey" - token = jose.serialize_compact(jwe) - - jwt = jose.decrypt(jose.deserialize_compact(token), aes_128_key) - self.assertNotIn(jose._TEMP_VER_KEY, claims) + jwe = jose.encrypt(claims, "", alg = "dir",enc="A128CBC-HS256", + dir_key = symmetric_key) - self.assertEqual(jwt.claims, claims) - + # make sure the body can't be loaded as json (should be encrypted) + try: + json.loads(jose.b64decode_url(jwe.ciphertext)) + self.fail() + except ValueError: + pass + token = jose.serialize_compact(jwe) + + jwt = jose.decrypt(jose.deserialize_compact(token),"", + dir_key = symmetric_key) + self.assertNotIn(jose._TEMP_VER_KEY, claims) + + self.assertEqual(jwt.claims, claims) + + # invalid key + badkey = "1234123412341234" + try: + jose.decrypt(jose.deserialize_compact(token), '', dir_key=badkey) + self.fail() + except jose.Error as e: + self.assertEqual(e.message, 'Mismatched authentication tags') + def test_jwe_adata(self): adata = '42' for (alg, jwk), enc in product(self.algs, self.encs): From 34969127a13656530e1af39b22af9fadf411065a Mon Sep 17 00:00:00 2001 From: Ridge Wendt Date: Tue, 8 Mar 2016 22:35:29 -0800 Subject: [PATCH 7/7] Removed aes_128_key variable from tests.py --- tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index c1ee5f3..f9029e1 100644 --- a/tests.py +++ b/tests.py @@ -11,7 +11,7 @@ from Crypto.Random import get_random_bytes import jose -aes_128_key = "This is a key123" + rsa_key = RSA.generate(2048) rsa_priv_key = {