Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
],
"scripts": {
"lint": "aegir lint",
"prepare": "aegir build",
"build": "aegir build",
"test": "npm run test:node && npm run test:browser",
"test:node": "aegir test -t node -f \"./test/**/*.{node,spec}.js\"",
Expand Down Expand Up @@ -70,7 +71,7 @@
"it-pipe": "^1.1.0",
"it-protocol-buffers": "^0.2.0",
"libp2p-crypto": "^0.18.0",
"libp2p-interfaces": "^0.8.0",
"libp2p-interfaces": "^0.8.1",
"libp2p-utils": "^0.2.2",
"mafmt": "^8.0.0",
"merge-options": "^2.0.0",
Expand Down
19 changes: 15 additions & 4 deletions src/keychain/cms.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// @ts-nocheck
'use strict'

require('node-forge/lib/pkcs7')
Expand All @@ -9,6 +8,8 @@ const errcode = require('err-code')
const uint8ArrayFromString = require('uint8arrays/from-string')
const uint8ArrayToString = require('uint8arrays/to-string')

const privates = new WeakMap()

/**
* Cryptographic Message Syntax (aka PKCS #7)
*
Expand All @@ -23,13 +24,15 @@ class CMS {
* Creates a new instance with a keychain
*
* @param {import('./index')} keychain - the available keys
* @param {string} dek
*/
constructor (keychain) {
constructor (keychain, dek) {
if (!keychain) {
throw errcode(new Error('keychain is required'), 'ERR_KEYCHAIN_REQUIRED')
}

this.keychain = keychain
privates.set(this, { dek })
}

/**
Expand All @@ -48,7 +51,9 @@ class CMS {

const key = await this.keychain.findKeyByName(name)
const pem = await this.keychain._getPrivateKey(name)
const privateKey = forge.pki.decryptRsaPrivateKey(pem, this.keychain._())
/** @type {string} */
const dek = privates.get(this).dek
const privateKey = forge.pki.decryptRsaPrivateKey(pem, dek)
const certificate = await certificateForKey(key, privateKey)

// create a p7 enveloped message
Expand Down Expand Up @@ -115,8 +120,14 @@ class CMS {
}

const key = await this.keychain.findKeyById(r.keyId)

if (!key) {
throw errcode(new Error('No key available to decrypto'), 'ERR_NO_KEY')
}

const pem = await this.keychain._getPrivateKey(key.name)
const privateKey = forge.pki.decryptRsaPrivateKey(pem, this.keychain._())
const dek = privates.get(this).dek
const privateKey = forge.pki.decryptRsaPrivateKey(pem, dek)
cms.decrypt(r.recipient, privateKey)
return uint8ArrayFromString(cms.content.getBytes(), 'ascii')
}
Expand Down
78 changes: 49 additions & 29 deletions src/keychain/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
// @ts-nocheck
/* eslint max-nested-callbacks: ["error", 5] */
'use strict'

const sanitize = require('sanitize-filename')
const mergeOptions = require('merge-options')
const crypto = require('libp2p-crypto')
const DS = require('interface-datastore')
const Datastore = require('interface-datastore')
const CMS = require('./cms')
const errcode = require('err-code')
const { Number } = require('ipfs-utils/src/globalthis')
Expand All @@ -14,8 +13,14 @@ const uint8ArrayFromString = require('uint8arrays/from-string')

require('node-forge/lib/sha512')

/**
* @typedef {import('peer-id')} PeerId
* @typedef {import('interface-datastore/src/key')} Key
*/

const keyPrefix = '/pkcs8/'
const infoPrefix = '/info/'
const privates = new WeakMap()

// NIST SP 800-132
const NIST = {
Expand Down Expand Up @@ -46,7 +51,8 @@ function validateKeyName (name) {
* This assumes than an error indicates that the keychain is under attack. Delay returning an
* error to make brute force attacks harder.
*
* @param {string | Error} err - The error
* @param {string|Error} err - The error
* @returns {Promise<never>}
* @private
*/
async function throwDelayed (err) {
Expand All @@ -62,29 +68,28 @@ async function throwDelayed (err) {
* Converts a key name into a datastore name.
*
* @param {string} name
* @returns {DS.Key}
* @returns {Key}
* @private
*/
function DsName (name) {
return new DS.Key(keyPrefix + name)
return new Datastore.Key(keyPrefix + name)
}

/**
* Converts a key name into a datastore info name.
*
* @param {string} name
* @returns {DS.Key}
* @returns {Key}
* @private
*/
function DsInfoName (name) {
return new DS.Key(infoPrefix + name)
return new Datastore.Key(infoPrefix + name)
}

/**
* Information about a key.
*
* @typedef {Object} KeyInfo
*
* @property {string} id - The universally unique key id.
* @property {string} name - The local key name.
*/
Expand All @@ -101,7 +106,7 @@ class Keychain {
/**
* Creates a new instance of a key chain.
*
* @param {DS} store - where the key are.
* @param {Datastore} store - where the key are.
* @param {object} options
* @class
*/
Expand Down Expand Up @@ -134,7 +139,7 @@ class Keychain {
this.opts.dek.keyLength,
this.opts.dek.hash) : ''

Object.defineProperty(this, '_', { value: () => dek })
privates.set(this, { dek })
}

/**
Expand All @@ -148,13 +153,13 @@ class Keychain {
* @returns {CMS}
*/
get cms () {
return new CMS(this)
return new CMS(this, privates.get(this).dek)
}

/**
* Generates the options for a keychain. A random salt is produced.
*
* @returns {object}
* @returns {Object}
*/
static generateOptions () {
const options = Object.assign({}, defaultOptions)
Expand All @@ -167,7 +172,7 @@ class Keychain {
* Gets an object that can encrypt/decrypt protected data.
* The default options for a keychain.
*
* @returns {object}
* @returns {Object}
*/
static get options () {
return defaultOptions
Expand All @@ -178,10 +183,10 @@ class Keychain {
*
* @param {string} name - The local key name; cannot already exist.
* @param {string} type - One of the key types; 'rsa'.
* @param {int} [size] - The key size in bits. Used for rsa keys only.
* @returns {KeyInfo}
* @param {number} [size = 2048] - The key size in bits. Used for rsa keys only.
* @returns {Promise<KeyInfo>}
*/
async createKey (name, type, size) {
async createKey (name, type, size = 2048) {
const self = this

if (!validateKeyName(name) || name === 'self') {
Expand All @@ -208,9 +213,12 @@ class Keychain {

let keyInfo
try {
// @ts-ignore Differences between several crypto return types need to be fixed in libp2p-crypto
const keypair = await crypto.keys.generateKeyPair(type, size)
const kid = await keypair.id()
const pem = await keypair.export(this._())
/** @type {string} */
const dek = privates.get(this).dek
const pem = await keypair.export(dek)
keyInfo = {
name: name,
id: kid
Expand All @@ -230,7 +238,7 @@ class Keychain {
/**
* List all the keys.
*
* @returns {KeyInfo[]}
* @returns {Promise<KeyInfo[]>}
*/
async listKeys () {
const self = this
Expand All @@ -250,7 +258,7 @@ class Keychain {
* Find a key by it's id.
*
* @param {string} id - The universally unique key identifier.
* @returns {KeyInfo}
* @returns {Promise<KeyInfo|undefined>}
*/
async findKeyById (id) {
try {
Expand All @@ -265,7 +273,7 @@ class Keychain {
* Find a key by it's name.
*
* @param {string} name - The local key name.
* @returns {KeyInfo}
* @returns {Promise<KeyInfo>}
*/
async findKeyByName (name) {
if (!validateKeyName(name)) {
Expand All @@ -285,7 +293,7 @@ class Keychain {
* Remove an existing key.
*
* @param {string} name - The local key name; must already exist.
* @returns {KeyInfo}
* @returns {Promise<KeyInfo>}
*/
async removeKey (name) {
const self = this
Expand All @@ -306,7 +314,7 @@ class Keychain {
*
* @param {string} oldName - The old local key name; must already exist.
* @param {string} newName - The new local key name; must not already exist.
* @returns {KeyInfo}
* @returns {Promise<KeyInfo>}
*/
async renameKey (oldName, newName) {
const self = this
Expand Down Expand Up @@ -347,7 +355,7 @@ class Keychain {
*
* @param {string} name - The local key name; must already exist.
* @param {string} password - The password
* @returns {string}
* @returns {Promise<string>}
*/
async exportKey (name, password) {
if (!validateKeyName(name)) {
Expand All @@ -361,7 +369,9 @@ class Keychain {
try {
const res = await this.store.get(dsname)
const pem = uint8ArrayToString(res)
const privateKey = await crypto.keys.import(pem, this._())
/** @type {string} */
const dek = privates.get(this).dek
const privateKey = await crypto.keys.import(pem, dek)
return privateKey.export(password)
} catch (err) {
return throwDelayed(err)
Expand All @@ -374,7 +384,7 @@ class Keychain {
* @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.
* @returns {KeyInfo}
* @returns {Promise<KeyInfo>}
*/
async importKey (name, pem, password) {
const self = this
Expand All @@ -398,7 +408,9 @@ class Keychain {
let kid
try {
kid = await privateKey.id()
pem = await privateKey.export(this._())
/** @type {string} */
const dek = privates.get(this).dek
pem = await privateKey.export(dek)
} catch (err) {
return throwDelayed(err)
}
Expand All @@ -415,6 +427,13 @@ class Keychain {
return keyInfo
}

/**
* Import a peer key
*
* @param {string} name - The local key name; must not already exist.
* @param {PeerId} peer - The PEM encoded PKCS #8 string
* @returns {Promise<KeyInfo>}
*/
async importPeer (name, peer) {
const self = this
if (!validateKeyName(name)) {
Expand All @@ -431,7 +450,9 @@ class Keychain {

try {
const kid = await privateKey.id()
const pem = await privateKey.export(this._())
/** @type {string} */
const dek = privates.get(this).dek
const pem = await privateKey.export(dek)
const keyInfo = {
name: name,
id: kid
Expand All @@ -450,8 +471,7 @@ class Keychain {
* Gets the private key as PEM encoded PKCS #8 string.
*
* @param {string} name
* @returns {string}
* @private
* @returns {Promise<string>}
*/
async _getPrivateKey (name) {
if (!validateKeyName(name)) {
Expand Down
2 changes: 1 addition & 1 deletion src/peer-routing.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class PeerRouting {
/**
* Iterates over all peer routers in series to find the given peer.
*
* @param {string} id - The id of the peer to find
* @param {PeerId} id - The id of the peer to find
* @param {object} [options]
* @param {number} [options.timeout] - How long the query should run
* @returns {Promise<{ id: PeerId, multiaddrs: Multiaddr[] }>}
Expand Down