app/core/crypto.js
'use strict'
/**
* crypto.js
* Provides the crypto functionality required
******************************/
const fs = require('fs-extra')
const path = require('path')
const scrypto = require('crypto')
const logger = require('electron-log')
const Readable = require('stream').Readable
const tar = require('tar-fs')
const { CRYPTO, REGEX, ERRORS } = require('../config')
// Helper functions
let readFile = path => {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf-8', (err, data) => {
if (err) reject(err)
resolve(data)
})
})
}
// Exports
exports.crypt = (origpath, masterpass) => {
return new Promise((resolve, reject) => {
logger.verbose(`Encrypting ${origpath}...`)
// Resolve the destination path for encrypted file
exports
.encrypt(origpath, masterpass)
.then(creds => {
resolve({
op: CRYPTO.ENCRYPT_OP, // Crypter operation
name: path.basename(origpath), // filename
path: origpath, // path of the (unencrypted) file
cryptPath: creds.cryptpath, // path of the encrypted file
salt: creds.salt.toString('hex'), // salt used to derivekey in hex
key: creds.key.toString('hex'), // dervived key in hex
iv: creds.iv.toString('hex'), // iv in hex
authTag: creds.tag.toString('hex') // authTag in hex
})
})
.catch(err => {
reject(err)
})
})
}
exports.encrypt = (origpath, mpkey) => {
// Encrypts any arbitrary data passed with the pass
return new Promise((resolve, reject) => {
// derive the encryption key
exports
.deriveKey(mpkey, null, CRYPTO.DEFAULTS.ITERATIONS)
.then(dcreds => {
let tag
let isDirectory = fs.lstatSync(origpath).isDirectory()
let tempd = `${path.dirname(origpath)}/${CRYPTO.ENCRYPTION_TMP_DIR}`
let dataDestPath = `${tempd}/data`
let credsDestPath = `${tempd}/creds`
logger.verbose(
`tempd: ${tempd}, dataDestPath: ${dataDestPath}, credsDestPath: ${credsDestPath}, isDirectory: ${isDirectory}`
)
// create tempd temporary directory
fs.mkdirs(tempd, err => {
if (err) reject(err)
logger.verbose(`Created ${tempd} successfully`)
// readstream to read the (unencrypted) file
const origin = isDirectory
? tar.pack(origpath)
: fs.createReadStream(origpath)
// create data and creds file
const dataDest = fs.createWriteStream(dataDestPath)
const credsDest = fs.createWriteStream(credsDestPath)
// generate a cryptographically secure random iv
const iv = scrypto.randomBytes(CRYPTO.DEFAULTS.IVLENGTH)
// create the AES-256-GCM cipher with iv and derive encryption key
const cipher = scrypto.createCipheriv(
CRYPTO.DEFAULTS.ALGORITHM,
dcreds.key,
iv
)
// Read file, apply tranformation (encryption) to stream and
// then write stream to filesystem
origin
.on('error', err => reject(err))
.pipe(cipher)
.on('error', err => reject(err))
.pipe(dataDest)
.on('error', err => reject(err))
.on('finish', () => {
// get the generated Message Authentication Code
tag = cipher.getAuthTag()
// Write credentials used to encrypt in creds file
const creds = {
type: 'CRYPTO',
iv: iv.toString('hex'),
authTag: tag.toString('hex'),
salt: dcreds.salt.toString('hex'),
isDir: isDirectory
}
credsDest.end(JSON.stringify(creds))
})
// writestream finish handler
credsDest.on('finish', () => {
let tarDestPath = origpath + CRYPTO.EXT
const tarDest = fs.createWriteStream(tarDestPath)
const tarPack = tar.pack(tempd)
// Pack directory and zip into a .crypto file
tarPack
.on('error', err => reject(err))
.pipe(tarDest)
.on('error', err => reject(err))
.on('finish', () => {
// Remove temporary dir tempd
fs.remove(tempd, err => {
if (err) reject(err)
// return all the credentials and parameters used for encryption
logger.verbose('Successfully deleted tempd!')
resolve({
salt: dcreds.salt,
key: dcreds.key,
cryptpath: tarDestPath,
tag: tag,
iv: iv
})
})
})
})
})
})
.catch(err => reject(err))
})
}
exports.decrypt = (origpath, mpkey) => {
// Decrypts a crypto format file passed with the pass
return new Promise((resolve, reject) => {
logger.verbose(`Decrypting ${origpath}...`)
// Extract a directory
let tempd = `${path.dirname(origpath)}/${CRYPTO.DECRYPTION_TMP_DIR}`
let dataOrigPath = `${tempd}/${CRYPTO.FILE_DATA}`
let credsOrigPath = `${tempd}/${CRYPTO.FILE_CREDS}`
let dataDestPath = origpath.replace(CRYPTO.EXT, '')
dataDestPath = dataDestPath.replace(
path.basename(dataDestPath),
path.basename(dataDestPath)
)
let tarOrig = fs.createReadStream(origpath)
let tarExtr = tar.extract(tempd)
// Extract tar to CRYPTO.DECRYPTION_TMP_DIR directory
tarOrig
.on('error', err => reject(err))
.pipe(tarExtr)
.on('error', err => reject(err))
.on('finish', () => {
// Now read creds and use to decrypt data
logger.verbose('Finished extracting')
readFile(credsOrigPath)
.then(credsData => {
let creds
try {
// Try creds v2
creds = JSON.parse(credsData)
creds = [creds.iv, creds.authTag, creds.salt, creds.isDir]
} catch (error) {
// Try creds v1
let credsLine = credsData.trim().match(REGEX.ENCRYPTION_CREDS)
if (!credsLine) {
return reject(new Error(ERRORS.DECRYPT))
}
creds = credsLine[0].split('#').slice(1)
}
const iv = Buffer.from(creds[0], 'hex')
const authTag = Buffer.from(creds[1], 'hex')
const salt = Buffer.from(creds[2], 'hex')
const isDir = creds[3]
logger.verbose(
`Extracted data, iv: ${iv}, authTag: ${authTag}, salt: ${salt}`
)
// Read encrypted data stream
const dataOrig = fs.createReadStream(dataOrigPath)
// derive the original encryption key for the file
exports
.deriveKey(mpkey, salt, CRYPTO.DEFAULTS.ITERATIONS)
.then(dcreds => {
try {
let decipher = scrypto.createDecipheriv(
CRYPTO.DEFAULTS.ALGORITHM,
dcreds.key,
iv
)
decipher.setAuthTag(authTag)
let dataDest = isDir
? tar.extract(dataDestPath)
: fs.createWriteStream(dataDestPath)
dataOrig
.on('error', err => reject(err))
.pipe(decipher)
.on('error', err => reject(err))
.pipe(dataDest)
.on('error', err => reject(err))
.on('finish', () => {
logger.verbose(`Encrypted to ${dataDestPath}`)
// Now delete tempd (temporary directory)
fs.remove(tempd, err => {
if (err) reject(err)
logger.verbose(`Removed temp dir ${tempd}`)
resolve({
op: CRYPTO.DECRYPT_OP,
name: path.basename(origpath),
path: origpath,
cryptPath: dataDestPath,
salt: salt.toString('hex'),
key: dcreds.key.toString('hex'),
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
})
})
})
} catch (err) {
reject(err)
}
})
})
.catch(err => {
reject(err)
})
})
})
}
exports.deriveKey = (pass, psalt) => {
return new Promise((resolve, reject) => {
// reject with error if pass not provided
if (!pass) reject(new Error('Pass to derive key from not provided'))
// If psalt is provided and is a Buffer then assign it
// If psalt is provided and is not a Buffer then coerce it and assign it
// If psalt is not provided then generate a cryptographically secure salt
// and assign it
const salt = psalt
? Buffer.isBuffer(psalt)
? psalt
: Buffer.from(psalt)
: scrypto.randomBytes(CRYPTO.DEFAULTS.KEYLENGTH)
// derive the key using the salt, password and default crypto setup
scrypto.pbkdf2(
pass,
salt,
CRYPTO.DEFAULTS.MPK_ITERATIONS,
CRYPTO.DEFAULTS.KEYLENGTH,
CRYPTO.DEFAULTS.DIGEST,
(err, key) => {
if (err) reject(err)
// return the key and the salt
resolve({ key, salt })
}
)
})
}
// create a sha256 hash of the MasterPassKey
exports.genPassHash = (masterpass, salt) => {
return new Promise((resolve, reject) => {
// convert the masterpass (of type Buffer) to a hex encoded string
// if it is not already one
const pass = Buffer.isBuffer(masterpass)
? masterpass.toString('hex')
: masterpass
// if salt provided then the MasterPass is being checked
// if salt not provided then the MasterPass is being set
if (salt) {
// create hash from the contanation of the pass and salt
// assign the hex digest of the created hash
const hash = scrypto
.createHash(CRYPTO.DEFAULTS.HASH_ALG)
.update(`${pass}${salt}`)
.digest('hex')
resolve({ hash, key: masterpass })
} else {
// generate a cryptographically secure salt and use it as the salt
const salt = scrypto
.randomBytes(CRYPTO.DEFAULTS.KEYLENGTH)
.toString('hex')
// create hash from the contanation of the pass and salt
// assign the hex digest of the created hash
const hash = scrypto
.createHash(CRYPTO.DEFAULTS.HASH_ALG)
.update(`${pass}${salt}`)
.digest('hex')
resolve({ hash, salt, key: masterpass })
}
})
}
// Converts a buffer array to a hex string
exports.buf2hex = arr => {
const buf = Buffer.from(arr)
return buf.toString('hex')
}
// Compares vars in a constant time (protects against timing attacks)
exports.timingSafeEqual = (a, b) => {
// convert args to buffers if not already
a = Buffer.isBuffer(a) ? a : Buffer.from(a)
b = Buffer.isBuffer(b) ? b : Buffer.from(b)
var result = 0
var l = a.length
while (l--) {
// bitwise comparison
result |= a[l] ^ b[l]
}
return result === 0
}