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?
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.