javascriptpythonnode.jsencryptionjwe

Unable to decrypt JWE Token from Python jwcrypt to NodeJS jose


I am unable to decrypt a JWE Token generated by jwcrypt (Python library for JWE Encryption and Decryption) to jose (Javascript library for JWE Encryption and Decryption)

I have explicitly defined the A256KW and A256CBC-HS512 on both jwcrypt and jose as well. I also need to provide a "Secret Key" to the JWK's "k" parameter, which will be used for the Symmetric Encryption and Decryption.

Below is the Javascript code for jose

import {
  CompactEncrypt,
  compactDecrypt,
  CompactDecryptResult,
  KeyLike,
  importJWK,
} from 'jose';

class JweService {

  async createSecretKey(jwtKey: string): Promise<KeyLike | Uint8Array> {
    const base64Key = Buffer.from(jwtKey).toString('base64');

    return importJWK(
      {
        kty: 'oct',
        use: 'enc',
        k: base64Key
      },
      'A256KW',
    );
  }

  async createAccessToken(
    payload,
    secretKey: KeyLike | Uint8Array,
  ): Promise<string> {
    const encoder = new TextEncoder();

    return new CompactEncrypt(encoder.encode(payload))
      .setProtectedHeader({
        alg: 'A256KW',
        enc: 'A256CBC-HS512',
      })
      .encrypt(secretKey);
  }

  async decryptAccessToken(
    token: string,
    secretKey: KeyLike | Uint8Array,
  ): Promise<string> {
    const decoder = new TextDecoder();
    const { plaintext }: CompactDecryptResult = await compactDecrypt(
      token,
      secretKey,
    );

    return decoder.decode(plaintext);
  }
}

This is how I use the JweService

  async createAccessTokenForDemo(reqBody): Promise<string> {
    const secretKey = await this.jweService.createSecretKey(
      'QEO89KnZ8GJNAZEhBGRlQaBVtuA9asd2',
    );

    return this.jweService.createAccessToken(
      JSON.stringify(reqBody),
      secretKey,
    );
  }

  async decryptAccessTokenForDemo(reqBody): Promise<any> {
    const secretKey = await this.jweService.createSecretKey(
      'QEO89KnZ8GJNAZEhBGRlQaBVtuA9asd2',
    );

    const decrypted = await this.jweService.decryptAccessToken(
      reqBody.Token,
      secretKey,
    );

    return JSON.parse(decrypted);
  }

Now for Python which uses jwcrypto:

from jwcrypto import jwk, jwe
from jwcrypto.common import json_encode
import base64

def create_secret_key(jwt_key, alg='A256KW'):
    jwt_key_bytes = jwt_key.encode('utf-8')  # Convert string to bytes
    jwt_key_base64 = base64.b64encode(jwt_key_bytes)  # Base64 encode the bytes

    return jwk.JWK.generate(
        kty = "oct",
        use = "enc",
        k = jwt_key_base64,
        alg = alg
    )

def create_access_token(payload, secret_key, alg='A256KW', enc='A256CBC-HS512'):
    jwetoken = jwe.JWE(payload.encode('utf-8'), json_encode({"alg": alg, "enc": enc}))
    jwetoken.add_recipient(secret_key)
    return jwetoken.serialize(compact=True)

def decrypt_access_token(token, secret_key):
    jwetoken = jwe.JWE()
    jwetoken.deserialize(token)
    jwetoken.decrypt(secret_key)
    return jwetoken.payload.decode('utf-8')

# Secret key and payload
jwt_key = 'QEO89KnZ8GJNAZEhBGRlQaBVtuA9asd2'
payload = '{"name": "John"}'

# Create secret key
secret_key = create_secret_key(jwt_key)

# Create JWE access token
access_token = create_access_token(payload, secret_key)
print("Encrypted Payload:", access_token)

# Decrypt JWE access token
decrypted_payload = decrypt_access_token(access_token, secret_key)

print("Decrypted Payload:", decrypted_payload)

This is an example JWE Token from the Python script: (When decrypted using NodeJS jose, jose library returns an error of "Error: decryption operation failed")

Encrypted JWE token: b'eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0.4xd2sw9FFFBZjzOo7ofz-Y7M_rUK_2-DACZPZyvVEHtFVa29eOu43s5rCtlh3xP8_9oMBOuoWXnzJWLEOyxyzl3axDYo-a8Y.QH-iHG2dqduw6r8agd5ZIg.W7MmKBaBEe5EwaGTHfXAUI2MhPFOHMGA5ZkHBNHWqUY.xRUr1jaYxwN5JDkazJ6mQYeSafiXCeidgPurl9qMJLs'
Decrypted JWE token: b'{"name": "John Doe"}'

Is there any configuration I am missing? or is there some encoding that is different between NodeJS and Python behind the scenes which could be the reason why?


Solution

  • In the Python code, a new, random 32 bytes key is generated in create_secret_key() with jwk.JWK.generate() with each call. The passed jwt_key is ignored (i.e. k is not the Base64url encoding of QEO8... at all). This can be easily verified by exporting the generated key with secret_key.export() and comparing the k parameter.

    As a consequence, the decryption of the token with the NodeJS code fails when the key QEO8... is applied.


    To import a key (instead of generating a new one), the following implementation can be used, s. here:

    def create_secret_key(jwt_key, alg='A256KW'):
        jwt_key_bytes = jwt_key.encode('utf-8')  
        jwt_key_base64url = base64.urlsafe_b64encode(jwt_key_bytes).replace(b"=", b"").decode('utf-8')  
    
        key = {
            'kty': 'oct',
            'use': 'enc',
            'k': jwt_key_base64url,
            "alg": alg
        }
        
        return jwk.JWK(**key)
    

    Also note that the implementation applies Base64url and not Base64 as defined for JWKs.

    With the following code:

    jwt_key = 'QEO89KnZ8GJNAZEhBGRlQaBVtuA9asd2'
    secret_key = create_secret_key(jwt_key)
    print('Key:' + secret_key.export()) 
    

    the import can be checked: k is UUVPODlLblo4R0pOQVpFaEJHUmxRYUJWdHVBOWFzZDI, which corresponds to the Base64url encoding of QEO89KnZ8GJNAZEhBGRlQaBVtuA9asd2.


    Also in the corresponding method in the NodeJS code, Base64url must be used instead of Base64:

    async createSecretKey(jwtKey) {
        const base64urlKey = Buffer.from(jwtKey).toString('base64url');
    
        return importJWK(
            {
                kty: 'oct',
                use: 'enc',
                k: base64urlKey
            },
            'A256KW',
        );
    }
    

    Now, if an encrypted token is generated with the fixed Python code, e.g.

    eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0.BAhqPzCOT6u0xEGd40z6sYBU7XWJqwLMWxY1L6B2YYK0YdLvVe6WA8ypHJ6Q0cm4TWFw2xL4n6DeqqyoC9zkEd4fLqU04U4D.pK_f_0tBwXIj7hy79IjC2g.9vtRX6kKCBFoPD2UcfuUyIMevY3d7Pj7ydOM9XBWiJU.cYVtCc9O_8xuBxXg21316rmeNA2NHYPLF3NOjk7RSrw
    

    this can be decrypted with the key QEO8... and the adapted NodeJS code.