encrypted-attr.js 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. 'use strict'
  2. const alg = 'aes-256-gcm'
  3. const crypto = require('crypto')
  4. const { get, set } = require('lodash')
  5. function EncryptedAttributes (attributes, options) {
  6. options = options || {}
  7. let prefix = Buffer.from(`${alg}$`).toString('base64')
  8. function encryptAttribute (obj, val) {
  9. // Encrypted attributes are prefixed with "aes-256-gcm$", the base64
  10. // encoding of which is in `prefix`. Nulls are not encrypted.
  11. if (val == null || (typeof val === 'string' && val.startsWith(prefix))) {
  12. return val
  13. }
  14. if (typeof val !== 'string') {
  15. throw new Error('Encrypted attribute must be a string')
  16. }
  17. if (options.verifyId && !obj.id) {
  18. throw new Error('Cannot encrypt without \'id\' attribute')
  19. }
  20. // Recommended 96-bit nonce with AES-GCM.
  21. let iv = crypto.randomBytes(12)
  22. let aad = Buffer.from(
  23. `aes-256-gcm$${options.verifyId ? obj.id.toString() : ''}$${options.keyId}`)
  24. let key = Buffer.from(options.keys[options.keyId], 'base64')
  25. let gcm = crypto.createCipheriv('aes-256-gcm', key, iv).setAAD(aad)
  26. let result = gcm.update(val, 'utf8', 'base64') + gcm.final('base64')
  27. return aad.toString('base64') + '$' +
  28. iv.toString('base64') + '$' +
  29. result + '$' +
  30. gcm.getAuthTag().toString('base64').slice(0, 22)
  31. }
  32. function encryptAll (obj) {
  33. for (let attr of attributes) {
  34. let val = get(obj, attr)
  35. if (val != null) {
  36. set(obj, attr, encryptAttribute(obj, val))
  37. }
  38. }
  39. return obj
  40. }
  41. function decryptAttribute (obj, val) {
  42. // Encrypted attributes are prefixed with "aes-256-gcm$", the base64
  43. // encoding of which is in `prefix`. Nulls are not encrypted.
  44. if (typeof val !== 'string' || !val.startsWith(prefix)) {
  45. return val
  46. }
  47. if (options.verifyId && !obj.id) {
  48. throw new Error('Cannot decrypt without \'id\' attribute')
  49. }
  50. let [aad, iv, payload, tag] = val.split('$').map((x) => Buffer.from(x, 'base64'))
  51. let [, id, keyId] = aad.toString().split('$')
  52. if (options.verifyId && (id !== obj.id.toString())) {
  53. throw new Error('Encrypted attribute has invalid id')
  54. }
  55. if (!options.keys[keyId]) {
  56. throw new Error('Encrypted attribute has invalid key id')
  57. }
  58. let key = Buffer.from(options.keys[keyId], 'base64')
  59. let gcm = crypto.createDecipheriv('aes-256-gcm', key, iv).setAAD(aad).setAuthTag(tag)
  60. return gcm.update(payload, 'binary', 'utf8') + gcm.final('utf8')
  61. }
  62. function decryptAll (obj) {
  63. for (let attr of attributes) {
  64. let val = get(obj, attr)
  65. if (val != null) {
  66. set(obj, attr, decryptAttribute(obj, val))
  67. }
  68. }
  69. return obj
  70. }
  71. return {
  72. attributes,
  73. options,
  74. encryptAttribute,
  75. encryptAll,
  76. decryptAttribute,
  77. decryptAll
  78. }
  79. }
  80. module.exports = EncryptedAttributes