12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091 |
- 'use strict'
- const alg = 'aes-256-gcm'
- const crypto = require('crypto')
- const { get, set } = require('lodash')
- function EncryptedAttributes (attributes, options) {
- options = options || {}
- let prefix = Buffer.from(`${alg}$`).toString('base64')
- function encryptAttribute (obj, val) {
- // Encrypted attributes are prefixed with "aes-256-gcm$", the base64
- // encoding of which is in `prefix`. Nulls are not encrypted.
- if (val == null || (typeof val === 'string' && val.startsWith(prefix))) {
- return val
- }
- if (typeof val !== 'string') {
- throw new Error('Encrypted attribute must be a string')
- }
- if (options.verifyId && !obj.id) {
- throw new Error('Cannot encrypt without \'id\' attribute')
- }
- // Recommended 96-bit nonce with AES-GCM.
- let iv = crypto.randomBytes(12)
- let aad = Buffer.from(
- `aes-256-gcm$${options.verifyId ? obj.id.toString() : ''}$${options.keyId}`)
- let key = Buffer.from(options.keys[options.keyId], 'base64')
- let gcm = crypto.createCipheriv('aes-256-gcm', key, iv).setAAD(aad)
- let result = gcm.update(val, 'utf8', 'base64') + gcm.final('base64')
- return aad.toString('base64') + '$' +
- iv.toString('base64') + '$' +
- result + '$' +
- gcm.getAuthTag().toString('base64').slice(0, 22)
- }
- function encryptAll (obj) {
- for (let attr of attributes) {
- let val = get(obj, attr)
- if (val != null) {
- set(obj, attr, encryptAttribute(obj, val))
- }
- }
- return obj
- }
- function decryptAttribute (obj, val) {
- // Encrypted attributes are prefixed with "aes-256-gcm$", the base64
- // encoding of which is in `prefix`. Nulls are not encrypted.
- if (typeof val !== 'string' || !val.startsWith(prefix)) {
- return val
- }
- if (options.verifyId && !obj.id) {
- throw new Error('Cannot decrypt without \'id\' attribute')
- }
- let [aad, iv, payload, tag] = val.split('$').map((x) => Buffer.from(x, 'base64'))
- let [, id, keyId] = aad.toString().split('$')
- if (options.verifyId && (id !== obj.id.toString())) {
- throw new Error('Encrypted attribute has invalid id')
- }
- if (!options.keys[keyId]) {
- throw new Error('Encrypted attribute has invalid key id')
- }
- let key = Buffer.from(options.keys[keyId], 'base64')
- let gcm = crypto.createDecipheriv('aes-256-gcm', key, iv).setAAD(aad).setAuthTag(tag)
- return gcm.update(payload, 'binary', 'utf8') + gcm.final('utf8')
- }
- function decryptAll (obj) {
- for (let attr of attributes) {
- let val = get(obj, attr)
- if (val != null) {
- set(obj, attr, decryptAttribute(obj, val))
- }
- }
- return obj
- }
- return {
- attributes,
- options,
- encryptAttribute,
- encryptAll,
- decryptAttribute,
- decryptAll
- }
- }
- module.exports = EncryptedAttributes
|