mpdavis / python-jose

A JOSE implementation in Python
MIT License
1.51k stars 234 forks source link

How to use JWE encrypt using a simple symmetric Key? #193

Open ari75 opened 3 years ago

ari75 commented 3 years ago

Hello everybody,

I try to do encrypt a simple string using JWE, however I cannot manage to get a valid symmetric key to be used by the JWE engine.

I tried both: dev_key = { "alg": "dir", "k": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "kid": "AAA", "kty": "oct", "use": "enc" }

and dev_key_raw = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"

Followed by:

encrypted_text = jwe.encrypt("my string", dev_key, constants.Algorithms.A128CBC_HS256, constants.Algorithms.DIR, None,
                                     None, "AAA")

But this always fails. When using dev_key_raw, he complains that the Key is not 128 bits long. When using dev_key, it fails to construct the JWK which is used to do the encryption.

What I am missing here?

Thanks for your support!

blag commented 3 years ago

You're pretty close to figuring it out by yourself, but I'll walk you through it.

When you specify the "Direct use of a shared symmetric key" with constants.Algorithms.DIR, the key and algorithm parameters to jwe.encrypt are passed directly into jwk.construct:

def encrypt(plaintext, key, encryption=ALGORITHMS.A256GCM,
            algorithm=ALGORITHMS.DIR, zip=None, cty=None, kid=None):
    ...
    key = jwk.construct(key, algorithm)
    ...

That means that you cannot use a pre-constructed JWK by passing it into jwe.encrypt. This is definitely something that could be and probably should be supported, but JWE support is new.

However, it also means that your main problem is simply figuring out how to correctly construct a JWK, because once you have that then you can pass that directly into jwe.encrypt.

Digging into the code for JWKs a bit, the jwk.construct method is actually pretty concise:

def construct(key_data, algorithm=None):
    """
    Construct a Key object for the given algorithm with the given
    key_data.
    """

    # Allow for pulling the algorithm off of the passed in jwk.
    if not algorithm and isinstance(key_data, dict):
        algorithm = key_data.get('alg', None)

    if not algorithm:
        raise JWKError('Unable to find an algorithm for key: %s' % key_data)

    key_class = get_key(algorithm)
    if not key_class:
        raise JWKError('Unable to find an algorithm for key: %s' % key_data)
    return key_class(key_data, algorithm)

After figuring out the imports, you find that the key_data parameter is being passed into DIRKey.__init__(), which is a class that is also pretty succinct:

class DIRKey(Key):
    def __init__(self, key_data, algorithm):
        self._key = six.ensure_binary(key_data)
        self._alg = algorithm

    def to_dict(self):
        return {
            'alg': self._alg,
            'kty': 'oct',
            'k': base64url_encode(self._key),
        }

(Side note: I'm not expecting you to do this digging, just showing you the process I took to answer your question)

You didn't specify the exact error when it fails to create a JWK, but I suspect it's something like TypeError: not expecting type '<class 'dict'>', because six.ensure_binary doesn't know how to convert your dict object into a bytestring.

When you correctly tried to use dev_key_raw you got a little further, but it didn't do what you expected because the symmetric key that you specified wasn't the correct length for the encryption scheme you specified.

>>> len(six.ensure_binary("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) * 8
344  # bits

So the solution is to specify raw key data that is the correct size:

>>> len(six.ensure_binary("A" * 32)) * 8
256  # bits
>>> encrypted_text = jwe.encrypt("my string", "A" * 32, encryption=constants.Algorithms.A128CBC_HS256, kid="AAA")
>>> encrypted_text
'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoiQUFBIn0..izgGOGLkLoJ1Wbx1O3iuyw.BEudQb9KTthRYCbTCrGbKw.5Ni2aPLRISJtody5ttDEhQ'
>>> jwe.decrypt(encrypted_text, "A" * 32)
'my string'

Another side note: if you name your arguments when you call functions, you don't have to explicitly specify parameters that have default values, and it makes your code much easier to read and understand at a glance. In other words, this monstrosity:

jwe.encrypt("my string", dev_key, constants.Algorithms.A128CBC_HS256, constants.Algorithms.DIR, None,
                                     None, "AAA")

can be reduced down to:

jwe.encrypt("my string", dev_key, encryption=constants.Algorithms.A128CBC_HS256, kid="AAA")

which is a lot more readable, and anybody reading your code doesn't have to dig through the documentation for jwe.encrypt to figure out which argument is being assigned to which parameter. Hope this helps!

I'll keep this issue open until we add the ability to pass a pre-constructed JWK into jwe.encrypt and jwe.decrypt.

ari75 commented 3 years ago

Hi Blag,

Thanks first for your very detailed answer , it's really cool 👍

I actually followed your digging too, hence the reason why I ended testing with the "raw" key. Your answer helped me understand that the provided raw key, although correct (this was just an example), was not provided in the expected encoding.

Indeed, Following the JWA specifications, the k parameter is base64url encoded: JWA specs

I hence decided to use the jose.utils.base64url_decode to get the right encoding. This still didn't work, as I received this error message: input += b'=' * (4 - rem) TypeError: can only concatenate str (not "bytes") to str

This is because, although this function advertise it expecting a string, it actually works with a byte. Indeed only a byte can be added to another byte. Here the fix would be for this method to either advertise expecting a byte, or instead to concatenate a string instead of a byte.

My final working code is:

    dev_key_b64 = self.dev_key_raw.encode('UTF-8')
    dev_key_b = utils.base64url_decode(dev_key_b64) 
    jwe.encrypt("my string", dev_key_b, encryption=constants.Algorithms.A128CBC_HS256, kid="AAA")

Should I make a pull request for the utils.base64url_decode method? If yes should I change the advertised type, or should I modify it to concatenate a string instead of a byte?

blag commented 3 years ago

The str vs. bytes issue should be resolved with #189, which I'm trying to finish for the next release.

ari75 commented 3 years ago

Super, thanks!

grandmair40 commented 2 years ago

Hi Blag, I am trying to execute what you describe above:

encrypted_text = jwe.encrypt("my string", "A" * 32, encryption=constants.Algorithms.A128CBC_HS256, kid="AAA") But it fails with jose.exceptions.JWKError: Unable to find an algorithm for key: b'AAAAAAAAAAAAAAAA' I debugged it through and see that it fails at key_class = get_key(algorithm) It does not matter what you pass there, I tried different algos as well , for example the one that is given in the Examples section jwe.encrypt('Hello, World!', 'asecret128bitkey', algorithm='dir', encryption='A128GCM')

still fails with the same error. Could you help me with this please?

grandmair40 commented 2 years ago

the following also fails with the same error: encrypted_text = jwe.encrypt("my string", "A" * 32) I am using the latest version: Name: python-jose Version: 3.3.0 Summary: JOSE implementation in Python Home-page: http://github.com/mpdavis/python-jose Author: Michael Davis Author-email: mike.philip.davis@gmail.com License: MIT Location: /Users/vladimir.syrstov/work/projects/smb-backend/venv/lib/python3.8/site-packages Requires: ecdsa, pyasn1, rsa Required-by:

grandmair40 commented 2 years ago

Sorry, I forgot to mention that for the example jwe.encrypt('Hello, World!', 'asecret128bitkey', algorithm='dir', encryption='A128GCM') I passed 16 bytes instead of asecret128bitkey so a key with the correct length

blag commented 2 years ago

Hi, I've actually stepped down from maintaining this because I simply don't have the time to devote to it anymore.

Best of luck figuring it out.

grandmair40 commented 2 years ago

Thanks for the update. I found the problem. The missing cryptography lib dependency. pip install cryptography fixes the problem

grandmair40 commented 2 years ago

I did not look at it but probably your requirements.txt file does not have it