OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-booking / node_modules / bare-crypto / lib / cipher.js

373 lines · javascript ref:
const { Transform } = require('bare-stream')
const assert = require('bare-assert')
const binding = require('../binding')
const constants = require('./constants')

const {
  cipher: {
    AES128ECB,
    AES128CBC,
    AES128CTR,
    AES128OFB,
    AES256ECB,
    AES256CBC,
    AES256CTR,
    AES256OFB,
    AES128GCM,
    AES256GCM,
    CHACHA20POLY1305,
    XCHACHA20POLY1305
  }
} = constants

class CryptoCipher {
  constructor(algorithm, key, iv, encrypt, opts = {}) {
    const { encoding = 'utf8' } = opts

    if (typeof key === 'string') key = Buffer.from(key, encoding)
    if (typeof iv === 'string') iv = Buffer.from(iv, encoding)

    assert(ArrayBuffer.isView(key))
    assert(ArrayBuffer.isView(iv))

    if (key.byteLength !== binding.cipherKeyLength(algorithm)) {
      throw new RangeError('Invalid key length')
    }

    if (iv.byteLength < binding.cipherIVLength(algorithm)) {
      throw new RangeError('Invalid iv length')
    }

    this._handle = binding.cipherInit(
      algorithm,
      key.buffer,
      key.byteOffset,
      key.byteLength,
      iv.buffer,
      iv.byteOffset,
      iv.byteLength,
      encrypt
    )
  }

  update(data, inputEncoding = 'utf8', outputEncoding) {
    if (this._handle === null) {
      throw new Error('Cipher has already been finalized')
    }

    if (typeof data === 'string') data = Buffer.from(data, inputEncoding)

    assert(ArrayBuffer.isView(data))

    const out = new ArrayBuffer(data.byteLength + binding.cipherBlockSize(this._handle))

    const written = binding.cipherUpdate(
      this._handle,
      data.buffer,
      data.byteOffset,
      data.byteLength,
      out
    )

    const result = Buffer.from(out, 0, written)

    return outputEncoding ? result.toString(outputEncoding) : result
  }

  final(outputEncoding) {
    if (this._handle === null) {
      throw new Error('Cipher has already been finalized')
    }

    const out = new ArrayBuffer(binding.cipherBlockSize(this._handle))

    const written = binding.cipherFinal(this._handle, out)

    this._handle = null

    const result = Buffer.from(out, 0, written)

    return outputEncoding ? result.toString(outputEncoding) : result
  }

  setAutoPadding(pad) {
    if (this._handle === null) {
      throw new Error('Cipher has already been finalized')
    }

    binding.cipherSetPadding(this._handle, pad)
  }
}

class CryptoAuthenticatedCipher {
  constructor(algorithm, key, nonce, opts = {}) {
    const { encoding = 'utf8', authTagLength = 16 } = opts

    if (typeof key === 'string') key = Buffer.from(key, encoding)
    if (typeof nonce === 'string') nonce = Buffer.from(nonce, encoding)

    assert(ArrayBuffer.isView(key))
    assert(ArrayBuffer.isView(nonce))

    if (key.byteLength !== binding.aeadKeyLength(algorithm)) {
      throw new RangeError('Invalid key length')
    }

    if (nonce.byteLength < binding.aeadNonceLength(algorithm)) {
      throw new RangeError('Invalid nonce length')
    }

    this._buffer = []
    this._nonce = nonce
    this._authTag = null
    this._authTagLength = authTagLength
    this._additionalData = null

    this._handle = binding.aeadInit(
      algorithm,
      key.buffer,
      key.byteOffset,
      key.byteLength,
      authTagLength
    )
  }

  update(data, inputEncoding = 'utf8', outputEncoding) {
    if (typeof data === 'string') data = Buffer.from(data, inputEncoding)

    assert(ArrayBuffer.isView(data))

    this._buffer.push(data)

    return outputEncoding ? '' : Buffer.alloc(0)
  }

  setAAD(buffer, opts = {}) {
    const { encoding = 'utf8' } = opts

    if (typeof buffer === 'string') buffer = Buffer.from(buffer, encoding)

    assert(ArrayBuffer.isView(buffer))

    this._additionalData = buffer
  }

  getAuthTag() {
    return this._authTag
  }

  setAuthTag(authTag, encoding) {
    if (typeof authTag === 'string') authTag = Buffer.from(authTag, encoding)

    assert(ArrayBuffer.isView(authTag))

    this._authTag = authTag
  }
}

class CryptoAuthenticatedSeal extends CryptoAuthenticatedCipher {
  final(outputEncoding) {
    if (this._handle === null) {
      throw new Error('Cipher has already been finalized')
    }

    const data = this._buffer.length === 1 ? this._buffer[0] : Buffer.concat(this._buffer)

    const nonce = this._nonce
    const ad = this._additionalData || Buffer.alloc(0)

    const out = new ArrayBuffer(data.byteLength + binding.aeadMaxOverhead(this._handle))

    const written = binding.aeadSeal(
      this._handle,
      data.buffer,
      data.byteOffset,
      data.byteLength,
      nonce.buffer,
      nonce.byteOffset,
      nonce.byteLength,
      ad.buffer,
      ad.byteOffset,
      ad.byteLength,
      out
    )

    this._handle = null

    const cipherLength = written - this._authTagLength

    this._authTag = Buffer.from(out, cipherLength)

    const result = Buffer.from(out, 0, cipherLength)

    return outputEncoding ? result.toString(outputEncoding) : result
  }
}

class CryptoAuthenticatedOpen extends CryptoAuthenticatedCipher {
  final(outputEncoding) {
    if (this._handle === null) {
      throw new Error('Decipher has already been finalized')
    }

    this._buffer.push(this._authTag)

    const data = Buffer.concat(this._buffer)

    const nonce = this._nonce
    const ad = this._additionalData || Buffer.alloc(0)

    const out = new ArrayBuffer(data.byteLength)

    const written = binding.aeadOpen(
      this._handle,
      data.buffer,
      data.byteOffset,
      data.byteLength,
      nonce.buffer,
      nonce.byteOffset,
      nonce.byteLength,
      ad.buffer,
      ad.byteOffset,
      ad.byteLength,
      out
    )

    this._handle = null

    const result = Buffer.from(out, 0, written)

    return outputEncoding ? result.toString(outputEncoding) : result
  }
}

exports.Cipheriv = class CryptoCipheriv extends Transform {
  constructor(algorithm, key, iv, opts = {}) {
    super(opts)

    algorithm = constants.toCipher(algorithm)

    switch (algorithm) {
      case AES128ECB:
      case AES128CBC:
      case AES128CTR:
      case AES128OFB:
      case AES256ECB:
      case AES256CBC:
      case AES256CTR:
      case AES256OFB:
        this._cipher = new CryptoCipher(algorithm, key, iv, true, opts)
        break

      case AES128GCM:
      case AES256GCM:
      case CHACHA20POLY1305:
      case XCHACHA20POLY1305:
        this._cipher = new CryptoAuthenticatedSeal(algorithm, key, iv, opts)
        break
    }
  }

  update(data, inputEncoding, outputEncoding) {
    return this._cipher.update(data, inputEncoding, outputEncoding)
  }

  final(outputEncoding) {
    return this._cipher.final(outputEncoding)
  }

  setAutoPadding(pad) {
    this._cipher.setAutoPadding(pad)

    return this
  }

  setAAD(buffer, opts) {
    this._cipher.setAAD(buffer, opts)

    return this
  }

  getAuthTag() {
    return this._cipher.getAuthTag()
  }

  _transform(data, encoding, cb) {
    this.push(this.update(data))

    cb(null)
  }

  _flush(cb) {
    this.push(this.final())

    cb(null)
  }
}

exports.Decipheriv = class CryptoDeipheriv extends Transform {
  constructor(algorithm, key, iv, opts = {}) {
    super(opts)

    algorithm = constants.toCipher(algorithm)

    switch (algorithm) {
      case AES128ECB:
      case AES128CBC:
      case AES128CTR:
      case AES128OFB:
      case AES256ECB:
      case AES256CBC:
      case AES256CTR:
      case AES256OFB:
        this._cipher = new CryptoCipher(algorithm, key, iv, false, opts)
        break

      case AES128GCM:
      case AES256GCM:
      case CHACHA20POLY1305:
      case XCHACHA20POLY1305:
        this._cipher = new CryptoAuthenticatedOpen(algorithm, key, iv, opts)
        break
    }
  }

  update(data, inputEncoding, outputEncoding) {
    return this._cipher.update(data, inputEncoding, outputEncoding)
  }

  final(outputEncoding) {
    return this._cipher.final(outputEncoding)
  }

  setAutoPadding(pad) {
    this._cipher.setAutoPadding(pad)

    return this
  }

  setAAD(buffer, opts) {
    this._cipher.setAAD(buffer, opts)

    return this
  }

  setAuthTag(authTag, encoding) {
    this._cipher.setAuthTag(authTag, encoding)

    return this
  }

  _transform(data, encoding, cb) {
    this.push(this.update(data))

    cb(null)
  }

  _flush(cb) {
    this.push(this.final())

    cb(null)
  }
}