Pavithran Ravichandiran
Pavithran Ravichandiran

Reputation: 1899

Need help in interpreting the aes-256-cbc encyption with oaepHash

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": "[email protected]",
    "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'
}

Upvotes: 2

Views: 1258

Answers (1)

Topaco
Topaco

Reputation: 49351

The problem is in both the symmetric encryption (wrong usage of update and final) and the symmetric decryption (wrong separation of the ciphertext). In detail the current version does not work for the following reasons:

  • In the symmetric encryption only the final part is considered. But of course the preceding update statement must be considered as well, which has to be stored in a Buffer for the subsequent concatenation, i.e. the third argument (the encoding) must be removed. Furthermore the IV is usually placed before (and not after) the ciphertext. The latter is not a bug, but it is still useful to follow conventions. All in all, therefore, for symmetric encryption it must be:

    ...
    let encryptedPayloadUpd = cipherKey.update(dataToEncrypt, 'utf8');                // store in buffer
    let encryptedPayloadFin = cipherKey.final()
    let tempBuffer =  Buffer.concat([iv, encryptedPayloadUpd, encryptedPayloadFin]);  // consider encryptedPayloadUpd, place IV before ciphertext
    return tempBuffer.toString('base64'); 
    ...
    

    which produces e.g. the following ciphertext:

    {"applicant":{"ssn":"zFbx9fiBSu47bMiAP7whaG+fkOBrCu+CWBzfYjPcV14=","date_of_birth":"K/GzpKNIDY4Bb0MJpNfvv/wE3iUBP31y5OS1t8LTEJg=","first_name":"HbVtwcy4wVV5n7JLpt87IhX27JiLn9ewaqj08EXw8Ss=","last_name":"D5lqNNYywt88MOSlMcZQY6oTLuntTYzFvOy1op7PhjY=","email_address":"hNBSep2jzczUiBm0M7iGTZcPo3GZVScOgKzjd+t3uYA=","phone_number":"0l4PgCW12WFb1jv9lfOftHngQlE8BWsbqi/HHdcmjhk=","income":"nu16KkULL/xyBgKQjxAn//Q34fdA0kAOMS+AJTYXh4k=","address":{"line_1":"ce2BBt+Qbpe8KpJR81zaqQh7CSF3WXni6snLYZYGPuHknR3qBCY2fLdKvgMl8D2E","city":"01eVK0h7zGOSnL8I4aQ+CICSQV1t7bU470/S1HY5ZmY=","country":"XHjNTEc8ZapnuBSgLgg2YIZ9fIc7m8hH/j/nULL1UZo=","state":"17m0tTQQaT8c4y+XXVQsz8tfjIDGrOh2tBMTAcH+5PY=","zipcode":"ygjxgvF3B0HAnvtpys5s7bDMABvg6IcJDKJAIMNuLjk="}},"session_key":"jEqblsQ5ZbGDmZBlzZgXZWAxtQptL+9FL2WKvMQHL5PdTDwez1XKMl6aAKHRoMjb3oH0GDw941ICGL99WHW+nxJaanxqV9mlU9NDBE84T/fdrov/YAS5NDb5CD20ZFT8YL+/QC3ldf4VvJlzLy18EvSgt1nPYUZ6WEfdpNs6YckxtV4NAQ1wNiB/zQ07RUUfIegdNE9vn828TjOqxTUDKkwtZiyKKtaIetWS9LnCSDh7PXEnWyAcHZ19WRTZimvoMuqPUjotChzCjNrwTEkoOp/XzPN3NhG/7nxxw9vFNSP0Gy6jPHXUBiJ9sMPkg99TZCk9+2hWGdMiuP4JHpvk4g=="}
    
  • For the symmetric decryption it is assumed that the ciphertext is only one block (16 bytes for AES) large, which is generally not true. Any plaintext consisting of more than 1 block will generate a larger ciphertext (even a 1 block plaintext generates a 2 block ciphertext because of the PKCS7 padding used). For the symmetric decryption (with the order IV, ciphertext) it must therefore be:

    ...
    let encryptedDataBuffer = Buffer.from(value, 'base64'); 
    let bufferIv = encryptedDataBuffer.slice(0, 16);  // consider order (IV, ciphertext)
    let bufferData = encryptedDataBuffer.slice(16);   // consider complete ciphertext
    ...
    

    With this the above ciphertext can be decrypted:

    {"applicant":{"ssn":"987452343","date_of_birth":"1938-09-09","first_name":"WILLIAM","last_name":"SCALICI","email_address":"[email protected]","phone_number":"7327474747","income":"1000","address":{"line_1":"732484THSTREETss","city":"TACOMA","country":"US","state":"WA","zipcode":"98498"}}}
    

Please note: The encryption and Base64 encoding in encryptPayload of the posted code in the question has been changed relative to the original post. Before the change ciphertext and IV were each Base64 encoded and then concatenated. This is unusual, as Base64 encoding generally occurs after concatenation. But this is not a bug as long as the decryption is implemented consistently. In contrast, the code after the change did not work, as explained in detail above. The posted code snippets in this answer implement the usual scheme: concatenation of IV and ciphertext in this order, followed by Base64 encoding.

Upvotes: 3

Related Questions