I'm seeing following error while decrypting cipherText part of JWE. CipherText (alg : aes-256-gcm ) is created using node Node Jose and I am trying to decrypt using Node crypto
Error: Unsupported state or unable to authenticate data
at Decipheriv.final (node:internal/crypto/cipher:193:29)
at Object.<anonymous> (/index.js:100:24)
at Module._compile (node:internal/modules/cjs/loader:1103:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1157:10)
at Module.load (node:internal/modules/cjs/loader:981:32)
at Function.Module._load (node:internal/modules/cjs/loader:822:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
at node:internal/main/run_main_module:17:47
Node jose encrypt decrypt : Works fine
const jose = require('node-jose');
(async () => {
let keypair = {
"p": "wP8U4hF_Dyj-Z-YY_89gmT1ngnKER_0e_WTxOS5SWQp-giImmsCEVpP9mqKrwHBrkPJenFwa8kIxNgmJtGSuXkNMd8NoHdqzwtq50-mMCV1oDAIjXl0UUX8_CdVyy9TFe3-P5lJuQoOIRlCs_hvVJ_4x3fT9Xipo85OMoPTmwoM",
"kty": "RSA",
"q": "uk0U0Wx9GfpyLLJhJSJ1gyx5M31kAP9vMGQLrp0P40g-BOl7YYusv8H1EVcscZQuMrNBQvlIyJRTx229r_Wz961YqQ6fw-hJetQQ08CiLj8AuYMm6KHNbEyhzFV8lFyIZCwsDXbA-Ek6F5FLfOqfeFHjo0fOq77R7kcp8cepcP0",
"d": "bd1sMKhIu3QDbEvzrb8cj6f0egUQQU0S1M53okm9ByBWYOFCNPayoluvO9BWBBY2kMsIWQ64q7-sQgjc894yaffppF4Cz0jFCzIIIgoWmT4L4syQkdiiV7xoIrbKvZkloH7bSATf6QioxkcX-9Dz4Z5VHUg0GKzFkK74a655odc-VzlDp7cbrfeL29ETlbb9864IJIoC_hAhzfaVOHCMb8y5Rr5UTgGjf1QlYjSjMhE5L0BOty4QeyxDoy_dI7sosJPj9tVFQCa6yDJaOAFF_h3QTLyhXU1qL6GJbG_JklsTn5z93Vy8haVyf3FvHDW2PVkKy46HRTINpB72QWkV-Q",
"e": "AQAB",
"use": "enc",
"kid": "tester",
"qi": "tItxE9gwrpmifSWur8DdsaMZN7rylhhR18PIgtZaHhw58i9EBRpzW_CMZIHuzl8-ujh8ZsuUsmU2HbYX0VWZnPty3z_-hCWV1DjoElQ93WThcQa0HviIVSxbXEtYoLoNM_gzIFjihICGCtXhlF9Di3C4FKcCP0dYzYWpVFq4uac",
"dp": "M8j-IH7TWg0E3noWQSWy5MteJ9l0dyCLHTDlrRMp02yGb4KcWy_HErgY91IoxbUkl7sA-fGY5WIvdDFw-q99PhvOu9_54vDZBTLNY_gptCWVEovMU7ikCA4dqxTT_a904eNjiEib_0rt2PgywuhS9K03Ujg3d_nnOVxhAptUA-M",
"alg": "RSA-OAEP-256",
"dq": "NsOr5_gNOlK9t1fkaKcdhibPpgwpFoX_6Giwam7vGa_F02nTBBSr_l6ErMlEXkrh3bOF7qsa8yNvEUO4K_59HcSOOHv9CPjCiOHH5IdO5WtNyjq8eEv_9-L6-Pb0PSSKT3AQrxCGnzXfZsgmOZ06rYLc-MWGAkSAr5upv9Iig_0",
"n": "jHNxl1hhNxvYEn5PPObRia7LM6_koGcrcHLgWVVc-zU5loWn33xdd3R3EPs10ZwrhRBmXthN1WLFB0V4w-1QrGSM5wuBm2AqIFglDaYWW7d_aFCYMubCC6YiKYgrXezZtjngGtjBJPNwov4PC6KJgh7xwtqt5MTXX7TH8H6BhvQvNiD_IEH_vxF9hEhN-f5wKR6yNGlCT3X0NWwUiavG0vgtW0y1g6BHUskA1HogdrpURAfmcSSrDya9IoYjlAmUmql-0JGeEJIU53hoDB0ZVNREZbhJxZLN1hV8KrYeVPjfHCjXaJ6-Fw9MvnC9m-FGiX9dvE1A-Yp5sowUOqKLdw"
}
keypairob = await jose.JWK.asKey(JSON.stringify(keypair));
const contentAlg = "A256GCM";
const payload = JSON.stringify({test: 'test'});
const options = {
compact: true,
contentAlg: contentAlg,
protect: Object.keys({
alg: keypairob.alg,
kid: keypairob.kid,
enc: contentAlg
}),
fields: {
alg: keypairob.alg,
kid: keypairob.kid,
enc: contentAlg
}
};
const enc = await jose.JWE.createEncrypt(options, keypairob).update(payload, "utf8").final();
console.log("encrypted payload node jose : "+enc);
const dec = await jose.JWE.createDecrypt(keypairob,options).decrypt(enc)
let decryptJSON = JSON.stringify(dec);
let originalInput = Buffer.from(dec.plaintext).toString("utf-8");
console.log("decrypted payload node jose :"+originalInput);
})();
Console output:
encrypted payload node jose : eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJraWQiOiJ0ZXN0ZXIiLCJlbmMiOiJBMjU2R0NNIn0.Dw_EDEJnahKdk8mwlY3juSWd4jTIM8Go8NdQLpLWeH2kCmDHNg0RZKvbINEc3jF70bPECCzuIi_fH3ovlUeQ_o7KqbF_5ZMiyOIYQvfsVlgSZpl1YSRHgoX1QaRg_FvOvUilE6xGJ_nRnLSk1VjdTWJFARu-IGEfed0aQ1mCsh5Hq9e2q55iQGlMUQVIHQ7X6ZzXNHu4QW7jSMYLskP7Vg-EjQizrGvHC04S2SsglZ0znm5eyz0KR71zcdkR2O80_v6u2GM6APMCQrJ3GQ7SSsguwEKMxxvCpQvZ65lCV4bujvP7tD4A8pn_SCKTjFGU8MulGzHi2z-K4EZU6yZXFQ.DOkezaeJUsRg6h8q.oXC0AtIkn53UgjF2KdxW.1NBMp9IdXW-U5emoiIrE7A
decrypted payload node jose :{"test":"test"}
node crypto decrypt :
const crypto = require('crypto');
const testJwk = {
"p": "wP8U4hF_Dyj-Z-YY_89gmT1ngnKER_0e_WTxOS5SWQp-giImmsCEVpP9mqKrwHBrkPJenFwa8kIxNgmJtGSuXkNMd8NoHdqzwtq50-mMCV1oDAIjXl0UUX8_CdVyy9TFe3-P5lJuQoOIRlCs_hvVJ_4x3fT9Xipo85OMoPTmwoM",
"kty": "RSA",
"q": "uk0U0Wx9GfpyLLJhJSJ1gyx5M31kAP9vMGQLrp0P40g-BOl7YYusv8H1EVcscZQuMrNBQvlIyJRTx229r_Wz961YqQ6fw-hJetQQ08CiLj8AuYMm6KHNbEyhzFV8lFyIZCwsDXbA-Ek6F5FLfOqfeFHjo0fOq77R7kcp8cepcP0",
"d": "bd1sMKhIu3QDbEvzrb8cj6f0egUQQU0S1M53okm9ByBWYOFCNPayoluvO9BWBBY2kMsIWQ64q7-sQgjc894yaffppF4Cz0jFCzIIIgoWmT4L4syQkdiiV7xoIrbKvZkloH7bSATf6QioxkcX-9Dz4Z5VHUg0GKzFkK74a655odc-VzlDp7cbrfeL29ETlbb9864IJIoC_hAhzfaVOHCMb8y5Rr5UTgGjf1QlYjSjMhE5L0BOty4QeyxDoy_dI7sosJPj9tVFQCa6yDJaOAFF_h3QTLyhXU1qL6GJbG_JklsTn5z93Vy8haVyf3FvHDW2PVkKy46HRTINpB72QWkV-Q",
"e": "AQAB",
"use": "enc",
"kid": "tester",
"qi": "tItxE9gwrpmifSWur8DdsaMZN7rylhhR18PIgtZaHhw58i9EBRpzW_CMZIHuzl8-ujh8ZsuUsmU2HbYX0VWZnPty3z_-hCWV1DjoElQ93WThcQa0HviIVSxbXEtYoLoNM_gzIFjihICGCtXhlF9Di3C4FKcCP0dYzYWpVFq4uac",
"dp": "M8j-IH7TWg0E3noWQSWy5MteJ9l0dyCLHTDlrRMp02yGb4KcWy_HErgY91IoxbUkl7sA-fGY5WIvdDFw-q99PhvOu9_54vDZBTLNY_gptCWVEovMU7ikCA4dqxTT_a904eNjiEib_0rt2PgywuhS9K03Ujg3d_nnOVxhAptUA-M",
"alg": "RSA-OAEP-256",
"dq": "NsOr5_gNOlK9t1fkaKcdhibPpgwpFoX_6Giwam7vGa_F02nTBBSr_l6ErMlEXkrh3bOF7qsa8yNvEUO4K_59HcSOOHv9CPjCiOHH5IdO5WtNyjq8eEv_9-L6-Pb0PSSKT3AQrxCGnzXfZsgmOZ06rYLc-MWGAkSAr5upv9Iig_0",
"n": "jHNxl1hhNxvYEn5PPObRia7LM6_koGcrcHLgWVVc-zU5loWn33xdd3R3EPs10ZwrhRBmXthN1WLFB0V4w-1QrGSM5wuBm2AqIFglDaYWW7d_aFCYMubCC6YiKYgrXezZtjngGtjBJPNwov4PC6KJgh7xwtqt5MTXX7TH8H6BhvQvNiD_IEH_vxF9hEhN-f5wKR6yNGlCT3X0NWwUiavG0vgtW0y1g6BHUskA1HogdrpURAfmcSSrDya9IoYjlAmUmql-0JGeEJIU53hoDB0ZVNREZbhJxZLN1hV8KrYeVPjfHCjXaJ6-Fw9MvnC9m-FGiX9dvE1A-Yp5sowUOqKLdw"
}
// node crypto generated JWE ciphertext part - decrypt with node crypto code : success
const nodeCryptoEncPayload = "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoidGVzdGVyIn0.FHEgUliMX84RcU_Vh50VsyvpradjaUclFyXpyTikqx6bhsBJ3KCUiE34plNNtI_e-CdU-NQEH-Aa7lhkpW4WsVoBi_2WO7IeIZ_Q2RkASlyB4Br0U98ExA6_vXzmZ1RtWaXsvcsRSpcwQTc5fmAah2eOG0A0glxYqf-dwM7PjOoIdP0ryc-6jrp5w1d2dE3ImoC-pMyOLTagsDj5iVUtIq8Y4xCFdXh4J1WYSZ6UAnyxYY0ctPVTv3NXJvlcbccy_4GsqgpSIk0Whcun0Jyjr0jPXw5Jaxl50VVW2Me8srgTSbFWugqEAfxkgVQHu2GDwd60oYyznMrfmYI8t4Q8lQ.8yeQaqpcresDf6Gy.7Bz_yj77Vfa1QTlfKZD1.ODRr0itIlYurZ3bbNH47jQ"
// node jose generated JWE ciphertext part - decrypt with node crypto code : error
const nodejoseEncPayload = "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJraWQiOiJ0ZXN0ZXIiLCJlbmMiOiJBMjU2R0NNIn0.Dw_EDEJnahKdk8mwlY3juSWd4jTIM8Go8NdQLpLWeH2kCmDHNg0RZKvbINEc3jF70bPECCzuIi_fH3ovlUeQ_o7KqbF_5ZMiyOIYQvfsVlgSZpl1YSRHgoX1QaRg_FvOvUilE6xGJ_nRnLSk1VjdTWJFARu-IGEfed0aQ1mCsh5Hq9e2q55iQGlMUQVIHQ7X6ZzXNHu4QW7jSMYLskP7Vg-EjQizrGvHC04S2SsglZ0znm5eyz0KR71zcdkR2O80_v6u2GM6APMCQrJ3GQ7SSsguwEKMxxvCpQvZ65lCV4bujvP7tD4A8pn_SCKTjFGU8MulGzHi2z-K4EZU6yZXFQ.DOkezaeJUsRg6h8q.oXC0AtIkn53UgjF2KdxW.1NBMp9IdXW-U5emoiIrE7A"
// Step 1 : Split JWE string
// const jweParts = nodejoseEncPayload.split(".");
const jweParts = nodeCryptoEncPayload.split(".");
const jweProtectedHeaderPart = jweParts[0];
const jweEncryptedKeyPart = jweParts[1];
const jweIvPart = jweParts[2];
const jweCipherTextPart = jweParts[3];
const jweAuthTagPart = jweParts[4];
//step 2 : Decrypt CEK
const sk = crypto.createPrivateKey({ key: testJwk, format: 'jwk',encoding: "utf-8" });
const decryptedCek = crypto.privateDecrypt({key:sk,oaepHash: 'sha256',padding: crypto.constants.RSA_PKCS1_OAEP_PADDING},
Buffer.from(jweEncryptedKeyPart,"base64url")
);
//step 3 : Decrypt Content
const dataToDecryptPart = jweCipherTextPart.slice(0, jweCipherTextPart.length - jweCipherTextPart.length);
const deCipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(decryptedCek, 'base64url'), Buffer.from(jweIvPart, 'base64url'));
deCipher.setAuthTag(Buffer.from(jweAuthTagPart, 'base64url'))
deCipher.setAAD(Buffer.from(jweProtectedHeaderPart, 'base64url'));
let decrypted = deCipher.update(Buffer.from(jweCipherTextPart, 'base64url'), null, 'utf8');
decrypted += deCipher.final('utf8');
console.log("Decrypted text : "+decrypted)
**Console output**
Decrypted text : {"test":"test"}
node crypto decrypt code block work with node crypto generated JWE nodeCryptoEncPayload but errors out for node jose generated JWE nodejoseEncPayload is used
The header must be passed as AAD as is, i.e. Base64 encoded. The bug in the current code is the Base64 decoding. Fix:
...
deCipher.setAAD(Buffer.from(jweProtectedHeaderPart, 'utf8'));
...
With this fix decryption of the JOSE token works.
A few inconsistencies, but without impact: The components of the token are actually Base64url encoded. Therefore, it would be cleaner to apply Base64url, i.e. the identifier base64url
instead of base64
. Although the crypto module fixes this under the hood, it would be clearer with the correct identifier.
Additionally, decryptedCek
is already a buffer and does not need to be Base64 decoded (which, however, is also implicitly fixed by the crypto module).
Also, as far as I know, padding is automatically disabled for GCM, so that explicit disabling with setAutoPadding()
is not required.