Search code examples
encryptioncompatibilitynode-cryptonode-jose

decrypt error - aes-256-gcm/A256GCM encrypted using node jose and decrypt using node crypto


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


Solution

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