diff --git a/packages/interface-ipfs-core/src/key/gen.js b/packages/interface-ipfs-core/src/key/gen.js index 0fd94ff424..8a76b9f734 100644 --- a/packages/interface-ipfs-core/src/key/gen.js +++ b/packages/interface-ipfs-core/src/key/gen.js @@ -3,6 +3,7 @@ const { nanoid } = require('nanoid') const { getDescribe, getIt, expect } = require('../utils/mocha') +const { keys: { supportedKeys, import: importKey } } = require('libp2p-crypto') /** * @typedef {import('ipfsd-ctl').Factory} Factory @@ -18,7 +19,18 @@ module.exports = (factory, options) => { describe('.key.gen', () => { const keyTypes = [ - { type: 'rsa', size: 2048 } + { + opts: { type: 'rsa', size: 2048 }, + expectedType: supportedKeys.rsa.RsaPrivateKey + }, + { + opts: { type: 'ed25519' }, + expectedType: supportedKeys.ed25519.Ed25519PrivateKey + }, + { + opts: { }, + expectedType: supportedKeys.ed25519.Ed25519PrivateKey + } ] /** @type {import('ipfs-core-types').IPFS} */ @@ -31,14 +43,30 @@ module.exports = (factory, options) => { after(() => factory.clean()) keyTypes.forEach((kt) => { - it(`should generate a new ${kt.type} key`, async function () { + it(`should generate a new ${kt.opts.type || 'default'} key`, async function () { // @ts-ignore this is mocha this.timeout(20 * 1000) const name = nanoid() - const key = await ipfs.key.gen(name, kt) + const key = await ipfs.key.gen(name, kt.opts) expect(key).to.exist() expect(key).to.have.property('name', name) expect(key).to.have.property('id') + + try { + const password = nanoid() + '-' + nanoid() + const exported = await ipfs.key.export(name, password) + const imported = await importKey(exported, password) + + expect(imported).to.be.an.instanceOf(kt.expectedType) + } catch (err) { + if (err.code === 'ERR_NOT_IMPLEMENTED') { + // key export is not exposed over the HTTP API + // @ts-ignore this is mocha + this.skip('Cannot verify key type') + } + + throw err + } }) }) }) diff --git a/packages/ipfs-cli/src/commands/init.js b/packages/ipfs-cli/src/commands/init.js index f3544c67dc..2fb280b6c7 100644 --- a/packages/ipfs-cli/src/commands/init.js +++ b/packages/ipfs-cli/src/commands/init.js @@ -4,6 +4,13 @@ const fs = require('fs') const debug = require('debug')('ipfs:cli:init') const { ipfsPathHelp } = require('../utils') +/** @type {Record} */ +const keyTypes = { + ed25519: 'Ed25519', + rsa: 'RSA', + secp256k1: 'secp256k1' +} + module.exports = { command: 'init [default-config] [options]', describe: 'Initialize a local IPFS node\n\n' + @@ -23,15 +30,16 @@ module.exports = { }) .option('algorithm', { type: 'string', + choices: Object.keys(keyTypes), alias: 'a', - default: 'RSA', - describe: 'Cryptographic algorithm to use for key generation. Supports [RSA, Ed25519, secp256k1]' + default: 'ed25519', + describe: 'Cryptographic algorithm to use for key generation' }) .option('bits', { type: 'number', alias: 'b', default: '2048', - describe: 'Number of bits to use in the generated RSA private key (defaults to 2048)', + describe: 'Number of bits to use if the generated private key is RSA (defaults to 2048)', coerce: Number }) .option('empty-repo', { @@ -58,7 +66,7 @@ module.exports = { * @param {object} argv * @param {import('../types').Context} argv.ctx * @param {string} argv.defaultConfig - * @param {'RSA' | 'Ed25519' | 'secp256k1'} argv.algorithm + * @param {'rsa' | 'ed25519' | 'secp256k1'} argv.algorithm * @param {number} argv.bits * @param {boolean} argv.emptyRepo * @param {string} argv.privateKey @@ -88,7 +96,7 @@ module.exports = { await IPFS.create({ repo: repoPath, init: { - algorithm: argv.algorithm, + algorithm: keyTypes[argv.algorithm], bits: argv.bits, privateKey: argv.privateKey, emptyRepo: argv.emptyRepo, diff --git a/packages/ipfs-cli/src/commands/key/gen.js b/packages/ipfs-cli/src/commands/key/gen.js index 50f53ba9c9..db5b64fe63 100644 --- a/packages/ipfs-cli/src/commands/key/gen.js +++ b/packages/ipfs-cli/src/commands/key/gen.js @@ -13,8 +13,9 @@ module.exports = { builder: { type: { alias: 't', - describe: 'type of the key to create [rsa, ed25519].', - default: 'rsa' + describe: 'type of the key to create', + choices: ['rsa', 'ed25519'], + default: 'ed25519' }, size: { alias: 's', diff --git a/packages/ipfs-cli/test/key.js b/packages/ipfs-cli/test/key.js index 5d90bf6e3a..f811dacce3 100644 --- a/packages/ipfs-cli/test/key.js +++ b/packages/ipfs-cli/test/key.js @@ -25,7 +25,7 @@ describe('key', () => { const name = 'key-name' const id = 'key-id' const defaultOptions = { - type: 'rsa', + type: 'ed25519', size: 2048, timeout: undefined } @@ -43,28 +43,28 @@ describe('key', () => { it('gen with args', async () => { ipfs.key.gen.withArgs(name, { ...defaultOptions, - type: 'rsb', + type: 'rsa', size: 7 }).resolves({ id, name }) - const out = await cli(`key gen ${name} --type rsb --size 7`, { ipfs }) + const out = await cli(`key gen ${name} --type rsa --size 7`, { ipfs }) expect(out).to.equal(`generated ${id} ${name}\n`) }) it('gen with short args', async () => { ipfs.key.gen.withArgs(name, { ...defaultOptions, - type: 'rsc', + type: 'rsa', size: 5 }).resolves({ id, name }) - const out = await cli(`key gen ${name} -t rsc -s 5`, { ipfs }) + const out = await cli(`key gen ${name} -t rsa -s 5`, { ipfs }) expect(out).to.equal(`generated ${id} ${name}\n`) }) diff --git a/packages/ipfs-core/src/components/key/gen.js b/packages/ipfs-core/src/components/key/gen.js index f1ec77ee17..c437179354 100644 --- a/packages/ipfs-core/src/components/key/gen.js +++ b/packages/ipfs-core/src/components/key/gen.js @@ -2,7 +2,7 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') -const DEFAULT_KEY_TYPE = 'rsa' +const DEFAULT_KEY_TYPE = 'ed25519' const DEFAULT_KEY_SIZE = 2048 /** diff --git a/packages/ipfs-core/src/components/libp2p.js b/packages/ipfs-core/src/components/libp2p.js index 7967153011..a3517bfb38 100644 --- a/packages/ipfs-core/src/components/libp2p.js +++ b/packages/ipfs-core/src/components/libp2p.js @@ -13,8 +13,15 @@ const { Multiaddr } = require('multiaddr') const pkgversion = require('../../package.json').version /** + * @typedef {object} DekOptions + * @property {string} hash + * @property {string} salt + * @property {number} iterationCount + * @property {number} keyLength + * * @typedef {Object} KeychainConfig * @property {string} [pass] + * @property {DekOptions} [dek] * * @typedef {import('ipfs-repo').IPFSRepo} Repo * @typedef {import('peer-id')} PeerId diff --git a/packages/ipfs-core/src/components/resolve.js b/packages/ipfs-core/src/components/resolve.js index fb8b408892..8c60279f76 100644 --- a/packages/ipfs-core/src/components/resolve.js +++ b/packages/ipfs-core/src/components/resolve.js @@ -2,6 +2,7 @@ const isIpfs = require('is-ipfs') const { CID } = require('multiformats/cid') +const PeerID = require('peer-id') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') const { resolve: res } = require('../utils') @@ -28,14 +29,18 @@ module.exports = ({ repo, codecs, bases, name }) => { } const [, schema, hash, ...rest] = path.split('/') // ['', 'ipfs', 'hash', ...path] - const cid = CID.parse(hash) const base = opts.cidBase ? await bases.getBase(opts.cidBase) : undefined + const bytes = parseBytes(hash) // nothing to resolve return the input if (rest.length === 0) { - return `/${schema}/${cid.toString(base && base.encoder)}` + const str = base ? base.encoder.encode(bytes) : hash + + return `/${schema}/${str}` } + const cid = CID.decode(bytes) + path = rest.join('/') const results = res(cid, path, codecs, repo, opts) @@ -54,3 +59,16 @@ module.exports = ({ repo, codecs, bases, name }) => { return withTimeoutOption(resolve) } + +/** + * Parse the input as a PeerID or a CID or throw an error + * + * @param {string} str + */ +function parseBytes (str) { + try { + return PeerID.parse(str).toBytes() + } catch { + return CID.parse(str).bytes + } +} diff --git a/packages/ipfs-core/src/components/storage.js b/packages/ipfs-core/src/components/storage.js index 08d732be84..b70f47dd6d 100644 --- a/packages/ipfs-core/src/components/storage.js +++ b/packages/ipfs-core/src/components/storage.js @@ -130,6 +130,19 @@ const initRepo = async (print, repo, options) => { log('repo opened') + /** @type {import('./libp2p').KeychainConfig} */ + const keychainConfig = { + pass: options.pass + } + + try { + keychainConfig.dek = await repo.config.get('Keychain.DEK') + } catch (err) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + // Create libp2p for Keychain creation const libp2p = await createLibP2P({ options: undefined, @@ -137,16 +150,14 @@ const initRepo = async (print, repo, options) => { peerId, repo, config, - keychainConfig: { - pass: options.pass - } + keychainConfig }) if (libp2p.keychain && libp2p.keychain.opts) { await libp2p.loadKeychain() await repo.config.set('Keychain', { - dek: libp2p.keychain.opts.dek + DEK: libp2p.keychain.opts.dek }) } @@ -172,13 +183,13 @@ const decodePeerId = (peerId) => { * * @param {Print} print * @param {Object} options - * @param {KeyType} [options.algorithm='RSA'] + * @param {KeyType} [options.algorithm='Ed25519'] * @param {number} [options.bits=2048] * @returns {Promise} */ -const initPeerId = (print, { algorithm = 'RSA', bits = 2048 }) => { +const initPeerId = (print, { algorithm = 'Ed25519', bits = 2048 }) => { // Generate peer identity keypair + transform to desired format + add to config. - print('generating %s-bit (rsa only) %s keypair...', bits, algorithm) + print('generating %s keypair...', algorithm) return PeerId.create({ keyType: algorithm, bits }) } diff --git a/packages/ipfs-core/src/types.d.ts b/packages/ipfs-core/src/types.d.ts index 37bd06b61b..3cba063718 100644 --- a/packages/ipfs-core/src/types.d.ts +++ b/packages/ipfs-core/src/types.d.ts @@ -84,7 +84,7 @@ export interface Options { /** * Occasionally a repo migration is necessary - pass true here to to this automatically at startup - * when a new version of IPFS is being run for the first time and a migration is necssary, otherwise + * when a new version of IPFS is being run for the first time and a migration is necessary, otherwise * the node will refuse to start */ repoAutoMigrate?: boolean diff --git a/packages/ipfs-core/test/create-node.spec.js b/packages/ipfs-core/test/create-node.spec.js index 71499c4536..567573791f 100644 --- a/packages/ipfs-core/test/create-node.spec.js +++ b/packages/ipfs-core/test/create-node.spec.js @@ -98,7 +98,10 @@ describe('create node', function () { it('should throw on boot error', () => { return expect(IPFS.create({ repo: tempRepo, - init: { bits: 256 }, // Too few bits will cause error on boot + init: { + algorithm: 'RSA', + bits: 256 + }, // Too few bits will cause error on boot config: { Addresses: { Swarm: [] } } })).to.eventually.be.rejected() }) @@ -109,6 +112,7 @@ describe('create node', function () { const node = await IPFS.create({ repo: tempRepo, init: { + algorithm: 'RSA', bits: 1024 }, config: { diff --git a/packages/ipfs-core/test/init.spec.js b/packages/ipfs-core/test/init.spec.js index abea71ac15..3d0bb2ee46 100644 --- a/packages/ipfs-core/test/init.spec.js +++ b/packages/ipfs-core/test/init.spec.js @@ -24,11 +24,11 @@ describe('init', function () { let cleanup /** - * @param {import('../src/types').InitOptions} options + * @param {import('../src/types').Options} [options] */ const init = async (options) => { const res = await createNode({ - init: options, + ...options, start: false }) @@ -41,33 +41,63 @@ describe('init', function () { afterEach(() => cleanup()) it('should init successfully', async () => { - await init({ bits: 512 }) + await init() const res = await repo.exists() expect(res).to.equal(true) const config = await repo.config.getAll() - expect(config.Identity).to.exist() - expect(config.Keychain).to.exist() + expect(config).to.have.property('Identity') + expect(config).to.have.nested.property('Keychain.DEK') }) it('should init successfully with a keychain pass', async () => { - await init({ bits: 512 }) + await init({ + pass: 'super-super-secure-1234', + init: { + algorithm: 'RSA', + bits: 512 + } + }) const res = await repo.exists() expect(res).to.equal(true) const config = await repo.config.getAll() - expect(config.Keychain).to.exist() + const { ipfs: ipfs2, repo: repo2 } = await createNode({ + repo: repo, + pass: 'something-else-that-is-long-enough', + start: false, + init: { + algorithm: 'RSA', + bits: 512 + } + }) + + // same repo, same peer id + expect(repo.path).to.equal(repo2.path) + expect(await ipfs2.id()).to.deep.equal(await ipfs.id()) + + // opened with correct password + await expect(ipfs.key.export('self', 'some-other-password')).to.eventually.be.ok() + + // opened with incorrect password + await expect(ipfs2.key.export('self', 'some-other-password')).to.eventually.be.rejected() + }) + + it('should init with a key algorithm (RSA)', async () => { + await init({ init: { algorithm: 'RSA' } }) + + const config = await repo.config.getAll() const peerId = await PeerId.createFromPrivKey(`${config.Identity?.PrivKey}`) expect(peerId.privKey).is.instanceOf(supportedKeys.rsa.RsaPrivateKey) }) it('should init with a key algorithm (Ed25519)', async () => { - await init({ algorithm: 'Ed25519' }) + await init({ init: { algorithm: 'Ed25519' } }) const config = await repo.config.getAll() const peerId = await PeerId.createFromPrivKey(`${config.Identity?.PrivKey}`) @@ -75,7 +105,7 @@ describe('init', function () { }) it('should init with a key algorithm (secp256k1)', async () => { - await init({ algorithm: 'secp256k1' }) + await init({ init: { algorithm: 'secp256k1' } }) const config = await repo.config.getAll() const peerId = await PeerId.createFromPrivKey(`${config.Identity?.PrivKey}`) @@ -85,35 +115,40 @@ describe('init', function () { it('should set # of bits in key', async function () { this.timeout(120 * 1000) - await init({ bits: 1024 }) + await init({ + init: { + algorithm: 'RSA', + bits: 1024 + } + }) const config = await repo.config.getAll() expect(config.Identity?.PrivKey.length).is.above(256) }) it('should allow a pregenerated key to be used', async () => { - await init({ privateKey }) + await init({ init: { privateKey } }) const config = await repo.config.getAll() expect(config.Identity?.PeerID).is.equal('QmRsooYQasV5f5r834NSpdUtmejdQcpxXkK6qsozZWEihC') }) it('should allow a pregenerated ed25519 key to be used', async () => { - await init({ privateKey: edPrivateKey }) + await init({ init: { privateKey: edPrivateKey } }) const config = await repo.config.getAll() expect(config.Identity?.PeerID).is.equal('12D3KooWRm8J3iL796zPFi2EtGGtUJn58AG67gcqzMFHZnnsTzqD') }) it('should allow a pregenerated secp256k1 key to be used', async () => { - await init({ privateKey: secpPrivateKey }) + await init({ init: { privateKey: secpPrivateKey } }) const config = await repo.config.getAll() expect(config.Identity?.PeerID).is.equal('16Uiu2HAm5qw8UyXP2RLxQUx5KvtSN8DsTKz8quRGqGNC3SYiaB8E') }) it('should write init docs', async () => { - await init({ bits: 512 }) + await init() const multihash = CID.parse('QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB') const node = await ipfs.object.get(multihash, { enc: 'base58' }) @@ -121,7 +156,7 @@ describe('init', function () { }) it('should allow init with an empty repo', async () => { - await init({ bits: 512, emptyRepo: true }) + await init({ init: { emptyRepo: true } }) // Should not have default assets const multihash = CID.parse('QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB') @@ -129,14 +164,14 @@ describe('init', function () { }) it('should apply one profile', async () => { - await init({ bits: 512, profiles: ['test'] }) + await init({ init: { profiles: ['test'] } }) const config = await repo.config.getAll() expect(config.Bootstrap).to.be.empty() }) it('should apply multiple profiles', async () => { - await init({ bits: 512, profiles: ['test', 'local-discovery'] }) + await init({ init: { profiles: ['test', 'local-discovery'] } }) const config = await repo.config.getAll() expect(config.Bootstrap).to.be.empty() diff --git a/packages/ipfs-core/test/utils/create-node.js b/packages/ipfs-core/test/utils/create-node.js index 96bff38b6d..afa5c5c459 100644 --- a/packages/ipfs-core/test/utils/create-node.js +++ b/packages/ipfs-core/test/utils/create-node.js @@ -8,7 +8,18 @@ const createTempRepo = require('./create-repo') * @param {import('../../src/types').Options} config */ module.exports = async (config = {}) => { - const repo = await createTempRepo() + let repo + + if (config.repo) { + if (typeof config.repo === 'string') { + repo = await createTempRepo({ path: config.repo }) + } else { + repo = config.repo + } + } else { + repo = await createTempRepo() + } + const ipfs = await create(mergeOptions({ silent: true, repo, diff --git a/packages/ipfs-http-client/src/key/export.js b/packages/ipfs-http-client/src/key/export.js index 285d3ab988..e84a560547 100644 --- a/packages/ipfs-http-client/src/key/export.js +++ b/packages/ipfs-http-client/src/key/export.js @@ -1,6 +1,7 @@ 'use strict' const configure = require('../lib/configure') +const errCode = require('err-code') /** * @typedef {import('../types').HTTPClientExtraOptions} HTTPClientExtraOptions @@ -12,7 +13,7 @@ module.exports = configure(api => { * @type {KeyAPI["export"]} */ const exportKey = async (name, password, options = {}) => { - throw new Error('Not implemented') + throw errCode(new Error('Not implemented'), 'ERR_NOT_IMPLEMENTED') } return exportKey diff --git a/packages/ipfs-http-client/src/key/info.js b/packages/ipfs-http-client/src/key/info.js index 4b5948082f..79142efaa3 100644 --- a/packages/ipfs-http-client/src/key/info.js +++ b/packages/ipfs-http-client/src/key/info.js @@ -1,6 +1,7 @@ 'use strict' const configure = require('../lib/configure') +const errCode = require('err-code') /** * @typedef {import('../types').HTTPClientExtraOptions} HTTPClientExtraOptions @@ -12,7 +13,7 @@ module.exports = configure(api => { * @type {KeyAPI["info"]} */ const info = async (name, options = {}) => { - throw new Error('Not implemented') + throw errCode(new Error('Not implemented'), 'ERR_NOT_IMPLEMENTED') } return info diff --git a/packages/ipfs-http-client/src/start.js b/packages/ipfs-http-client/src/start.js index 62d748ccce..cb3e1dd0c4 100644 --- a/packages/ipfs-http-client/src/start.js +++ b/packages/ipfs-http-client/src/start.js @@ -1,6 +1,7 @@ 'use strict' const configure = require('./lib/configure') +const errCode = require('err-code') /** * @typedef {import('./types').HTTPClientExtraOptions} HTTPClientExtraOptions @@ -12,7 +13,7 @@ module.exports = configure(api => { * @type {RootAPI["start"]} */ const start = async (options = {}) => { - throw new Error('Not implemented') + throw errCode(new Error('Not implemented'), 'ERR_NOT_IMPLEMENTED') } return start