/**
* @file Multi-Factor Derived Key Enveloped Secret Functions
* @copyright Multifactor 2022 All Rights Reserved
*
* @description
* Enveloped secret operations using a multi-factor derived key
*
* @author Vivek Nair (https://nair.me) <[email protected]>
*/
const crypto = require('crypto')
/**
* Add enveloped secret to a multi-factor derived key
*
* @example
* // setup multi-factor derived key
* const key = await mfkdf.setup.key([ await mfkdf.setup.factors.password('password') ])
*
* // add enveloped secret to key
* await key.addEnvelopedSecret('mySecret', Buffer.from('hello world'))
*
* // later... derive key
* const derived = await mfkdf.derive.key(key.policy, { password: mfkdf.derive.factors.password('password') })
*
* // retrieve secret
* const secret = await derived.getEnvelopedSecret('mySecret')
* secret.toString() // -> hello world
*
* @param {string} id - String which uniquely identifies the enveloped secret to add
* @param {Buffer} value - The plaintext secret value to be encrypted with this key
* @param {string} [type='raw'] - The type of the enveloped secret to add
* @author Vivek Nair (https://nair.me) <[email protected]>
* @since 0.20.0
* @memberOf MFKDFDerivedKey
* @async
*/
async function addEnvelopedSecret (id, value, type = 'raw') {
if (typeof id !== 'string') throw new TypeError('id must be a string')
if (!Buffer.isBuffer(value)) throw new TypeError('value must be a buffer')
if (typeof type !== 'string') throw new TypeError('type must be a string')
if (this.hasEnvelopedSecret(id)) throw new RangeError('id must be unique')
if (!Array.isArray(this.policy.secrets)) this.policy.secrets = []
const ct = await this.encrypt(value)
this.policy.secrets.push({
id,
value: ct.toString('base64'),
type
})
}
module.exports.addEnvelopedSecret = addEnvelopedSecret
/**
* Check if multi-factor derived key has enveloped secret with id
*
* @example
* // setup multi-factor derived key
* const key = await mfkdf.setup.key([ await mfkdf.setup.factors.password('password') ])
*
* // add enveloped secret to key
* await key.addEnvelopedSecret('mySecret', Buffer.from('hello world'))
*
* // later... derive key
* const derived = await mfkdf.derive.key(key.policy, { password: mfkdf.derive.factors.password('password') })
*
* // check secret
* const check1 = derived.hasEnvelopedSecret('mySecret') // -> true
*
* // remove secret
* derived.removeEnvelopedSecret('mySecret')
*
* // check secret
* const check2 = derived.hasEnvelopedSecret('mySecret') // -> false
*
* @param {string} id - String which uniquely identifies the enveloped secret
* @returns {boolean} - Whether the key has enveloped secret with given id
* @author Vivek Nair (https://nair.me) <[email protected]>
* @since 0.20.0
* @memberOf MFKDFDerivedKey
*/
function hasEnvelopedSecret (id) {
if (typeof id !== 'string') throw new TypeError('id must be a string')
if (!Array.isArray(this.policy.secrets)) return false
return this.policy.secrets.some(x => x.id === id)
}
module.exports.hasEnvelopedSecret = hasEnvelopedSecret
/**
* Remove enveloped secret from a multi-factor derived key
*
* @example
* // setup multi-factor derived key
* const key = await mfkdf.setup.key([ await mfkdf.setup.factors.password('password') ])
*
* // add enveloped secret to key
* await key.addEnvelopedSecret('mySecret', Buffer.from('hello world'))
*
* // later... derive key
* const derived = await mfkdf.derive.key(key.policy, { password: mfkdf.derive.factors.password('password') })
*
* // check secret
* const check1 = derived.hasEnvelopedSecret('mySecret') // -> true
*
* // remove secret
* derived.removeEnvelopedSecret('mySecret')
*
* // check secret
* const check2 = derived.hasEnvelopedSecret('mySecret') // -> false
*
* @param {string} id - ID of the enveloped secret to remove
* @author Vivek Nair (https://nair.me) <[email protected]>
* @since 0.20.0
* @memberOf MFKDFDerivedKey
*/
function removeEnvelopedSecret (id) {
if (typeof id !== 'string') throw new TypeError('id must be a string')
if (!this.hasEnvelopedSecret(id)) throw new RangeError('secret with id does not exist')
this.policy.secrets = this.policy.secrets.filter(x => x.id !== id)
}
module.exports.removeEnvelopedSecret = removeEnvelopedSecret
/**
* Add enveloped key to a multi-factor derived key
*
* @example
* // setup multi-factor derived key
* const key = await mfkdf.setup.key([ await mfkdf.setup.factors.password('password') ])
*
* // add enveloped rsa1024 key
* await key.addEnvelopedKey('myKey', 'rsa1024')
*
* // later... derive key
* const derived = await mfkdf.derive.key(key.policy, { password: mfkdf.derive.factors.password('password') })
*
* // retrieve enveloped key
* const enveloped = await derived.getEnvelopedKey('myKey') // -> PrivateKeyObject
*
* @param {string} id - String which uniquely identifies the enveloped key to add
* @param {string} [type='rsa1024'] - The type of the enveloped key to add; rsa1024, rsa2048, or ed25519
* @author Vivek Nair (https://nair.me) <[email protected]>
* @since 0.20.0
* @memberOf MFKDFDerivedKey
* @async
*/
async function addEnvelopedKey (id, type = 'rsa1024') {
if (typeof id !== 'string') throw new TypeError('id must be a string')
if (typeof type !== 'string') throw new TypeError('type must be a string')
const options = {
privateKeyEncoding: {
type: 'pkcs8',
format: 'der'
}
}
let myType
if (type === 'rsa1024') {
myType = 'rsa'
options.modulusLength = 1024
} else if (type === 'rsa2048') {
myType = 'rsa'
options.modulusLength = 2048
} else if (type === 'ed25519') {
myType = 'ed25519'
} else {
throw new RangeError('invalid key type')
}
const keyPair = await crypto.generateKeyPairSync(myType, options)
return await this.addEnvelopedSecret(id, keyPair.privateKey, type)
}
module.exports.addEnvelopedKey = addEnvelopedKey
/**
* Get enveloped secret from a multi-factor derived key
*
* @example
* // setup multi-factor derived key
* const key = await mfkdf.setup.key([ await mfkdf.setup.factors.password('password') ])
*
* // add enveloped secret to key
* await key.addEnvelopedSecret('mySecret', Buffer.from('hello world'))
*
* // later... derive key
* const derived = await mfkdf.derive.key(key.policy, { password: mfkdf.derive.factors.password('password') })
*
* // retrieve secret
* const secret = await derived.getEnvelopedSecret('mySecret')
* secret.toString() // -> hello world
*
* @param {string} id - ID of the enveloped secret to get
* @returns {Buffer} The retrieved plaintext secret value
* @author Vivek Nair (https://nair.me) <[email protected]>
* @since 0.20.0
* @memberOf MFKDFDerivedKey
* @async
*/
async function getEnvelopedSecret (id) {
if (typeof id !== 'string') throw new TypeError('id must be a string')
if (!this.hasEnvelopedSecret(id)) throw new RangeError('secret with id does not exist')
const secret = this.policy.secrets.find(x => x.id === id)
const ct = Buffer.from(secret.value, 'base64')
return await this.decrypt(ct)
}
module.exports.getEnvelopedSecret = getEnvelopedSecret
/**
* Get enveloped secret from a multi-factor derived key
*
* @example
* // setup multi-factor derived key
* const key = await mfkdf.setup.key([ await mfkdf.setup.factors.password('password') ])
*
* // add enveloped rsa1024 key
* await key.addEnvelopedKey('myKey', 'rsa1024')
*
* // later... derive key
* const derived = await mfkdf.derive.key(key.policy, { password: mfkdf.derive.factors.password('password') })
*
* // retrieve enveloped key
* const enveloped = await derived.getEnvelopedKey('myKey') // -> PrivateKeyObject
*
* @param {string} id - ID of the enveloped key to get
* @returns {PrivateKeyObject} The retrieved enveloped key
* @author Vivek Nair (https://nair.me) <[email protected]>
* @since 0.20.0
* @memberOf MFKDFDerivedKey
* @async
*/
async function getEnvelopedKey (id) {
if (typeof id !== 'string') throw new TypeError('id must be a string')
const privateKey = await this.getEnvelopedSecret(id)
return await crypto.createPrivateKey({
key: privateKey,
format: 'der',
type: 'pkcs8'
})
}
module.exports.getEnvelopedKey = getEnvelopedKey