How to zlib deflate a string in browser - javascript

I have in my backend a simple system for encrypting and compressing data.
Backend NodeJS
export const aesEncrypt = (text: string, key: string = ENCRYPTION_KEY) => {
let iv = randomBytes(IV_LENGTH);
let cipher = createCipheriv('aes-256-cbc', Buffer.from(key), iv);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString('hex') + ':' + encrypted.toString('hex');
}
export const textCompressToBase64Url = (text: string) => {
return deflateSync(text).toString('base64url')
}
export const packDataForUrl = (data: string, key: string = ENCRYPTION_KEY) => {
return textCompressToBase64Url(aesEncrypt(data, key))
}
Result string:
eJwVU8cRxDAIbIkowXUjUv8lHP55BoM2Bp_W59hmGWH0DrxKpMlumMlfgAKD2DBKEx0gJs9j2ll-D7Gfl2PAgn1dPN696eDor3dWdR4iHH3hGQRKqKVy790Dneo7VZMEelNVag-n4rKAjhn4NNCiMmqwQ7QHMjgfkk0Rd3GlyviJuxz2zJWh6-_oAd4z9WHQPMhXgQ7P3LkegZHMt9-8h_IA-qSfm5LVIxn3PMJ0DEZSefcNW4CVKMluHkfhqjdIyXSPPWdeJGeJUWjInVq4E3tCClEQq2NSDFfA1QjSi71twSRRoMMk7GtceDIzTCHm9iqjFcB3D0U-N6tTIdnnkpnf_VpHDBJ5VnVZas7DQMVUrUu5TWXZV81j378DjVsYsGhdI7j7my6mFT6HH5dxxusT6_CiKpISF-u3A8U4O-aguJbnMapTBa0R8qEf6iK_GsZoZ89ZuTkBNoCazYWwUR8nQ0jBq9xu6Vr70nMKwAHeXWSNhI4qOS6QjSvtDg_iLoQVRmfAy_RVv3yN7buUFS6MiKCf0yVsgqr8KqYYXf0zR56utsdzXLFGauMhJ7QggHSphe0bwH1DyuQJT-DGAyfgUltmP4JcPUJ0akPPB0NTW6bV-61OIGcSV7N1zcik1_67QIH1c6i3An5X41WgZZfugnkWRxYF-2LxMuskueL8dNV_z7Ub5StKPVmZ2e52Y5M3I905SlaVdG2rF9X5dFg307uxiak2vgYSruvggU07bhP5wV273c6-Z65Zd_mjb0HXXs7pe-mrA-lsHzQZNvp8z2jt4rrID89spGGlyLdsagOyWd-eX13TX1DmNhnRdkMzt0NRHivqORe3nH_9zDtg
I then try to use Pako (port of zlib for browser) in the frontend to deflate before doing a AES decrypt however i keep getting various errors:
export const textDecompressFromBase64Url = (text: string) => {
//#ts-ignore
const sanitizedbase64 = text.replace(/_/g, '/').replace(/-/g, '+')
const testData = convertDataURIToBinary(sanitizedbase64)
const inflated = inflate(testData).toLocaleString()
return inflated
}
const convertDataURIToBinary = (base64Data: string) => {
const raw = window.atob(base64Data);
const rawLength = raw.length;
let array = new Uint8Array(new ArrayBuffer(rawLength));
for (let i = 0; i < rawLength; i++) {
array[i] = raw.charCodeAt(i);
}
return array;
}
If I don't use { raw: true } I get the following which is not usable in my aes function in the inflate method:
98,51,54,101,53,97,57,49,101,56,56,99,98,98,56,50,97,54,48,97,100,99,49,50,102,99,101,101,48,102,102,99,58,98,48,53,48,51,48,52,56,102,51,49,52,101,50,50,54,48,50,51,50,57,99,54,56,53,101,99,100,57,55,54,50,51,57,54,97,99,102,56,48,51,52,49,101,55,57,52,57,98,97,55,55,99,57,48,57,49,57,97,101,51,57,54,100,100,54,97,49,49,48,54,53,97,98,57,99,98,50,48,53,50,49,53,100,53,52,55,55,55,53,101,99,101,99,53,57,97,49,49,53,56,52,99,48,50,97,102,100,100,100,53,56,97,49,102,100,98,55,51,52,48,53,102,56,56,48,57,102,101,48,50,56,50,97,56,50,101,48,56,54,50,50,50,48,53,99,98,51,99,97,49,50,56,102,100,50,51,101,100,51,100,99,53,52,102,57,54,98,55,57,49,101,52,99,48,55,52,102,50,55,57,97,54,53,54,48,51,98,55,51,100,101,51,57,54,53,99,54,49,51,55,53,48,50,54,51,102,102,55,102,55,57,98,98,49,98,99,51,51,55,101,97,102,97,97,49,52,97,48,48,101,54,99,57,54,55,99,52,99,100,101,102,52,99,98,55,54,97,50,49,99,57,49,98,51,49,50,53,52,97,55,97,102,51,56,98,48,56,100,52,53,50,52,98,99,51,54,57,49,52,51,100,100,97,102,49,50,99,51,50,55,54,56,97,57,51,51,51,100,99,54,53,52,55,50,98,53,98,52,55,102,100,48,56,54,102,98,99,57,49,52,100,49,49,52,49,49,100,101,98,102,99,52,56,49,50,54,48,52,57,98,48,99,57,100,51,57,101,56,55,102,55,99,50,50,98,49,57,48,102,99,48,49,98,51,51,100,49,54,99,99,99,98,56,53,48,98,102,55,101,49,53,56,53,100,98,48,51,55,98,99,57,98,99,97,57,56,56,100,54,100,98,52,99,101,54,55,50,56,56,57,55,52,99,101,50,50,54,56,48,99,49,51,102,55,55,99,52,102,55,57,57,51,102,51,48,50,100,51,50,100,101,53,55,53,48,101,56,53,52,99,54,49,100,100,102,97,51,57,99,101,50,98,49,56,51,101,52,51,48,49,100,50,97,99,102,50,48,55,100,101,53,53,50,54,48,100,53,56,99,102,51,97,51,100,56,51,99,98,97,101,54,98,99,54,56,49,57,48,100,50,52,100,52,57,52,56,101,97,100,56,51,53,49,98,54,56,51,99,51,98,50,98,55,56,99,54,97,51,49,53,57,50,100,98,50,102,100,50,52,99,48,49,98,102,50,101,100,50,57,55,53,98,56,51,49,56,54,99,102,51,56,100,57,56,57,50,48,49,101,48,48,53,56,56,102,55,48,98,56,102,53,57,102,57,50,56,49,48,99,52,49,55,53,51,101,57,56,99,57,53,100,49,57,48,97,57,50,98,48,49,102,48,51,51,49,56,49,51,53,98,99,48,101,98,100,100,52,54,57,52,48,99,101,49,100,99,54,48,49,102,54,49,49,101,57,56,98,56,100,49,98,101,99,98,48,97,99,99,57,51,50,55,100,57,48,50,54,101,55,100,51,50,53,48,55,48,102,52,52,52,49,57,54,54,101,100,52,51,56,52,49,53,53,51,97,100,98,102,100,51,49,57,53,57,52,55,50,98,52,97,53,53,48,98,54,57,99,102,57,53,49,100,102,52,100,57,54,53,52,54,98,53,100,48,98,48,50,53,98,50,98,98,56,101,57,56,48,51,101,55,98,52,100,56,52,97,52,51,102,98,49,48,101,54,49,102,98,48,55,50,101,56,99,99,101,97,50,48,99,97,100,56,98,52,53,102,100,52,49,101,51,54,49,98,53,99,53,101,52,102,101,53,57,101,97,56,51,53,48,52,54,102,99,49,101,97,100,99,57,98,56,50,56,52,101,49,49,100,55,50,55,100,48,51,53,102,55,57,57,101,50,49,53,57,55,101,54,98,49,55,53,101,52,101,53,57,55,99,102,57,97,56,98,54,52,53,100,48,51,57,98,53,100,57,100,56,56,101,99,50,52,55,52,57,51,97,53,56,101,97,97,97,57,53,101,101,49,52,56,52,99,48,100,97,52,100,50,57,51,56,55,57,102,101,54,56,97,102,102,52,101,101,99,102,53,50,56,100,100,99,50,55,56,57,48,57,98,100,101,99,97,53,102,51,53,99,57,54,56,52,99,100,98,52,100,101,56,51,55,56,48,52,98,57,53,56,51,99,54,48,54,99,57,49,53,50,49,51,97,48,55,99,98,97,57,56,54,56,56,101,56,57,53,99,100,55,98,98,56,49,57,100,53,52,54,97,51,51,99,102,101,55,55,50,52,99,98,55,50,53,102,50,49,99,53,99,51,48,54,57,49,51,55,54,102,53,100,55,99,98,49,48,99,51,97,49,54,102,54,48,52,48,102,100,52,99,97,101,52,101,100,51,99,51,100,101,98,48,53,50,55,53,53,56,56,97,98,50,99,99,53,102,56,49,49,56,100,55,99,53,99,99,56,100,52,98,100,57,98,102,98,49,54,54,55,49,51,57,54
I have try to convert this to hex as I do in the backend but it is not working
If I pass the {raw: true} I get the following:
invalid code lengths set
In the backend however i'm able to inflate and decrypt with zlib with no issue:
export const textDecompressFromBase64Url = (text: string) => {
return inflateSync(Buffer.from(text, 'base64')).toString()
}
export const aesDecrypt = (text: string, key: string = ENCRYPTION_KEY) => {
let textParts = text.split(':');
let iv = Buffer.from(textParts.shift(), 'hex');
let encryptedText = Buffer.from(textParts.join(':'), 'hex');
let decipher = createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
}
From the comments it is said that this is impossible to make it work but here is a working example in nodejs of the full circle:
https://jdoodle.com/ia/s5Y
TIMELINE:
Stringify object
AES encrypt the string + add the IV in front in format of hex string
Deflate the string with zlib and output as base64url
Inflate the string with zlib
Decrypt the string with AES
The issue is using pako I'm not able to execute step 4

Related

crypto.subtle decrypting compact jwe with ECDH-ES+A128KW - The operation failed for an operation-specific reason

I am trying to decrypt a compact JWE formatted response message with the crypto.subtle libraries.
I am sending to the Server my public key in JWK format with curve algo ECDH-ES+A128KW, encryption A256GCM, curve name P-256.
The server sends me back a compact JWE response.
As I understand this flow, it should be something like:
Client sends the public key to the Server
Server responds to client back the compact JWE message
Client derives the shared AES 128 KW key based on servers public key and own private key
Client unwraps the AES 128 GCM key using the shared AES 128 KW key
Clients decrypts the ciphertext using the AES 128 GCM key.
When my code reaches the unwrapKey step, i am only getting the error The operation failed for an operation-specific reason. At the moment I fail to find the problem.
My code looks like this right now:
export const decryptCompactJWE = async (
compactJWE: string,
privateKey: CryptoKey
) => {
const [protectedHeader, encryptedKey, iv, ciphertext, tag] =
compactJWE.split(".");
const header = JSON.parse(Buffer.from(protectedHeader, "base64").toString());
console.log("header:", header);
const publicKey = await crypto.subtle.importKey(
"jwk",
header.epk,
{
name: "ECDH",
namedCurve: "P-256",
},
true,
["deriveKey", "deriveBits"]
);
const derivedKey = await crypto.subtle.deriveKey(
{ name: "ECDH", public: publicKey },
privateKey,
{ name: "AES-KW", length: 128 },
true,
["unwrapKey"]
);
const myJWK = await crypto.subtle.exportKey("jwk", derivedKey);
console.log("jwk", myJWK);
const myAESKey = await crypto.subtle.unwrapKey(
"raw",
Buffer.from(encryptedKey, "base64url"),
derivedKey,
"AES-KW",
{ name: "AES-GCM" },
false,
["decrypt"]
);
console.log(myAESKey);
return crypto.subtle.decrypt(
{ name: "AES-GCM", iv: Buffer.from(iv, "base64url") },
myAESKey,
Buffer.from(ciphertext, "base64url")
);
};
Here is my test data:
const privateKey = {
kty: "EC",
crv: "P-256",
ext: true,
key_ops: ["deriveKey", "deriveBits"],
d: "vPZxnkg-j1xZ_8BZfH6jIvV52NvG2pxsZhmYgI9BEec",
x: "CorZZG9qa5korQ6eVLenbFz2QyGKkpoEYlAJxF1JzGA",
y: "yIEnQSGlMNVp6JEzZO3QvjQ0UDAwepzUZqwgsv0OTQE",
};
const JWE_RESPONSE = "eyJhbGciOiJFQ0RILUVTK0ExMjhLVyIsImVuYyI6IkExMjhHQ00iLCJraWQiOiJhYmMxMjMiLCJlcGsiOnsia3R5IjoiRUMiLCJ4IjoiNmNReW1GUlJSTjVkVHdoOHA5dWx1NkgwS3paSkRGcm4xdjFKb2NzVURCUSIsInkiOiJTSGliQjFEMnBHMmVMbUxMV09HTTB4UUtCRDFpM3ZtZjJRNjZIM2RnbzJ3IiwiY3J2IjoiUC0yNTYifX0.OwriqBm-PXkIj_QwbqKZRVxql0sja2-p.UrZs5Ixu_rFCxpCw.z9Rfhw.m6AgqKsttsp9TV2dREgbWw";
So far I looked up a all examples I could find to implement this and based on those it kinda looks okay. The debugger is not stepping into the native crypto.subtle code and the error message is also not telling much about what is going wrong for me. The existing examples I found so far, are mostly less complex and skip the key derive part.
WebCrypto is a low level API that in particular does not support JWT/JWS/JWE, so decrypting the token with WebCrypto alone means a corresponding effort, since some functionalities have to be implemented by yourself.
According to the header, the token is encrypted with:
alg: "ECDH-ES+A128KW"
enc: "A128GCM"
Here ECDH-ES+A128KW means that a shared secret is derived with ECDH, from which a wrapping key is determined using Concat KDF. With this key the encrypted key is unwrapped using AES-KW. Finally, the unwrapped key is applied to decrypt the content using AES-128/GCM, see here.
In the posted code Concat KDF is not taken into account. This and some other issues are the reason why decryption fails. Since WebCrypto does not support Concat KDF, a custom implementation is needed (or an additional library), which affects the whole implementation.
The following changes and fixes are required in the individual processing steps:
Deriving the shared secret
One of the inputs to Concat KDF is the shared secret. First, the private and the public key involved are imported. Then the shared secret can be determined most efficiently with deriveBits().
The gives as shared secret (hex encoded):
832bb9a5ac5c1b7febc64ed9522aefedd9f5d62830972224b1226e5498a6d13a
Keep in mind here:
The shared secret for P-256 is 32 bytes in size.
When importing the public key, no key usages may be specified.
(async () => {
// input data: encrypted token and private JWK
const compactJWE = "eyJhbGciOiJFQ0RILUVTK0ExMjhLVyIsImVuYyI6IkExMjhHQ00iLCJraWQiOiJhYmMxMjMiLCJlcGsiOnsia3R5IjoiRUMiLCJ4IjoiNmNReW1GUlJSTjVkVHdoOHA5dWx1NkgwS3paSkRGcm4xdjFKb2NzVURCUSIsInkiOiJTSGliQjFEMnBHMmVMbUxMV09HTTB4UUtCRDFpM3ZtZjJRNjZIM2RnbzJ3IiwiY3J2IjoiUC0yNTYifX0.OwriqBm-PXkIj_QwbqKZRVxql0sja2-p.UrZs5Ixu_rFCxpCw.z9Rfhw.m6AgqKsttsp9TV2dREgbWw";
const privateKey = {
kty: "EC",
crv: "P-256",
ext: true,
key_ops: ["deriveKey", "deriveBits"],
d: "vPZxnkg-j1xZ_8BZfH6jIvV52NvG2pxsZhmYgI9BEec",
x: "CorZZG9qa5korQ6eVLenbFz2QyGKkpoEYlAJxF1JzGA",
y: "yIEnQSGlMNVp6JEzZO3QvjQ0UDAwepzUZqwgsv0OTQE",
};
const [protectedHeader, encryptedKey, iv, ciphertext, tag] = compactJWE.split(".");
// import private key and public key (header.epk)
const privateCryptoKey = await crypto.subtle.importKey(
"jwk",
privateKey,
{name: "ECDH", namedCurve: "P-256"},
false,
["deriveBits"]
);
const decoder = new TextDecoder();
const header = JSON.parse(decoder.decode(b64url2ab(protectedHeader)));
const publicCryptoKey = await crypto.subtle.importKey(
"jwk",
header.epk,
{name: "ECDH", namedCurve: "P-256"},
false,
[]
);
// ECDH: derive shared secret (size: 32 bytes for P-256)
const sharedSecret = await crypto.subtle.deriveBits(
{ name: "ECDH", public: publicCryptoKey },
privateCryptoKey,
256
);
console.log("ECDH - shared secret: " + ab2hex(sharedSecret)); // ECDH - shared secret: 832bb9a5ac5c1b7febc64ed9522aefedd9f5d62830972224b1226e5498a6d13a
})();
// Helper -------------------------------------------------------------------------------------------------------
function b64url2ab(base64_string){
base64_string = base64_string.replace(/-/g, '+').replace(/_/g, '/');
return Uint8Array.from(window.atob(base64_string), c => c.charCodeAt(0));
}
function ab2hex(ab) {
return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
}
Determining the wrapping key
From the shared secret the 16 bytes wrapping key can now be derived with Concat KDF. Concat KDF is described in Section 5.8.1 of NIST.800-56A. A JavaScript implementation can be found e.g. here.
Concat KDF has a number of other input data in addition to the shared secret, which are described here and illustrated here with an example. These are:
the algorithm, here: ECDH-ES+A128KW
the length of the output key (in bits), here: 128
the Base64url decoding of the "apu" (Agreement PartyUInfo) header parameter, if present, here: not present
the Base64url decoding of the "apv" (Agreement PartyVInfo) header parameter, if present, here: not present
This gives as wrapping key (hex encoded):
64c845c913d6a61208464a087ce72b81
(async () => {
const sharedSecret = hex2ab("832bb9a5ac5c1b7febc64ed9522aefedd9f5d62830972224b1226e5498a6d13a").buffer;
const encoder = new TextEncoder();
const algorithm = encoder.encode('ECDH-ES+A128KW'); // from header.alg
const keyLength = 128;
const apu = '';
const apv = '';
const otherInfo = concat(
lengthAndInput(algorithm),
lengthAndInput(apu),
lengthAndInput(apv),
uint32be(keyLength),
);
const wrappingKey = await concatKdf(new Uint8Array(sharedSecret), keyLength, otherInfo);
console.log("Concat KDF - wrapping key: " + ab2hex(wrappingKey)); // Concat KDF - wrapping key: 64c845c913d6a61208464a087ce72b81
})();
// Concat KDF implementation -------------------------------------------------------------------------------------------------------
function writeUInt32BE(buf, value, offset) {
buf.set([value >>> 24, value >>> 16, value >>> 8, value & 0xff], offset);
}
function uint32be(value) {
const buf = new Uint8Array(4);
writeUInt32BE(buf, value);
return buf;
}
async function concatKdf(secret, bits, value) {
const iterations = Math.ceil((bits >> 3) / 32);
const res = new Uint8Array(iterations * 32);
for (let iter = 0; iter < iterations; iter++) {
const buf = new Uint8Array(4 + secret.length + value.length);
buf.set(uint32be(iter + 1));
buf.set(secret, 4);
buf.set(value, 4 + secret.length);
res.set(Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', buf))), iter * 32);
}
return res.slice(0, bits >> 3);
}
function concat(...buffers) {
const size = buffers.reduce((acc, { length }) => acc + length, 0);
const buf = new Uint8Array(size);
let i = 0;
buffers.forEach((buffer) => {
buf.set(buffer, i);
i += buffer.length;
});
return buf;
}
function lengthAndInput(input) {
return concat(uint32be(input.length), input);
}
// Helper -------------------------------------------------------------------------------------------------------
function hex2ab(hex){
return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {return parseInt(h, 16)}));
}
function ab2hex(ab) {
return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
}
Note that aside from the sample data in this question, the Concat KDF implementation adapted for above code has not been tested further!
Unwrapping of the encrypted key and decryption of the ciphertext
After importing the wrapping key, the encrypted key can be unwrapped (AES-KW). With the unwrapped key the ciphertext can be decrypted (AES-128, GCM).
The gives as decrypted data (UTF-8 decoded):
8807
Note regarding AES/GCM that:
WebCrypto expects as ciphertext the concatenation of the actual ciphertext and the tag.
The token header has to be specified as additional authenticated data (AAD) (otherwise authentication fails).
(async () => {
const wrappingKey = hex2ab("64c845c913d6a61208464a087ce72b81").buffer;
// input data: encrypted token and private JWK
const compactJWE = "eyJhbGciOiJFQ0RILUVTK0ExMjhLVyIsImVuYyI6IkExMjhHQ00iLCJraWQiOiJhYmMxMjMiLCJlcGsiOnsia3R5IjoiRUMiLCJ4IjoiNmNReW1GUlJSTjVkVHdoOHA5dWx1NkgwS3paSkRGcm4xdjFKb2NzVURCUSIsInkiOiJTSGliQjFEMnBHMmVMbUxMV09HTTB4UUtCRDFpM3ZtZjJRNjZIM2RnbzJ3IiwiY3J2IjoiUC0yNTYifX0.OwriqBm-PXkIj_QwbqKZRVxql0sja2-p.UrZs5Ixu_rFCxpCw.z9Rfhw.m6AgqKsttsp9TV2dREgbWw";
const [protectedHeader, encryptedKey, iv, ciphertext, tag] = compactJWE.split(".");
// Import wrapping key, decrypt wrapped key:
const wrappingCryptoKey = await crypto.subtle.importKey(
"raw",
wrappingKey,
"AES-KW",
false,
["unwrapKey"]
);
const unwrappedCryptoKey = await crypto.subtle.unwrapKey(
"raw",
b64url2ab(encryptedKey),
wrappingCryptoKey,
"AES-KW",
{ name: "AES-GCM" },
false,
["decrypt"]
);
// Decrypt ciphertext
// - Concatenate ciphertext and tag: ciphertext|tag
// - Consider header as AAD
const encoder = new TextEncoder();
const ciphertextAB = new Uint8Array(b64url2ab(ciphertext));
const tagAB = new Uint8Array(b64url2ab(tag));
const ciphertextTag = new Uint8Array(ciphertextAB.length + tagAB.length);
ciphertextTag.set(ciphertextAB);
ciphertextTag.set(tagAB, ciphertextAB.length);
const additionalData = encoder.encode(protectedHeader);
const decryptedText = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: b64url2ab(iv), additionalData: additionalData },
unwrappedCryptoKey,
ciphertextTag
);
const decoder = new TextDecoder();
console.log("Decrypted text: " + decoder.decode(decryptedText)); // Decrypted text: 8807
})();
// Helper -------------------------------------------------------------------------------------------------------
function hex2ab(hex){
return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {return parseInt(h, 16)}));
}
function b64url2ab(base64_string){
base64_string = base64_string.replace(/-/g, '+').replace(/_/g, '/');
return Uint8Array.from(window.atob(base64_string), c => c.charCodeAt(0));
}
All together:
(async () => {
// input data: encrypted token and private JWK
const compactJWE = "eyJhbGciOiJFQ0RILUVTK0ExMjhLVyIsImVuYyI6IkExMjhHQ00iLCJraWQiOiJhYmMxMjMiLCJlcGsiOnsia3R5IjoiRUMiLCJ4IjoiNmNReW1GUlJSTjVkVHdoOHA5dWx1NkgwS3paSkRGcm4xdjFKb2NzVURCUSIsInkiOiJTSGliQjFEMnBHMmVMbUxMV09HTTB4UUtCRDFpM3ZtZjJRNjZIM2RnbzJ3IiwiY3J2IjoiUC0yNTYifX0.OwriqBm-PXkIj_QwbqKZRVxql0sja2-p.UrZs5Ixu_rFCxpCw.z9Rfhw.m6AgqKsttsp9TV2dREgbWw";
const privateKey = {
kty: "EC",
crv: "P-256",
ext: true,
key_ops: ["deriveKey", "deriveBits"],
d: "vPZxnkg-j1xZ_8BZfH6jIvV52NvG2pxsZhmYgI9BEec",
x: "CorZZG9qa5korQ6eVLenbFz2QyGKkpoEYlAJxF1JzGA",
y: "yIEnQSGlMNVp6JEzZO3QvjQ0UDAwepzUZqwgsv0OTQE",
};
const [protectedHeader, encryptedKey, iv, ciphertext, tag] = compactJWE.split(".");
// import private key and public key (header.epk)
const privateCryptoKey = await crypto.subtle.importKey(
"jwk",
privateKey,
{name: "ECDH", namedCurve: "P-256"},
false,
["deriveBits"]
);
const decoder = new TextDecoder();
const header = JSON.parse(decoder.decode(b64url2ab(protectedHeader)));
const publicCryptoKey = await crypto.subtle.importKey(
"jwk",
header.epk,
{name: "ECDH", namedCurve: "P-256"},
false,
[]
);
// ECDH: derive shared secret (size: 32 bytes for P-256)
const sharedSecret = await crypto.subtle.deriveBits(
{ name: "ECDH", public: publicCryptoKey },
privateCryptoKey,
256
);
// Concat KDF: determine wrapping key
const encoder = new TextEncoder();
const algorithm = encoder.encode('ECDH-ES+A128KW'); // from header.alg
const keyLength = 128;
const apu = '';
const apv = '';
const otherInfo = concat(
lengthAndInput(algorithm),
lengthAndInput(apu),
lengthAndInput(apv),
uint32be(keyLength),
);
const wrappingKey = await concatKdf(new Uint8Array(sharedSecret), keyLength, otherInfo);
// import wrapping key, decrypt wrapped key:
const wrappingCryptoKey = await crypto.subtle.importKey(
"raw",
wrappingKey,
"AES-KW",
false,
["unwrapKey"]
);
const unwrappedCryptoKey = await crypto.subtle.unwrapKey(
"raw",
b64url2ab(encryptedKey),
wrappingCryptoKey,
"AES-KW",
{ name: "AES-GCM" },
false,
["decrypt"]
);
// decrypt ciphertext
// - Concatenate ciphertext and tag: ciphertext|tag
// - Consider header as AAD
const ciphertextAB = new Uint8Array(b64url2ab(ciphertext));
const tagAB = new Uint8Array(b64url2ab(tag));
const ciphertextTag = new Uint8Array(ciphertextAB.length + tagAB.length);
ciphertextTag.set(ciphertextAB);
ciphertextTag.set(tagAB, ciphertextAB.length);
const additionalData = encoder.encode(protectedHeader);
const decryptedText = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: b64url2ab(iv), additionalData: additionalData },
unwrappedCryptoKey,
ciphertextTag
);
console.log("Decrypted text: " + decoder.decode(decryptedText)); // Decrypted text: 8807
})();
// Concat KDF implementation -------------------------------------------------------------------------------------------------------
function writeUInt32BE(buf, value, offset) {
buf.set([value >>> 24, value >>> 16, value >>> 8, value & 0xff], offset);
}
function uint32be(value) {
const buf = new Uint8Array(4);
writeUInt32BE(buf, value);
return buf;
}
async function concatKdf(secret, bits, value) {
const iterations = Math.ceil((bits >> 3) / 32);
const res = new Uint8Array(iterations * 32);
for (let iter = 0; iter < iterations; iter++) {
const buf = new Uint8Array(4 + secret.length + value.length);
buf.set(uint32be(iter + 1));
buf.set(secret, 4);
buf.set(value, 4 + secret.length);
res.set(Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', buf))), iter * 32);
}
return res.slice(0, bits >> 3);
}
function concat(...buffers) {
const size = buffers.reduce((acc, { length }) => acc + length, 0);
const buf = new Uint8Array(size);
let i = 0;
buffers.forEach((buffer) => {
buf.set(buffer, i);
i += buffer.length;
});
return buf;
}
function lengthAndInput(input) {
return concat(uint32be(input.length), input);
}
// Helper -------------------------------------------------------------------------------------------------------
function b64url2ab(base64_string){
base64_string = base64_string.replace(/-/g, '+').replace(/_/g, '/');
return Uint8Array.from(window.atob(base64_string), c => c.charCodeAt(0));
}

How to get the output string from words array and sigBytes from the decoded object in Crypto JS in Javascript or Node JS?

I am trying to get json decrypted and base 64 decoded stringified payload coming from API. but after using CryptoJS.AES.decrypt I am reciving following as a object.below given is the original payload, key, initVector and encryptedString of JSON.stringify(payload).
iv: 1110000348111000
key: 8880002784888000
Original: {"msg":"This is encrypted using AES"}
Encrypted: QxpJspQ5AjeX0HK3uLjSNLZK/i8VWxcXfjpMLghZrSKui/m5XNO5at0jvsI4qGMC
Below is the output of CryptoJS.AES.decrypt function
Decrypted: {
words: [
-1965411475, 1913178924,
601984393, -1493862515,
1415172454, -1789306172,
750380297, -1517138640,
-934802361, 1164290040,
-1702889832, 1716466309
],
sigBytes: -85
}
Below is my encryption code
encrypt.js
const encryptPayload = (customerId, payload) => {
const initializationVector = CryptoJS.enc.Utf8.parse(
initVectorGenerator(customerId)
);
const keyForEncryption = CryptoJS.enc.Utf8.parse(keyGenerator(customerId));
// console.log(initializationVector, keyForEncryption);
console.log(JSON.stringify(payload));
const encryptedPayload = CryptoJS.AES.encrypt(
JSON.stringify(payload),
keyForEncryption,
{
iv: initializationVector,
mode: CryptoJS.mode.CBC,
}
);
const encryptedStr = CryptoJS.enc.Base64.stringify(
encryptedPayload.ciphertext
);
console.log(encryptedStr);
};
Below is my encryption code
decrypt.js
const decryptPayload = (customerId, payloadStr) => {
const initializationVector = CryptoJS.enc.Utf8.parse(
initVectorGenerator(customerId)
);
const keyForDecryption = CryptoJS.enc.Utf8.parse(keyGenerator(customerId));
// console.log(initializationVector, keyForDecryption);
// console.log(payloadStr, "atob(): ", atob(payloadStr));
const decryptedPayload = CryptoJS.AES.decrypt(payloadStr, keyForDecryption, {
iv: initializationVector,
mode: CryptoJS.mode.CBC,
});
console.log("key: ", decryptedPayload, "iv: ", initializationVector);
const str = decryptedPayload;
console.log("str: ", str);
console.log(atob(str));
return atob(str.toString(CryptoJS.enc.Base64));
};
How can I get JSON stringified version of original payload (Original Human Readable string) from this obj of array words and sigBytes ?? Are there any function in crypto js or normal function to convert from words array to original human readable text content ??

(NODEJS) AES-256-GCM break pdf,gzip,png encoding after decryption

I wish I had help because i don't know why my implementation of AES-GCM break file encoding.
I have an API that uses 1 function to encrypt/decrypt with AES-256-GCM. (With KEY=buffer of 32 random bytes)
Here is the function:
const aes256gcm = (key) => {
const ALGO = 'aes-256-gcm';
const encrypt = (str) => {
try {
const salt = crypto.randomBytes(64);
const iv = crypto.randomBytes(32);
let derivedkey = crypto.pbkdf2Sync(key, salt, 55000, 32, 'sha512');
const cipher = crypto.createCipheriv(ALGO, derivedkey, iv);
let encrypted = Buffer.concat([cipher.update(str), cipher.final()]);
const tag = cipher.getAuthTag();
let buffer = Buffer.concat([salt, iv, encrypted]);
encrypted = {
tag: tag,
buffer: buffer
}
return encrypted;
} catch (e) {
console.log(e);
}
};
const decrypt = (data, authTag) => {
try {
const salt = data.slice(0, 64);
const iv = data.slice(64, 96);
const text = data.slice(96, data.length);
authTag = new Buffer.from(authTag, 'base64');
let derivedkey = crypto.pbkdf2Sync(key, salt, 55000, 32, 'sha512');
let decipher = crypto.createDecipheriv(ALGO, derivedkey, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(text, 'binary') + decipher.final();
return decrypted;
} catch (e) {
console.log(e);
}
};
return {
encrypt,
decrypt
};
};
With this code i encrypt and write in file the result:
const aesCipher = aes.aes256gcm(aes.loadKey(path.resolve(__dirname, `key`)));
const encrypted = aesCipher.encrypt(file.data);
if (encrypted !== undefined) {
fs.writeFile(`${file.name}.enc`, encrypted.buffer, function (err) {
if (err) return console.log(err);
console.log(`${file.name}.enc successfully created`);
});
}
And finaly with this i decrypt and write the content in a file:
const aesCipher = aes.aes256gcm(aes.loadKey(path.resolve(__dirname, `key`)));
let filename = 'test1.gz';
let authTag = 'puI0FfV4Btiy7iPiZFbwew==';
let encrypted = fs.readFileSync(path.resolve(__dirname, `test1.gz.enc`));
const decrypted = aesCipher.decrypt(encrypted, authTag);
if (decrypted !== undefined) {
const file = fs.createWriteStream(filename);
file.write(new Buffer.from(decrypted, 'ascii'), function (err) {
if (err) return console.log(err);
console.log(`Successfully decrypted`);
file.close();
});
res.send({
status: true,
message: 'File is decrypted',
});
}
Diff of my input/output files :
Diff
So, what am i doing wrong ? Is my encryption process good ? Why this only work well with .txt files ?
Thanks you !
I think a fairly small change to your decrypt function should fix the issue, if you just update it to:
const decrypt = (data, authTag) => {
try {
const salt = data.slice(0, 64);
const iv = data.slice(64, 96);
const text = data.slice(96, data.length);
authTag = new Buffer.from(authTag, 'base64');
let derivedkey = crypto.pbkdf2Sync(key, salt, 55000, 32, 'sha512');
let decipher = crypto.createDecipheriv(ALGO, derivedkey, iv);
decipher.setAuthTag(authTag);
let decrypted = Buffer.concat([decipher.update(text), decipher.final()]);
return decrypted;
} catch (e) {
console.log(e);
}
};
I think the previous implementation was not concatenating the result correctly for non-text files.

Encrypt in CryptoJS (Angular) and do the same in Crypto (NodeJS)

I am having trouble encrypting a password in Angular vs NodeJS.
For example, the password I'm using is: test
In Angular I am using CryptoJS:
encryptUsingAES256(password) {
let _key = CryptoJS.enc.Utf8.parse("elservidordelgatotuerto88");
let _iv = CryptoJS.enc.Utf8.parse("elservidordelgatotuerto88");
let encrypted = CryptoJS.AES.encrypt(
password, _key, {
iv: _iv,
format: CryptoJS.format.Hex,
mode: CryptoJS.mode.CTR,
padding: CryptoJS.pad.Pkcs7
}).toString();
return encrypted;
}
The value I get from this function is: b75d0db663be668a24498aaa460f8896
Now, I also want to encrypt the same value in NodeJS using Crypto:
public encrypt(text) {
try {
var cipher = createCipher("aes-256-ctr", "elservidordelgatotuerto88");
var crypted = cipher.update(text,'utf8','hex');
crypted += cipher.final('hex');
return crypted;
} catch (error) {
throw new Error( 'couldn\'t encrypt text' );
}
}
And the value I get is different: 067e0c77
I want to obtain in Angular the same result that NodeJS gives me (067e0c77).
What could be the fixes that I should make in Angular?
Thank you!!
With a few changes we can make the output of the Crypto.js and Node crypto encryption identical.
We need to remove the padding in Crypto.js and also ensure that the key and iv are exactly the same.
The code will look like so:
const CryptoJS = require("crypto-js");
const crypto = require("crypto");
function encryptUsingAES256(password, key, iv) {
let _key = CryptoJS.enc.Utf8.parse(key);
let _iv = CryptoJS.enc.Utf8.parse(iv);
let encrypted = CryptoJS.AES.encrypt(
password, _key, {
iv: _iv,
format: CryptoJS.format.Hex,
mode: CryptoJS.mode.CTR,
padding: CryptoJS.pad.NoPadding
}).toString();
return encrypted;
}
function encryptUsingNodeCrypto(text, key, iv) {
try {
const cipher = crypto.createCipheriv("aes-256-ctr", key, iv);
let crypted = cipher.update(text,'utf8','hex');
crypted += cipher.final('hex');
return crypted;
} catch (error) {
console.error("encryptUsingNodeCrypto: An error occurred: ", error);
throw error;
}
}
const originalText = "test";
const key = "elservidordelgatelservidordelgat";
const iv = "elservidordelgat";
const encryptedTextCryptoJs = encryptUsingAES256(originalText, key, iv);
console.log(`Crypto.js: "${originalText}" was encrypted to "${encryptedTextCryptoJs}"`);
const encryptedTextNodeJs = encryptUsingNodeCrypto(originalText, key, iv);
console.log(`Node.js: "${originalText}" was encrypted to "${encryptedTextNodeJs}"`);
I'm getting identical results for the same input.

Decrypt a AES-CTR 256 bits message from browser with window.crypto.subtle APIs

I need to decrypt in the browser a message encoded with AES-CTR 256 bits (encoded using OpenSSL).
Using OpenSSL I get something like:
key=189BBBB00C5F1FB7FBA9AD9285F193D1771D7611CB891E5C1F4E24C20E50FB1D
iv =4103C88663AE12CE18EA46E894280C4D
msg=nhVKeu8zNO2PRTwJrDE=
Well, my problem is converting those strings into objects the window.crypto.subtle APIs can manage. Eg.
const counter = ???;
const ciphertext = ???;
const rawKey = ???;
const key = window.crypto.subtle.importKey(
"raw",
key,
"AES-CTR",
true,
["encrypt", "decrypt"]
);
const decrypted = await window.crypto.subtle.decrypt(
{
name: "AES-CTR",
counter,
length: 64
},
key,
ciphertext
);
let dec = new TextDecoder();
const msg = dec.decode(decrypted);
console.log(msg);
Could anyone help me passing from key, iv, msg to counter, ciphertext, rawkey?
Thank you very much
Key, counter (or IV) and ciphertext can be passed as TypedArray, i.e. you need two conversions, one from a hexadecimal, and a second from a Base64 encoded string into a TypedArray, e.g.
from a hexadecimal encoded string, here:
const fromHex = hexString => new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
from a Base64 encoded string, here:
const fromBase64 = base64String => Uint8Array.from(atob(base64String), c => c.charCodeAt(0));
In the code itself an await operator is missing and in the importKey function rawKey must be used instead of key (probably copy/paste errors). Alltogether:
const fromHex = hexString => new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
const fromBase64 = base64String => Uint8Array.from(atob(base64String), c => c.charCodeAt(0));
async function test(){
const rawKey = fromHex("189BBBB00C5F1FB7FBA9AD9285F193D1771D7611CB891E5C1F4E24C20E50FB1D");
const counter = fromHex("4103C88663AE12CE18EA46E894280C4D");
const ciphertext = fromBase64("nhVKeu8zNO2PRTwJrDE=");
const key = await window.crypto.subtle.importKey( // add >await<
"raw",
rawKey, // replace >key< with >rawKey<
"AES-CTR",
true,
["encrypt", "decrypt"]
);
const decrypted = await window.crypto.subtle.decrypt(
{
name: "AES-CTR",
counter,
length: 64
},
key,
ciphertext
);
let dec = new TextDecoder();
const msg = dec.decode(decrypted);
console.log(msg);
}
test();
This decrypts the ciphertext to:
hello, world!

Categories

Resources