javascript - 需要帮助来解释使用 oaep 哈希的 aes-256-cbc 加密

标签 javascript node.js security encryption cryptography

加密策略:

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. 

我正在尝试使用 crypto 模块在 Node js 中实现上述加密策略,但是我遗漏了一些东西...我在过去的两天里陷入了困境...有人可以帮我弄清楚吗我在这里缺少什么?

到目前为止我的加密脚本实现如下:

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": "<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="b4d2d5d580f4d9d5ddd89ad7dbd9" rel="noreferrer noopener nofollow">[email protected]</a>",
    "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);
});

解密脚本:


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);
});

我有一种感觉,我在将 IV 附加到加密有效负载的部分弄乱了一些东西......这里需要一些启发。

编辑:我添加了一个脚本来解密它。我仅无法成功解密某些情况。 例如,如果 line_1 的值为“732484THSTREETs”,我可以解密,但如果值为“732484THSTREETss”,则无法解密...解密后者时,我收到以下错误

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'
}

最佳答案

问题在于对称加密(错误使用updatefinal)和对称解密(错误分离密文)。详细来说,当前版本由于以下原因无法工作:

  • 在对称加密中,仅考虑最后部分。但是当然也必须考虑前面的 update 语句,它必须存储在 Buffer 中以供后续连接使用,即第三个参数(编码)必须是已删除。此外,IV 通常放置在密文之前(而不是之后)。后者不是一个错误,但遵循约定仍然很有用。因此,总而言之,对于对称加密,它必须是:

    ...
    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'); 
    ...
    

    它产生例如以下密文:

    {"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=="}
    
  • 对于对称解密,假设密文只有一个 block (AES 为 16 字节)大,但这通常是不正确的。任何包含超过 1 个 block 的明文都将生成更大的密文(即使是 1 block 明文也会生成 2 block 密文,因为使用了 PKCS7 填充)。对于对称解密(具有 IV 阶,密文),它必须是:

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

    这样就可以解密上面的密文了:

    {"applicant":{"ssn":"987452343","date_of_birth":"1938-09-09","first_name":"WILLIAM","last_name":"SCALICI","email_address":"<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="90f6f1f1a4d0fdf1f9fcbef3fffd" rel="noreferrer noopener nofollow">[email protected]</a>","phone_number":"7327474747","income":"1000","address":{"line_1":"732484THSTREETss","city":"TACOMA","country":"US","state":"WA","zipcode":"98498"}}}
    

请注意:问题中发布的代码的 encryptPayload 中的加密和 Base64 编码已相对于原始帖子进行了更改。 在更改之前,密文和 IV 均经过 Base64 编码,然后连接。这是不寻常的,因为 Base64 编码通常发生在串联之后。但只要解密始终如一地实现,这不是一个错误。相反,正如上面详细解释的,更改后的代码不起作用。本答案中发布的代码片段实现了通常的方案:按顺序连接 IV 和密文,然后进行 Base64 编码。

关于javascript - 需要帮助来解释使用 oaep 哈希的 aes-256-cbc 加密,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64167966/

相关文章:

javascript - socketstream 和第 3 方存储库

windows - 如何使文件真正不可变(不可删除且只读)?

javascript - Angular 1.5 组件问题 - 当更改为另一个状态时,如何将来自其 Controller 的函数 'reset' ?

node.js - multer 不允许使用默认图像的问题

传入的Javascript回调函数参数

security - HTTPS/SSL 在什么时候加密 POST 数据

html - 为什么我应该在评论表单中使用 BBCode 而不是 HTML?

Javascript 类共享函数?

javascript - React.js 中已弃用的 setProps() 的替代方案是什么?

javascript - DIV 向下滚动时向上移动