Skip to content

Commit 8bf5a70

Browse files
vasco-santosdaviddiasrichardschneidermkg20001victorb
committedMay 28, 2020
chore: integrate libp2p-keychain into js-libp2p (#633)
Integrates the libp2p-keychain codebase into this repo Co-authored-by: David Dias <daviddias.p@gmail.com> Co-authored-by: Richard Schneider <makaretu@gmail.com> Co-authored-by: Maciej Krüger <mkg20001@gmail.com> Co-authored-by: Victor Bjelkholm <victorbjelkholm@gmail.com> Co-authored-by: Masahiro Saito <camelmasa@gmail.com> Co-authored-by: Alan Shaw <alan.shaw@protocol.ai> Co-authored-by: Hugo Dias <mail@hugodias.me> Co-authored-by: Alberto Elias <hi@albertoelias.me> Co-authored-by: Alex Potsides <alex@achingbrain.net> Co-authored-by: Jacob Heun <jacobheun@gmail.com>
1 parent 6627278 commit 8bf5a70

13 files changed

+1393
-1
lines changed
 

‎package.json

+10-1
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,14 @@
6767
"multiaddr": "^7.4.3",
6868
"multistream-select": "^0.15.0",
6969
"mutable-proxy": "^1.0.0",
70+
"node-forge": "^0.9.1",
7071
"p-any": "^3.0.0",
7172
"p-fifo": "^1.0.0",
7273
"p-settle": "^4.0.1",
7374
"peer-id": "^0.13.11",
7475
"protons": "^1.0.1",
7576
"retimer": "^2.0.0",
77+
"sanitize-filename": "^1.6.3",
7678
"streaming-iterables": "^4.1.0",
7779
"timeout-abort-controller": "^1.0.0",
7880
"xsalsa20": "^1.0.2"
@@ -84,31 +86,38 @@
8486
"chai": "^4.2.0",
8587
"chai-as-promised": "^7.1.1",
8688
"chai-bytes": "^0.1.2",
89+
"chai-string": "^1.5.0",
8790
"cids": "^0.8.0",
91+
"datastore-fs": "^1.1.0",
92+
"datastore-level": "^1.1.0",
8893
"delay": "^4.3.0",
8994
"dirty-chai": "^2.0.1",
9095
"interop-libp2p": "libp2p/interop#chore/update-libp2p-daemon-with-peerstore",
9196
"ipfs-http-client": "^44.0.0",
9297
"it-concat": "^1.0.0",
9398
"it-pair": "^1.0.0",
9499
"it-pushable": "^1.4.0",
100+
"level": "^6.0.1",
95101
"libp2p-bootstrap": "^0.11.0",
96102
"libp2p-delegated-content-routing": "^0.5.0",
97103
"libp2p-delegated-peer-routing": "^0.5.0",
98104
"libp2p-floodsub": "^0.21.0",
99105
"libp2p-gossipsub": "^0.4.0",
100106
"libp2p-kad-dht": "^0.19.1",
101107
"libp2p-mdns": "^0.14.1",
102-
"libp2p-noise": "^1.1.0",
103108
"libp2p-mplex": "^0.9.5",
109+
"libp2p-noise": "^1.1.0",
104110
"libp2p-secio": "^0.12.4",
105111
"libp2p-tcp": "^0.14.1",
106112
"libp2p-webrtc-star": "^0.18.0",
107113
"libp2p-websockets": "^0.13.1",
114+
"multihashes": "^0.4.19",
108115
"nock": "^12.0.3",
109116
"p-defer": "^3.0.0",
110117
"p-times": "^3.0.0",
111118
"p-wait-for": "^3.1.0",
119+
"promisify-es6": "^1.0.3",
120+
"rimraf": "^3.0.2",
112121
"sinon": "^9.0.2"
113122
},
114123
"contributors": [

‎src/keychain/README.md

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# js-libp2p-keychain
2+
3+
[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai)
4+
[![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/)
5+
[![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p)
6+
[![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io)
7+
[![](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-keychain.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-keychain)
8+
[![](https://img.shields.io/travis/libp2p/js-libp2p-keychain.svg?style=flat-square)](https://travis-ci.com/libp2p/js-libp2p-keychain)
9+
[![Dependency Status](https://david-dm.org/libp2p/js-libp2p-keychain.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-keychain)
10+
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard)
11+
12+
> A secure key chain for libp2p in JavaScript
13+
14+
## Lead Maintainer
15+
16+
[Vasco Santos](https://github.com/vasco-santos).
17+
18+
## Features
19+
20+
- Manages the lifecycle of a key
21+
- Keys are encrypted at rest
22+
- Enforces the use of safe key names
23+
- Uses encrypted PKCS 8 for key storage
24+
- Uses PBKDF2 for a "stetched" key encryption key
25+
- Enforces NIST SP 800-131A and NIST SP 800-132
26+
- Uses PKCS 7: CMS (aka RFC 5652) to provide cryptographically protected messages
27+
- Delays reporting errors to slow down brute force attacks
28+
29+
## Table of Contents
30+
31+
## Install
32+
33+
```sh
34+
npm install --save libp2p-keychain
35+
```
36+
37+
### Usage
38+
39+
```js
40+
const Keychain = require('libp2p-keychain')
41+
const FsStore = require('datastore-fs')
42+
43+
const datastore = new FsStore('./a-keystore')
44+
const opts = {
45+
passPhrase: 'some long easily remembered phrase'
46+
}
47+
const keychain = new Keychain(datastore, opts)
48+
```
49+
50+
## API
51+
52+
Managing a key
53+
54+
- `async createKey (name, type, size)`
55+
- `async renameKey (oldName, newName)`
56+
- `async removeKey (name)`
57+
- `async exportKey (name, password)`
58+
- `async importKey (name, pem, password)`
59+
- `async importPeer (name, peer)`
60+
61+
A naming service for a key
62+
63+
- `async listKeys ()`
64+
- `async findKeyById (id)`
65+
- `async findKeyByName (name)`
66+
67+
Cryptographically protected messages
68+
69+
- `async cms.encrypt (name, plain)`
70+
- `async cms.decrypt (cmsData)`
71+
72+
### KeyInfo
73+
74+
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.
75+
76+
```js
77+
{
78+
name: 'rsa-key',
79+
id: 'QmYWYSUZ4PV6MRFYpdtEDJBiGs4UrmE6g8wmAWSePekXVW'
80+
}
81+
```
82+
83+
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).
84+
85+
### Private key storage
86+
87+
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**.
88+
89+
The default options for generating the derived encryption key are in the `dek` object. This, along with the passPhrase, is the input to a `PBKDF2` function.
90+
91+
```js
92+
const defaultOptions = {
93+
//See https://cryptosense.com/parameter-choice-for-pbkdf2/
94+
dek: {
95+
keyLength: 512 / 8,
96+
iterationCount: 1000,
97+
salt: 'at least 16 characters long',
98+
hash: 'sha2-512'
99+
}
100+
}
101+
```
102+
103+
![key storage](./doc/private-key.png?raw=true)
104+
105+
### Physical storage
106+
107+
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.
108+
109+
### Cryptographic Message Syntax (CMS)
110+
111+
CMS, aka [PKCS #7](https://en.wikipedia.org/wiki/PKCS) and [RFC 5652](https://tools.ietf.org/html/rfc5652), describes an encapsulation syntax for data protection. It is used to digitally sign, digest, authenticate, or encrypt arbitrary message content. Basically, `cms.encrypt` creates a DER message that can be only be read by someone holding the private key.
112+
113+
## Contribute
114+
115+
Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-keychain/issues)!
116+
117+
This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md).
118+
119+
[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md)
120+
121+
## License
122+
123+
[MIT](LICENSE)

‎src/keychain/cms.js

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
'use strict'
2+
3+
require('node-forge/lib/pkcs7')
4+
require('node-forge/lib/pbe')
5+
const forge = require('node-forge/lib/forge')
6+
const { certificateForKey, findAsync } = require('./util')
7+
const errcode = require('err-code')
8+
9+
/**
10+
* Cryptographic Message Syntax (aka PKCS #7)
11+
*
12+
* CMS describes an encapsulation syntax for data protection. It
13+
* is used to digitally sign, digest, authenticate, or encrypt
14+
* arbitrary message content.
15+
*
16+
* See RFC 5652 for all the details.
17+
*/
18+
class CMS {
19+
/**
20+
* Creates a new instance with a keychain
21+
*
22+
* @param {Keychain} keychain - the available keys
23+
*/
24+
constructor (keychain) {
25+
if (!keychain) {
26+
throw errcode(new Error('keychain is required'), 'ERR_KEYCHAIN_REQUIRED')
27+
}
28+
29+
this.keychain = keychain
30+
}
31+
32+
/**
33+
* Creates some protected data.
34+
*
35+
* The output Buffer contains the PKCS #7 message in DER.
36+
*
37+
* @param {string} name - The local key name.
38+
* @param {Buffer} plain - The data to encrypt.
39+
* @returns {undefined}
40+
*/
41+
async encrypt (name, plain) {
42+
if (!Buffer.isBuffer(plain)) {
43+
throw errcode(new Error('Plain data must be a Buffer'), 'ERR_INVALID_PARAMS')
44+
}
45+
46+
const key = await this.keychain.findKeyByName(name)
47+
const pem = await this.keychain._getPrivateKey(name)
48+
const privateKey = forge.pki.decryptRsaPrivateKey(pem, this.keychain._())
49+
const certificate = await certificateForKey(key, privateKey)
50+
51+
// create a p7 enveloped message
52+
const p7 = forge.pkcs7.createEnvelopedData()
53+
p7.addRecipient(certificate)
54+
p7.content = forge.util.createBuffer(plain)
55+
p7.encrypt()
56+
57+
// convert message to DER
58+
const der = forge.asn1.toDer(p7.toAsn1()).getBytes()
59+
return Buffer.from(der, 'binary')
60+
}
61+
62+
/**
63+
* Reads some protected data.
64+
*
65+
* The keychain must contain one of the keys used to encrypt the data. If none of the keys
66+
* exists, an Error is returned with the property 'missingKeys'. It is array of key ids.
67+
*
68+
* @param {Buffer} cmsData - The CMS encrypted data to decrypt.
69+
* @returns {undefined}
70+
*/
71+
async decrypt (cmsData) {
72+
if (!Buffer.isBuffer(cmsData)) {
73+
throw errcode(new Error('CMS data is required'), 'ERR_INVALID_PARAMS')
74+
}
75+
76+
let cms
77+
try {
78+
const buf = forge.util.createBuffer(cmsData.toString('binary'))
79+
const obj = forge.asn1.fromDer(buf)
80+
cms = forge.pkcs7.messageFromAsn1(obj)
81+
} catch (err) {
82+
throw errcode(new Error('Invalid CMS: ' + err.message), 'ERR_INVALID_CMS')
83+
}
84+
85+
// Find a recipient whose key we hold. We only deal with recipient certs
86+
// issued by ipfs (O=ipfs).
87+
const recipients = cms.recipients
88+
.filter(r => r.issuer.find(a => a.shortName === 'O' && a.value === 'ipfs'))
89+
.filter(r => r.issuer.find(a => a.shortName === 'CN'))
90+
.map(r => {
91+
return {
92+
recipient: r,
93+
keyId: r.issuer.find(a => a.shortName === 'CN').value
94+
}
95+
})
96+
97+
const r = await findAsync(recipients, async (recipient) => {
98+
try {
99+
const key = await this.keychain.findKeyById(recipient.keyId)
100+
if (key) return true
101+
} catch (err) {
102+
return false
103+
}
104+
return false
105+
})
106+
107+
if (!r) {
108+
const missingKeys = recipients.map(r => r.keyId)
109+
throw errcode(new Error('Decryption needs one of the key(s): ' + missingKeys.join(', ')), 'ERR_MISSING_KEYS', {
110+
missingKeys
111+
})
112+
}
113+
114+
const key = await this.keychain.findKeyById(r.keyId)
115+
const pem = await this.keychain._getPrivateKey(key.name)
116+
const privateKey = forge.pki.decryptRsaPrivateKey(pem, this.keychain._())
117+
cms.decrypt(r.recipient, privateKey)
118+
return Buffer.from(cms.content.getBytes(), 'binary')
119+
}
120+
}
121+
122+
module.exports = CMS

‎src/keychain/doc/private-key.png

24.9 KB
Loading

‎src/keychain/doc/private-key.xml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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>

‎src/keychain/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use strict'
2+
3+
module.exports = require('./keychain')

‎src/keychain/keychain.js

+469
Large diffs are not rendered by default.

‎src/keychain/util.js

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
'use strict'
2+
3+
require('node-forge/lib/x509')
4+
const forge = require('node-forge/lib/forge')
5+
const pki = forge.pki
6+
exports = module.exports
7+
8+
/**
9+
* Gets a self-signed X.509 certificate for the key.
10+
*
11+
* The output Buffer contains the PKCS #7 message in DER.
12+
*
13+
* TODO: move to libp2p-crypto package
14+
*
15+
* @param {KeyInfo} key - The id and name of the key
16+
* @param {RsaPrivateKey} privateKey - The naked key
17+
* @returns {undefined}
18+
*/
19+
exports.certificateForKey = (key, privateKey) => {
20+
const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e)
21+
const cert = pki.createCertificate()
22+
cert.publicKey = publicKey
23+
cert.serialNumber = '01'
24+
cert.validity.notBefore = new Date()
25+
cert.validity.notAfter = new Date()
26+
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10)
27+
const attrs = [{
28+
name: 'organizationName',
29+
value: 'ipfs'
30+
}, {
31+
shortName: 'OU',
32+
value: 'keystore'
33+
}, {
34+
name: 'commonName',
35+
value: key.id
36+
}]
37+
cert.setSubject(attrs)
38+
cert.setIssuer(attrs)
39+
cert.setExtensions([{
40+
name: 'basicConstraints',
41+
cA: true
42+
}, {
43+
name: 'keyUsage',
44+
keyCertSign: true,
45+
digitalSignature: true,
46+
nonRepudiation: true,
47+
keyEncipherment: true,
48+
dataEncipherment: true
49+
}, {
50+
name: 'extKeyUsage',
51+
serverAuth: true,
52+
clientAuth: true,
53+
codeSigning: true,
54+
emailProtection: true,
55+
timeStamping: true
56+
}, {
57+
name: 'nsCertType',
58+
client: true,
59+
server: true,
60+
email: true,
61+
objsign: true,
62+
sslCA: true,
63+
emailCA: true,
64+
objCA: true
65+
}])
66+
// self-sign certificate
67+
cert.sign(privateKey)
68+
69+
return cert
70+
}
71+
72+
/**
73+
* Finds the first item in a collection that is matched in the
74+
* `asyncCompare` function.
75+
*
76+
* `asyncCompare` is an async function that must
77+
* resolve to either `true` or `false`.
78+
*
79+
* @param {Array} array
80+
* @param {function(*)} asyncCompare An async function that returns a boolean
81+
*/
82+
async function findAsync (array, asyncCompare) {
83+
const promises = array.map(asyncCompare)
84+
const results = await Promise.all(promises)
85+
const index = results.findIndex(result => result)
86+
return array[index]
87+
}
88+
89+
module.exports.findAsync = findAsync

‎test/keychain/browser.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const LevelStore = require('datastore-level')
5+
6+
describe('browser', () => {
7+
const datastore1 = new LevelStore('test-keystore-1', { db: require('level') })
8+
const datastore2 = new LevelStore('test-keystore-2', { db: require('level') })
9+
10+
before(() => {
11+
return Promise.all([
12+
datastore1.open(),
13+
datastore2.open()
14+
])
15+
})
16+
17+
after(() => {
18+
return Promise.all([
19+
datastore1.close(),
20+
datastore2.close()
21+
])
22+
})
23+
24+
require('./keychain.spec')(datastore1, datastore2)
25+
require('./cms-interop')(datastore2)
26+
require('./peerid')
27+
})

‎test/keychain/cms-interop.js

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/* eslint max-nested-callbacks: ["error", 8] */
2+
/* eslint-env mocha */
3+
'use strict'
4+
5+
const chai = require('chai')
6+
const dirtyChai = require('dirty-chai')
7+
const expect = chai.expect
8+
chai.use(dirtyChai)
9+
chai.use(require('chai-string'))
10+
const Keychain = require('../../src/keychain')
11+
12+
module.exports = (datastore) => {
13+
describe('cms interop', () => {
14+
const passPhrase = 'this is not a secure phrase'
15+
const aliceKeyName = 'cms-interop-alice'
16+
let ks
17+
18+
before(() => {
19+
ks = new Keychain(datastore, { passPhrase: passPhrase })
20+
})
21+
22+
const plainData = Buffer.from('This is a message from Alice to Bob')
23+
24+
it('imports openssl key', async function () {
25+
this.timeout(10 * 1000)
26+
const aliceKid = 'QmNzBqPwp42HZJccsLtc4ok6LjZAspckgs2du5tTmjPfFA'
27+
const alice = `-----BEGIN ENCRYPTED PRIVATE KEY-----
28+
MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIMhYqiVoLJMICAggA
29+
MBQGCCqGSIb3DQMHBAhU7J9bcJPLDQSCAoDzi0dP6z97wJBs3jK2hDvZYdoScknG
30+
QMPOnpG1LO3IZ7nFha1dta5liWX+xRFV04nmVYkkNTJAPS0xjJOG9B5Hm7wm8uTd
31+
1rOaYKOW5S9+1sD03N+fAx9DDFtB7OyvSdw9ty6BtHAqlFk3+/APASJS12ak2pg7
32+
/Ei6hChSYYRS9WWGw4lmSitOBxTmrPY1HmODXkR3txR17LjikrMTd6wyky9l/u7A
33+
CgkMnj1kn49McOBJ4gO14c9524lw9OkPatyZK39evFhx8AET73LrzCnsf74HW9Ri
34+
dKq0FiKLVm2wAXBZqdd5ll/TPj3wmFqhhLSj/txCAGg+079gq2XPYxxYC61JNekA
35+
ATKev5zh8x1Mf1maarKN72sD28kS/J+aVFoARIOTxbG3g+1UbYs/00iFcuIaM4IY
36+
zB1kQUFe13iWBsJ9nfvN7TJNSVnh8NqHNbSg0SdzKlpZHHSWwOUrsKmxmw/XRVy/
37+
ufvN0hZQ3BuK5MZLixMWAyKc9zbZSOB7E7VNaK5Fmm85FRz0L1qRjHvoGcEIhrOt
38+
0sjbsRvjs33J8fia0FF9nVfOXvt/67IGBKxIMF9eE91pY5wJNwmXcBk8jghTZs83
39+
GNmMB+cGH1XFX4cT4kUGzvqTF2zt7IP+P2cQTS1+imKm7r8GJ7ClEZ9COWWdZIcH
40+
igg5jozKCW82JsuWSiW9tu0F/6DuvYiZwHS3OLiJP0CuLfbOaRw8Jia1RTvXEH7m
41+
3N0/kZ8hJIK4M/t/UAlALjeNtFxYrFgsPgLxxcq7al1ruG7zBq8L/G3RnkSjtHqE
42+
cn4oisOvxCprs4aM9UVjtZTCjfyNpX8UWwT1W3rySV+KQNhxuMy3RzmL
43+
-----END ENCRYPTED PRIVATE KEY-----
44+
`
45+
const key = await ks.importKey(aliceKeyName, alice, 'mypassword')
46+
expect(key.name).to.equal(aliceKeyName)
47+
expect(key.id).to.equal(aliceKid)
48+
})
49+
50+
it('decrypts node-forge example', async () => {
51+
const example = `
52+
MIIBcwYJKoZIhvcNAQcDoIIBZDCCAWACAQAxgfowgfcCAQAwYDBbMQ0wCwYDVQQK
53+
EwRpcGZzMREwDwYDVQQLEwhrZXlzdG9yZTE3MDUGA1UEAxMuUW1OekJxUHdwNDJI
54+
WkpjY3NMdGM0b2s2TGpaQXNwY2tnczJkdTV0VG1qUGZGQQIBATANBgkqhkiG9w0B
55+
AQEFAASBgLKXCZQYmMLuQ8m0Ex/rr3KNK+Q2+QG1zIbIQ9MFPUNQ7AOgGOHyL40k
56+
d1gr188EHuiwd90PafZoQF9VRSX9YtwGNqAE8+LD8VaITxCFbLGRTjAqeOUHR8cO
57+
knU1yykWGkdlbclCuu0NaAfmb8o0OX50CbEKZB7xmsv8tnqn0H0jMF4GCSqGSIb3
58+
DQEHATAdBglghkgBZQMEASoEEP/PW1JWehQx6/dsLkp/Mf+gMgQwFM9liLTqC56B
59+
nHILFmhac/+a/StQOKuf9dx5qXeGvt9LnwKuGGSfNX4g+dTkoa6N
60+
`
61+
const plain = await ks.cms.decrypt(Buffer.from(example, 'base64'))
62+
expect(plain).to.exist()
63+
expect(plain.toString()).to.equal(plainData.toString())
64+
})
65+
})
66+
}

‎test/keychain/keychain.spec.js

+383
Large diffs are not rendered by default.

‎test/keychain/node.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const os = require('os')
5+
const path = require('path')
6+
const promisify = require('promisify-es6')
7+
const rimraf = promisify(require('rimraf'))
8+
const FsStore = require('datastore-fs')
9+
10+
describe('node', () => {
11+
const store1 = path.join(os.tmpdir(), 'test-keystore-1-' + Date.now())
12+
const store2 = path.join(os.tmpdir(), 'test-keystore-2-' + Date.now())
13+
const datastore1 = new FsStore(store1)
14+
const datastore2 = new FsStore(store2)
15+
16+
before(async () => {
17+
await datastore1.open()
18+
await datastore2.open()
19+
})
20+
21+
after(async () => {
22+
await datastore1.close()
23+
await datastore2.close()
24+
await rimraf(store1)
25+
await rimraf(store2)
26+
})
27+
28+
require('./keychain.spec')(datastore1, datastore2)
29+
require('./cms-interop')(datastore2)
30+
require('./peerid')
31+
})

‎test/keychain/peerid.js

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const chai = require('chai')
5+
const dirtyChai = require('dirty-chai')
6+
const expect = chai.expect
7+
chai.use(dirtyChai)
8+
const PeerId = require('peer-id')
9+
const multihash = require('multihashes')
10+
const crypto = require('libp2p-crypto')
11+
const rsaUtils = require('libp2p-crypto/src/keys/rsa-utils')
12+
const rsaClass = require('libp2p-crypto/src/keys/rsa-class')
13+
14+
const sample = {
15+
id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9',
16+
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==',
17+
pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAE='
18+
}
19+
20+
describe('peer ID', () => {
21+
let peer
22+
let publicKeyDer // a buffer
23+
24+
before(async () => {
25+
const encoded = Buffer.from(sample.privKey, 'base64')
26+
peer = await PeerId.createFromPrivKey(encoded)
27+
})
28+
29+
it('decoded public key', async () => {
30+
// get protobuf version of the public key
31+
const publicKeyProtobuf = peer.marshalPubKey()
32+
const publicKey = crypto.keys.unmarshalPublicKey(publicKeyProtobuf)
33+
publicKeyDer = publicKey.marshal()
34+
35+
// get protobuf version of the private key
36+
const privateKeyProtobuf = peer.marshalPrivKey()
37+
const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf)
38+
expect(key).to.exist()
39+
})
40+
41+
it('encoded public key with DER', async () => {
42+
const jwk = rsaUtils.pkixToJwk(publicKeyDer)
43+
const rsa = new rsaClass.RsaPublicKey(jwk)
44+
const keyId = await rsa.hash()
45+
const kids = multihash.toB58String(keyId)
46+
expect(kids).to.equal(peer.toB58String())
47+
})
48+
49+
it('encoded public key with JWT', async () => {
50+
const jwk = {
51+
kty: 'RSA',
52+
n: 'tkiqPxzBWXgZpdQBd14o868a30F3Sc43jwWQG3caikdTHOo7kR14o-h12D45QJNNQYRdUty5eC8ItHAB4YIH-Oe7DIOeVFsnhinlL9LnILwqQcJUeXENNtItDIM4z1ji1qta7b0mzXAItmRFZ-vkNhHB6N8FL1kbS3is_g2UmX8NjxAwvgxjyT5e3_IO85eemMpppsx_ZYmSza84P6onaJFL-btaXRq3KS7jzXkzg5NHKigfjlG7io_RkoWBAghI2smyQ5fdu-qGpS_YIQbUnhL9tJLoGrU72MufdMBZSZJL8pfpz8SB9BBGDCivV0VpbvV2J6En26IsHL_DN0pbIw',
53+
e: 'AQAB',
54+
alg: 'RS256',
55+
kid: '2011-04-29'
56+
}
57+
const rsa = new rsaClass.RsaPublicKey(jwk)
58+
const keyId = await rsa.hash()
59+
const kids = multihash.toB58String(keyId)
60+
expect(kids).to.equal(peer.toB58String())
61+
})
62+
63+
it('decoded private key', async () => {
64+
// get protobuf version of the private key
65+
const privateKeyProtobuf = peer.marshalPrivKey()
66+
const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf)
67+
expect(key).to.exist()
68+
})
69+
})

0 commit comments

Comments
 (0)
Please sign in to comment.