HR/CryptoSync

View on GitHub
src/crypto.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict'
/**
 * crypto.js
 * Provides the crypto functionality required
 ******************************/

const secrets = require('secrets.js')
const fs = require('fs-extra')
const util = require('./util')
const logger = require('../script/logger')
const _ = require('lodash')
const Readable = require('stream').Readable
const crypto = require('crypto')

// Crypto default constants
// TODO: change accordingly when changed in settings
let defaults = {
  iterations: 5000, // file encryption key iterations
  keyLength: 32, // 32 bytes
  ivLength: 12,
  algorithm: 'aes-256-gcm',
  salgorithm: 'aes-256-ctr',
  digest: 'sha256',
  hash_alg: 'sha256',
  check_hash_alg: 'md5',
  padLength: 1024, // 1 MB
  mpk_iterations: 100000, // masterpass key iterations
  shares: 3,
  threshold: 2
}

/*    Crypto
 *
 *    TODO:
 *    - Implement bitcoin blockchain as source of randomness (in iv generation)
 *  - rewrite as promises
 */

exports.encrypt = function (origpath, destpath, mpkey, callback) {
  // decrypts any arbitrary data passed with the pass
  let pass = (Array.isArray(mpkey)) ? exports.shares2pass(mpkey) : mpkey
  // pass = password
  const salt = crypto.randomBytes(defaults.keyLength) // generate pseudorandom salt
  crypto.pbkdf2(pass, salt, defaults.iterations, defaults.keyLength, defaults.digest, (err, key) => {
    if (err) {
      // return error to callback YOLO#101
      callback(err)
    }
      // logger.verbose(`Pbkdf2 generated key ${key.toString('hex')} using iv = ${iv.toString('hex')}, salt = ${salt.toString('hex')}`)
      const origin = fs.createReadStream(origpath)
      const dest = fs.createWriteStream(destpath)
      const iv = crypto.randomBytes(defaults.ivLength) // generate pseudorandom iv
      const cipher = crypto.createCipheriv(defaults.algorithm, key, iv)

      origin.pipe(cipher).pipe(dest, {
        end: false
      })

      cipher.on('error', () => {
        logger.verbose(`CIPHER STREAM: Error while encrypting file`)
        callback(err)
      })

      origin.on('error', () => {
        logger.verbose(`ORIGIN STREAM: Error while reading file to ${destpath}`)
        callback(err)
      })

      dest.on('error', () => {
        logger.verbose(`DEST STREAM: Error while writting file to ${destpath}`)
        callback(err)
      })

      origin.on('end', () => {
        // Append iv used to encrypt the file to end of file
        dest.write(`CryptoSync#${iv.toString('hex')}#${cipher.getAuthTag().toString('hex')}`)
        dest.end()
      // logger.verbose(`End (of writestream) for ${destf} called, IV&authTag appended`)
      })

      dest.on('finish', () => {
        const tag = cipher.getAuthTag()
        // logger.verbose(`Finished encrypted/written to ${destf}`)
        callback(null, key, iv, tag)
      })

  })
}

exports.encryptObj = function (obj, destpath, mpkey, viv, callback) {
  // decrypts any arbitrary data passed with the pass
  // pass = (Array.isArray(password)) ? shares2pass(password) : password,

  const iv = (viv instanceof Buffer) ? viv : new Buffer(viv.data)
  const origin = new Readable()
  try {
    const json = JSON.stringify(obj)
    origin.push(json) // writes the json string of obj to stream
    origin.push(null) // indicates end-of-file basically - the end of the stream
  } catch (err) {
    logger.verbose(`JSON.stringify error for ${destpath}`)
    callback(err)
  }
  const dest = fs.createWriteStream(destpath)
  const cipher = crypto.createCipheriv(defaults.algorithm, mpkey, iv)

  origin.on('error', function (e) {
    callback(e)
  })
  .pipe(cipher).on('error', function (e) {
    callback(e)
  })
  .pipe(dest).on('error', function (e) {
    callback(e)
  })

  dest.on('finish', () => {
    const tag = cipher.getAuthTag()
    // logger.verbose(`Finished encrypted/written to ${destpath} with authtag = ${tag.toString('hex')}`)
    callback(null, tag)
  })
}

exports.decryptObj = function (origpath, mpkey, viv, vtag, callback) {
  const iv = (viv instanceof Buffer) ? viv : new Buffer(viv.data)
  const tag = (vtag instanceof Buffer) ? vtag : new Buffer(vtag.data)

  // logger.verbose(`Decrypting using MasterPass = ${mpkey.toString('hex')}, iv = ${iv.toString('hex')}, tag = ${tag.toString('hex')}`)
  // pass = (Array.isArray(password)) ? shares2pass(password) : password
  const origin = fs.createReadStream(origpath)
  const decipher = crypto.createDecipheriv(defaults.algorithm, mpkey, iv)
  decipher.setAuthTag(tag)

  const JSONstream = origin.on('error', function (e) {
    callback(e)
  }).pipe(decipher).on('error', function (e) {
    callback(e)
  })

  util.streamToString(JSONstream, function (err, json) {
    // logger.verbose(`Finished decrypting from ${origpath}`)
    if (err) callback(err)
    try {
      let vault = JSON.parse(json)
      callback(null, vault)
    } catch (err) {
      logger.verbose(`JSON.parse error for ${origpath}`)
      callback(err)
    }
  })
}

exports.genIV = function () {
  return new Promise(function (resolve, reject) {
    try {
      const iv = crypto.randomBytes(defaults.ivLength) // Synchronous gen
      resolve(iv)
    } catch (err) {
      reject(err)
    }
  })
}

exports.deriveMasterPassKey = function (masterpass, mpsalt, callback) {
  if (!masterpass) return callback(new Error('MasterPassKey not provided'))
  const salt = (mpsalt) ? ((mpsalt instanceof Buffer) ? mpsalt : new Buffer(mpsalt.data)) : crypto.randomBytes(defaults.keyLength)
  crypto.pbkdf2(masterpass, salt, defaults.mpk_iterations, defaults.keyLength, defaults.digest, (err, mpkey) => {
    if (err) {
      // return error to callback
      return callback(err)
    } else {
      // logger.verbose(`Pbkdf2 generated: \nmpkey = ${mpkey.toString('hex')} \nwith salt = ${salt.toString('hex')}`)
      return callback(null, mpkey, salt)
    }
  })
}

exports.genPassHash = function (mpass, salt, callback) {
  // logger.verbose(`crypto.genPassHash() invoked`)
  const pass = (mpass instanceof Buffer) ? mpass.toString('hex') : mpass

  if (salt) {
    const hash = crypto.createHash(defaults.hash_alg).update(`${pass}${salt}`).digest('hex')
    // logger.verbose(`genPassHash: S, pass = ${pass}, salt = ${salt}, hash = ${hash}`)
    callback(hash)
  } else {
    const salt = crypto.randomBytes(defaults.keyLength).toString('hex')
    const hash = crypto.createHash(defaults.hash_alg).update(`${pass}${salt}`).digest('hex')
    // logger.verbose(`genPassHash: NS, pass = ${pass}, salt = ${salt}, hash = ${hash}`)
    callback(hash, salt)
  }
}

exports.verifyPassHash = function (mpkhash, gmpkhash) {
  return _.isEqual(mpkhash, gmpkhash)
}

exports.genFileHash = function (origpath, callback) {
  return new Promise(function (resolve, reject) {
    let fd = fs.createReadStream(origpath)
    const hash = crypto.createHash(defaults.check_hash_alg)
    hash.setEncoding('hex')
    fd.on('end', function () {
      hash.end()
      const fhash = hash.read()
      logger.verbose(`genFileHash: fhash = ${fhash} for ${origpath}`)
      resolve(fhash)
    })

    fd.on('error', function (e) {
      reject(e)
    }).pipe(hash).on('error', function (e) {
      reject(e)
    })
  })
}

exports.verifyFileHash = function (fhash, gfhash) {
  return _.isEqual(fhash, gfhash)
}

exports.decrypt = function (origpath, destpath, key, iv, authTag, callback) {
  // encrypts any arbitrary data passed with the pass
  // const pass = (Array.isArray(key)) ? shares2pass(key) : key
  if (!authTag || !iv) {
    // extract from last line of file
    fs.readFile(origpath, 'utf-8', function (err, data) {
      if (err) callback(err)

      let lines = data.trim().split('\n')
      let lastLine = lines.slice(-1)[0]
      let fields = lastLine.split('#')
      if (_.isEqual(fields[0], 'CryptoSync')) {
        const iv = new Buffer(fields[1], 'hex')
        const authTag = new Buffer(fields[2], 'hex')
        const mainData = lines.slice(0, -1).join()
        let origin = new Readable()
        // read as stream
        origin.push(mainData)
        origin.push(null)

        const decipher = crypto.createDecipheriv(defaults.algorithm, key, iv)
        decipher.setAuthTag(authTag)
        const dest = fs.createWriteStream(destpath)

        origin.pipe(decipher).pipe(dest)

        decipher.on('error', () => {
          callback(err)
        })

        origin.on('error', () => {
          callback(err)
        })

        dest.on('error', () => {
          callback(err)
        })

        dest.on('finish', () => {
          logger.verbose(`Finished encrypted/written to ${destpath}`)
          callback(null, iv)
        })
      } else {
        callback(new Error('IV and authTag not supplied'))
      }
    })
  } else {
    // TODO: Implement normal flow
  }
}

exports.pass2shares = function (pass, total = defaults.shares, th = defaults.threshold) {
  // splits the pass into shares using Shamir's Secret Sharing
  // convert the text into a hex string
  try {
    // pass = secrets.str2hex(pass)
    // split into N shares, with a threshold of th
    // Zero padding of defaults.padLength applied to ensure minimal info leak (i.e size of pass)
    const shares = secrets.share(pass, total, th, defaults.padLength)
    const sharesd = {
      data: shares,
      total: total,
      threshold: th
    }
    return sharesd
  } catch (err) {
    throw err
  }
}

/**
 * @param {array} of at least the threshold length
 */
exports.shares2pass = function (sharesd) {
  // reconstructs the pass from the shares of the pass
  // using Shamir's Secret Sharing
  // let S = sharedata[2],
  // let N = sharedata[1]
  try {
    // Extract the shares
    const shares = (_.isArray(sharesd)) ? sharesd : sharesd.data
    const pass = secrets.combine(shares)
    // convert back to str
    const hpass = (pass).toString('hex')
    return hpass
  } catch (err) {
    throw err
  }
}