mirror of
https://github.com/fluencelabs/js-libp2p
synced 2025-07-07 12:51:32 +00:00
feat: move bits from https://github.com/richardschneider/ipfs-encryption
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.png binary
|
||||||
|
* crlf=input
|
79
README.md
79
README.md
@ -12,14 +12,91 @@
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
> Keychain primitives for libp2p in JavaScript
|
> A secure key chain for libp2p in JavaScript
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Manages the lifecycle of a key
|
||||||
|
- Keys are encrypted at rest
|
||||||
|
- Enforces the use of safe key names
|
||||||
|
- Uses encrypted PKCS 8 for key storage
|
||||||
|
- Uses PBKDF2 for a "stetched" key encryption key
|
||||||
|
- Enforces NIST SP 800-131A and NIST SP 800-132
|
||||||
|
- Uses PKCS 7: CMS (aka RFC 5652) to provide cryptographically protected messages
|
||||||
|
- Delays reporting errors to slow down brute force attacks
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
const datastore = new FsStore('./a-keystore')
|
||||||
|
const opts = {
|
||||||
|
passPhrase: 'some long easily remembered phrase'
|
||||||
|
}
|
||||||
|
const keychain = new Keychain(datastore, opts)
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
|
Managing a key
|
||||||
|
|
||||||
|
- `createKey (name, type, size, callback)`
|
||||||
|
- `renameKey (oldName, newName, callback)`
|
||||||
|
- `removeKey (name, callback)`
|
||||||
|
- `exportKey (name, password, callback)`
|
||||||
|
- `importKey (name, pem, password, callback)`
|
||||||
|
- `importPeer (name, peer, callback)`
|
||||||
|
|
||||||
|
A naming service for a key
|
||||||
|
|
||||||
|
- `listKeys (callback)`
|
||||||
|
- `findKeyById (id, callback)`
|
||||||
|
- `findKeyByName (name, callback)`
|
||||||
|
|
||||||
|
Cryptographically protected messages
|
||||||
|
|
||||||
|
- `cms.createAnonymousEncryptedData (name, plain, callback)`
|
||||||
|
- `cms.readData (cmsData, callback)`
|
||||||
|
|
||||||
|
### KeyInfo
|
||||||
|
|
||||||
|
The key management and naming service API all return a `KeyInfo` object. The `id` is a universally unique identifier for the key. The `name` is local to the key chain.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
name: 'rsa-key',
|
||||||
|
id: 'QmYWYSUZ4PV6MRFYpdtEDJBiGs4UrmE6g8wmAWSePekXVW'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The **key id** is the SHA-256 [multihash](https://github.com/multiformats/multihash) of its public key. The *public key* is a [protobuf encoding](https://github.com/libp2p/js-libp2p-crypto/blob/master/src/keys/keys.proto.js) containing a type and the [DER encoding](https://en.wikipedia.org/wiki/X.690) of the PKCS [SubjectPublicKeyInfo](https://www.ietf.org/rfc/rfc3279.txt).
|
||||||
|
|
||||||
|
### Private key storage
|
||||||
|
|
||||||
|
A private key is stored as an encrypted PKCS 8 structure in the PEM format. It is protected by a key generated from the key chain's *passPhrase* using **PBKDF2**. Its file extension is `.p8`.
|
||||||
|
|
||||||
|
The default options for generating the derived encryption key are in the `dek` object
|
||||||
|
```
|
||||||
|
const defaultOptions = {
|
||||||
|
createIfNeeded: true,
|
||||||
|
|
||||||
|
//See https://cryptosense.com/parameter-choice-for-pbkdf2/
|
||||||
|
dek: {
|
||||||
|
keyLength: 512 / 8,
|
||||||
|
iterationCount: 10000,
|
||||||
|
salt: 'you should override this value with a crypto secure random number',
|
||||||
|
hash: 'sha512'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Physical storage
|
||||||
|
|
||||||
|
The actual physical storage of an encrypted key is left to implementations of [interface-datastore](https://github.com/ipfs/interface-datastore/). A key benifit is that now the key chain can be used in browser with the [js-datastore-level](https://github.com/ipfs/js-datastore-level) implementation.
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-crypto/issues)!
|
Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-crypto/issues)!
|
||||||
|
BIN
doc/private-key.png
Normal file
BIN
doc/private-key.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
1
doc/private-key.xml
Normal file
1
doc/private-key.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<mxfile userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36" version="7.8.2" editor="www.draw.io"><diagram id="a8b2919f-aefc-d24c-c550-ea0bf34e92af" name="Page-1">7VlNb6MwEP01HLfCGBJ6bNJ2V9pdqVIP2x4dcMAKYGScJumvXxNsvkw+SmgSVe2hMs9mbL839swQA07j9U+G0vAv9XFkWKa/NuC9YVmua4n/ObApAOjCAggY8QsIVMAzeccSNCW6JD7OGgM5pREnaRP0aJJgjzcwxBhdNYfNadScNUUB1oBnD0U6+o/4PJTbssYV/guTIFQzg9Ft0TND3iJgdJnI+QwLzrd/RXeMlC250SxEPl3VIPhgwCmjlBeteD3FUU6toq1473FHb7luhhN+zAtSpzcULeXWU5RluYmQoQzLRfKNIobjtbA7CXkcCQCIZsYZXeApjSgTSEITMXIyJ1HUglBEgkQ8emJlWOCTN8w4EZTfyY6Y+H4+zWQVEo6fU+Tlc66EfwlsSynOF22KJ7loYQCvd24clHQKL8U0xpxtxBDlolIA6aBgJJ9Xldy2hMKa0ko3JB0sKA1XJIuG5Lmbc6hx/jT5ff9oaWQL50jzZsqoh4Uq3dTUtBiAF9AmxtaJAVYHM6MBmLE1Zny8EABNOaFJ9nW9sfQryfr4fN7oaJxrNOPEv8sv1ZyvSFwPxGuSLjbJNi85GzcmGCvgdQvAUQk8YUbE8nK6a7xhX7uKD7JWo8XpoEVhDEeIk7em+S6u5AxPlIiJq6PQEgWMraaJjC6Zh+Vb9Uu2bUiFw12GOGIB5pqhrXTlto9SczSomk5Dyw9IJsL1dku1C+9SKpYHR5Fvmj1VhE1D2ukbTkX3WlQsuGmErbqw4KLnE5oHBDlWWbt10K22i+xQVgiANrVhaT4g271g22xfKI3kTDQKi33d5rY7fB4Mmgxn5B3NtgNy/5D7EKOdieHcfyhcRmiGo0mZBauwW+XBe+KlzOblSoxSz7pjunvj6A8RgcpaY9Mw3tfZ1BA6n2f41IOt6puaRAucrz/AiSbUNaR/Fjxj+geAxk668PJqRLiPexX8QPuS/OjVmo84yjhleqV2CXac9o18Vnb06uEm3e01PvWW8XZfh4iZFdn+n9mQTLWSCQhcjanRntB5ElF6yl9cQl++zGpfbo7unp9VZgE9M2dJoFFdbRmc5cRarRMLLd0P3S5KnAEoGWuUaHwcTHPXhL/U2q/NjPdF+k6tIHV6J8AqeF9PBtzyZxu2HLVvaQPdlqHhShswaG0zmLQdVWsRbb+lPV5avf44Qdpm2Vo/67JLnfb+oo86RDeNKxLdHkr0208TXcXGz/pW0S066C+61SG6/S36x0TXC7VTRP9SH43VLahyzHZpc/xHY7DfUG85xWP1A2MxvPoRFz78Bw==</diagram></mxfile>
|
29
package.json
29
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "libp2p-keychain",
|
"name": "libp2p-keychain",
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"description": "",
|
"description": "Key management and cryptographically protected messages",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "aegir lint",
|
"lint": "aegir lint",
|
||||||
@ -29,16 +29,37 @@
|
|||||||
"IPFS",
|
"IPFS",
|
||||||
"libp2p",
|
"libp2p",
|
||||||
"keys",
|
"keys",
|
||||||
|
"encryption",
|
||||||
|
"secure",
|
||||||
"crypto"
|
"crypto"
|
||||||
],
|
],
|
||||||
"author": "David Dias <daviddias@ipfs.io>",
|
"author": "Richard Schneider <makaretu@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/libp2p/js-libp2p-keychain/issues"
|
"url": "https://github.com/libp2p/js-libp2p-keychain/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/libp2p/js-libp2p-keychain#readme",
|
"homepage": "https://github.com/libp2p/js-libp2p-keychain#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"async": "^2.6.0",
|
||||||
|
"deepmerge": "^1.5.2",
|
||||||
|
"interface-datastore": "~0.4.1",
|
||||||
|
"libp2p-crypto": "~0.10.3",
|
||||||
|
"multihashes": "~0.4.12",
|
||||||
|
"node-forge": "~0.7.1",
|
||||||
|
"pull-stream": "^3.6.1",
|
||||||
|
"sanitize-filename": "^1.6.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"aegir": "^12.2.0",
|
"aegir": "^12.2.0",
|
||||||
"pre-commit": "^1.2.2"
|
"chai": "^4.1.2",
|
||||||
|
"chai-string": "^1.4.0",
|
||||||
|
"datastore-fs": "^0.4.1",
|
||||||
|
"datastore-level": "^0.7.0",
|
||||||
|
"dirty-chai": "^2.0.1",
|
||||||
|
"level-js": "^2.2.4",
|
||||||
|
"mocha": "^4.0.1",
|
||||||
|
"peer-id": "^0.10.2",
|
||||||
|
"pre-commit": "^1.2.2",
|
||||||
|
"rimraf": "^2.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
97
src/cms.js
Normal file
97
src/cms.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const async = require('async')
|
||||||
|
const forge = require('node-forge')
|
||||||
|
const util = require('./util')
|
||||||
|
|
||||||
|
class CMS {
|
||||||
|
constructor (keystore) {
|
||||||
|
if (!keystore) {
|
||||||
|
throw new Error('keystore is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.keystore = keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
createAnonymousEncryptedData (name, plain, callback) {
|
||||||
|
const self = this
|
||||||
|
if (!Buffer.isBuffer(plain)) {
|
||||||
|
return callback(new Error('Data is required'))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.keystore._getPrivateKey(name, (err, key) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const privateKey = forge.pki.decryptRsaPrivateKey(key, self.keystore._())
|
||||||
|
util.certificateForKey(privateKey, (err, certificate) => {
|
||||||
|
if (err) return callback(err)
|
||||||
|
|
||||||
|
// create a p7 enveloped message
|
||||||
|
const p7 = forge.pkcs7.createEnvelopedData()
|
||||||
|
p7.addRecipient(certificate)
|
||||||
|
p7.content = forge.util.createBuffer(plain)
|
||||||
|
p7.encrypt()
|
||||||
|
|
||||||
|
// convert message to DER
|
||||||
|
const der = forge.asn1.toDer(p7.toAsn1()).getBytes()
|
||||||
|
callback(null, Buffer.from(der, 'binary'))
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
callback(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
readData (cmsData, callback) {
|
||||||
|
if (!Buffer.isBuffer(cmsData)) {
|
||||||
|
return callback(new Error('CMS data is required'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const self = this
|
||||||
|
let cms
|
||||||
|
try {
|
||||||
|
const buf = forge.util.createBuffer(cmsData.toString('binary'));
|
||||||
|
const obj = forge.asn1.fromDer(buf)
|
||||||
|
cms = forge.pkcs7.messageFromAsn1(obj)
|
||||||
|
} catch (err) {
|
||||||
|
return callback(new Error('Invalid CMS: ' + err.message))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a recipient whose key we hold. We only deal with recipient certs
|
||||||
|
// issued by ipfs (O=ipfs).
|
||||||
|
const recipients = cms.recipients
|
||||||
|
.filter(r => r.issuer.find(a => a.shortName === 'O' && a.value === 'ipfs'))
|
||||||
|
.filter(r => r.issuer.find(a => a.shortName === 'CN'))
|
||||||
|
.map(r => {
|
||||||
|
return {
|
||||||
|
recipient: r,
|
||||||
|
keyId: r.issuer.find(a => a.shortName === 'CN').value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
async.detect(
|
||||||
|
recipients,
|
||||||
|
(r, cb) => self.keystore.findKeyById(r.keyId, (err, info) => cb(null, !err && info)),
|
||||||
|
(err, r) => {
|
||||||
|
if (err) return callback(err)
|
||||||
|
if (!r) return callback(new Error('No key found for decryption'))
|
||||||
|
|
||||||
|
async.waterfall([
|
||||||
|
(cb) => self.keystore.findKeyById(r.keyId, cb),
|
||||||
|
(key, cb) => self.keystore._getPrivateKey(key.name, cb)
|
||||||
|
], (err, pem) => {
|
||||||
|
if (err) return callback(err);
|
||||||
|
|
||||||
|
const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keystore._())
|
||||||
|
cms.decrypt(r.recipient, privateKey)
|
||||||
|
async.setImmediate(() => callback(null, Buffer.from(cms.content.getBytes(), 'binary')))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CMS
|
@ -1 +1,3 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
|
module.exports = require('./keychain')
|
||||||
|
362
src/keychain.js
Normal file
362
src/keychain.js
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const async = require('async')
|
||||||
|
const sanitize = require("sanitize-filename")
|
||||||
|
const forge = require('node-forge')
|
||||||
|
const deepmerge = require('deepmerge')
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const libp2pCrypto = require('libp2p-crypto')
|
||||||
|
const util = require('./util')
|
||||||
|
const CMS = require('./cms')
|
||||||
|
const DS = require('interface-datastore')
|
||||||
|
const pull = require('pull-stream')
|
||||||
|
|
||||||
|
const keyExtension = '.p8'
|
||||||
|
|
||||||
|
// NIST SP 800-132
|
||||||
|
const NIST = {
|
||||||
|
minKeyLength: 112 / 8,
|
||||||
|
minSaltLength: 128 / 8,
|
||||||
|
minIterationCount: 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
// See https://cryptosense.com/parametesr-choice-for-pbkdf2/
|
||||||
|
dek: {
|
||||||
|
keyLength: 512 / 8,
|
||||||
|
iterationCount: 10000,
|
||||||
|
salt: 'you should override this value with a crypto secure random number',
|
||||||
|
hash: 'sha512'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateKeyName (name) {
|
||||||
|
if (!name) return false
|
||||||
|
|
||||||
|
return name === sanitize(name.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an error to the caller, after a delay
|
||||||
|
*
|
||||||
|
* This assumes than an error indicates that the keychain is under attack. Delay returning an
|
||||||
|
* error to make brute force attacks harder.
|
||||||
|
*
|
||||||
|
* @param {function(Error)} callback - The caller
|
||||||
|
* @param {string | Error} err - The error
|
||||||
|
*/
|
||||||
|
function _error(callback, err) {
|
||||||
|
const min = 200
|
||||||
|
const max = 1000
|
||||||
|
const delay = Math.random() * (max - min) + min
|
||||||
|
if (typeof err === 'string') err = new Error(err)
|
||||||
|
setTimeout(callback, delay, err, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a key name into a datastore name.
|
||||||
|
*/
|
||||||
|
function DsName (name) {
|
||||||
|
return new DS.Key('/' + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a datastore name into a key name.
|
||||||
|
*/
|
||||||
|
function KsName(name) {
|
||||||
|
return name.toString().slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Keychain {
|
||||||
|
constructor (store, options) {
|
||||||
|
if (!store) {
|
||||||
|
throw new Error('store is required')
|
||||||
|
}
|
||||||
|
this.store = store
|
||||||
|
if (this.store.opts) {
|
||||||
|
this.store.opts.extension = keyExtension
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = deepmerge(defaultOptions, options)
|
||||||
|
|
||||||
|
// Enforce NIST SP 800-132
|
||||||
|
if (!opts.passPhrase || opts.passPhrase.length < 20) {
|
||||||
|
throw new Error('passPhrase must be least 20 characters')
|
||||||
|
}
|
||||||
|
if (opts.dek.keyLength < NIST.minKeyLength) {
|
||||||
|
throw new Error(`dek.keyLength must be least ${NIST.minKeyLength} bytes`)
|
||||||
|
}
|
||||||
|
if (opts.dek.salt.length < NIST.minSaltLength) {
|
||||||
|
throw new Error(`dek.saltLength must be least ${NIST.minSaltLength} bytes`)
|
||||||
|
}
|
||||||
|
if (opts.dek.iterationCount < NIST.minIterationCount) {
|
||||||
|
throw new Error(`dek.iterationCount must be least ${NIST.minIterationCount}`)
|
||||||
|
}
|
||||||
|
this.dek = opts.dek
|
||||||
|
|
||||||
|
// Create the derived encrypting key
|
||||||
|
let dek = forge.pkcs5.pbkdf2(
|
||||||
|
opts.passPhrase,
|
||||||
|
opts.dek.salt,
|
||||||
|
opts.dek.iterationCount,
|
||||||
|
opts.dek.keyLength,
|
||||||
|
opts.dek.hash)
|
||||||
|
dek = forge.util.bytesToHex(dek)
|
||||||
|
Object.defineProperty(this, '_', { value: () => dek })
|
||||||
|
|
||||||
|
// JS magick
|
||||||
|
this._getKeyInfo = this.findKeyByName = this._getKeyInfo.bind(this)
|
||||||
|
|
||||||
|
// Provide access to protected messages
|
||||||
|
this.cms = new CMS(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
static get options() {
|
||||||
|
return defaultOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
createKey (name, type, size, callback) {
|
||||||
|
const self = this
|
||||||
|
|
||||||
|
if (!validateKeyName(name) || name === 'self') {
|
||||||
|
return _error(callback, `Invalid key name '${name}'`)
|
||||||
|
}
|
||||||
|
const dsname = DsName(name)
|
||||||
|
self.store.has(dsname, (err, exists) => {
|
||||||
|
if (exists) return _error(callback, `Key '${name}' already exists'`)
|
||||||
|
|
||||||
|
switch (type.toLowerCase()) {
|
||||||
|
case 'rsa':
|
||||||
|
if (size < 2048) {
|
||||||
|
return _error(callback, `Invalid RSA key size ${size}`)
|
||||||
|
}
|
||||||
|
forge.pki.rsa.generateKeyPair({bits: size, workers: -1}, (err, keypair) => {
|
||||||
|
if (err) return _error(callback, err)
|
||||||
|
|
||||||
|
const pem = forge.pki.encryptRsaPrivateKey(keypair.privateKey, this._());
|
||||||
|
return self.store.put(dsname, pem, (err) => {
|
||||||
|
if (err) return _error(callback, err)
|
||||||
|
|
||||||
|
self._getKeyInfo(name, callback)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return _error(callback, `Invalid key type '${type}'`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
listKeys (callback) {
|
||||||
|
const self = this
|
||||||
|
const query = {
|
||||||
|
keysOnly: true
|
||||||
|
}
|
||||||
|
pull(
|
||||||
|
self.store.query(query),
|
||||||
|
pull.collect((err, res) => {
|
||||||
|
if (err) return _error(callback, err)
|
||||||
|
|
||||||
|
const names = res.map(r => KsName(r.key))
|
||||||
|
async.map(names, self._getKeyInfo, callback)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: not very efficent.
|
||||||
|
findKeyById (id, callback) {
|
||||||
|
this.listKeys((err, keys) => {
|
||||||
|
if (err) return _error(callback, err)
|
||||||
|
|
||||||
|
const key = keys.find((k) => k.id === id)
|
||||||
|
callback(null, key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
removeKey (name, callback) {
|
||||||
|
const self = this
|
||||||
|
if (!validateKeyName(name) || name === 'self') {
|
||||||
|
return _error(callback, `Invalid key name '${name}'`)
|
||||||
|
}
|
||||||
|
const dsname = DsName(name)
|
||||||
|
self.store.has(dsname, (err, exists) => {
|
||||||
|
if (!exists) return _error(callback, `Key '${name}' does not exist'`)
|
||||||
|
|
||||||
|
self.store.delete(dsname, callback)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renameKey(oldName, newName, callback) {
|
||||||
|
const self = this
|
||||||
|
if (!validateKeyName(oldName) || oldName === 'self') {
|
||||||
|
return _error(callback, `Invalid old key name '${oldName}'`)
|
||||||
|
}
|
||||||
|
if (!validateKeyName(newName) || newName === 'self') {
|
||||||
|
return _error(callback, `Invalid new key name '${newName}'`)
|
||||||
|
}
|
||||||
|
const oldDsname = DsName(oldName)
|
||||||
|
const newDsname = DsName(newName)
|
||||||
|
this.store.get(oldDsname, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
return _error(callback, `Key '${oldName}' does not exist. ${err.message}`)
|
||||||
|
}
|
||||||
|
const pem = res.toString()
|
||||||
|
self.store.has(newDsname, (err, exists) => {
|
||||||
|
if (exists) return _error(callback, `Key '${newName}' already exists'`)
|
||||||
|
|
||||||
|
const batch = self.store.batch()
|
||||||
|
batch.put(newDsname, pem)
|
||||||
|
batch.delete(oldDsname)
|
||||||
|
batch.commit((err) => {
|
||||||
|
if (err) return _error(callback, err)
|
||||||
|
self._getKeyInfo(newName, callback)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exportKey (name, password, callback) {
|
||||||
|
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()
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
algorithm: 'aes256',
|
||||||
|
count: this.dek.iterationCount,
|
||||||
|
saltSize: NIST.minSaltLength,
|
||||||
|
prfAlgorithm: 'sha512'
|
||||||
|
}
|
||||||
|
const privateKey = forge.pki.decryptRsaPrivateKey(pem, this._())
|
||||||
|
const res = forge.pki.encryptRsaPrivateKey(privateKey, password, options)
|
||||||
|
return callback(null, res)
|
||||||
|
} catch (e) {
|
||||||
|
_error(callback, e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
importKey(name, pem, password, callback) {
|
||||||
|
const self = this
|
||||||
|
if (!validateKeyName(name) || name === 'self') {
|
||||||
|
return _error(callback, `Invalid key name '${name}'`)
|
||||||
|
}
|
||||||
|
if (!pem) {
|
||||||
|
return _error(callback, 'PEM encoded key is required')
|
||||||
|
}
|
||||||
|
const dsname = DsName(name)
|
||||||
|
self.store.has(dsname, (err, exists) => {
|
||||||
|
if (exists) return _error(callback, `Key '${name}' already exists'`)
|
||||||
|
try {
|
||||||
|
const privateKey = forge.pki.decryptRsaPrivateKey(pem, password)
|
||||||
|
if (privateKey === null) {
|
||||||
|
return _error(callback, 'Cannot read the key, most likely the password is wrong')
|
||||||
|
}
|
||||||
|
const newpem = forge.pki.encryptRsaPrivateKey(privateKey, this._());
|
||||||
|
return self.store.put(dsname, newpem, (err) => {
|
||||||
|
if (err) return _error(callback, err)
|
||||||
|
|
||||||
|
this._getKeyInfo(name, callback)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
_error(callback, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
importPeer (name, peer, callback) {
|
||||||
|
const self = this
|
||||||
|
if (!validateKeyName(name)) {
|
||||||
|
return _error(callback, `Invalid key name '${name}'`)
|
||||||
|
}
|
||||||
|
if (!peer || !peer.privKey) {
|
||||||
|
return _error(callback, 'Peer.privKey \is required')
|
||||||
|
}
|
||||||
|
const dsname = DsName(name)
|
||||||
|
self.store.has(dsname, (err, exists) => {
|
||||||
|
if (exists) return _error(callback, `Key '${name}' already exists'`)
|
||||||
|
|
||||||
|
const privateKeyProtobuf = peer.marshalPrivKey()
|
||||||
|
libp2pCrypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => {
|
||||||
|
try {
|
||||||
|
const der = key.marshal()
|
||||||
|
const buf = forge.util.createBuffer(der.toString('binary'));
|
||||||
|
const obj = forge.asn1.fromDer(buf)
|
||||||
|
const privateKey = forge.pki.privateKeyFromAsn1(obj)
|
||||||
|
if (privateKey === null) {
|
||||||
|
return _error(callback, 'Cannot read the peer private key')
|
||||||
|
}
|
||||||
|
const pem = forge.pki.encryptRsaPrivateKey(privateKey, this._());
|
||||||
|
return self.store.put(dsname, pem, (err) => {
|
||||||
|
if (err) return _error(callback, err)
|
||||||
|
|
||||||
|
this._getKeyInfo(name, callback)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
_error(callback, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the private key as PEM encoded PKCS #8
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {function(Error, string)} callback
|
||||||
|
*/
|
||||||
|
_getPrivateKey (name, callback) {
|
||||||
|
const self = this
|
||||||
|
if (!validateKeyName(name)) {
|
||||||
|
return _error(callback, `Invalid key name '${name}'`)
|
||||||
|
}
|
||||||
|
this.store.get(DsName(name), (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
return _error(callback, `Key '${name}' does not exist. ${err.message}`)
|
||||||
|
}
|
||||||
|
callback(null, res.toString())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_getKeyInfo (name, callback) {
|
||||||
|
const self = this
|
||||||
|
if (!validateKeyName(name)) {
|
||||||
|
return _error(callback, `Invalid key name '${name}'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
try {
|
||||||
|
const privateKey = forge.pki.decryptRsaPrivateKey(pem, this._())
|
||||||
|
util.keyId(privateKey, (err, kid) => {
|
||||||
|
if (err) return _error(callback, err)
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
name: name,
|
||||||
|
id: kid
|
||||||
|
}
|
||||||
|
return callback(null, info)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
_error(callback, e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Keychain
|
86
src/util.js
Normal file
86
src/util.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const forge = require('node-forge')
|
||||||
|
const pki = forge.pki
|
||||||
|
const multihash = require('multihashes')
|
||||||
|
const rsaUtils = require('libp2p-crypto/src/keys/rsa-utils')
|
||||||
|
const rsaClass = require('libp2p-crypto/src/keys/rsa-class')
|
||||||
|
|
||||||
|
exports = module.exports
|
||||||
|
|
||||||
|
// Create an IPFS key id; the SHA-256 multihash of a public key.
|
||||||
|
// See https://github.com/richardschneider/ipfs-encryption/issues/16
|
||||||
|
exports.keyId = (privateKey, callback) => {
|
||||||
|
try {
|
||||||
|
const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e)
|
||||||
|
const spki = pki.publicKeyToSubjectPublicKeyInfo(publicKey)
|
||||||
|
const der = new Buffer(forge.asn1.toDer(spki).getBytes(), 'binary')
|
||||||
|
const jwk = rsaUtils.pkixToJwk(der)
|
||||||
|
const rsa = new rsaClass.RsaPublicKey(jwk)
|
||||||
|
rsa.hash((err, kid) => {
|
||||||
|
if (err) return callback(err)
|
||||||
|
|
||||||
|
const kids = multihash.toB58String(kid)
|
||||||
|
return callback(null, kids)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
callback(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.certificateForKey = (privateKey, callback) => {
|
||||||
|
exports.keyId(privateKey, (err, kid) => {
|
||||||
|
if (err) return callback(err)
|
||||||
|
|
||||||
|
const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e)
|
||||||
|
const cert = pki.createCertificate();
|
||||||
|
cert.publicKey = publicKey;
|
||||||
|
cert.serialNumber = '01';
|
||||||
|
cert.validity.notBefore = new Date();
|
||||||
|
cert.validity.notAfter = new Date();
|
||||||
|
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
|
||||||
|
var attrs = [{
|
||||||
|
name: 'organizationName',
|
||||||
|
value: 'ipfs'
|
||||||
|
}, {
|
||||||
|
shortName: 'OU',
|
||||||
|
value: 'keystore'
|
||||||
|
}, {
|
||||||
|
name: 'commonName',
|
||||||
|
value: kid
|
||||||
|
}];
|
||||||
|
cert.setSubject(attrs);
|
||||||
|
cert.setIssuer(attrs);
|
||||||
|
cert.setExtensions([{
|
||||||
|
name: 'basicConstraints',
|
||||||
|
cA: true
|
||||||
|
}, {
|
||||||
|
name: 'keyUsage',
|
||||||
|
keyCertSign: true,
|
||||||
|
digitalSignature: true,
|
||||||
|
nonRepudiation: true,
|
||||||
|
keyEncipherment: true,
|
||||||
|
dataEncipherment: true
|
||||||
|
}, {
|
||||||
|
name: 'extKeyUsage',
|
||||||
|
serverAuth: true,
|
||||||
|
clientAuth: true,
|
||||||
|
codeSigning: true,
|
||||||
|
emailProtection: true,
|
||||||
|
timeStamping: true
|
||||||
|
}, {
|
||||||
|
name: 'nsCertType',
|
||||||
|
client: true,
|
||||||
|
server: true,
|
||||||
|
email: true,
|
||||||
|
objsign: true,
|
||||||
|
sslCA: true,
|
||||||
|
emailCA: true,
|
||||||
|
objCA: true
|
||||||
|
}]);
|
||||||
|
// self-sign certificate
|
||||||
|
cert.sign(privateKey)
|
||||||
|
|
||||||
|
return callback(null, cert)
|
||||||
|
})
|
||||||
|
}
|
30
test/browser.js
Normal file
30
test/browser.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/* eslint-env mocha */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const async = require('async')
|
||||||
|
const LevelStore = require('datastore-level')
|
||||||
|
|
||||||
|
// use in the browser with level.js
|
||||||
|
const browserStore = new LevelStore('my/db/name', {db: require('level-js')})
|
||||||
|
|
||||||
|
describe('browser', () => {
|
||||||
|
const datastore1 = new LevelStore('test-keystore-1', {db: require('level-js')})
|
||||||
|
const datastore2 = new LevelStore('test-keystore-2', {db: require('level-js')})
|
||||||
|
|
||||||
|
before((done) => {
|
||||||
|
async.series([
|
||||||
|
(cb) => datastore1.open(cb),
|
||||||
|
(cb) => datastore2.open(cb)
|
||||||
|
], done)
|
||||||
|
})
|
||||||
|
|
||||||
|
after((done) => {
|
||||||
|
async.series([
|
||||||
|
(cb) => datastore1.close(cb),
|
||||||
|
(cb) => datastore2.close(cb)
|
||||||
|
], done)
|
||||||
|
})
|
||||||
|
|
||||||
|
require('./keychain.spec')(datastore1, datastore2)
|
||||||
|
require('./peerid')
|
||||||
|
})
|
@ -1,4 +0,0 @@
|
|||||||
/* eslint-env mocha */
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
it('so much testing', () => {})
|
|
356
test/keychain.spec.js
Normal file
356
test/keychain.spec.js
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
/* eslint-env mocha */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const chai = require('chai')
|
||||||
|
const dirtyChai = require('dirty-chai')
|
||||||
|
const expect = chai.expect
|
||||||
|
chai.use(dirtyChai)
|
||||||
|
chai.use(require('chai-string'))
|
||||||
|
const Keychain = require('..')
|
||||||
|
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
|
||||||
|
let emptyKeystore
|
||||||
|
let ks
|
||||||
|
|
||||||
|
before((done) => {
|
||||||
|
emptyKeystore = new Keychain(datastore1, { passPhrase: passPhrase })
|
||||||
|
ks = new Keychain(datastore2, { passPhrase: passPhrase })
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('needs a pass phrase to encrypt a key', () => {
|
||||||
|
expect(() => new Keychain(datastore2)).to.throw()
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('needs a NIST SP 800-132 non-weak pass phrase', () => {
|
||||||
|
expect(() => new Keychain(datastore2, { passPhrase: '< 20 character'})).to.throw()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('needs a store to persist a key', () => {
|
||||||
|
expect(() => new Keychain(null, { passPhrase: passPhrase})).to.throw()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has default options', () => {
|
||||||
|
expect(Keychain.options).to.exist()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('key name', () => {
|
||||||
|
it('is a valid filename and non-ASCII', () => {
|
||||||
|
ks.removeKey('../../nasty', (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
expect(err).to.have.property('message', 'Invalid key name \'../../nasty\'')
|
||||||
|
})
|
||||||
|
ks.removeKey('', (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
expect(err).to.have.property('message', 'Invalid key name \'\'')
|
||||||
|
})
|
||||||
|
ks.removeKey(' ', (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
expect(err).to.have.property('message', 'Invalid key name \' \'')
|
||||||
|
})
|
||||||
|
ks.removeKey(null, (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
expect(err).to.have.property('message', 'Invalid key name \'null\'')
|
||||||
|
})
|
||||||
|
ks.removeKey(undefined, (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
expect(err).to.have.property('message', 'Invalid key name \'undefined\'')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('key', () => {
|
||||||
|
it('can be an RSA key', function (done) {
|
||||||
|
this.timeout(20 * 1000)
|
||||||
|
ks.createKey(rsaKeyName, 'rsa', 2048, (err, info) => {
|
||||||
|
expect(err).to.not.exist()
|
||||||
|
expect(info).exist()
|
||||||
|
rsaKeyInfo = info
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a name and id', () => {
|
||||||
|
expect(rsaKeyInfo).to.have.property('name', rsaKeyName)
|
||||||
|
expect(rsaKeyInfo).to.have.property('id')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is encrypted PEM encoded PKCS #8', (done) => {
|
||||||
|
ks._getPrivateKey(rsaKeyName, (err, pem) => {
|
||||||
|
expect(err).to.not.exist()
|
||||||
|
expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not overwrite existing key', (done) => {
|
||||||
|
ks.createKey(rsaKeyName, 'rsa', 2048, (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cannot create the "self" key', (done) => {
|
||||||
|
ks.createKey('self', 'rsa', 2048, (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('implements NIST SP 800-131A', () => {
|
||||||
|
it('disallows RSA length < 2048', (done) => {
|
||||||
|
ks.createKey('bad-nist-rsa', 'rsa', 1024, (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
expect(err).to.have.property('message', 'Invalid RSA key size 1024')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('query', () => {
|
||||||
|
it('finds all existing keys', (done) => {
|
||||||
|
ks.listKeys((err, keys) => {
|
||||||
|
expect(err).to.not.exist()
|
||||||
|
expect(keys).to.exist()
|
||||||
|
const mykey = keys.find((k) => k.name === rsaKeyName)
|
||||||
|
expect(mykey).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('finds a key by name', (done) => {
|
||||||
|
ks.findKeyByName(rsaKeyName, (err, key) => {
|
||||||
|
expect(err).to.not.exist()
|
||||||
|
expect(key).to.exist()
|
||||||
|
expect(key).to.deep.equal(rsaKeyInfo)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('finds a key by id', (done) => {
|
||||||
|
ks.findKeyById(rsaKeyInfo.id, (err, key) => {
|
||||||
|
expect(err).to.not.exist()
|
||||||
|
expect(key).to.exist()
|
||||||
|
expect(key).to.deep.equal(rsaKeyInfo)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the key\'s name and id', (done) => {
|
||||||
|
ks.listKeys((err, keys) => {
|
||||||
|
expect(err).to.not.exist()
|
||||||
|
expect(keys).to.exist()
|
||||||
|
keys.forEach((key) => {
|
||||||
|
expect(key).to.have.property('name')
|
||||||
|
expect(key).to.have.property('id')
|
||||||
|
})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('CMS protected data', () => {
|
||||||
|
const plainData = Buffer.from('This is a message from Alice to Bob')
|
||||||
|
let cms
|
||||||
|
|
||||||
|
it('service is available', (done) => {
|
||||||
|
expect(ks).to.have.property('cms')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is anonymous', (done) => {
|
||||||
|
ks.cms.createAnonymousEncryptedData(rsaKeyName, plainData, (err, msg) => {
|
||||||
|
expect(err).to.not.exist()
|
||||||
|
expect(msg).to.exist()
|
||||||
|
expect(msg).to.be.instanceOf(Buffer)
|
||||||
|
cms = msg
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is a PKCS #7 message', (done) => {
|
||||||
|
ks.cms.readData("not CMS", (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is a PKCS #7 binary message', (done) => {
|
||||||
|
ks.cms.readData(plainData, (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cannot be read without the key', (done) => {
|
||||||
|
emptyKeystore.cms.readData(cms, (err, plain) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can be read with the key', (done) => {
|
||||||
|
ks.cms.readData(cms, (err, plain) => {
|
||||||
|
expect(err).to.not.exist()
|
||||||
|
expect(plain).to.exist()
|
||||||
|
expect(plain.toString()).to.equal(plainData.toString())
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('exported key', () => {
|
||||||
|
let pemKey
|
||||||
|
|
||||||
|
it('is a PKCS #8 encrypted pem', (done) => {
|
||||||
|
ks.exportKey(rsaKeyName, 'password', (err, pem) => {
|
||||||
|
expect(err).to.not.exist()
|
||||||
|
expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----')
|
||||||
|
pemKey = pem
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can be imported', (done) => {
|
||||||
|
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)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cannot be imported as an existing key name', (done) => {
|
||||||
|
ks.importKey(rsaKeyName, pemKey, 'password', (err, key) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cannot be imported with the wrong password', function (done) {
|
||||||
|
this.timeout(5 * 1000)
|
||||||
|
ks.importKey('a-new-name-for-import', pemKey, 'not the password', (err, key) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('peer id', () => {
|
||||||
|
const alicePrivKey = 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw=='
|
||||||
|
let alice
|
||||||
|
|
||||||
|
before(function (done) {
|
||||||
|
const encoded = Buffer.from(alicePrivKey, 'base64')
|
||||||
|
PeerId.createFromPrivKey(encoded, (err, id) => {
|
||||||
|
alice = id
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('private key can be imported', (done) => {
|
||||||
|
ks.importPeer('alice', alice, (err, key) => {
|
||||||
|
expect(err).to.not.exist()
|
||||||
|
expect(key.name).to.equal('alice')
|
||||||
|
expect(key.id).to.equal(alice.toB58String())
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('rename', () => {
|
||||||
|
it('requires an existing key name', (done) => {
|
||||||
|
ks.renameKey('not-there', renamedRsaKeyName, (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requires a valid new key name', (done) => {
|
||||||
|
ks.renameKey(rsaKeyName, '..\not-valid', (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not overwrite existing key', (done) => {
|
||||||
|
ks.renameKey(rsaKeyName, rsaKeyName, (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cannot create the "self" key', (done) => {
|
||||||
|
ks.renameKey(rsaKeyName, 'self', (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes the existing key name', (done) => {
|
||||||
|
ks.renameKey(rsaKeyName, renamedRsaKeyName, (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(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates the new key name', (done) => {
|
||||||
|
ks.findKeyByName(renamedRsaKeyName, (err, key) => {
|
||||||
|
expect(err).to.not.exist()
|
||||||
|
expect(key).to.exist()
|
||||||
|
expect(key).to.have.property('name', renamedRsaKeyName)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not change the key ID', (done) => {
|
||||||
|
ks.findKeyByName(renamedRsaKeyName, (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)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('key removal', () => {
|
||||||
|
it('cannot remove the "self" key', (done) => {
|
||||||
|
ks.removeKey('self', (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cannot remove an unknown key', (done) => {
|
||||||
|
ks.removeKey('not-there', (err) => {
|
||||||
|
expect(err).to.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can remove a known key', (done) => {
|
||||||
|
ks.removeKey(renamedRsaKeyName, (err) => {
|
||||||
|
expect(err).to.not.exist()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
34
test/node.js
Normal file
34
test/node.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/* eslint-env mocha */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const os = require('os')
|
||||||
|
const path = require('path')
|
||||||
|
const rimraf = require('rimraf')
|
||||||
|
const async = require('async')
|
||||||
|
const FsStore = require('datastore-fs')
|
||||||
|
|
||||||
|
describe('node', () => {
|
||||||
|
const store1 = path.join(os.tmpdir(), 'test-keystore-1')
|
||||||
|
const store2 = path.join(os.tmpdir(), 'test-keystore-2')
|
||||||
|
const datastore1 = new FsStore(store1)
|
||||||
|
const datastore2 = new FsStore(store2)
|
||||||
|
|
||||||
|
before((done) => {
|
||||||
|
async.series([
|
||||||
|
(cb) => datastore1.open(cb),
|
||||||
|
(cb) => datastore2.open(cb)
|
||||||
|
], done)
|
||||||
|
})
|
||||||
|
|
||||||
|
after((done) => {
|
||||||
|
async.series([
|
||||||
|
(cb) => datastore1.close(cb),
|
||||||
|
(cb) => datastore2.close(cb),
|
||||||
|
(cb) => rimraf(store1, cb),
|
||||||
|
(cb) => rimraf(store2, cb)
|
||||||
|
], done)
|
||||||
|
})
|
||||||
|
|
||||||
|
require('./keychain.spec')(datastore1, datastore2)
|
||||||
|
require('./peerid')
|
||||||
|
})
|
105
test/peerid.js
Normal file
105
test/peerid.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/* eslint-env mocha */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const chai = require('chai')
|
||||||
|
const dirtyChai = require('dirty-chai')
|
||||||
|
const expect = chai.expect
|
||||||
|
chai.use(dirtyChai)
|
||||||
|
const PeerId = require('peer-id')
|
||||||
|
const multihash = require('multihashes')
|
||||||
|
const crypto = require('libp2p-crypto')
|
||||||
|
const rsaUtils = require('libp2p-crypto/src/keys/rsa-utils')
|
||||||
|
const rsaClass = require('libp2p-crypto/src/keys/rsa-class')
|
||||||
|
|
||||||
|
const sample = {
|
||||||
|
id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9',
|
||||||
|
privKey: 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw==',
|
||||||
|
pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAE='
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('peer ID', () => {
|
||||||
|
let peer
|
||||||
|
let publicKeyDer // a buffer
|
||||||
|
|
||||||
|
before(function (done) {
|
||||||
|
const encoded = Buffer.from(sample.privKey, 'base64')
|
||||||
|
PeerId.createFromPrivKey(encoded, (err, id) => {
|
||||||
|
peer = id
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('decoded public key', (done) => {
|
||||||
|
// console.log('peer id', peer.toJSON())
|
||||||
|
// console.log('id', peer.toB58String())
|
||||||
|
// console.log('id decoded', multihash.decode(peer.id))
|
||||||
|
|
||||||
|
// get protobuf version of the public key
|
||||||
|
const publicKeyProtobuf = peer.marshalPubKey()
|
||||||
|
const publicKey = crypto.keys.unmarshalPublicKey(publicKeyProtobuf)
|
||||||
|
// console.log('public key', publicKey)
|
||||||
|
publicKeyDer = publicKey.marshal()
|
||||||
|
// console.log('public key der', publicKeyDer.toString('base64'))
|
||||||
|
|
||||||
|
// get protobuf version of the private key
|
||||||
|
const privateKeyProtobuf = peer.marshalPrivKey()
|
||||||
|
crypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => {
|
||||||
|
// console.log('private key', key)
|
||||||
|
// console.log('\nprivate key der', key.marshal().toString('base64'))
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('encoded public key with DER', (done) => {
|
||||||
|
const jwk = rsaUtils.pkixToJwk(publicKeyDer)
|
||||||
|
// console.log('jwk', jwk)
|
||||||
|
const rsa = new rsaClass.RsaPublicKey(jwk)
|
||||||
|
// console.log('rsa', rsa)
|
||||||
|
rsa.hash((err, keyId) => {
|
||||||
|
// console.log('err', err)
|
||||||
|
// console.log('keyId', keyId)
|
||||||
|
// console.log('id decoded', multihash.decode(keyId))
|
||||||
|
const kids = multihash.toB58String(keyId)
|
||||||
|
// console.log('id', kids)
|
||||||
|
expect(kids).to.equal(peer.toB58String())
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('encoded public key with JWT', (done) => {
|
||||||
|
const jwk = {
|
||||||
|
kty: 'RSA',
|
||||||
|
n: 'tkiqPxzBWXgZpdQBd14o868a30F3Sc43jwWQG3caikdTHOo7kR14o-h12D45QJNNQYRdUty5eC8ItHAB4YIH-Oe7DIOeVFsnhinlL9LnILwqQcJUeXENNtItDIM4z1ji1qta7b0mzXAItmRFZ-vkNhHB6N8FL1kbS3is_g2UmX8NjxAwvgxjyT5e3_IO85eemMpppsx_ZYmSza84P6onaJFL-btaXRq3KS7jzXkzg5NHKigfjlG7io_RkoWBAghI2smyQ5fdu-qGpS_YIQbUnhL9tJLoGrU72MufdMBZSZJL8pfpz8SB9BBGDCivV0VpbvV2J6En26IsHL_DN0pbIw',
|
||||||
|
e: 'AQAB',
|
||||||
|
alg: 'RS256',
|
||||||
|
kid: '2011-04-29'
|
||||||
|
}
|
||||||
|
// console.log('jwk', jwk)
|
||||||
|
const rsa = new rsaClass.RsaPublicKey(jwk)
|
||||||
|
// console.log('rsa', rsa)
|
||||||
|
rsa.hash((err, keyId) => {
|
||||||
|
// console.log('err', err)
|
||||||
|
// console.log('keyId', keyId)
|
||||||
|
// console.log('id decoded', multihash.decode(keyId))
|
||||||
|
const kids = multihash.toB58String(keyId)
|
||||||
|
// console.log('id', kids)
|
||||||
|
expect(kids).to.equal(peer.toB58String())
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('decoded private key', (done) => {
|
||||||
|
// console.log('peer id', peer.toJSON())
|
||||||
|
// console.log('id', peer.toB58String())
|
||||||
|
// console.log('id decoded', multihash.decode(peer.id))
|
||||||
|
|
||||||
|
// get protobuf version of the private key
|
||||||
|
const privateKeyProtobuf = peer.marshalPrivKey()
|
||||||
|
crypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => {
|
||||||
|
// console.log('private key', key)
|
||||||
|
//console.log('\nprivate key der', key.marshal().toString('base64'))
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
Reference in New Issue
Block a user