Skip to content

Commit

Permalink
Merge pull request #13 from libp2p/ds-keyinfo
Browse files Browse the repository at this point in the history
Persist the key info in the store
  • Loading branch information
richardschneider committed Dec 16, 2017
2 parents 9129d20 + 1b2664a commit 97bf98f
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 63 deletions.
164 changes: 101 additions & 63 deletions src/keychain.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict'

const async = require('async')
const sanitize = require('sanitize-filename')
const forge = require('node-forge')
const deepmerge = require('deepmerge')
Expand All @@ -10,7 +9,8 @@ const CMS = require('./cms')
const DS = require('interface-datastore')
const pull = require('pull-stream')

const keyExtension = '.p8'
const keyPrefix = '/pkcs8/'
const infoPrefix = '/info/'

// NIST SP 800-132
const NIST = {
Expand Down Expand Up @@ -74,18 +74,18 @@ function _error (callback, err) {
* @private
*/
function DsName (name) {
return new DS.Key('/' + name)
return new DS.Key(keyPrefix + name)
}

/**
* Converts a datastore name into a key name.
* Converts a key name into a datastore info name.
*
* @param {DS.Key} name - A datastore name
* @returns {string}
* @param {string} name
* @returns {DS.Key}
* @private
*/
function KsName (name) {
return name.toString().slice(1)
function DsInfoName (name) {
return new DS.Key(infoPrefix + name)
}

/**
Expand All @@ -98,7 +98,12 @@ function KsName (name) {
*/

/**
* Key management
* Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8.
*
* A key in the store has two entries
* - '/info/key-name', contains the KeyInfo for the key
* - '/pkcs8/key-name', contains the PKCS #8 for the key
*
*/
class Keychain {
/**
Expand All @@ -112,9 +117,6 @@ class Keychain {
throw new Error('store is required')
}
this.store = store
if (this.store.opts) {
this.store.opts.extension = keyExtension
}

const opts = deepmerge(defaultOptions, options)

Expand Down Expand Up @@ -149,9 +151,6 @@ class Keychain {
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)
}
Expand Down Expand Up @@ -192,12 +191,22 @@ class Keychain {
}
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) => {
util.keyId(keypair.privateKey, (err, kid) => {
if (err) return _error(callback, err)

self._getKeyInfo(name, callback)
const pem = forge.pki.encryptRsaPrivateKey(keypair.privateKey, this._())
const keyInfo = {
name: name,
id: kid
}
const batch = self.store.batch()
batch.put(dsname, pem)
batch.put(DsInfoName(name), JSON.stringify(keyInfo))
batch.commit((err) => {
if (err) return _error(callback, err)

callback(null, keyInfo)
})
})
})
break
Expand All @@ -217,28 +226,27 @@ class Keychain {
listKeys (callback) {
const self = this
const query = {
keysOnly: true
prefix: infoPrefix
}
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)
const info = res.map(r => JSON.parse(r.value))
callback(null, info)
})
)
}

/**
* Find a key by it's name.
* Find a key by it's id.
*
* @param {string} id - The universally unique key identifier.
* @param {function(Error, KeyInfo)} callback
* @returns {undefined}
*/
findKeyById (id, callback) {
// TODO: not very efficent.
this.listKeys((err, keys) => {
if (err) return _error(callback, err)

Expand All @@ -247,6 +255,28 @@ class Keychain {
})
}

/**
* Find a key by it's name.
*
* @param {string} name - The local key name.
* @param {function(Error, KeyInfo)} callback
* @returns {undefined}
*/
findKeyByName (name, callback) {
if (!validateKeyName(name)) {
return _error(callback, `Invalid key name '${name}'`)
}

const dsname = DsInfoName(name)
this.store.get(dsname, (err, res) => {
if (err) {
return _error(callback, `Key '${name}' does not exist. ${err.message}`)
}

callback(null, JSON.parse(res.toString()))
})
}

/**
* Remove an existing key.
*
Expand All @@ -260,9 +290,12 @@ class Keychain {
return _error(callback, `Invalid key name '${name}'`)
}
const dsname = DsName(name)
self._getKeyInfo(name, (err, keyinfo) => {
self.findKeyByName(name, (err, keyinfo) => {
if (err) return _error(callback, err)
self.store.delete(dsname, (err) => {
const batch = self.store.batch()
batch.delete(dsname)
batch.delete(DsInfoName(name))
batch.commit((err) => {
if (err) return _error(callback, err)
callback(null, keyinfo)
})
Expand All @@ -287,6 +320,8 @@ class Keychain {
}
const oldDsname = DsName(oldName)
const newDsname = DsName(newName)
const oldInfoName = DsInfoName(oldName)
const newInfoName = DsInfoName(newName)
this.store.get(oldDsname, (err, res) => {
if (err) {
return _error(callback, `Key '${oldName}' does not exist. ${err.message}`)
Expand All @@ -296,12 +331,20 @@ class Keychain {
if (err) return _error(callback, err)
if (exists) return _error(callback, `Key '${newName}' already exists`)

const batch = self.store.batch()
batch.put(newDsname, pem)
batch.delete(oldDsname)
batch.commit((err) => {
self.store.get(oldInfoName, (err, res) => {
if (err) return _error(callback, err)
self._getKeyInfo(newName, callback)

const keyInfo = JSON.parse(res.toString())
keyInfo.name = newName
const batch = self.store.batch()
batch.put(newDsname, pem)
batch.put(newInfoName, JSON.stringify(keyInfo))
batch.delete(oldDsname)
batch.delete(oldInfoName)
batch.commit((err) => {
if (err) return _error(callback, err)
callback(null, keyInfo)
})
})
})
})
Expand Down Expand Up @@ -372,10 +415,21 @@ class Keychain {
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) => {
util.keyId(privateKey, (err, kid) => {
if (err) return _error(callback, err)

this._getKeyInfo(name, callback)
const keyInfo = {
name: name,
id: kid
}
const batch = self.store.batch()
batch.put(dsname, newpem)
batch.put(DsInfoName(name), JSON.stringify(keyInfo))
batch.commit((err) => {
if (err) return _error(callback, err)

callback(null, keyInfo)
})
})
} catch (err) {
_error(callback, err)
Expand Down Expand Up @@ -408,10 +462,21 @@ class Keychain {
return _error(callback, 'Cannot read the peer private key')
}
const pem = forge.pki.encryptRsaPrivateKey(privateKey, this._())
return self.store.put(dsname, pem, (err) => {
util.keyId(privateKey, (err, kid) => {
if (err) return _error(callback, err)

this._getKeyInfo(name, callback)
const keyInfo = {
name: name,
id: kid
}
const batch = self.store.batch()
batch.put(dsname, pem)
batch.put(DsInfoName(name), JSON.stringify(keyInfo))
batch.commit((err) => {
if (err) return _error(callback, err)

callback(null, keyInfo)
})
})
} catch (err) {
_error(callback, err)
Expand All @@ -426,6 +491,7 @@ class Keychain {
* @param {string} name
* @param {function(Error, string)} callback
* @returns {undefined}
* @private
*/
_getPrivateKey (name, callback) {
if (!validateKeyName(name)) {
Expand All @@ -438,34 +504,6 @@ class Keychain {
callback(null, res.toString())
})
}

_getKeyInfo (name, callback) {
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
10 changes: 10 additions & 0 deletions test/keychain.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,16 @@ module.exports = (datastore1, datastore2) => {
done()
})
})

it('key exists', (done) => {
ks.findKeyByName('alice', (err, key) => {
expect(err).to.not.exist()
expect(key).to.exist()
expect(key).to.have.property('name', 'alice')
expect(key).to.have.property('id', alice.toB58String())
done()
})
})
})

describe('rename', () => {
Expand Down

0 comments on commit 97bf98f

Please sign in to comment.