vault-nacl
Version:
A symmetric encrypted vault using nacl elliptic curves
164 lines (138 loc) • 4.89 kB
JavaScript
const { promisify } = require('util')
const crypto = require('crypto')
const nacl = require('tweetnacl/nacl-fast')
const pbkdf2 = promisify(crypto.pbkdf2)
const VERSION = 1
const PASSWORD = Symbol('PASSWORD')
const NONCE_LEN = nacl.box.nonceLength
const KEY_LEN = nacl.box.secretKeyLength
const DIGESTS = [
'sha256',
'sha384',
'sha512',
'ripemd',
'whirlpool'
]
const digestToId = (digest) => Buffer.from([DIGESTS.indexOf(digest)])
const idToDigest = (id) => DIGESTS[id[0]]
const numToBuffer = (number) => {
const buf = Buffer.alloc(4)
buf.writeUInt32LE(number)
return buf
}
const bufferToNum = (buf) => buf.readUInt32LE()
class Vault {
/**
* @param {string} password
* @param {object} opts
* @param {string} [opts.digest='sha256']
* @param {number} [opts.iterations=310000]
* @param {BufferEncoding} [opts.inputEncoding='utf8']
* @param {BufferEncoding} [opts.outputEncoding='base64']
*/
constructor (password, opts = {}) {
const {
digest = 'sha256',
iterations,
inputEncoding = 'utf8',
outputEncoding = 'base64'
} = opts
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
this.iterations = iterations ?? digest === 'sha512'
? 120000
: 310000
this.digest = digest
this.inputEncoding = inputEncoding
this.outputEncoding = outputEncoding
Object.defineProperty(this, PASSWORD, { value: password, writable: true })
}
clear () {
Object.defineProperty(this, PASSWORD, { value: undefined })
}
_derivedKeySync ({ salt, iterations, digest }) {
if (!this[PASSWORD]) throw new Error('No password')
if (!DIGESTS.includes(digest)) throw new Error('Unsupported digest')
const derivedKey = crypto.pbkdf2Sync(this[PASSWORD], salt, iterations, NONCE_LEN + KEY_LEN, digest)
return nonceKey(derivedKey)
}
_joinV1 ({ salt, digest, iterations, box }) {
return Buffer.concat([
Buffer.from([VERSION]), // 1 byte
digestToId(digest), // 1 byte
numToBuffer(iterations), // 4 byte
salt, // KEY_LEN bytes
box
])
}
_sliceV1 (buf) {
let tmp = 0
const version = buf.slice(tmp, tmp += 1)[0]
const digest = idToDigest(buf.slice(tmp, tmp += 1))
const iterations = bufferToNum(buf.slice(tmp, tmp += 4))
const salt = buf.slice(tmp, tmp += KEY_LEN)
const box = buf.slice(tmp)
if (version > VERSION) throw new Error(`Unsupported version ${version}`)
// console.log(digest, iterations, version)
return { salt, digest, iterations, box, version }
}
_enc ({ salt, digest, iterations, msgBuffer, nonce, key }) {
const box = nacl.secretbox(msgBuffer, nonce, key)
const buf = this._joinV1({ salt, digest, iterations, box })
return buf.toString(this.outputEncoding)
}
_dec ({ box, nonce, key }) {
try {
const msgBuffer = Buffer.from(nacl.secretbox.open(box, nonce, key))
return msgBuffer.toString(this.inputEncoding)
} catch (e) {
throw new Error('Decrypt failed')
}
}
encryptSync (message) {
const msgBuffer = Buffer.from(message, this.inputEncoding)
const salt = nacl.randomBytes(KEY_LEN)
const { digest, iterations } = this
const { nonce, key } = this._derivedKeySync({ salt, digest, iterations })
return this._enc({ salt, digest, iterations, msgBuffer, nonce, key })
}
decryptSync (message) {
const buf = Buffer.from(message, this.outputEncoding)
const { salt, digest, iterations, box } = this._sliceV1(buf)
const { nonce, key } = this._derivedKeySync({ salt, digest, iterations })
return this._dec({ box, nonce, key })
}
async _derivedKey ({ salt, iterations, digest }) {
if (!this[PASSWORD]) throw new Error('No password')
if (!DIGESTS.includes(digest)) throw new Error('Unsupported digest')
const derivedKey = await pbkdf2(this[PASSWORD], salt, iterations, NONCE_LEN + KEY_LEN, digest)
return nonceKey(derivedKey)
}
async encrypt (message) {
const msgBuffer = Buffer.from(message, this.inputEncoding)
const salt = nacl.randomBytes(KEY_LEN)
const { digest, iterations } = this
const { nonce, key } = await this._derivedKey({ salt, digest, iterations })
return this._enc({ salt, digest, iterations, msgBuffer, nonce, key })
}
async decrypt (message) {
const buf = Buffer.from(message, this.outputEncoding)
const { salt, digest, iterations, box } = this._sliceV1(buf)
const { nonce, key } = await this._derivedKey({ salt, digest, iterations })
return this._dec({ box, nonce, key })
}
}
module.exports = { Vault }
/**
* @private
* @param {Buffer} derivedKey
* @returns {{nonce: Buffer, key: Buffer}}
*/
const nonceKey = (derivedKey) => {
let tmp = 0
const nonce = derivedKey.slice(tmp, tmp += NONCE_LEN)
const key = derivedKey.slice(tmp, tmp += KEY_LEN)
return {
nonce,
key
}
}