From d945fceeb09c1c4289a61edb40209271e8318d25 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Wed, 31 Jan 2018 07:48:08 +1300 Subject: [PATCH] feat: Implementation of the ipfs.key API (#1133) --- README.md | 17 +++++ package.json | 3 +- src/cli/bin.js | 7 +- src/cli/commands/init.js | 1 + src/cli/commands/key.js | 14 ++++ src/cli/commands/key/export.js | 37 ++++++++++ src/cli/commands/key/gen.js | 35 +++++++++ src/cli/commands/key/import.js | 34 +++++++++ src/cli/commands/key/list.js | 20 +++++ src/cli/commands/key/rename.js | 20 +++++ src/cli/commands/key/rm.js | 20 +++++ src/cli/utils.js | 7 +- src/core/boot.js | 5 +- src/core/components/index.js | 1 + src/core/components/init.js | 18 ++++- src/core/components/key.js | 46 ++++++++++++ src/core/components/no-keychain.js | 24 ++++++ src/core/components/pre-start.js | 47 +++++++++++- src/core/index.js | 1 + src/http/api/resources/index.js | 1 + src/http/api/resources/key.js | 102 ++++++++++++++++++++++++++ src/http/api/routes/index.js | 1 + src/http/api/routes/key.js | 43 +++++++++++ src/http/index.js | 1 + test/cli/commands.js | 3 +- test/cli/files.js | 1 + test/cli/key.js | 53 +++++++++++++ test/core/init.spec.js | 8 +- test/core/interface/interface.spec.js | 1 + test/core/interface/key.js | 36 +++++++++ test/core/key-exchange.js | 51 +++++++++++++ test/http-api/index.js | 6 +- test/http-api/interface/key.js | 35 +++++++++ 33 files changed, 683 insertions(+), 16 deletions(-) create mode 100644 src/cli/commands/key.js create mode 100644 src/cli/commands/key/export.js create mode 100644 src/cli/commands/key/gen.js create mode 100644 src/cli/commands/key/import.js create mode 100644 src/cli/commands/key/list.js create mode 100644 src/cli/commands/key/rename.js create mode 100644 src/cli/commands/key/rm.js create mode 100644 src/core/components/key.js create mode 100644 src/core/components/no-keychain.js create mode 100644 src/http/api/resources/key.js create mode 100644 src/http/api/routes/key.js create mode 100644 test/cli/key.js create mode 100644 test/core/interface/key.js create mode 100644 test/core/key-exchange.js create mode 100644 test/http-api/interface/key.js diff --git a/README.md b/README.md index bef1e12285..4d3ccc8f52 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,8 @@ const node = new IPFS({ // }, start: true, // default // start: false, + pass: undefined // default + // pass: 'pass phrase for key access', EXPERIMENTAL: { // enable experimental features pubsub: true, sharding: true, // enable dir sharding @@ -275,6 +277,17 @@ A complete API definition is in the works. Meanwhile, you can learn how to you u - [`ipfs.object.patch.setData(multihash, data, [options, callback])`](https://github.com/ipfs/interface-ipfs-core/tree/master/SPEC/OBJECT.md#objectpatchsetdata) - [pin (not implemented, yet!)](https://github.com/ipfs/interface-ipfs-core/tree/master/SPEC/) +#### `Crypto and Key Management` + +- [key](https://github.com/ipfs/interface-ipfs-core/tree/master/SPEC/KEY.md) + - `ipfs.key.export(name, password, [callback])` + - `ipfs.key.gen(name, options, [callback])` + - `ipfs.key.import(name, pem, password, [callback])` + - `ipfs.key.list([callback])` + - `ipfs.key.rename(oldName, newName, [callback])` + - `ipfs.key.rm(name, [callback])` +- [crypto (not yet implemented)](https://github.com/ipfs/interface-ipfs-core/tree/master/SPEC) + #### `Network` - [bootstrap](https://github.com/ipfs/interface-ipfs-core/tree/master/SPEC/) @@ -523,6 +536,10 @@ If you find any other issue, please check the [`Electron Support` issue](https:/ | [`is-ipfs`](https://github.com/ipfs/is-ipfs) | [![npm](https://img.shields.io/npm/v/is-ipfs.svg?maxAge=86400&style=flat-square)](//github.com/ipfs/is-ipfs/releases) | [![Dep](https://david-dm.org/ipfs/is-ipfs.svg?style=flat-square)](https://david-dm.org/ipfs/is-ipfs) | [![devDep](https://david-dm.org/ipfs/is-ipfs/dev-status.svg?style=flat-square)](https://david-dm.org/ipfs/is-ipfs?type=dev) | [![Travis](https://travis-ci.org/ipfs/is-ipfs.svg?branch=master)](https://travis-ci.org/ipfs/is-ipfs) | | ![Appveyor CI](https://ci.appveyor.com/api/projects/status/github/ipfs/is-ipfs?svg=true) | [![Coverage Status](https://coveralls.io/repos/github/ipfs/is-ipfs/badge.svg?branch=master)](https://coveralls.io/github/ipfs/is-ipfs?branch=master) | | [`multihashing`](//github.com/multiformats/js-multihashing) | [![npm](https://img.shields.io/npm/v/multihashing.svg?maxAge=86400&style=flat-square)](//github.com/multiformats/js-multihashing/releases) | [![Dep](https://david-dm.org/multiformats/js-multihashing.svg?style=flat-square)](https://david-dm.org/multiformats/js-multihashing) | [![devDep](https://david-dm.org/multiformats/js-multihashing/dev-status.svg?style=flat-square)](https://david-dm.org/multiformats/js-multihashing?type=dev) | [![Travis](https://travis-ci.org/multiformats/js-multihashing.svg?branch=master)](https://travis-ci.org/multiformats/js-multihashing) | [![Circle CI](https://circleci.com/gh/multiformats/js-multihashing.svg?style=svg)](https://circleci.com/gh/jbenet/js-multihashing) | ![Appveyor CI](https://ci.appveyor.com/api/projects/status/github/multiformats/js-multihashing?svg=true) | [![Coverage Status](https://coveralls.io/repos/github/jbenet/js-multihashing/badge.svg?branch=master)](https://coveralls.io/github/jbenet/js-multihashing?branch=master) | | [`mafmt`](//github.com/whyrusleeping/js-mafmt) | [![npm](https://img.shields.io/npm/v/mafmt.svg?maxAge=86400&style=flat-square)](//github.com/whyrusleeping/js-mafmt/releases) | [![Dep](https://david-dm.org/whyrusleeping/js-mafmt.svg?style=flat-square)](https://david-dm.org/whyrusleeping/js-mafmt) | [![devDep](https://david-dm.org/whyrusleeping/js-mafmt/dev-status.svg?style=flat-square)](https://david-dm.org/whyrusleeping/js-mafmt?type=dev) | [![Travis](https://travis-ci.org/whyrusleeping/js-mafmt.svg?branch=master)](https://travis-ci.org/whyrusleeping/js-mafmt) | [![Circle CI](https://circleci.com/gh/whyrusleeping/js-mafmt.svg?style=svg)](https://circleci.com/gh/whyrusleeping/js-mafmt) | ![Appveyor CI](https://ci.appveyor.com/api/projects/status/github/whyrusleeping/js-mafmt?svg=true) | [![Coverage Status](https://coveralls.io/repos/github/whyrusleeping/js-mafmt/badge.svg?branch=master)](https://coveralls.io/github/whyrusleeping/js-mafmt?branch=master) | +| **Crypto** +| [`libp2p-crypto`](https://github.com/libp2p/js-libp2p-crypto) | [![npm](https://img.shields.io/npm/v/libp2p-crypto.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-crypto/releases) | [![Dep](https://david-dm.org/libp2p/js-libp2p-crypto.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-crypto) | [![devDep](https://david-dm.org/libp2p/js-libp2p-crypto/dev-status.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-crypto?type=dev) | [![Travis](https://travis-ci.org/libp2p/js-libp2p-crypto.svg?branch=master)](https://travis-ci.org/libp2p/js-libp2p-crypto) | [![Circle CI](https://circleci.com/gh/libp2p/js-libp2p-crypto.svg?style=svg)](https://circleci.com/gh/libp2p/js-libp2p-crypto) | ![Appveyor CI](https://ci.appveyor.com/api/projects/status/github/libp2p/js-libp2p-crypto?svg=true) | [![Coverage Status](https://coveralls.io/repos/github/libp2p/js-libp2p-crypto/badge.svg?branch=master)](https://coveralls.io/github/libp2p/js-libp2p-crypto?branch=master) | +| [`libp2p-keychain`](https://github.com/libp2p/js-libp2p-keychain) | [![npm](https://img.shields.io/npm/v/libp2p-keychain.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-keychain/releases) | [![Dep](https://david-dm.org/libp2p/js-libp2p-keychain.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-keychain) | [![devDep](https://david-dm.org/libp2p/js-libp2p-keychain/dev-status.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-keychain?type=dev) | [![Travis](https://travis-ci.org/libp2p/js-libp2p-keychain.svg?branch=master)](https://travis-ci.org/libp2p/js-libp2p-keychain) | [![Circle CI](https://circleci.com/gh/libp2p/js-libp2p-keychain.svg?style=svg)](https://circleci.com/gh/libp2p/js-libp2p-keychain) | ![Appveyor CI](https://ci.appveyor.com/api/projects/status/github/libp2p/js-libp2p-keychain?svg=true) | [![Coverage Status](https://coveralls.io/repos/github/libp2p/js-libp2p-keychain/badge.svg?branch=master)](https://coveralls.io/github/libp2p/js-libp2p-keychain?branch=master) | + ## Development diff --git a/package.json b/package.json index de9635ca59..fa3e7acf7e 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "go-ipfs-dep": "^0.4.13", "hat": "0.0.3", "interface-ipfs-core": "~0.42.1", - "ipfsd-ctl": "^0.27.0", + "ipfsd-ctl": "~0.27.2", "left-pad": "^1.2.0", "lodash": "^4.17.4", "mocha": "^4.1.0", @@ -121,6 +121,7 @@ "libp2p-circuit": "~0.1.4", "libp2p-floodsub": "~0.13.1", "libp2p-kad-dht": "~0.6.0", + "libp2p-keychain": "~0.3.0", "libp2p-mdns": "~0.9.1", "libp2p-multiplex": "~0.5.1", "libp2p-railing": "~0.7.1", diff --git a/src/cli/bin.js b/src/cli/bin.js index 3ea4ecc812..67e60f8362 100755 --- a/src/cli/bin.js +++ b/src/cli/bin.js @@ -21,6 +21,11 @@ const cli = yargs default: false, coerce: ('silent', silent => silent ? utils.disablePrinting() : silent) }) + .option('pass', { + desc: 'Pass phrase for the keys', + type: 'string', + default: '' + }) .commandDir('commands') .demandCommand(1) .fail((msg, err, yargs) => { @@ -59,7 +64,7 @@ if (args[0] === 'daemon' || args[0] === 'init') { if (err) { throw err } - utils.getIPFS(argv.api, (err, ipfs, cleanup) => { + utils.getIPFS(argv, (err, ipfs, cleanup) => { if (err) { throw err } cli diff --git a/src/cli/commands/init.js b/src/cli/commands/init.js index b53639fec0..741a313750 100644 --- a/src/cli/commands/init.js +++ b/src/cli/commands/init.js @@ -38,6 +38,7 @@ module.exports = { node.init({ bits: argv.bits, emptyRepo: argv.emptyRepo, + pass: argv.pass, log: print }, (err) => { if (err) { diff --git a/src/cli/commands/key.js b/src/cli/commands/key.js new file mode 100644 index 0000000000..c6d620dc61 --- /dev/null +++ b/src/cli/commands/key.js @@ -0,0 +1,14 @@ +'use strict' + +module.exports = { + command: 'key', + + description: 'Manage your keys', + + builder (yargs) { + return yargs + .commandDir('key') + }, + + handler (argv) {} +} diff --git a/src/cli/commands/key/export.js b/src/cli/commands/key/export.js new file mode 100644 index 0000000000..66adde248c --- /dev/null +++ b/src/cli/commands/key/export.js @@ -0,0 +1,37 @@ +'use strict' + +const fs = require('fs') + +module.exports = { + command: 'export ', + + describe: 'Export the key as a password protected PKCS #8 PEM file', + + builder: { + passout: { + alias: 'p', + describe: 'Password for the PEM', + type: 'string', + demandOption: true + }, + output: { + alias: 'o', + describe: 'Output file', + type: 'string', + default: 'stdout' + } + }, + + handler (argv) { + argv.ipfs.key.export(argv.name, argv.passout, (err, pem) => { + if (err) { + throw err + } + if (argv.output === 'stdout') { + process.stdout.write(pem) + } else { + fs.writeFileSync(argv.output, pem) + } + }) + } +} diff --git a/src/cli/commands/key/gen.js b/src/cli/commands/key/gen.js new file mode 100644 index 0000000000..a3f108eeaa --- /dev/null +++ b/src/cli/commands/key/gen.js @@ -0,0 +1,35 @@ +'use strict' + +const print = require('../../utils').print + +module.exports = { + command: 'gen ', + + describe: 'Create a new key', + + builder: { + type: { + alias: 't', + describe: 'type of the key to create [rsa, ed25519].', + default: 'rsa' + }, + size: { + alias: 's', + describe: 'size of the key to generate.', + default: '2048' + } + }, + + handler (argv) { + const opts = { + type: argv.type, + size: argv.size + } + argv.ipfs.key.gen(argv.name, opts, (err, key) => { + if (err) { + throw err + } + print(`generated ${key.id} ${key.name}`) + }) + } +} diff --git a/src/cli/commands/key/import.js b/src/cli/commands/key/import.js new file mode 100644 index 0000000000..ae0363b25d --- /dev/null +++ b/src/cli/commands/key/import.js @@ -0,0 +1,34 @@ +'use strict' + +const fs = require('fs') +const print = require('../../utils').print + +module.exports = { + command: 'import ', + + describe: 'Import the key from a PKCS #8 PEM file', + + builder: { + passin: { + alias: 'p', + describe: 'Password for the PEM', + type: 'string' + }, + input: { + alias: 'i', + describe: 'Input PEM file', + type: 'string', + demandOption: true, + coerce: ('input', input => fs.readFileSync(input, 'utf8')) + } + }, + + handler (argv) { + argv.ipfs.key.import(argv.name, argv.input, argv.passin, (err, key) => { + if (err) { + throw err + } + print(`imported ${key.id} ${key.name}`) + }) + } +} diff --git a/src/cli/commands/key/list.js b/src/cli/commands/key/list.js new file mode 100644 index 0000000000..d50db99697 --- /dev/null +++ b/src/cli/commands/key/list.js @@ -0,0 +1,20 @@ +'use strict' + +const print = require('../../utils').print + +module.exports = { + command: 'list', + + describe: 'List all local keys', + + builder: {}, + + handler (argv) { + argv.ipfs.key.list((err, keys) => { + if (err) { + throw err + } + keys.forEach((ki) => print(`${ki.id} ${ki.name}`)) + }) + } +} diff --git a/src/cli/commands/key/rename.js b/src/cli/commands/key/rename.js new file mode 100644 index 0000000000..9126dbbb36 --- /dev/null +++ b/src/cli/commands/key/rename.js @@ -0,0 +1,20 @@ +'use strict' + +const print = require('../../utils').print + +module.exports = { + command: 'rename ', + + describe: 'Rename a key', + + builder: {}, + + handler (argv) { + argv.ipfs.key.rename(argv.name, argv.newName, (err, res) => { + if (err) { + throw err + } + print(`renamed to ${res.id} ${res.now}`) + }) + } +} diff --git a/src/cli/commands/key/rm.js b/src/cli/commands/key/rm.js new file mode 100644 index 0000000000..a7a5daf658 --- /dev/null +++ b/src/cli/commands/key/rm.js @@ -0,0 +1,20 @@ +'use strict' + +const print = require('../../utils').print + +module.exports = { + command: 'rm ', + + describe: 'Remove a key', + + builder: {}, + + handler (argv) { + argv.ipfs.key.rm(argv.name, (err, key) => { + if (err) { + throw err + } + print(`${key.id} ${key.name}`) + }) + } +} diff --git a/src/cli/utils.js b/src/cli/utils.js index b3f2f85e9f..37cd49010f 100644 --- a/src/cli/utils.js +++ b/src/cli/utils.js @@ -38,15 +38,16 @@ function getAPICtl (apiAddr) { return APIctl(apiAddr) } -exports.getIPFS = (apiAddr, callback) => { - if (apiAddr || isDaemonOn()) { - return callback(null, getAPICtl(apiAddr), (cb) => cb()) +exports.getIPFS = (argv, callback) => { + if (argv.api || isDaemonOn()) { + return callback(null, getAPICtl(argv.api), (cb) => cb()) } const node = new IPFS({ repo: exports.getRepoPath(), init: false, start: false, + pass: argv.pass, EXPERIMENTAL: { pubsub: true } diff --git a/src/core/boot.js b/src/core/boot.js index 64f62ca631..cc7c14970e 100644 --- a/src/core/boot.js +++ b/src/core/boot.js @@ -15,7 +15,7 @@ module.exports = (self) => { const repoOpen = !self._repo.closed const customInitOptions = typeof options.init === 'object' ? options.init : {} - const initOptions = Object.assign({ bits: 2048 }, customInitOptions) + const initOptions = Object.assign({ bits: 2048, pass: self._options.pass }, customInitOptions) // Checks if a repo exists, and if so opens it // Will return callback with a bool indicating the existence @@ -30,6 +30,7 @@ module.exports = (self) => { (cb) => self._repo.open(cb), (cb) => self.preStart(cb), (cb) => { + self.log('initialized') self.state.initialized() cb(null, true) } @@ -56,8 +57,8 @@ module.exports = (self) => { if (err) { return self.emit('error', err) } + self.log('boot:done') self.emit('ready') - self.log('boot:done', err) } const tasks = [] diff --git a/src/core/components/index.js b/src/core/components/index.js index 248243a88e..4aea90db9e 100644 --- a/src/core/components/index.js +++ b/src/core/components/index.js @@ -21,3 +21,4 @@ exports.bitswap = require('./bitswap') exports.pubsub = require('./pubsub') exports.dht = require('./dht') exports.dns = require('./dns') +exports.key = require('./key') diff --git a/src/core/components/init.js b/src/core/components/init.js index 87e9072811..21504c9128 100644 --- a/src/core/components/init.js +++ b/src/core/components/init.js @@ -5,6 +5,7 @@ const waterfall = require('async/waterfall') const parallel = require('async/parallel') const promisify = require('promisify-es6') const config = require('../runtime/config-nodejs.json') +const Keychain = require('libp2p-keychain') const addDefaultAssets = require('./init-assets') @@ -36,7 +37,7 @@ module.exports = function init (self) { opts.emptyRepo = opts.emptyRepo || false opts.bits = Number(opts.bits) || 2048 opts.log = opts.log || function () {} - + let privateKey waterfall([ // Verify repo does not yet exist. (cb) => self._repo.exists(cb), @@ -57,6 +58,10 @@ module.exports = function init (self) { PeerID: keys.toB58String(), PrivKey: keys.privKey.bytes.toString('base64') } + if (opts.pass) { + privateKey = keys.privKey + config.Keychain = Keychain.generateOptions() + } opts.log('done') opts.log('peer identity: ' + config.Identity.PeerID) @@ -65,10 +70,21 @@ module.exports = function init (self) { (_, cb) => self._repo.open(cb), (cb) => { self.log('repo opened') + if (opts.pass) { + self.log('creating keychain') + const keychainOptions = Object.assign({passPhrase: opts.pass}, config.Keychain) + self._keychain = new Keychain(self._repo.keys, keychainOptions) + self._keychain.importPeer('self', { privKey: privateKey }, cb) + } else { + cb(null, true) + } + }, + (_, cb) => { if (opts.emptyRepo) { return cb(null, true) } + self.log('adding assets') const tasks = [ // add empty unixfs dir object (go-ipfs assumes this exists) (cb) => self.object.new('unixfs-dir', cb) diff --git a/src/core/components/key.js b/src/core/components/key.js new file mode 100644 index 0000000000..b5bc5cd434 --- /dev/null +++ b/src/core/components/key.js @@ -0,0 +1,46 @@ +'use strict' + +// See https://github.com/ipfs/specs/tree/master/keystore + +const promisify = require('promisify-es6') + +module.exports = function key (self) { + return { + gen: promisify((name, opts, callback) => { + self._keychain.createKey(name, opts.type, opts.size, callback) + }), + + info: promisify((name, callback) => { + self._keychain.findKeyByName(name, callback) + }), + + list: promisify((callback) => { + self._keychain.listKeys(callback) + }), + + rm: promisify((name, callback) => { + self._keychain.removeKey(name, callback) + }), + + rename: promisify((oldName, newName, callback) => { + self._keychain.renameKey(oldName, newName, (err, key) => { + if (err) return callback(err) + const result = { + was: oldName, + now: key.name, + id: key.id, + overwrite: false + } + callback(null, result) + }) + }), + + import: promisify((name, pem, password, callback) => { + self._keychain.importKey(name, pem, password, callback) + }), + + export: promisify((name, password, callback) => { + self._keychain.exportKey(name, password, callback) + }) + } +} diff --git a/src/core/components/no-keychain.js b/src/core/components/no-keychain.js new file mode 100644 index 0000000000..6f07f4068e --- /dev/null +++ b/src/core/components/no-keychain.js @@ -0,0 +1,24 @@ +'use strict' + +function fail () { + throw new Error('Key management requires \'--pass ...\' option') +} + +class NoKeychain { + static get options () { fail() } + static generateOptions () { fail() } + + createKey () { fail() } + listKeys () { fail() } + findKeyById () { fail() } + findKeyByName () { fail() } + renameKey () { fail() } + removeKey () { fail() } + exportKey () { fail() } + importKey () { fail() } + importPeer () { fail() } + + get cms () { fail() } +} + +module.exports = NoKeychain diff --git a/src/core/components/pre-start.js b/src/core/components/pre-start.js index 32017bd4f0..2121ff6e22 100644 --- a/src/core/components/pre-start.js +++ b/src/core/components/pre-start.js @@ -4,7 +4,8 @@ const peerId = require('peer-id') const PeerInfo = require('peer-info') const multiaddr = require('multiaddr') const waterfall = require('async/waterfall') - +const Keychain = require('libp2p-keychain') +const NoKeychain = require('./no-keychain') /* * Load stuff from Repo into memory */ @@ -12,14 +13,56 @@ module.exports = function preStart (self) { return (callback) => { self.log('pre-start') + const pass = self._options.pass waterfall([ (cb) => self._repo.config.get(cb), + (config, cb) => { + // Create keychain configuration, if needed. + if (config.Keychain) { + return cb(null, config) + } + config.Keychain = Keychain.generateOptions() + self.config.set('Keychain', config.Keychain, (err) => { + self.log('using default keychain options') + cb(err, config) + }) + }, + (config, cb) => { + // Construct the keychain + if (self._keychain) { + // most likely an init or upgrade has happened + } else if (pass) { + const keychainOptions = Object.assign({passPhrase: pass}, config.Keychain) + self._keychain = new Keychain(self._repo.keys, keychainOptions) + self.log('keychain constructed') + } else { + self._keychain = new NoKeychain() + self.log('no keychain, use --pass') + } + cb(null, config) + }, (config, cb) => { const privKey = config.Identity.PrivKey - peerId.createFromPrivKey(privKey, (err, id) => cb(err, config, id)) + peerId.createFromPrivKey(privKey, (err, id) => { + cb(err, config, id) + }) + }, + (config, id, cb) => { + // Import the private key as 'self', if needed. + if (!pass) { + return cb(null, config, id) + } + self._keychain.findKeyByName('self', (err) => { + if (err) { + self.log('Creating "self" key') + return self._keychain.importPeer('self', id, (err) => cb(err, config, id)) + } + cb(null, config, id) + }) }, (config, id, cb) => { + self.log('peer created') self._peerInfo = new PeerInfo(id) if (config.Addresses && config.Addresses.Swarm) { diff --git a/src/core/index.js b/src/core/index.js index fa60876061..b0e5719b2f 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -95,6 +95,7 @@ class IPFS extends EventEmitter { this.pubsub = components.pubsub(this) this.dht = components.dht(this) this.dns = components.dns(this) + this.key = components.key(this) if (this._options.EXPERIMENTAL.pubsub) { this.log('EXPERIMENTAL pubsub is enabled') diff --git a/src/http/api/resources/index.js b/src/http/api/resources/index.js index 16b0f17128..42eec1d0b0 100644 --- a/src/http/api/resources/index.js +++ b/src/http/api/resources/index.js @@ -13,3 +13,4 @@ exports.file = require('./file') exports.files = require('./files') exports.pubsub = require('./pubsub') exports.dns = require('./dns') +exports.key = require('./key') diff --git a/src/http/api/resources/key.js b/src/http/api/resources/key.js new file mode 100644 index 0000000000..15de675c8e --- /dev/null +++ b/src/http/api/resources/key.js @@ -0,0 +1,102 @@ +'use strict' + +exports = module.exports + +function applyError (reply, err) { + reply({ + Message: err.message, + Code: 0 + }).code(500).takeover() +} + +function toKeyInfo (key) { + return { + Name: key.name, + Id: key.id + } +} + +exports.list = (request, reply) => { + const ipfs = request.server.app.ipfs + + ipfs._keychain.listKeys((err, keys) => { + if (err) { + return applyError(reply, err) + } + + keys = keys.map(toKeyInfo) + return reply({ Keys: keys }) + }) +} + +exports.rm = (request, reply) => { + const ipfs = request.server.app.ipfs + const name = request.query.arg + ipfs._keychain.removeKey(name, (err, key) => { + if (err) { + return applyError(reply, err) + } + + return reply({ Keys: [ toKeyInfo(key) ] }) + }) +} + +exports.rename = (request, reply) => { + const ipfs = request.server.app.ipfs + const oldName = request.query.arg[0] + const newName = request.query.arg[1] + ipfs._keychain.renameKey(oldName, newName, (err, key) => { + if (err) { + return applyError(reply, err) + } + + const result = { + Was: oldName, + Now: key.name, + Id: key.id, + Overwrite: false + } + return reply(result) + }) +} + +exports.gen = (request, reply) => { + const ipfs = request.server.app.ipfs + const name = request.query.arg + const type = request.query.type + const size = request.query.size + ipfs._keychain.createKey(name, type, size, (err, key) => { + if (err) { + return applyError(reply, err) + } + + return reply(toKeyInfo(key)) + }) +} + +exports.export = (request, reply) => { + const ipfs = request.server.app.ipfs + const name = request.query.arg + const password = request.query.password + ipfs._keychain.exportKey(name, password, (err, pem) => { + if (err) { + return applyError(reply, err) + } + + return reply(pem).type('application/x-pem-file') + }) +} + +exports.import = (request, reply) => { + const ipfs = request.server.app.ipfs + const name = request.query.arg + const pem = request.query.pem + const password = request.query.password + ipfs._keychain.importKey(name, pem, password, (err, key) => { + if (err) { + return applyError(reply, err) + } + + return reply(toKeyInfo(key)) + }) +} diff --git a/src/http/api/routes/index.js b/src/http/api/routes/index.js index 908c0c0878..9e405ae6b5 100644 --- a/src/http/api/routes/index.js +++ b/src/http/api/routes/index.js @@ -16,4 +16,5 @@ module.exports = (server) => { require('./debug')(server) require('./webui')(server) require('./dns')(server) + require('./key')(server) } diff --git a/src/http/api/routes/key.js b/src/http/api/routes/key.js new file mode 100644 index 0000000000..4493bdab39 --- /dev/null +++ b/src/http/api/routes/key.js @@ -0,0 +1,43 @@ +'use strict' + +const resources = require('./../resources') + +module.exports = (server) => { + const api = server.select('API') + + api.route({ + method: '*', + path: '/api/v0/key/list', + handler: resources.key.list + }) + + api.route({ + method: '*', + path: '/api/v0/key/gen', + handler: resources.key.gen + }) + + api.route({ + method: '*', + path: '/api/v0/key/rm', + handler: resources.key.rm + }) + + api.route({ + method: '*', + path: '/api/v0/key/rename', + handler: resources.key.rename + }) + + api.route({ + method: '*', + path: '/api/v0/key/export', + handler: resources.key.export + }) + + api.route({ + method: '*', + path: '/api/v0/key/import', + handler: resources.key.import + }) +} diff --git a/src/http/index.js b/src/http/index.js index 2df407dd5d..4a985ce3c6 100644 --- a/src/http/index.js +++ b/src/http/index.js @@ -67,6 +67,7 @@ function HttpApi (repo, config, cliArgs) { init: init, start: true, config: config, + pass: cliArgs && cliArgs.pass, EXPERIMENTAL: { pubsub: cliArgs && cliArgs.enablePubsubExperiment, sharding: cliArgs && cliArgs.enableShardingExperiment diff --git a/test/cli/commands.js b/test/cli/commands.js index 3a2d85a418..1fabdc857f 100644 --- a/test/cli/commands.js +++ b/test/cli/commands.js @@ -4,8 +4,7 @@ const expect = require('chai').expect const runOnAndOff = require('../utils/on-and-off') -const commandCount = 60 - +const commandCount = 67 describe('commands', () => runOnAndOff((thing) => { let ipfs diff --git a/test/cli/files.js b/test/cli/files.js index 5a36f52a4e..738b07e317 100644 --- a/test/cli/files.js +++ b/test/cli/files.js @@ -353,6 +353,7 @@ describe('files', () => runOnAndOff((thing) => { 'Options:', ' --version Show version number [boolean]', ' --silent Write no output [boolean] [default: false]', + ' --pass Pass phrase for the keys [string] [default: ""]', ' --help Show help [boolean]', ' -v, --headers Print table headers (Hash, Size, Name).', ' [boolean] [default: false]', diff --git a/test/cli/key.js b/test/cli/key.js new file mode 100644 index 0000000000..8a39c4919c --- /dev/null +++ b/test/cli/key.js @@ -0,0 +1,53 @@ +/* eslint-env mocha */ +'use strict' + +const expect = require('chai').expect +const runOnAndOff = require('../utils/on-and-off') +const hat = require('hat') + +describe('key', () => runOnAndOff.off((thing) => { + const name = 'test-key-' + hat() + const newName = 'test-key-' + hat() + const pass = '--pass ' + hat() + let ipfs + + before(() => { + ipfs = thing.ipfs + }) + + it('gen', function () { + this.timeout(40 * 1000) + + return ipfs(`${pass} key gen ${name} --type rsa --size 2048`) + .then((out) => { + expect(out).to.include(name) + }) + }) + + it('list', function () { + this.timeout(20 * 1000) + + return ipfs(`${pass} key list`) + .then((out) => { + expect(out).to.include(name) + }) + }) + + it('rename', function () { + this.timeout(20 * 1000) + + return ipfs(`${pass} key rename ${name} ${newName}`) + .then((out) => { + expect(out).to.include(newName) + }) + }) + + it('rm', function () { + this.timeout(20 * 1000) + + return ipfs(`${pass} key rm ${newName}`) + .then((out) => { + expect(out).to.include(newName) + }) + }) +})) diff --git a/test/core/init.spec.js b/test/core/init.spec.js index 07990027f2..58916ecbe4 100644 --- a/test/core/init.spec.js +++ b/test/core/init.spec.js @@ -7,6 +7,7 @@ const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) const isNode = require('detect-node') +const hat = require('hat') const PeerId = require('peer-id') const PeerInfo = require('peer-info') const multiaddr = require('multiaddr') @@ -36,7 +37,7 @@ describe('init', () => { afterEach((done) => repo.teardown(done)) it('basic', (done) => { - ipfs.init({ bits: 512 }, (err) => { + ipfs.init({ bits: 512, pass: hat() }, (err) => { expect(err).to.not.exist() repo.exists((err, res) => { @@ -46,6 +47,7 @@ describe('init', () => { repo.config.get((err, config) => { expect(err).to.not.exist() expect(config.Identity).to.exist() + expect(config.Keychain).to.exist() done() }) }) @@ -55,7 +57,7 @@ describe('init', () => { it('set # of bits in key', function (done) { this.timeout(40 * 1000) - ipfs.init({ bits: 1024 }, (err) => { + ipfs.init({ bits: 1024, pass: hat() }, (err) => { expect(err).to.not.exist() repo.config.get((err, config) => { @@ -67,7 +69,7 @@ describe('init', () => { }) it('init docs are written', (done) => { - ipfs.init({ bits: 512 }, (err) => { + ipfs.init({ bits: 512, pass: hat() }, (err) => { expect(err).to.not.exist() const multihash = 'QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB' diff --git a/test/core/interface/interface.spec.js b/test/core/interface/interface.spec.js index 96815d2596..3d8e370981 100644 --- a/test/core/interface/interface.spec.js +++ b/test/core/interface/interface.spec.js @@ -12,6 +12,7 @@ describe('interface-ipfs-core tests', () => { require('./object') require('./dag') require('./stats') + require('./key') if (isNode) { require('./swarm') require('./pubsub') diff --git a/test/core/interface/key.js b/test/core/interface/key.js new file mode 100644 index 0000000000..737d412743 --- /dev/null +++ b/test/core/interface/key.js @@ -0,0 +1,36 @@ +/* eslint-env mocha */ + +'use strict' + +const test = require('interface-ipfs-core') +const parallel = require('async/parallel') + +const IPFS = require('../../../src') + +const DaemonFactory = require('ipfsd-ctl') +const df = DaemonFactory.create({ type: 'proc', exec: IPFS }) +const options = { + args: ['--pass ipfs-is-awesome-software'] +} +const nodes = [] +const common = { + setup: function (callback) { + callback(null, { + spawnNode: (cb) => { + df.spawn(options, (err, _ipfsd) => { + if (err) { + return cb(err) + } + + nodes.push(_ipfsd) + cb(null, _ipfsd.api) + }) + } + }) + }, + teardown: function (callback) { + parallel(nodes.map((node) => (cb) => node.stop(cb)), callback) + } +} + +test.key(common) diff --git a/test/core/key-exchange.js b/test/core/key-exchange.js new file mode 100644 index 0000000000..86a2781ad6 --- /dev/null +++ b/test/core/key-exchange.js @@ -0,0 +1,51 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +const hat = require('hat') +const IPFS = require('../../src/core') + +// This gets replaced by `create-repo-browser.js` in the browser +const createTempRepo = require('../utils/create-repo-nodejs.js') + +describe('key exchange', () => { + let ipfs + let repo + let selfPem + let passwordPem = hat() + + before(function (done) { + this.timeout(20 * 1000) + repo = createTempRepo() + ipfs = new IPFS({ + repo: repo, + pass: hat() + }) + ipfs.on('ready', () => done()) + }) + + after((done) => repo.teardown(done)) + + it('exports', (done) => { + ipfs.key.export('self', passwordPem, (err, pem) => { + expect(err).to.not.exist() + expect(pem).to.exist() + selfPem = pem + done() + }) + }) + + it('imports', (done) => { + ipfs.key.import('clone', selfPem, passwordPem, (err, key) => { + expect(err).to.not.exist() + expect(key).to.exist() + expect(key).to.have.property('name', 'clone') + expect(key).to.have.property('id') + done() + }) + }) +}) diff --git a/test/http-api/index.js b/test/http-api/index.js index 374f50485c..1fa11622ad 100644 --- a/test/http-api/index.js +++ b/test/http-api/index.js @@ -6,6 +6,7 @@ const chai = require('chai') const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) +const hat = require('hat') const API = require('../../src/http') const APIctl = require('ipfs-api') const ncp = require('ncp').ncp @@ -21,7 +22,10 @@ describe('HTTP API', () => { before(function (done) { this.timeout(60 * 1000) - const options = { enablePubsubExperiment: true } + const options = { + pass: hat(), + enablePubsubExperiment: true + } http.api = new API(repoTests, null, options) ncp(repoExample, repoTests, (err) => { diff --git a/test/http-api/interface/key.js b/test/http-api/interface/key.js new file mode 100644 index 0000000000..75d5cf5769 --- /dev/null +++ b/test/http-api/interface/key.js @@ -0,0 +1,35 @@ +/* eslint-env mocha */ + +'use strict' + +const test = require('interface-ipfs-core') +const parallel = require('async/parallel') + +const DaemonFactory = require('ipfsd-ctl') +const df = DaemonFactory.create({ exec: 'src/cli/bin.js' }) +const options = { + args: ['--pass', 'ipfs-is-awesome-software'] +} + +const nodes = [] +const common = { + setup: function (callback) { + callback(null, { + spawnNode: (cb) => { + df.spawn(options, (err, _ipfsd) => { + if (err) { + return cb(err) + } + + nodes.push(_ipfsd) + cb(null, _ipfsd.api) + }) + } + }) + }, + teardown: function (callback) { + parallel(nodes.map((node) => (cb) => node.stop(cb)), callback) + } +} + +test.key(common)