diff --git a/README.md b/README.md index 24bcf0d667..655ff03d2d 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ Managing a key - `createKey (name, type, size, callback)` - `renameKey (oldName, newName, callback)` - `removeKey (name, callback)` -- `exportKey (name, password, callback)` -- `importKey (name, pem, password, callback)` +- `exportKey (name, password, callback)` // Omit _password_ for `ed25199` or `secp256k1` keys +- `importKey (name, encKey, password, callback)` // Omit _password_ for `ed25199` or `secp256k1` keys - `importPeer (name, peer, callback)` A naming service for a key @@ -67,7 +67,7 @@ A naming service for a key - `findKeyById (id, callback)` - `findKeyByName (name, callback)` -Cryptographically protected messages +Cryptographically protected messages (Only supported with RSA keys) - `cms.encrypt (name, plain, callback)` - `cms.decrypt (cmsData, callback)` diff --git a/src/keychain.js b/src/keychain.js index cecc3207e7..e236d8a860 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -20,7 +20,7 @@ const NIST = { } const defaultOptions = { - // See https://cryptosense.com/parametesr-choice-for-pbkdf2/ + // See https://cryptosense.com/blog/parameter-choice-for-pbkdf2/ dek: { keyLength: 512 / 8, iterationCount: 10000, @@ -197,7 +197,8 @@ class Keychain { if (err) return _error(callback, err) if (exists) return _error(callback, `Key '${name}' already exists`) - switch (type.toLowerCase()) { + type = type.toLowerCase() + switch (type) { case 'rsa': if (size < 2048) { return _error(callback, `Invalid RSA key size ${size}`) @@ -211,21 +212,16 @@ class Keychain { if (err) return _error(callback, err) keypair.id((err, kid) => { if (err) return _error(callback, err) - keypair.export(this._(), (err, pem) => { - if (err) return _error(callback, err) - 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) + if (type === 'ed25519' || type === 'secp256k1') { + const keypairMarshal = keypair.bytes + self._storeKey(name, kid, keypairMarshal, dsname, callback) + } else { + keypair.export(this._(), (err, pem) => { + if (err) return _error(callback, err) + self._storeKey(name, kid, pem, dsname, callback) }) - }) + } }) }) }) @@ -365,76 +361,85 @@ class Keychain { } /** - * Export an existing key as a PEM encrypted PKCS #8 string + * Export an existing key. + * If it's as an RSA key, include a password to export as a PEM encrypted PKCS #8 string * * @param {string} name - The local key name; must already exist. - * @param {string} password - The password + * @param {string} password - The password, for RSA keys (optional) * @param {function(Error, string)} callback * @returns {undefined} */ exportKey (name, password, callback) { + if (typeof password === 'function' && typeof callback === 'undefined') { + callback = password + password = undefined + } 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() - crypto.keys.import(pem, this._(), (err, privateKey) => { - if (err) return _error(callback, err) - privateKey.export(password, callback) - }) + if (password) { + const encKey = res.toString() + crypto.keys.import(encKey, this._(), (err, privateKey) => { + if (err) return _error(callback, err) + privateKey.export(password, callback) + }) + } else { + crypto.keys.unmarshalPrivateKey(res, callback) + } }) } /** - * Import a new key from a PEM encoded PKCS #8 string + * Import a new key + * If it's as an RSA key, include a password to import from a PEM encrypted PKCS #8 string * * @param {string} name - The local key name; must not already exist. - * @param {string} pem - The PEM encoded PKCS #8 string - * @param {string} password - The password. + * @param {string} encKey - The encoded key. If it's an RSA key, it needs to be a PEM encoded PKCS #8 string + * @param {string} password - The password for RSA keys. (optional) * @param {function(Error, KeyInfo)} callback * @returns {undefined} */ - importKey (name, pem, password, callback) { + importKey (name, encKey, password, callback) { const self = this + if (typeof password === 'function' && typeof callback === 'undefined') { + callback = password + password = undefined + } if (!validateKeyName(name) || name === 'self') { return _error(callback, `Invalid key name '${name}'`) } - if (!pem) { - return _error(callback, 'PEM encoded key is required') + if (!encKey) { + return _error(callback, 'The encoded key is required') } + const dsname = DsName(name) self.store.has(dsname, (err, exists) => { if (err) return _error(callback, err) if (exists) return _error(callback, `Key '${name}' already exists`) - crypto.keys.import(pem, password, (err, privateKey) => { - if (err) return _error(callback, 'Cannot read the key, most likely the password is wrong') - privateKey.id((err, kid) => { - if (err) return _error(callback, err) - privateKey.export(this._(), (err, pem) => { + + if (password) { + crypto.keys.import(encKey, password, (err, privateKey) => { + if (err) return _error(callback, 'Cannot read the key, most likely the password is wrong') + privateKey.id((err, kid) => { if (err) return _error(callback, err) - 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) => { + privateKey.export(this._(), (err, pem) => { if (err) return _error(callback, err) - - callback(null, keyInfo) + self._storeKey(name, kid, pem, dsname, callback) }) }) }) - }) + } else { + encKey.id((err, kid) => { + if (err) return _error(callback, err) + self._storeKey(name, kid, encKey.bytes, dsname, callback) + }) + } }) } @@ -457,23 +462,28 @@ class Keychain { if (err) return _error(callback, err) privateKey.export(this._(), (err, pem) => { if (err) return _error(callback, err) - 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) - }) + self._storeKey(name, kid, pem, dsname, callback) }) }) }) } + _storeKey (name, kid, encKey, dsname, callback) { + const self = this + const keyInfo = { + name: name, + id: kid + } + const batch = self.store.batch() + batch.put(dsname, encKey) + batch.put(DsInfoName(name), JSON.stringify(keyInfo)) + batch.commit((err) => { + if (err) return _error(callback, err) + + callback(null, keyInfo) + }) + } + /** * Gets the private key as PEM encoded PKCS #8 string. * diff --git a/test/keychain.spec.js b/test/keychain.spec.js index ed6f1a80f0..9e3c6dc6ab 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -13,9 +13,11 @@ const PeerId = require('peer-id') module.exports = (datastore1, datastore2) => { describe('keychain', () => { const passPhrase = 'this is not a secure phrase' - const rsaKeyName = 'tajné jméno' - const renamedRsaKeyName = 'ชื่อลับ' - let rsaKeyInfo + const keyName = 'tajné jméno' + const renamedKeyName = 'ชื่อลับ' + let keyInfo + let ecKeyInfo + let secpKeyInfo let emptyKeystore let ks @@ -80,23 +82,43 @@ module.exports = (datastore1, datastore2) => { }) describe('key', () => { + it('can be an ed25519 key', function (done) { + this.timeout(50 * 1000) + ks.createKey(keyName + 'ed25519', 'ed25519', 2048, (err, info) => { + expect(err).to.not.exist() + expect(info).exist() + ecKeyInfo = info + done() + }) + }) + + it('can be an secp256k1 key', function (done) { + this.timeout(50 * 1000) + ks.createKey(keyName + 'secp256k1', 'secp256k1', 2048, (err, info) => { + expect(err).to.not.exist() + expect(info).exist() + secpKeyInfo = info + done() + }) + }) + it('can be an RSA key', function (done) { this.timeout(50 * 1000) - ks.createKey(rsaKeyName, 'rsa', 2048, (err, info) => { + ks.createKey(keyName, 'rsa', 2048, (err, info) => { expect(err).to.not.exist() expect(info).exist() - rsaKeyInfo = info + keyInfo = info done() }) }) it('has a name and id', () => { - expect(rsaKeyInfo).to.have.property('name', rsaKeyName) - expect(rsaKeyInfo).to.have.property('id') + expect(keyInfo).to.have.property('name', keyName) + expect(keyInfo).to.have.property('id') }) it('is encrypted PEM encoded PKCS #8', (done) => { - ks._getPrivateKey(rsaKeyName, (err, pem) => { + ks._getPrivateKey(keyName, (err, pem) => { expect(err).to.not.exist() expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') done() @@ -104,7 +126,7 @@ module.exports = (datastore1, datastore2) => { }) it('does not overwrite existing key', (done) => { - ks.createKey(rsaKeyName, 'rsa', 2048, (err) => { + ks.createKey(keyName, 'rsa', 2048, (err) => { expect(err).to.exist() done() }) @@ -157,26 +179,26 @@ module.exports = (datastore1, datastore2) => { ks.listKeys((err, keys) => { expect(err).to.not.exist() expect(keys).to.exist() - const mykey = keys.find((k) => k.name.normalize() === rsaKeyName.normalize()) + const mykey = keys.find((k) => k.name.normalize() === keyName.normalize()) expect(mykey).to.exist() done() }) }) it('finds a key by name', (done) => { - ks.findKeyByName(rsaKeyName, (err, key) => { + ks.findKeyByName(keyName, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.deep.equal(rsaKeyInfo) + expect(key).to.deep.equal(keyInfo) done() }) }) it('finds a key by id', (done) => { - ks.findKeyById(rsaKeyInfo.id, (err, key) => { + ks.findKeyById(keyInfo.id, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.deep.equal(rsaKeyInfo) + expect(key).to.deep.equal(keyInfo) done() }) }) @@ -211,14 +233,14 @@ module.exports = (datastore1, datastore2) => { }) it('requires plain data as a Buffer', (done) => { - ks.cms.encrypt(rsaKeyName, 'plain data', (err, msg) => { + ks.cms.encrypt(keyName, 'plain data', (err, msg) => { expect(err).to.exist() done() }) }) it('encrypts', (done) => { - ks.cms.encrypt(rsaKeyName, plainData, (err, msg) => { + ks.cms.encrypt(keyName, plainData, (err, msg) => { expect(err).to.not.exist() expect(msg).to.exist() expect(msg).to.be.instanceOf(Buffer) @@ -245,7 +267,7 @@ module.exports = (datastore1, datastore2) => { emptyKeystore.cms.decrypt(cms, (err, plain) => { expect(err).to.exist() expect(err).to.have.property('missingKeys') - expect(err.missingKeys).to.eql([rsaKeyInfo.id]) + expect(err.missingKeys).to.eql([keyInfo.id]) done() }) }) @@ -262,9 +284,11 @@ module.exports = (datastore1, datastore2) => { describe('exported key', () => { let pemKey + let ed25519Key + let secp256k1Key it('is a PKCS #8 encrypted pem', (done) => { - ks.exportKey(rsaKeyName, 'password', (err, pem) => { + ks.exportKey(keyName, 'password', (err, pem) => { expect(err).to.not.exist() expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') pemKey = pem @@ -276,13 +300,49 @@ module.exports = (datastore1, datastore2) => { ks.importKey('imported-key', pemKey, 'password', (err, key) => { expect(err).to.not.exist() expect(key.name).to.equal('imported-key') - expect(key.id).to.equal(rsaKeyInfo.id) + expect(key.id).to.equal(keyInfo.id) + done() + }) + }) + + it('can export ed25519 key', (done) => { + ks.exportKey(keyName + 'ed25519', (err, key) => { + expect(err).to.not.exist() + ed25519Key = key + expect(key).to.exist() + done() + }) + }) + + it('ed25519 key can be imported', (done) => { + ks.importKey('imported-key-ed25199', ed25519Key, (err, key) => { + expect(err).to.not.exist() + expect(key.name).to.equal('imported-key-ed25199') + expect(key.id).to.equal(ecKeyInfo.id) + done() + }) + }) + + it('can export secp256k1 key', (done) => { + ks.exportKey(keyName + 'secp256k1', (err, key) => { + expect(err).to.not.exist() + secp256k1Key = key + expect(key).to.exist() + done() + }) + }) + + it('secp256k1 key can be imported', (done) => { + ks.importKey('imported-key-secp256k1', secp256k1Key, (err, key) => { + expect(err).to.not.exist() + expect(key.name).to.equal('imported-key-secp256k1') + expect(key.id).to.equal(secpKeyInfo.id) done() }) }) it('cannot be imported as an existing key name', (done) => { - ks.importKey(rsaKeyName, pemKey, 'password', (err, key) => { + ks.importKey(keyName, pemKey, 'password', (err, key) => { expect(err).to.exist() done() }) @@ -342,40 +402,40 @@ module.exports = (datastore1, datastore2) => { describe('rename', () => { it('requires an existing key name', (done) => { - ks.renameKey('not-there', renamedRsaKeyName, (err) => { + ks.renameKey('not-there', renamedKeyName, (err) => { expect(err).to.exist() done() }) }) it('requires a valid new key name', (done) => { - ks.renameKey(rsaKeyName, '..\not-valid', (err) => { + ks.renameKey(keyName, '..\not-valid', (err) => { expect(err).to.exist() done() }) }) it('does not overwrite existing key', (done) => { - ks.renameKey(rsaKeyName, rsaKeyName, (err) => { + ks.renameKey(keyName, keyName, (err) => { expect(err).to.exist() done() }) }) it('cannot create the "self" key', (done) => { - ks.renameKey(rsaKeyName, 'self', (err) => { + ks.renameKey(keyName, 'self', (err) => { expect(err).to.exist() done() }) }) it('removes the existing key name', (done) => { - ks.renameKey(rsaKeyName, renamedRsaKeyName, (err, key) => { + ks.renameKey(keyName, renamedKeyName, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) - expect(key).to.have.property('id', rsaKeyInfo.id) - ks.findKeyByName(rsaKeyName, (err, key) => { + expect(key).to.have.property('name', renamedKeyName) + expect(key).to.have.property('id', keyInfo.id) + ks.findKeyByName(keyName, (err, key) => { expect(err).to.exist() done() }) @@ -383,20 +443,20 @@ module.exports = (datastore1, datastore2) => { }) it('creates the new key name', (done) => { - ks.findKeyByName(renamedRsaKeyName, (err, key) => { + ks.findKeyByName(renamedKeyName, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) + expect(key).to.have.property('name', renamedKeyName) done() }) }) it('does not change the key ID', (done) => { - ks.findKeyByName(renamedRsaKeyName, (err, key) => { + ks.findKeyByName(renamedKeyName, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) - expect(key).to.have.property('id', rsaKeyInfo.id) + expect(key).to.have.property('name', renamedKeyName) + expect(key).to.have.property('id', keyInfo.id) done() }) }) @@ -418,11 +478,11 @@ module.exports = (datastore1, datastore2) => { }) it('can remove a known key', (done) => { - ks.removeKey(renamedRsaKeyName, (err, key) => { + ks.removeKey(renamedKeyName, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) - expect(key).to.have.property('id', rsaKeyInfo.id) + expect(key).to.have.property('name', renamedKeyName) + expect(key).to.have.property('id', keyInfo.id) done() }) })