So having seen the working HMAC example in one of the replies on this post from flows-daniel, I am trying to use CryptoJs in postman flows to generate, export, and sign an RSA keyPair (a PKCS1-v1_5, SHA256 for example). Below is a description of my flow for others to use and test with. So far generating a key pair works, and so does signing using the generated private key, but export and import do not work.
This flow gives me 2 issues I can see from my logging:
First, trying to export either the public or private key has an error internal to crypto.subtle.exportKey that states "{"message":"'array' is not defined","stack":" at exportKey (eval.js)\n"}"
. I could understand why a private key might fail exporting for security, but the public key should export at least. Furthermore, this error seems more like a bug than a deliberate thing.
Second, trying to import a key using an externally-created pem string fails silently. I don’t hit the then nor the catch blocks after it, but the call itself returns undefined according to the error popup on the Evaluate block, and the fact a log can still be logged after the function is done.
I have been trying to get this approach to work so that I can automatically test my APIs that validate whether a saved public key pem and a given signature signed by a private key match. I would prefer not to clutter the collection with a pre-request script for this, nor a random public API call among all our private ones. Also just having a pem saved in scenarios here sounds insecure.
Description of flow testing CryptoJS:
- single input or a string block containing a private key pem. I asked ChatGPT to create one.
- a STRING block containing the following code. This makes logging and reuse of CryptoJS functions easier. Note that atob(), btoa(), and Buffer do not exist in the Flows environment, so base64 encode and decode are here instead.
function rsaUtils() {
function generateRSAKeyPair() {
return crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
true,
["sign", "verify"]
).then(keyPair =>
({
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey
})
).catch(genErr => {
return recordError(genErr, "Key generation");
});
}
function exportKeyToPEM(format, label, key) {
return crypto.subtle.exportKey(format, key).then(buffer => {
return toPEM(buffer, label).catch(pemErr => {
return recordError(pemErr, label + " toPEM");
});
}).catch(exportErr => {
return recordError(exportErr, label + " export");
});
}
function toPEM(buffer, label) {
const base64 = base64Encode(new Uint8Array(buffer));
const lines = base64.match(/.{1,64}/g) || [];
return '-----BEGIN ' + label + '-----\\n' + lines.join('\\n') + '\\n-----END ' + label + '-----';
}
function base64Encode(bytes) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
let result = '', i;
for (i = 0; i < bytes.length; i += 3) {
const b1 = bytes[i];
const b2 = i + 1 < bytes.length ? bytes[i + 1] : 0;
const b3 = i + 2 < bytes.length ? bytes[i + 2] : 0;
const triplet = (b1 << 16) | (b2 << 8) | b3;
result += chars[(triplet >> 18) & 0x3F];
result += chars[(triplet >> 12) & 0x3F];
result += i + 1 < bytes.length ? chars[(triplet >> 6) & 0x3F] : '=';
result += i + 2 < bytes.length ? chars[triplet & 0x3F] : '=';
}
console.log("encoded bytes:", result);
return result;
}
function base64Decode(base64) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const clean = base64.replace(/[^A-Za-z0-9+/]/g, '');
const bufferLength = clean.length * 3 / 4 - (clean.endsWith('==') ? 2 : clean.endsWith('=') ? 1 : 0);
const bytes = new Uint8Array(bufferLength);
let p = 0;
for (let i = 0; i < clean.length; i += 4) {
const encoded1 = chars.indexOf(clean[i]);
const encoded2 = chars.indexOf(clean[i + 1]);
const encoded3 = chars.indexOf(clean[i + 2]);
const encoded4 = chars.indexOf(clean[i + 3]);
const triplet = (encoded1 << 18) | (encoded2 << 12) | ((encoded3 & 63) << 6) | (encoded4 & 63);
if (p < bufferLength) bytes[p++] = (triplet >> 16) & 0xFF;
if (p < bufferLength) bytes[p++] = (triplet >> 8) & 0xFF;
if (p < bufferLength) bytes[p++] = triplet & 0xFF;
}
console.log("decoded bytes:", bytes);
return bytes;
}
function importPrivateKey(pem) {
const b64 = pem.replace(/-----[^-]+-----/g, '').replace(/\\s+/g, '');
const bytes = base64Decode(b64);
return crypto.subtle.importKey(
"pkcs8",
bytes.buffer,
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256"
},
false,
["sign"]
).then(key => {
console.log("key imported");
return key;
}).catch(importErr => {
return recordError(importErr, "Private key import");
});
}
function signPayloadWithPrivateKey(payload, privateKey) {
const encoder = new TextEncoder();
const data = encoder.encode(payload);
return crypto.subtle.sign(
{ name: "RSASSA-PKCS1-v1_5" },
privateKey,
data
).then(sig => {
return base64Encode(new Uint8Array(sig));
}).catch(signErr => {
return recordError(signErr, "Signing");
});
}
function recordError(err, functionName) {
console.error(functionName + " failed:", err);
console.error("Error type:", typeof err);
console.error("Error string:", err?.toString?.());
console.error("Error JSON:", JSON.stringify(err, Object.getOwnPropertyNames(err)));
return { error: err?.message || functionName + " error" };
}
return {
generateRSAKeyPair,
exportKeyToPEM,
toPEM,
base64Encode,
base64Decode,
importPrivateKey,
signPayloadWithPrivateKey,
recordError
};
}
- An Evaluate block with a variable
rsaString
taking in the string from step 2 and the following code. This shows that generateRSAKeyPair and signPayloadWithPrivateKey work.
function generateRSAKeyPairAndSign(payload, rsa) {
console.log("payload:", payload);
return rsa.generateRSAKeyPair().then(keyPair => {
console.log("Key pair generated Block 1:", keyPair);
return rsa.signPayloadWithPrivateKey(payload, keyPair.privateKey).then(signature => {
return {
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey,
signature: signature
};
});
});
}
console.log("block 1");
const cleanedString = rsaString.replace(/\\\\n/g, '\\n');
const rsa = eval(`(${rsaString})`)();
generateRSAKeyPairAndSign("POSTMAN", rsa);
- An Evaluate block with again the
rsaString
variable, and the following code. This shows the export errors in the console and within the output.
function generateRSAKeyPairAndExport(payload, rsa) {
console.log("payload:", payload);
return rsa.generateRSAKeyPair().then(keyPair => {
console.log("Key pair generated Block 2:", keyPair);
return rsa.exportKeyToPEM("pkcs8","PRIVATE KEY",keyPair.privateKey).then(privatePem => {
console.log("Private key exported Block 2: ", privatePem);
return rsa.exportKeyToPEM("spki", "PUBLIC KEY", keyPair.publicKey).then(publicPem => {
console.log("Public key exported Block 2: ", publicPem);
return {
privateKey: privatePem,
publicKey: publicPem
};
}).catch(exportErr => {
return rsa.recordError(exportErr, "Public Key export Block 2");
});
}).catch(exportErr => {
return rsa.recordError(exportErr, "Private Key export Block 2");
});
}).catch(genErr => {
return rsa.recordError(genErr, "Key generation Block 2");
});
}
console.log("block 2");
const cleanedString = rsaString.replace(/\\\\n/g, '\\n');
const rsa = eval(`(${rsaString})`)();
generateRSAKeyPairAndExport("POSTMAN", rsa);
- An Evaluate block with again the
rsaString
variable, but also a variableprivateKeyPEM
taking in the private pem input from step 1 and the following code. In my flow I also added the result of step 4 / block 2 to the variables, so that the blocks are forced to execute sequentially rather than asynchronously. This shows the logging in and around the import function behaving as I described above.
function importPrivateKeyAndSign(payload, privateKeyPEM, rsa) {
console.log("start import Block 3");
rsa.importPrivateKey(privateKeyPEM).then(importedPrivateKey => {
console.log("importedPrivateKey Block 3:", importedPrivateKey);
return rsa.signPayloadWithPrivateKey(payload, importedPrivateKey).then(signature => {
console.log("Final Signature Block 3:", signature);
return signature;
}).catch(signErr => {
return rsa.recordError(signErr, "Signing Block 3");
});
}).catch(importErr => {
return rsa.recordError(importErr, "Private key import Block 3");
});
}
console.log("block 3");
const cleanedString = rsaString.replace(/\\\\n/g, '\\n');
const rsa = eval(`(${rsaString})`)();
const result = importPrivateKeyAndSign("POSTMAN", privateKeyPEM, rsa);
console.log("block 3 finish:", result);
result;