Encryption strategy:
Generate random 256-bit encryption key (K_s).
For every PII value in payload:
1. Pad plaintext with PKCS#7 padding.
2. Generate random 128-bit Initialization Vector (IV).
3. Encrypt padded plaintext with AES-256-CBC Cipher generated with key K_s and IV to get ciphertext.
4. Append IV to cipher text and Base64 encode to get payload value.
5. Assign payload value to corresponding key in payload.
6. Encrypt K_s using RSA-OAEP with hash function SHA-256 and public RSA key to get K_enc.
7. Assign K_enc to session_key in payload.
I'm trying to implement the above encryption strategy in node js using crypto module, but I'm missing something... I'm stuck on this on the past 2 days... Can someone please help me figure out what I'm missing here?
My implementation of encryption script so far below:
const crypto = require('crypto'),
_ = require('lodash');
async function encryptPayload(dataToEncrypt, password) {
if (dataToEncrypt.constructor !== String) {
dataToEncrypt = JSON.stringify(dataToEncrypt);
}
let bufferKey = Buffer.from(password, 'hex');
const iv = crypto.randomBytes(16); // should this be crypto.randomBytes(32).toString('hex')?
let cipherKey = crypto.createCipheriv('aes-256-cbc', bufferKey, iv);
cipherKey.setAutoPadding(true);
let encryptedPayload = cipherKey.update(dataToEncrypt, 'utf8', 'base64');
// encryptedPayload += cipherKey.final('base64');
// return encryptedPayload + iv.toString('base64');
encryptedPayload = cipherKey.final()
let tempBuffer = Buffer.concat([encryptedPayload, iv]);
return tempBuffer.toString('base64');
}
async function encryptDataMultipleKeys(payload, publicKey, keysToEncrypt = []) {
if (!payload) {
return payload;
}
let password = crypto.randomBytes(32).toString('hex'); //uuid.v4();
console.log("The password is " + password + " \n");
let pendingPromisesArray = [], correspondingKeyNameArray = [];
for (const key of keysToEncrypt) {
let value = _.get(payload, key);
if (!value) {
continue;
}
//value = await encryptPayload(value, password);
pendingPromisesArray.push(encryptPayload(value, password));
correspondingKeyNameArray.push(key);
}
let promisesValueArray = await Promise.all(pendingPromisesArray);
let encryptedPayload = {}
for (let index = 0; index < correspondingKeyNameArray.length; index++) {
let key = correspondingKeyNameArray[index];
let value = promisesValueArray[index];
if (!value || !key) {
continue;
}
_.set(encryptedPayload, key, value);
//encryptedPayload[key] = value;
}
//REF: https://nodejs.org/api/crypto.html#crypto_crypto_publicencrypt_key_buffer
let encryptedPasswordBuffer = crypto.publicEncrypt({
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256"
}, Buffer.from(password, 'hex'));
let encryptedPassword = encryptedPasswordBuffer.toString('base64');
encryptedPayload.session_key = encryptedPassword
return encryptedPayload;
}
async function encryptPIIFields(payload) {
let fieldsToEncrypt = [
'applicant.ssn', 'applicant.date_of_birth', 'applicant.first_name', 'applicant.last_name',
'applicant.email_address', 'applicant.phone_number', 'applicant.income',
'applicant.address.line_1', 'applicant.address.line_2', 'applicant.address.city',
'applicant.address.country', 'applicant.address.state', 'applicant.address.zipcode'
];
let publicKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArYsdy+gGrdzvG5F9BYLl\nVwFwCfyCzeLQ7Vmvu+wvyoDrwvMXSfLnZfg7NsZMyPc3OVt8EeRvGLzrXvxtSWKG\n+mKBC7xEzb/LM8MoHQhXlgZ7L1nofBpAs74zEFXZNGHw5SnWXTuQ3Yym0u8hkYDZ\noqDJRgrczjXdbrqDVeB3GIvpMZMU9OkTFRmZZGMLVS3P3LIswyxfdxuMvU9dBBtP\nj3wofaLuxNWA384xBZYNV7AcWzOOHR3j3Iw7KfplgVawlpm4zXhBwFrKE44g0g5z\n4vL2N1eJs/OgaAMUYUM4kuZIW1fqFGB9cRAJpbjCO9d3dnvz4sPBWXchzZVjyzXh\njwIDAQAB\n-----END PUBLIC KEY-----\n";
payload = await encryptDataMultipleKeys(payload, publicKey, fieldsToEncrypt);
return payload
}
let data = {
"applicant": {
"address": {
"line_1": "732484THSTREETss",
"city": "TACOMA",
"country": "US",
"state": "WA",
"zipcode": "98498"
},
"income": 1000,
"date_of_birth": "1938-09-09",
"email_address": "faa4@mail.com",
"first_name": "WILLIAM",
"last_name": "SCALICI",
"phone_number": "7327474747",
"ssn": "987452343"
}
}
encryptPIIFields(data).then((encryptedData) => {
console.log(JSON.stringify(encryptedData)); //eslint-disable-line
process.exit(0);
}, (err) => {
console.log(err); //eslint-disable-line
process.exit(1);
});
Decryption script:
const crypto = require('crypto'),
_ = require('lodash');
async function decryptDataMultipleKeys(payload, privateKey, keysToDecrypt) {
if (!payload) {
return payload;
}
let decryptedPasswordBuffer = crypto.privateDecrypt({
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256"
}, Buffer.from(payload.session_key, 'base64'));
let password = decryptedPasswordBuffer.toString('hex');
console.log("password: " + password);
let decryptedPayload = {};
for (const key of keysToDecrypt) {
let value = _.get(payload, key);
if (!value) {
continue;
}
let encryptedDataBuffer = Buffer.from(value, 'base64');
let bufferData = encryptedDataBuffer.slice(0, 16);
let bufferIv = encryptedDataBuffer.slice(16, 32);
let cipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(password, 'hex'), bufferIv);
cipher.setAutoPadding(true);
let decryptedValue = cipher.update(bufferData, undefined, 'utf8');
decryptedValue += cipher.final('utf8');
_.set(decryptedPayload, key, decryptedValue);
}
return decryptedPayload;
}
async function decryptPIIFields(payload) {
let fieldsToDecrypt = [
'applicant.ssn', 'applicant.date_of_birth', 'applicant.first_name', 'applicant.last_name',
'applicant.email_address', 'applicant.phone_number', 'applicant.income',
'applicant.address.line_1', 'applicant.address.line_2', 'applicant.address.city',
'applicant.address.country', 'applicant.address.state', 'applicant.address.zipcode'
];
let privateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEArYsdy+gGrdzvG5F9BYLlVwFwCfyCzeLQ7Vmvu+wvyoDrwvMX\nSfLnZfg7NsZMyPc3OVt8EeRvGLzrXvxtSWKG+mKBC7xEzb/LM8MoHQhXlgZ7L1no\nfBpAs74zEFXZNGHw5SnWXTuQ3Yym0u8hkYDZoqDJRgrczjXdbrqDVeB3GIvpMZMU\n9OkTFRmZZGMLVS3P3LIswyxfdxuMvU9dBBtPj3wofaLuxNWA384xBZYNV7AcWzOO\nHR3j3Iw7KfplgVawlpm4zXhBwFrKE44g0g5z4vL2N1eJs/OgaAMUYUM4kuZIW1fq\nFGB9cRAJpbjCO9d3dnvz4sPBWXchzZVjyzXhjwIDAQABAoIBAQCBNy03bwrSF8fd\nUgWxvdW/Y62lceN/IxwHLhlAJksrT7S7kj7L69XJwfts/Fed5xyyU2Dc/aaO19O1\nBOTmmDsCYafOMh9UxzKo1u2eOGDmruq3xgzpoq58Zukkh5dTfn1cVDttbfWeUKTC\nOBVZfoQNqARVZ68ix06ZrLwvjBOBLSmH4l4XM8JzYtBFOntkU45ZHmPvxGfJBvYS\nhTOMvS3AvfxuEK2zW9A/vciDWVWmET0p0C22+pMahT+FSwOwYNTuP3BxQV2Aq6vY\nEc9ktr4hj0b2gGoRok/t4K4C/ufDhxRinNnFIFcPh9j39/st8kLwlkKCgii3Kpjv\ntzD4OyX5AoGBANwB77oOmbIGNdXGONTQ1aXnqpsO0tt1/ZAnZrQaNgCb6ThwLieN\nQ5tqem6GWbTtSSUuwpgFjxw5SMD8KxJihV+ySjo99SGhqssyPXyYHpMmOSEsbQhe\n0YeT4Epr6FuIBLuV0qFZJupI6jcHBKcmR0FQ2rXqCxPnfNopZizm5GnbAoGBAMnv\nOxIdpI2r2Z/+6WyQiBmwuEhd39ZKA8aoONJeoCp0MnAQvrbmr6kDfpP+aQWw6Xww\n+5GrAFgrtJ37STHPXw/lXPKDpXE573o8aDHTDB/WU0lpCVxJ6NY0sy/CArUIU7Pz\ntQiB11PrZZ6UDyiSmXoYzUHkR1I44EjF2/lnZlddAoGBALvx44s8Qcw1RfQzfAVB\nyeIKwFHqHfNhHpXxMumUoqFuj5OpMaSUJzczhRe6KhRHyP68rXwU86aWwTIrudfg\n1jNkKckLeMecRj2D08cGZMgsFQ3j19kYt0Js72RkPoFC91gQq3kuofHvDDaqBi2M\no76GhfB12bTNQnlUeHbPYD2VAoGALZ7kg4U65d7LPcBDUAmfFd6842yB41G5ZKog\nnDZQjQbPVk4SKBQZ318wu5Kge26qcSpHy3MMkt7c4UwiDyTAX0D8LLXdLKVgGweG\nqqr5dD/hdRZLzRPNjIc/bCyym9+TuXX3kkJzOTxXKupcOlhUYCc2SAqgqky7LvW0\narYXgukCgYEAjtfYSciex+Nv1GGaN7SjAozIBvrLAV0o9oo/zxhTblJpCkaM60aT\nimiT4NwkrEfB27zzguYduD0mgsq/Hg8BBkbe7FPKZ8GugZ6xlF0i02kVRzRDNlxT\n+cfqbL2vKt5FR9iFJFVWYjmvpVmvxZ/J1ybZD3MjT+YBNj/sf9DvclM=\n-----END RSA PRIVATE KEY-----\n";
payload = await decryptDataMultipleKeys(payload, privateKey, fieldsToDecrypt);
return payload
}
let data = {"applicant":{"ssn":"YR8BUBk+xrpQm5gHkCfrIXMFGjGJGLS192mVgcupF6U=","date_of_birth":"+ujL7mv/IZMALdFiL92Z0LACrVhb/lmzcwx8l89sIcs=","first_name":"l8nAmcQkIm8OctcaFq9t4q5TN2brkf4MTfdQ7K19PMw=","last_name":"yOqZpZjueZu10q0z3P4cTN2m5BP7ug4CqypumfzjbUc=","email_address":"2CftSOnWqRCINRF9ZK5QYTSP6TdpTUEpEanJE6PAhUQ=","phone_number":"cEQV5cbYJveBkn3XWqzCw2x9a8P2ZcEjiMX5+ezhdQc=","income":"TpM/4zOiTpCZ8to8jjjngJDLRcrDKOP8C2UVRYh9Wgs=","address":{"line_1":"MYzvsUFBl+Oav1aDOxqvjimpv8YW4g2hSjZChfOeri4=","city":"/3m9bvk1auwNgyNTJ2gtx1B0+gYxKQYy/VBThyuqrr0=","country":"H8GZ9rP+EAw9KdeVvNbPFtPyUBtU9NrCxXrQ0GMTltg=","state":"g7nshQ6rNrbsPq1vJd5vnBh/0HNjasfgN8Mhy59FW/U=","zipcode":"X5MGNTPA/Rh2Fxb8GOLUBwHx9ex8RGGrRM+RA7Wf8MU="}},"session_key":"CDfUI+12UzezVpp/7/9jbWXJ7AmR5jTcV5r9JsyIPinxZO2nEra05t8uL3lOotyE23ymr1e3Ia8mF7huReIbTma25I7p01+eBjKBR9Zv5NHV72is44wmJqXu5dB1fOiJFF7xBjUzZ5zClgBMsFNr025yc4dtDKQxPcj0xGPvQKmUbbbwTvq7TrSS0rDZrjcGLsxlpIXua1damYp+n6Jw9XjLyN4OTyiV2JtiOq7vnRMEYsdTr4hibVhtFwkDFqCrg7Y9tnvgLocg2lMwEOu/iF7QDA5UlAUyiFU+U0WThasVjPCNikoRi2FC2u/T/EAtmG9drWuohxX2DUvyKgm/bA=="}
decryptPIIFields(data).then((decryptedData) => {
console.log(JSON.stringify(decryptedData)); //eslint-disable-line
process.exit(0);
}, (err) => {
console.log(err); //eslint-disable-line
process.exit(1);
});
I have a feeling that I'm messing something in the part where I append the IV to the encrypted payload... Need some enlightenment here.
EDIT: I have added a script to decrypt the same. I'm unable to successfully decrypt only certain cases. For example, I can decrypt if the value of line_1 is "732484THSTREETs", but can't decrypt if the value is "732484THSTREETss"... I'm getting the following the error while decrypting the latter
Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt
at Decipheriv.final (internal/crypto/cipher.js:172:29)
at decryptDataMultipleKeys (/Users/pavithran/off/payment-service/oaep-decrypt.js:29:30)
at decryptPIIFields (/Users/pavithran/off/payment-service/oaep-decrypt.js:43:19)
at Object.<anonymous> (/Users/pavithran/off/payment-service/oaep-decrypt.js:48:1)
at Module._compile (internal/modules/cjs/loader.js:1158:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1178:10)
at Module.load (internal/modules/cjs/loader.js:1002:32)
at Function.Module._load (internal/modules/cjs/loader.js:901:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)
at internal/main/run_main_module.js:18:47 {
library: 'digital envelope routines',
function: 'EVP_DecryptFinal_ex',
reason: 'bad decrypt',
code: 'ERR_OSSL_EVP_BAD_DECRYPT'
}