Skip to content

Commit 97bf98f

Browse files
Merge pull request #13 from libp2p/ds-keyinfo
Persist the key info in the store
2 parents 9129d20 + 1b2664a commit 97bf98f

File tree

2 files changed

+111
-63
lines changed

2 files changed

+111
-63
lines changed

src/keychain.js

+101-63
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict'
22

3-
const async = require('async')
43
const sanitize = require('sanitize-filename')
54
const forge = require('node-forge')
65
const deepmerge = require('deepmerge')
@@ -10,7 +9,8 @@ const CMS = require('./cms')
109
const DS = require('interface-datastore')
1110
const pull = require('pull-stream')
1211

13-
const keyExtension = '.p8'
12+
const keyPrefix = '/pkcs8/'
13+
const infoPrefix = '/info/'
1414

1515
// NIST SP 800-132
1616
const NIST = {
@@ -74,18 +74,18 @@ function _error (callback, err) {
7474
* @private
7575
*/
7676
function DsName (name) {
77-
return new DS.Key('/' + name)
77+
return new DS.Key(keyPrefix + name)
7878
}
7979

8080
/**
81-
* Converts a datastore name into a key name.
81+
* Converts a key name into a datastore info name.
8282
*
83-
* @param {DS.Key} name - A datastore name
84-
* @returns {string}
83+
* @param {string} name
84+
* @returns {DS.Key}
8585
* @private
8686
*/
87-
function KsName (name) {
88-
return name.toString().slice(1)
87+
function DsInfoName (name) {
88+
return new DS.Key(infoPrefix + name)
8989
}
9090

9191
/**
@@ -98,7 +98,12 @@ function KsName (name) {
9898
*/
9999

100100
/**
101-
* Key management
101+
* Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8.
102+
*
103+
* A key in the store has two entries
104+
* - '/info/key-name', contains the KeyInfo for the key
105+
* - '/pkcs8/key-name', contains the PKCS #8 for the key
106+
*
102107
*/
103108
class Keychain {
104109
/**
@@ -112,9 +117,6 @@ class Keychain {
112117
throw new Error('store is required')
113118
}
114119
this.store = store
115-
if (this.store.opts) {
116-
this.store.opts.extension = keyExtension
117-
}
118120

119121
const opts = deepmerge(defaultOptions, options)
120122

@@ -149,9 +151,6 @@ class Keychain {
149151
dek = forge.util.bytesToHex(dek)
150152
Object.defineProperty(this, '_', { value: () => dek })
151153

152-
// JS magick
153-
this._getKeyInfo = this.findKeyByName = this._getKeyInfo.bind(this)
154-
155154
// Provide access to protected messages
156155
this.cms = new CMS(this)
157156
}
@@ -192,12 +191,22 @@ class Keychain {
192191
}
193192
forge.pki.rsa.generateKeyPair({bits: size, workers: -1}, (err, keypair) => {
194193
if (err) return _error(callback, err)
195-
196-
const pem = forge.pki.encryptRsaPrivateKey(keypair.privateKey, this._())
197-
return self.store.put(dsname, pem, (err) => {
194+
util.keyId(keypair.privateKey, (err, kid) => {
198195
if (err) return _error(callback, err)
199196

200-
self._getKeyInfo(name, callback)
197+
const pem = forge.pki.encryptRsaPrivateKey(keypair.privateKey, this._())
198+
const keyInfo = {
199+
name: name,
200+
id: kid
201+
}
202+
const batch = self.store.batch()
203+
batch.put(dsname, pem)
204+
batch.put(DsInfoName(name), JSON.stringify(keyInfo))
205+
batch.commit((err) => {
206+
if (err) return _error(callback, err)
207+
208+
callback(null, keyInfo)
209+
})
201210
})
202211
})
203212
break
@@ -217,28 +226,27 @@ class Keychain {
217226
listKeys (callback) {
218227
const self = this
219228
const query = {
220-
keysOnly: true
229+
prefix: infoPrefix
221230
}
222231
pull(
223232
self.store.query(query),
224233
pull.collect((err, res) => {
225234
if (err) return _error(callback, err)
226235

227-
const names = res.map(r => KsName(r.key))
228-
async.map(names, self._getKeyInfo, callback)
236+
const info = res.map(r => JSON.parse(r.value))
237+
callback(null, info)
229238
})
230239
)
231240
}
232241

233242
/**
234-
* Find a key by it's name.
243+
* Find a key by it's id.
235244
*
236245
* @param {string} id - The universally unique key identifier.
237246
* @param {function(Error, KeyInfo)} callback
238247
* @returns {undefined}
239248
*/
240249
findKeyById (id, callback) {
241-
// TODO: not very efficent.
242250
this.listKeys((err, keys) => {
243251
if (err) return _error(callback, err)
244252

@@ -247,6 +255,28 @@ class Keychain {
247255
})
248256
}
249257

258+
/**
259+
* Find a key by it's name.
260+
*
261+
* @param {string} name - The local key name.
262+
* @param {function(Error, KeyInfo)} callback
263+
* @returns {undefined}
264+
*/
265+
findKeyByName (name, callback) {
266+
if (!validateKeyName(name)) {
267+
return _error(callback, `Invalid key name '${name}'`)
268+
}
269+
270+
const dsname = DsInfoName(name)
271+
this.store.get(dsname, (err, res) => {
272+
if (err) {
273+
return _error(callback, `Key '${name}' does not exist. ${err.message}`)
274+
}
275+
276+
callback(null, JSON.parse(res.toString()))
277+
})
278+
}
279+
250280
/**
251281
* Remove an existing key.
252282
*
@@ -260,9 +290,12 @@ class Keychain {
260290
return _error(callback, `Invalid key name '${name}'`)
261291
}
262292
const dsname = DsName(name)
263-
self._getKeyInfo(name, (err, keyinfo) => {
293+
self.findKeyByName(name, (err, keyinfo) => {
264294
if (err) return _error(callback, err)
265-
self.store.delete(dsname, (err) => {
295+
const batch = self.store.batch()
296+
batch.delete(dsname)
297+
batch.delete(DsInfoName(name))
298+
batch.commit((err) => {
266299
if (err) return _error(callback, err)
267300
callback(null, keyinfo)
268301
})
@@ -287,6 +320,8 @@ class Keychain {
287320
}
288321
const oldDsname = DsName(oldName)
289322
const newDsname = DsName(newName)
323+
const oldInfoName = DsInfoName(oldName)
324+
const newInfoName = DsInfoName(newName)
290325
this.store.get(oldDsname, (err, res) => {
291326
if (err) {
292327
return _error(callback, `Key '${oldName}' does not exist. ${err.message}`)
@@ -296,12 +331,20 @@ class Keychain {
296331
if (err) return _error(callback, err)
297332
if (exists) return _error(callback, `Key '${newName}' already exists`)
298333

299-
const batch = self.store.batch()
300-
batch.put(newDsname, pem)
301-
batch.delete(oldDsname)
302-
batch.commit((err) => {
334+
self.store.get(oldInfoName, (err, res) => {
303335
if (err) return _error(callback, err)
304-
self._getKeyInfo(newName, callback)
336+
337+
const keyInfo = JSON.parse(res.toString())
338+
keyInfo.name = newName
339+
const batch = self.store.batch()
340+
batch.put(newDsname, pem)
341+
batch.put(newInfoName, JSON.stringify(keyInfo))
342+
batch.delete(oldDsname)
343+
batch.delete(oldInfoName)
344+
batch.commit((err) => {
345+
if (err) return _error(callback, err)
346+
callback(null, keyInfo)
347+
})
305348
})
306349
})
307350
})
@@ -372,10 +415,21 @@ class Keychain {
372415
return _error(callback, 'Cannot read the key, most likely the password is wrong')
373416
}
374417
const newpem = forge.pki.encryptRsaPrivateKey(privateKey, this._())
375-
return self.store.put(dsname, newpem, (err) => {
418+
util.keyId(privateKey, (err, kid) => {
376419
if (err) return _error(callback, err)
377420

378-
this._getKeyInfo(name, callback)
421+
const keyInfo = {
422+
name: name,
423+
id: kid
424+
}
425+
const batch = self.store.batch()
426+
batch.put(dsname, newpem)
427+
batch.put(DsInfoName(name), JSON.stringify(keyInfo))
428+
batch.commit((err) => {
429+
if (err) return _error(callback, err)
430+
431+
callback(null, keyInfo)
432+
})
379433
})
380434
} catch (err) {
381435
_error(callback, err)
@@ -408,10 +462,21 @@ class Keychain {
408462
return _error(callback, 'Cannot read the peer private key')
409463
}
410464
const pem = forge.pki.encryptRsaPrivateKey(privateKey, this._())
411-
return self.store.put(dsname, pem, (err) => {
465+
util.keyId(privateKey, (err, kid) => {
412466
if (err) return _error(callback, err)
413467

414-
this._getKeyInfo(name, callback)
468+
const keyInfo = {
469+
name: name,
470+
id: kid
471+
}
472+
const batch = self.store.batch()
473+
batch.put(dsname, pem)
474+
batch.put(DsInfoName(name), JSON.stringify(keyInfo))
475+
batch.commit((err) => {
476+
if (err) return _error(callback, err)
477+
478+
callback(null, keyInfo)
479+
})
415480
})
416481
} catch (err) {
417482
_error(callback, err)
@@ -426,6 +491,7 @@ class Keychain {
426491
* @param {string} name
427492
* @param {function(Error, string)} callback
428493
* @returns {undefined}
494+
* @private
429495
*/
430496
_getPrivateKey (name, callback) {
431497
if (!validateKeyName(name)) {
@@ -438,34 +504,6 @@ class Keychain {
438504
callback(null, res.toString())
439505
})
440506
}
441-
442-
_getKeyInfo (name, callback) {
443-
if (!validateKeyName(name)) {
444-
return _error(callback, `Invalid key name '${name}'`)
445-
}
446-
447-
const dsname = DsName(name)
448-
this.store.get(dsname, (err, res) => {
449-
if (err) {
450-
return _error(callback, `Key '${name}' does not exist. ${err.message}`)
451-
}
452-
const pem = res.toString()
453-
try {
454-
const privateKey = forge.pki.decryptRsaPrivateKey(pem, this._())
455-
util.keyId(privateKey, (err, kid) => {
456-
if (err) return _error(callback, err)
457-
458-
const info = {
459-
name: name,
460-
id: kid
461-
}
462-
return callback(null, info)
463-
})
464-
} catch (e) {
465-
_error(callback, e)
466-
}
467-
})
468-
}
469507
}
470508

471509
module.exports = Keychain

test/keychain.spec.js

+10
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,16 @@ module.exports = (datastore1, datastore2) => {
271271
done()
272272
})
273273
})
274+
275+
it('key exists', (done) => {
276+
ks.findKeyByName('alice', (err, key) => {
277+
expect(err).to.not.exist()
278+
expect(key).to.exist()
279+
expect(key).to.have.property('name', 'alice')
280+
expect(key).to.have.property('id', alice.toB58String())
281+
done()
282+
})
283+
})
274284
})
275285

276286
describe('rename', () => {

0 commit comments

Comments
 (0)