nbarikipoulos/json-cipher-value

View on GitHub
lib/CipherObject.js

Summary

Maintainability
A
0 mins
Test Coverage
import {
  createHash, randomBytes,
  createCipheriv, createDecipheriv
} from 'node:crypto'
import tr from './tr.js'

/**
 * Object in charge to cipher both supported primitive type or values of object.
 *
 * @memberof module:json-cipher-value
 * @inner
 */
class CipherObject {
  /**
   * Create a new Cipher object.
   *
   * @param {string} secret - The secret key or password that will be used to create
   *  key for (de)ciphering step.
   * @param {module:json-cipher-value~Options=} options - (De)Ciphering Settings.
   *  Note Use of default settings performs an aes-256-crt ciphering.
   * @example
   * import { CipherObject } from 'json-cipher-value'
   *
   * const secret = 'My secret password'
   *
   * const obj = {...} // Object to cipher
   *
   * const cipherObject = new CipherObject(secret)
   * const cipheredObject = cipherObject.perform('cipher', object)
   * const decipheredObject = cipherObject.perform('decipher', cipheredObject)
   */
  constructor (secret, {
    algo = 'aes-256-ctr',
    ivLength = 16
  } = {}) {
    this._encryptionKey = Buffer.from(
      createHash('sha256')
        .update(String(secret))
        .digest()
    )

    this._algo = algo
    this._ivLength = ivLength
  }

  /**
   * (De)Cipher content.
   *
   * Note about Ciphering case:
   *   - Supported input are **number**, **boolean**, **string** or **object** (including **array**).
   *     All other kind of values will be treated as string.
   *   - An iv vector is randomly generated at each ciphering of values
   *     _i.e._ for each object properties with a primitive type as value.
   *   - The return value concatenates the iv vector and the ciphertext.
   * @param {'cipher'|'decipher'} action - action to perform.
   * @param {*} value - Input to (de)cipher.
   * @type {*}
   */
  perform (action, object) {
    let result
    switch (action) {
      case 'cipher':
      case 'encrypt':
        result = tr(object, (v) => this.cipher(v))
        break
      case 'decipher':
      case 'decrypt':
        result = tr(object, (v) => this.decipher(v))
    }
    return result
  }

  /**
   * Cipher a primitive type.
   *
   * At each call, an iv vector is randomly generated.
   *
   * Supported type are **number**, **boolean**, or **string**. All other kind of values
   * will be treated as a string.
   * @param {string|number|boolean|*} value - value to cipher.
   * @type {string}
   * @example
   * import { CipherObject } from 'json-cipher-value'
   *
   * const secret = 'My secret password'
   *
   * const value = 'my value' // Value to cipher
   *
   * const cipherObject = new CipherObject(secret)
   * const cipheredValue = cipherObject.cipher(value)
   */
  cipher (value) {
    const iv = randomBytes(this._ivLength)

    const cipher = createCipheriv(
      this._algo,
      this._encryptionKey,
      iv
    )

    const ciphertext = Buffer.concat([
      cipher.update(TYPE[typeof value]),
      cipher.update(String(value)),
      cipher.final()
    ])

    return iv.toString('hex') +
      ciphertext.toString('hex')
  }

  /**
   * Decipher primitive type.
   * @param {string} cipheredText - ciphered data
   * @type {string|number|boolean}
   * @example
   * import { CipherObject } from 'json-cipher-value'
   *
   * const secret = 'My secret password'
   *
   * const cipheredText = ...
   *
   * const cipherObject = new CipherObject(secret)
   * const decipheredValue = cipherObject.decipher(cipheredText)
   */
  decipher (cipheredText) {
    const v = String(cipheredText)

    let idx = 2 * this._ivLength

    const [iv, ciphertext] = [v.slice(0, idx), v.slice(idx)]
      .map(str => Buffer.from(str, 'hex'))

    const decipher = createDecipheriv(
      this._algo,
      this._encryptionKey,
      iv
    )

    const decipheredValue = decipher.update(ciphertext, 'hex', 'utf8') +
      decipher.final('utf8')

    idx = 0

    const type = decipheredValue[idx++]

    return toType(
      type,
      decipheredValue.substring(idx)
    )
  }
}

// ///////////////////////////////
// Utility functions
// ///////////////////////////////

const toType = (type, data) => {
  let res
  switch (type) {
    case 'n':
      res = Number(data)
      break
    case 'b':
      res = data === 'true'
      break
    case 's':
    default:
      res = data
  }

  return res
}

const TYPE = {
  string: 's',
  number: 'n',
  boolean: 'b'
}

export default CipherObject