secret-box
Version:
Encrypt and decrypt secrets.
102 lines (79 loc) • 3.68 kB
JavaScript
'use strict'
const assert = require('assert')
const crypto = require('crypto')
const scrypt = require('scryptsy')
const vstruct = require('varstruct')
// http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf 8.2.2 RBG-based Construction (about initialization vectors)
// always 12, any other value will error, not sure why it won't allow higher... probably concat with freefield?
const IV_LEN_BYTES = 12
// must always be 16 for the time being.
const SALT_LEN_BYTES = 16
const struct = vstruct([
{ name: 'version', type: vstruct.UInt8 },
{ name: 'n', type: vstruct.UInt8 }, // log2(n)
{ name: 'r', type: vstruct.UInt8 },
{ name: 'p', type: vstruct.UInt8 },
{ name: 'salt', type: vstruct.Buffer(SALT_LEN_BYTES) },
{ name: 'iv', type: vstruct.Buffer(IV_LEN_BYTES) },
{ name: 'authTag', type: vstruct.Buffer(16) },
{ name: 'secret', type: vstruct.VarBuffer(vstruct.UInt32BE) }
])
function createScryptOptions (scryptOpts) {
return Object.assign({}, { salt: crypto.randomBytes(SALT_LEN_BYTES), n: 16384, r: 8, p: 1 }, scryptOpts)
}
// NOTE: currently, always returns 256 bit keys
function stretchPassphrase (passphrase, scryptOpts) {
assert(Buffer.isBuffer(passphrase) || typeof passphrase === 'string', 'paspshrase must a string or Buffer')
const so = createScryptOptions(scryptOpts)
assert.strictEqual(so.salt.length, SALT_LEN_BYTES, `salt must be ${SALT_LEN_BYTES} bytes`)
const key = scrypt(passphrase, so.salt, so.n, so.r, so.p, 32)
return Object.assign({}, so, { key: key })
}
function aesEncrypt (key, message, iv) {
assert(Buffer.isBuffer(key), 'key must be a buffer')
assert(Buffer.isBuffer(message), 'message must be a buffer')
iv = iv || crypto.randomBytes(IV_LEN_BYTES)
assert.strictEqual(iv.length, IV_LEN_BYTES, `iv must be ${IV_LEN_BYTES} bytes`)
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
const secret = Buffer.concat([cipher.update(message), cipher.final()])
const authTag = cipher.getAuthTag()
return { authTag: authTag, iv: iv, secret: secret }
}
function aesDecrypt (key, secret, opts) {
assert(Buffer.isBuffer(key), 'key must be a buffer')
assert(Buffer.isBuffer(secret), 'message must be a buffer')
assert(opts.iv, 'must pass iv')
assert(opts.authTag, 'must pass authTag')
const decipher = crypto.createDecipheriv('aes-256-gcm', key, opts.iv)
decipher.setAuthTag(opts.authTag)
const message = Buffer.concat([decipher.update(secret), decipher.final()])
return message
}
function encrypt (message, passphrase, opts) {
assert(Buffer.isBuffer(passphrase) || typeof passphrase === 'string', 'passphrase must a string or Buffer')
assert(Buffer.isBuffer(message), 'message must be a Buffer')
opts = Object.assign({}, opts)
let stretchedData = stretchPassphrase(passphrase, opts)
let secretData = aesEncrypt(stretchedData.key, message, opts.iv)
// don't want to return this so that the user doesn't accidentally store it
delete stretchedData.key
let data = Object.assign({ version: 0 }, stretchedData, secretData)
// change n
data.n = Math.log2(data.n)
return struct.encode(data)
}
function decrypt (secret, passphrase) {
assert(Buffer.isBuffer(passphrase) || typeof passphrase === 'string', 'paspshrase must a string or Buffer')
assert(Buffer.isBuffer(secret), 'parameter "secret" must be a Buffer')
let opts = struct.decode(secret)
opts.n = Math.pow(2, opts.n)
const secretMessage = opts.secret
const stretchedData = stretchPassphrase(passphrase, opts)
const message = aesDecrypt(stretchedData.key, secretMessage, opts)
return message
}
module.exports = {
decrypt: decrypt,
encrypt: encrypt,
struct: struct
}