From 3fc4ffb00bcd8a7d17dee992d79d112c66345cab Mon Sep 17 00:00:00 2001 From: Joseph Werle Date: Tue, 22 May 2018 14:01:35 -0400 Subject: [PATCH] feat({encrypt,decrypt}.js): Introduce encryption/decryption functions --- decrypt.js | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++ encrypt.js | 95 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 decrypt.js create mode 100644 encrypt.js diff --git a/decrypt.js b/decrypt.js new file mode 100644 index 0000000..9a26626 --- /dev/null +++ b/decrypt.js @@ -0,0 +1,109 @@ +'use strict' + +const { createDecipheriv, createHmac } = require('crypto') +const isBuffer = require('is-buffer') +const uint64 = require('./uint64') +const pkg = require('./package') + +const { + kVersion, + kDefaultCipher, + kDefaultDigest, +} = require('./constants') + +/** + * Decrypt an encrypted "crypto" object into the originally + * encoded buffer. + * @public + * @param {Object} value + * @param {Object} opts + * @param {Buffer} opts.iv + * @param {Buffer} opts.key + * @param {?(String)} opts.cipher + * @param {?(String)} opts.digest + * @return {Buffer} + */ +function decrypt(value, opts) { + if (null == value) { + throw new TypeError("crypto.decrypt: Encrypted value cannot be null.") + } else if (isBuffer(value)) { + throw new TypeError("crypto.decrypt: Encrypted value cannot be a buffer.") + } else if (!value || 'object' != typeof value){ + throw new TypeError("crypto.decrypt: Encrypted value to be an object.") + } + + if (null == value.version || 'string' != typeof value.version) { + throw new TypeError("crypto.decrypt: Missing encryption version.") + } + + if (null == value.id || 'string' != typeof value.id) { + throw new TypeError("crypto.decrypt: Missing encryption ID.") + } + + if (null == value.crypto || 'object' != typeof value.crypto) { + throw new TypeError( + "crypto.decrypt: Missing encryption crypto specification object.") + } + + if (!opts || 'object' != typeof opts) { + throw new TypeError("crypto.decrypt: Expecting options object.") + } + + if (null == opts.key) { + throw new TypeError("crypto.decrypt: Expecting decryption key.") + } else if ('string' != typeof opts.key && false == isBuffer(opts.key)) { + throw new TypeError( + "crypto.decrypt: Expecting decryption key to be a string or buffer.") + } + + if (null == opts.iv) { + throw new TypeError("crypto.decrypt: Expecting decryption iv.") + } else if ('string' != typeof opts.iv && false == isBuffer(opts.iv)) { + throw new TypeError( + "crypto.decrypt: Expecting decryption iv to be a string or buffer.") + } + + if (!opts.cipher || 'string' != typeof opts.cipher) { + opts.cipher = kDefaultCipher + } + + if (!opts.digest || 'string' != typeof opts.digest) { + opts.digest = kDefaultDigest + } + + if ('string' != typeof opts.cipher) { + throw new TypeError("crypto.decrypt: Expecting cipher type to be a string.") + } + + if ('string' != typeof opts.digest) { + throw new TypeError("crypto.decrypt: Expecting digest type to be a string.") + } + + const version = uint64.encode(kVersion).toString('hex') + if (version != value.version) { + throw new TypeError("crypto.decrypt: Encryption version does not match.") + } + + const { cipher, digest, key, iv } = opts + const decipheriv = createDecipheriv(cipher, key, iv) + const hmac = createHmac(digest, key) + const buffer = Buffer.from(value.crypto.ciphertext, 'hex') + + hmac.write(buffer) + hmac.end() + + const mac = hmac.read().toString('hex') + + if (mac != value.crypto.mac) { + throw new TypeError("crypto.decrypt: HMAC digest does not match.") + } + + return Buffer.concat([ + decipheriv.update(buffer), + decipheriv.final(), + ]) +} + +module.exports = { + decrypt +} diff --git a/encrypt.js b/encrypt.js new file mode 100644 index 0000000..11c6396 --- /dev/null +++ b/encrypt.js @@ -0,0 +1,95 @@ +'use strict' + +const { createCipheriv, createHmac } = require('crypto') +const toBuffer = require('to-buffer') +const isBuffer = require('is-buffer') +const uint64 = require('./uint64') +const uuid = require('uuid/v4') + +const { + kVersion, + kDefaultCipher, + kDefaultDigest, +} = require('./constants') + +/** + * Encrypts value into a "crypto" object configured by + * an initialization vector (iv) and secret key (key) with + * optional cipher and digest algorithms. + * @public + * @param {String|Buffer} value + * @param {Object} opts + * @param {Buffer} opts.iv + * @param {Buffer} opts.key + * @param {?(String)} opts.cipher + * @param {?(String)} opts.digest + * @return {Object} + */ +function encrypt(value, opts) { + if (null == value) { + throw new TypeError("crypto.encrypt: Expecting value. Got null.") + } else if ('string' != typeof value && false == isBuffer(value)) { + throw new TypeError( + "crypto.encrypt: Expecting value to be a string or buffer.") + } else if (0 == value.length) { + throw new TypeError( + "crypto.encrypt: Cannot encrypt empty value.") + } + + if (!opts || 'object' != typeof opts) { + throw new TypeError("crypto.encrypt: Expecting options object.") + } + + if (null == opts.key) { + throw new TypeError("crypto.encrypt: Expecting encryption key.") + } else if ('string' != typeof opts.key && false == isBuffer(opts.key)) { + throw new TypeError( + "crypto.encrypt: Expecting encryption key to be a string or buffer.") + } + + if (!opts.cipher || 'string' != typeof opts.cipher) { + opts.cipher = kDefaultCipher + } + + if (!opts.digest || 'string' != typeof opts.digest) { + opts.digest = kDefaultDigest + } + + if ('string' != typeof opts.cipher) { + throw new TypeError("crypto.encrypt: Expecting cipher type to be a string.") + } + + if ('string' != typeof opts.digest) { + throw new TypeError("crypto.encrypt: Expecting digest type to be a string.") + } + + const { cipher, digest, key, iv } = opts + const cipheriv = createCipheriv(cipher, key, iv) + const hmac = createHmac(digest, key) + + const buffer = Buffer.concat([cipheriv.update(value), cipheriv.final()]) + const ciphertext = buffer.toString('hex') + + hmac.write(buffer) + hmac.end() + + const id = uuid() + const mac = hmac.read().toString('hex') + const version = uint64.encode(kVersion).toString('hex') + const cipherparams = { iv: iv.toString('hex') } + + return { + id, + version, + crypto: { + cipherparams, + ciphertext, + cipher, + mac, + } + } +} + +module.exports = { + encrypt +}