js-libp2p/src/keychain.js

363 lines
10 KiB
JavaScript
Raw Normal View History

'use strict'
const async = require('async')
const sanitize = require("sanitize-filename")
const forge = require('node-forge')
const deepmerge = require('deepmerge')
const crypto = require('crypto')
const libp2pCrypto = require('libp2p-crypto')
const util = require('./util')
const CMS = require('./cms')
const DS = require('interface-datastore')
const pull = require('pull-stream')
const keyExtension = '.p8'
// NIST SP 800-132
const NIST = {
minKeyLength: 112 / 8,
minSaltLength: 128 / 8,
minIterationCount: 1000
}
const defaultOptions = {
// See https://cryptosense.com/parametesr-choice-for-pbkdf2/
dek: {
keyLength: 512 / 8,
iterationCount: 10000,
salt: 'you should override this value with a crypto secure random number',
hash: 'sha512'
}
}
function validateKeyName (name) {
if (!name) return false
return name === sanitize(name.trim())
}
/**
* Returns an error to the caller, after a delay
*
* This assumes than an error indicates that the keychain is under attack. Delay returning an
* error to make brute force attacks harder.
*
* @param {function(Error)} callback - The caller
* @param {string | Error} err - The error
*/
function _error(callback, err) {
const min = 200
const max = 1000
const delay = Math.random() * (max - min) + min
if (typeof err === 'string') err = new Error(err)
setTimeout(callback, delay, err, null)
}
/**
* Converts a key name into a datastore name.
*/
function DsName (name) {
return new DS.Key('/' + name)
}
/**
* Converts a datastore name into a key name.
*/
function KsName(name) {
return name.toString().slice(1)
}
class Keychain {
constructor (store, options) {
if (!store) {
throw new Error('store is required')
}
this.store = store
if (this.store.opts) {
this.store.opts.extension = keyExtension
}
const opts = deepmerge(defaultOptions, options)
// Enforce NIST SP 800-132
if (!opts.passPhrase || opts.passPhrase.length < 20) {
throw new Error('passPhrase must be least 20 characters')
}
if (opts.dek.keyLength < NIST.minKeyLength) {
throw new Error(`dek.keyLength must be least ${NIST.minKeyLength} bytes`)
}
if (opts.dek.salt.length < NIST.minSaltLength) {
throw new Error(`dek.saltLength must be least ${NIST.minSaltLength} bytes`)
}
if (opts.dek.iterationCount < NIST.minIterationCount) {
throw new Error(`dek.iterationCount must be least ${NIST.minIterationCount}`)
}
this.dek = opts.dek
// Create the derived encrypting key
let dek = forge.pkcs5.pbkdf2(
opts.passPhrase,
opts.dek.salt,
opts.dek.iterationCount,
opts.dek.keyLength,
opts.dek.hash)
dek = forge.util.bytesToHex(dek)
Object.defineProperty(this, '_', { value: () => dek })
// JS magick
this._getKeyInfo = this.findKeyByName = this._getKeyInfo.bind(this)
// Provide access to protected messages
this.cms = new CMS(this)
}
static get options() {
return defaultOptions
}
createKey (name, type, size, callback) {
const self = this
if (!validateKeyName(name) || name === 'self') {
return _error(callback, `Invalid key name '${name}'`)
}
const dsname = DsName(name)
self.store.has(dsname, (err, exists) => {
if (exists) return _error(callback, `Key '${name}' already exists'`)
switch (type.toLowerCase()) {
case 'rsa':
if (size < 2048) {
return _error(callback, `Invalid RSA key size ${size}`)
}
forge.pki.rsa.generateKeyPair({bits: size, workers: -1}, (err, keypair) => {
if (err) return _error(callback, err)
const pem = forge.pki.encryptRsaPrivateKey(keypair.privateKey, this._());
return self.store.put(dsname, pem, (err) => {
if (err) return _error(callback, err)
self._getKeyInfo(name, callback)
})
})
break;
default:
return _error(callback, `Invalid key type '${type}'`)
}
})
}
listKeys (callback) {
const self = this
const query = {
keysOnly: true
}
pull(
self.store.query(query),
pull.collect((err, res) => {
if (err) return _error(callback, err)
const names = res.map(r => KsName(r.key))
async.map(names, self._getKeyInfo, callback)
})
)
}
// TODO: not very efficent.
findKeyById (id, callback) {
this.listKeys((err, keys) => {
if (err) return _error(callback, err)
const key = keys.find((k) => k.id === id)
callback(null, key)
})
}
removeKey (name, callback) {
const self = this
if (!validateKeyName(name) || name === 'self') {
return _error(callback, `Invalid key name '${name}'`)
}
const dsname = DsName(name)
self.store.has(dsname, (err, exists) => {
if (!exists) return _error(callback, `Key '${name}' does not exist'`)
self.store.delete(dsname, callback)
})
}
renameKey(oldName, newName, callback) {
const self = this
if (!validateKeyName(oldName) || oldName === 'self') {
return _error(callback, `Invalid old key name '${oldName}'`)
}
if (!validateKeyName(newName) || newName === 'self') {
return _error(callback, `Invalid new key name '${newName}'`)
}
const oldDsname = DsName(oldName)
const newDsname = DsName(newName)
this.store.get(oldDsname, (err, res) => {
if (err) {
return _error(callback, `Key '${oldName}' does not exist. ${err.message}`)
}
const pem = res.toString()
self.store.has(newDsname, (err, exists) => {
if (exists) return _error(callback, `Key '${newName}' already exists'`)
const batch = self.store.batch()
batch.put(newDsname, pem)
batch.delete(oldDsname)
batch.commit((err) => {
if (err) return _error(callback, err)
self._getKeyInfo(newName, callback)
})
})
})
}
exportKey (name, password, callback) {
if (!validateKeyName(name)) {
return _error(callback, `Invalid key name '${name}'`)
}
if (!password) {
return _error(callback, 'Password is required')
}
const dsname = DsName(name)
this.store.get(dsname, (err, res) => {
if (err) {
return _error(callback, `Key '${name}' does not exist. ${err.message}`)
}
const pem = res.toString()
try {
const options = {
algorithm: 'aes256',
count: this.dek.iterationCount,
saltSize: NIST.minSaltLength,
prfAlgorithm: 'sha512'
}
const privateKey = forge.pki.decryptRsaPrivateKey(pem, this._())
const res = forge.pki.encryptRsaPrivateKey(privateKey, password, options)
return callback(null, res)
} catch (e) {
_error(callback, e)
}
})
}
importKey(name, pem, password, callback) {
const self = this
if (!validateKeyName(name) || name === 'self') {
return _error(callback, `Invalid key name '${name}'`)
}
if (!pem) {
return _error(callback, 'PEM encoded key is required')
}
const dsname = DsName(name)
self.store.has(dsname, (err, exists) => {
if (exists) return _error(callback, `Key '${name}' already exists'`)
try {
const privateKey = forge.pki.decryptRsaPrivateKey(pem, password)
if (privateKey === null) {
return _error(callback, 'Cannot read the key, most likely the password is wrong')
}
const newpem = forge.pki.encryptRsaPrivateKey(privateKey, this._());
return self.store.put(dsname, newpem, (err) => {
if (err) return _error(callback, err)
this._getKeyInfo(name, callback)
})
} catch (err) {
_error(callback, err)
}
})
}
importPeer (name, peer, callback) {
const self = this
if (!validateKeyName(name)) {
return _error(callback, `Invalid key name '${name}'`)
}
if (!peer || !peer.privKey) {
return _error(callback, 'Peer.privKey \is required')
}
const dsname = DsName(name)
self.store.has(dsname, (err, exists) => {
if (exists) return _error(callback, `Key '${name}' already exists'`)
const privateKeyProtobuf = peer.marshalPrivKey()
libp2pCrypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => {
try {
const der = key.marshal()
const buf = forge.util.createBuffer(der.toString('binary'));
const obj = forge.asn1.fromDer(buf)
const privateKey = forge.pki.privateKeyFromAsn1(obj)
if (privateKey === null) {
return _error(callback, 'Cannot read the peer private key')
}
const pem = forge.pki.encryptRsaPrivateKey(privateKey, this._());
return self.store.put(dsname, pem, (err) => {
if (err) return _error(callback, err)
this._getKeyInfo(name, callback)
})
} catch (err) {
_error(callback, err)
}
})
})
}
/**
* Gets the private key as PEM encoded PKCS #8
*
* @param {string} name
* @param {function(Error, string)} callback
*/
_getPrivateKey (name, callback) {
const self = this
if (!validateKeyName(name)) {
return _error(callback, `Invalid key name '${name}'`)
}
this.store.get(DsName(name), (err, res) => {
if (err) {
return _error(callback, `Key '${name}' does not exist. ${err.message}`)
}
callback(null, res.toString())
})
}
_getKeyInfo (name, callback) {
const self = this
if (!validateKeyName(name)) {
return _error(callback, `Invalid key name '${name}'`)
}
const dsname = DsName(name)
this.store.get(dsname, (err, res) => {
if (err) {
return _error(callback, `Key '${name}' does not exist. ${err.message}`)
}
const pem = res.toString()
try {
const privateKey = forge.pki.decryptRsaPrivateKey(pem, this._())
util.keyId(privateKey, (err, kid) => {
if (err) return _error(callback, err)
const info = {
name: name,
id: kid
}
return callback(null, info)
})
} catch (e) {
_error(callback, e)
}
})
}
}
module.exports = Keychain