├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── doc
├── private-key.png
└── private-key.xml
├── package.json
├── src
├── cms.js
├── index.js
├── keychain.js
├── keychain.md
└── util.js
└── test
├── browser.js
├── keychain.spec.js
├── node.js
└── peerid.js
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.png binary
2 | * crlf=input
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules/
2 | **/*.log
3 | test/repo-tests*
4 | **/bundle.js
5 |
6 | # Logs
7 | logs
8 | *.log
9 |
10 | coverage
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # node-waf configuration
27 | .lock-wscript
28 |
29 | build
30 |
31 | # Dependency directory
32 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
33 | node_modules
34 |
35 | lib
36 | dist
37 |
38 | # while testing npm5
39 | package-lock.json
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Richard Schneider
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ipfs-encryption
2 | Encrypting IPFS data. Start with the [**big picture**](https://github.com/richardschneider/ipfs-encryption/issues/3) and then the [smaller picture](https://github.com/richardschneider/ipfs-encryption/issues/8).
3 |
4 | I created this repo because encrypted data is an **EPIC** issue and affects many other projects. Hopefuly other IPFS members can contribute to the discussion.
5 |
6 | Everything is maintained in the [issues](https://github.com/richardschneider/ipfs-encryption/issues). Feel free to add your comments or raise new issues.
7 |
8 | Keystore implementations
9 | - JS is [here](https://github.com/richardschneider/ipfs-encryption/blob/master/src/keychain.md) and as always a WIP
10 | - GO id [there](https://github.com/ipfs/go-ipfs/tree/e8477b50c907c1544f8a7e15334d6fb595baac19/keystore), different names but same concepts
11 | - CS coming real soon
12 |
--------------------------------------------------------------------------------
/doc/private-key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/richardschneider/ipfs-encryption/f6eb3a94629ca4d827e2b937887217e4276ab45a/doc/private-key.png
--------------------------------------------------------------------------------
/doc/private-key.xml:
--------------------------------------------------------------------------------
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==
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ipfs-encryption",
3 | "version": "0.0.1",
4 | "description": "Encrypting IPFS data",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "test": "mocha --timeout 25000"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/richardschneider/ipfs-encryption.git"
12 | },
13 | "keywords": [
14 | "ipfs",
15 | "encryption",
16 | "secure"
17 | ],
18 | "author": "Richard Schneider ",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/richardschneider/ipfs-encryption/issues"
22 | },
23 | "homepage": "https://github.com/richardschneider/ipfs-encryption#readme",
24 | "dependencies": {
25 | "async": "^2.6.0",
26 | "deepmerge": "^1.5.2",
27 | "interface-datastore": "^0.4.1",
28 | "libp2p-crypto": "^0.10.3",
29 | "multihashes": "^0.4.12",
30 | "node-forge": "^0.7.1",
31 | "pull-stream": "^3.6.1",
32 | "sanitize-filename": "^1.6.1"
33 | },
34 | "devDependencies": {
35 | "chai": "^4.1.2",
36 | "chai-string": "^1.4.0",
37 | "datastore-fs": "^0.4.1",
38 | "datastore-level": "^0.7.0",
39 | "dirty-chai": "^2.0.1",
40 | "level-js": "^2.2.4",
41 | "mocha": "^4.0.1",
42 | "peer-id": "^0.10.2",
43 | "rimraf": "^2.6.2"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/cms.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const async = require('async')
4 | const forge = require('node-forge')
5 | const util = require('./util')
6 |
7 | class CMS {
8 | constructor (keystore) {
9 | if (!keystore) {
10 | throw new Error('keystore is required')
11 | }
12 |
13 | this.keystore = keystore;
14 | }
15 |
16 | createAnonymousEncryptedData (name, plain, callback) {
17 | const self = this
18 | if (!Buffer.isBuffer(plain)) {
19 | return callback(new Error('Data is required'))
20 | }
21 |
22 | self.keystore._getPrivateKey(name, (err, key) => {
23 | if (err) {
24 | return callback(err)
25 | }
26 |
27 | try {
28 | const privateKey = forge.pki.decryptRsaPrivateKey(key, self.keystore._())
29 | util.certificateForKey(privateKey, (err, certificate) => {
30 | if (err) return callback(err)
31 |
32 | // create a p7 enveloped message
33 | const p7 = forge.pkcs7.createEnvelopedData()
34 | p7.addRecipient(certificate)
35 | p7.content = forge.util.createBuffer(plain)
36 | p7.encrypt()
37 |
38 | // convert message to DER
39 | const der = forge.asn1.toDer(p7.toAsn1()).getBytes()
40 | callback(null, Buffer.from(der, 'binary'))
41 | })
42 | } catch (err) {
43 | callback(err)
44 | }
45 | })
46 | }
47 |
48 | readData (cmsData, callback) {
49 | if (!Buffer.isBuffer(cmsData)) {
50 | return callback(new Error('CMS data is required'))
51 | }
52 |
53 | const self = this
54 | let cms
55 | try {
56 | const buf = forge.util.createBuffer(cmsData.toString('binary'));
57 | const obj = forge.asn1.fromDer(buf)
58 | cms = forge.pkcs7.messageFromAsn1(obj)
59 | } catch (err) {
60 | return callback(new Error('Invalid CMS: ' + err.message))
61 | }
62 |
63 | // Find a recipient whose key we hold. We only deal with recipient certs
64 | // issued by ipfs (O=ipfs).
65 | const recipients = cms.recipients
66 | .filter(r => r.issuer.find(a => a.shortName === 'O' && a.value === 'ipfs'))
67 | .filter(r => r.issuer.find(a => a.shortName === 'CN'))
68 | .map(r => {
69 | return {
70 | recipient: r,
71 | keyId: r.issuer.find(a => a.shortName === 'CN').value
72 | }
73 | })
74 | async.detect(
75 | recipients,
76 | (r, cb) => self.keystore.findKeyById(r.keyId, (err, info) => cb(null, !err && info)),
77 | (err, r) => {
78 | if (err) return callback(err)
79 | if (!r) return callback(new Error('No key found for decryption'))
80 |
81 | async.waterfall([
82 | (cb) => self.keystore.findKeyById(r.keyId, cb),
83 | (key, cb) => self.keystore._getPrivateKey(key.name, cb)
84 | ], (err, pem) => {
85 | if (err) return callback(err);
86 |
87 | const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keystore._())
88 | cms.decrypt(r.recipient, privateKey)
89 | async.setImmediate(() => callback(null, Buffer.from(cms.content.getBytes(), 'binary')))
90 | })
91 | }
92 | )
93 | }
94 |
95 | }
96 |
97 | module.exports = CMS
98 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | exports.Keychain = require('./keychain')
4 |
--------------------------------------------------------------------------------
/src/keychain.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const async = require('async')
4 | const sanitize = require("sanitize-filename")
5 | const forge = require('node-forge')
6 | const deepmerge = require('deepmerge')
7 | const crypto = require('crypto')
8 | const libp2pCrypto = require('libp2p-crypto')
9 | const util = require('./util')
10 | const CMS = require('./cms')
11 | const DS = require('interface-datastore')
12 | const pull = require('pull-stream')
13 |
14 | const keyExtension = '.p8'
15 |
16 | // NIST SP 800-132
17 | const NIST = {
18 | minKeyLength: 112 / 8,
19 | minSaltLength: 128 / 8,
20 | minIterationCount: 1000
21 | }
22 |
23 | const defaultOptions = {
24 | // See https://cryptosense.com/parametesr-choice-for-pbkdf2/
25 | dek: {
26 | keyLength: 512 / 8,
27 | iterationCount: 10000,
28 | salt: 'you should override this value with a crypto secure random number',
29 | hash: 'sha512'
30 | }
31 | }
32 |
33 | function validateKeyName (name) {
34 | if (!name) return false
35 |
36 | return name === sanitize(name.trim())
37 | }
38 |
39 | /**
40 | * Returns an error to the caller, after a delay
41 | *
42 | * This assumes than an error indicates that the keychain is under attack. Delay returning an
43 | * error to make brute force attacks harder.
44 | *
45 | * @param {function(Error)} callback - The caller
46 | * @param {string | Error} err - The error
47 | */
48 | function _error(callback, err) {
49 | const min = 200
50 | const max = 1000
51 | const delay = Math.random() * (max - min) + min
52 | if (typeof err === 'string') err = new Error(err)
53 | setTimeout(callback, delay, err, null)
54 | }
55 |
56 | /**
57 | * Converts a key name into a datastore name.
58 | */
59 | function DsName (name) {
60 | return new DS.Key('/' + name)
61 | }
62 |
63 | /**
64 | * Converts a datastore name into a key name.
65 | */
66 | function KsName(name) {
67 | return name.toString().slice(1)
68 | }
69 |
70 | class Keychain {
71 | constructor (store, options) {
72 | if (!store) {
73 | throw new Error('store is required')
74 | }
75 | this.store = store
76 | if (this.store.opts) {
77 | this.store.opts.extension = keyExtension
78 | }
79 |
80 | const opts = deepmerge(defaultOptions, options)
81 |
82 | // Enforce NIST SP 800-132
83 | if (!opts.passPhrase || opts.passPhrase.length < 20) {
84 | throw new Error('passPhrase must be least 20 characters')
85 | }
86 | if (opts.dek.keyLength < NIST.minKeyLength) {
87 | throw new Error(`dek.keyLength must be least ${NIST.minKeyLength} bytes`)
88 | }
89 | if (opts.dek.salt.length < NIST.minSaltLength) {
90 | throw new Error(`dek.saltLength must be least ${NIST.minSaltLength} bytes`)
91 | }
92 | if (opts.dek.iterationCount < NIST.minIterationCount) {
93 | throw new Error(`dek.iterationCount must be least ${NIST.minIterationCount}`)
94 | }
95 | this.dek = opts.dek
96 |
97 | // Create the derived encrypting key
98 | let dek = forge.pkcs5.pbkdf2(
99 | opts.passPhrase,
100 | opts.dek.salt,
101 | opts.dek.iterationCount,
102 | opts.dek.keyLength,
103 | opts.dek.hash)
104 | dek = forge.util.bytesToHex(dek)
105 | Object.defineProperty(this, '_', { value: () => dek })
106 |
107 | // JS magick
108 | this._getKeyInfo = this.findKeyByName = this._getKeyInfo.bind(this)
109 |
110 | // Provide access to protected messages
111 | this.cms = new CMS(this)
112 | }
113 |
114 | static get options() {
115 | return defaultOptions
116 | }
117 |
118 | createKey (name, type, size, callback) {
119 | const self = this
120 |
121 | if (!validateKeyName(name) || name === 'self') {
122 | return _error(callback, `Invalid key name '${name}'`)
123 | }
124 | const dsname = DsName(name)
125 | self.store.has(dsname, (err, exists) => {
126 | if (exists) return _error(callback, `Key '${name}' already exists'`)
127 |
128 | switch (type.toLowerCase()) {
129 | case 'rsa':
130 | if (size < 2048) {
131 | return _error(callback, `Invalid RSA key size ${size}`)
132 | }
133 | forge.pki.rsa.generateKeyPair({bits: size, workers: -1}, (err, keypair) => {
134 | if (err) return _error(callback, err)
135 |
136 | const pem = forge.pki.encryptRsaPrivateKey(keypair.privateKey, this._());
137 | return self.store.put(dsname, pem, (err) => {
138 | if (err) return _error(callback, err)
139 |
140 | self._getKeyInfo(name, callback)
141 | })
142 | })
143 | break;
144 |
145 | default:
146 | return _error(callback, `Invalid key type '${type}'`)
147 | }
148 | })
149 | }
150 |
151 | listKeys (callback) {
152 | const self = this
153 | const query = {
154 | keysOnly: true
155 | }
156 | pull(
157 | self.store.query(query),
158 | pull.collect((err, res) => {
159 | if (err) return _error(callback, err)
160 |
161 | const names = res.map(r => KsName(r.key))
162 | async.map(names, self._getKeyInfo, callback)
163 | })
164 | )
165 | }
166 |
167 | // TODO: not very efficent.
168 | findKeyById (id, callback) {
169 | this.listKeys((err, keys) => {
170 | if (err) return _error(callback, err)
171 |
172 | const key = keys.find((k) => k.id === id)
173 | callback(null, key)
174 | })
175 | }
176 |
177 | removeKey (name, callback) {
178 | const self = this
179 | if (!validateKeyName(name) || name === 'self') {
180 | return _error(callback, `Invalid key name '${name}'`)
181 | }
182 | const dsname = DsName(name)
183 | self.store.has(dsname, (err, exists) => {
184 | if (!exists) return _error(callback, `Key '${name}' does not exist'`)
185 |
186 | self.store.delete(dsname, callback)
187 | })
188 | }
189 |
190 | renameKey(oldName, newName, callback) {
191 | const self = this
192 | if (!validateKeyName(oldName) || oldName === 'self') {
193 | return _error(callback, `Invalid old key name '${oldName}'`)
194 | }
195 | if (!validateKeyName(newName) || newName === 'self') {
196 | return _error(callback, `Invalid new key name '${newName}'`)
197 | }
198 | const oldDsname = DsName(oldName)
199 | const newDsname = DsName(newName)
200 | this.store.get(oldDsname, (err, res) => {
201 | if (err) {
202 | return _error(callback, `Key '${oldName}' does not exist. ${err.message}`)
203 | }
204 | const pem = res.toString()
205 | self.store.has(newDsname, (err, exists) => {
206 | if (exists) return _error(callback, `Key '${newName}' already exists'`)
207 |
208 | const batch = self.store.batch()
209 | batch.put(newDsname, pem)
210 | batch.delete(oldDsname)
211 | batch.commit((err) => {
212 | if (err) return _error(callback, err)
213 | self._getKeyInfo(newName, callback)
214 | })
215 | })
216 | })
217 | }
218 |
219 | exportKey (name, password, callback) {
220 | if (!validateKeyName(name)) {
221 | return _error(callback, `Invalid key name '${name}'`)
222 | }
223 | if (!password) {
224 | return _error(callback, 'Password is required')
225 | }
226 |
227 | const dsname = DsName(name)
228 | this.store.get(dsname, (err, res) => {
229 | if (err) {
230 | return _error(callback, `Key '${name}' does not exist. ${err.message}`)
231 | }
232 | const pem = res.toString()
233 | try {
234 | const options = {
235 | algorithm: 'aes256',
236 | count: this.dek.iterationCount,
237 | saltSize: NIST.minSaltLength,
238 | prfAlgorithm: 'sha512'
239 | }
240 | const privateKey = forge.pki.decryptRsaPrivateKey(pem, this._())
241 | const res = forge.pki.encryptRsaPrivateKey(privateKey, password, options)
242 | return callback(null, res)
243 | } catch (e) {
244 | _error(callback, e)
245 | }
246 | })
247 | }
248 |
249 | importKey(name, pem, password, callback) {
250 | const self = this
251 | if (!validateKeyName(name) || name === 'self') {
252 | return _error(callback, `Invalid key name '${name}'`)
253 | }
254 | if (!pem) {
255 | return _error(callback, 'PEM encoded key is required')
256 | }
257 | const dsname = DsName(name)
258 | self.store.has(dsname, (err, exists) => {
259 | if (exists) return _error(callback, `Key '${name}' already exists'`)
260 | try {
261 | const privateKey = forge.pki.decryptRsaPrivateKey(pem, password)
262 | if (privateKey === null) {
263 | return _error(callback, 'Cannot read the key, most likely the password is wrong')
264 | }
265 | const newpem = forge.pki.encryptRsaPrivateKey(privateKey, this._());
266 | return self.store.put(dsname, newpem, (err) => {
267 | if (err) return _error(callback, err)
268 |
269 | this._getKeyInfo(name, callback)
270 | })
271 | } catch (err) {
272 | _error(callback, err)
273 | }
274 | })
275 | }
276 |
277 | importPeer (name, peer, callback) {
278 | const self = this
279 | if (!validateKeyName(name)) {
280 | return _error(callback, `Invalid key name '${name}'`)
281 | }
282 | if (!peer || !peer.privKey) {
283 | return _error(callback, 'Peer.privKey \is required')
284 | }
285 | const dsname = DsName(name)
286 | self.store.has(dsname, (err, exists) => {
287 | if (exists) return _error(callback, `Key '${name}' already exists'`)
288 |
289 | const privateKeyProtobuf = peer.marshalPrivKey()
290 | libp2pCrypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => {
291 | try {
292 | const der = key.marshal()
293 | const buf = forge.util.createBuffer(der.toString('binary'));
294 | const obj = forge.asn1.fromDer(buf)
295 | const privateKey = forge.pki.privateKeyFromAsn1(obj)
296 | if (privateKey === null) {
297 | return _error(callback, 'Cannot read the peer private key')
298 | }
299 | const pem = forge.pki.encryptRsaPrivateKey(privateKey, this._());
300 | return self.store.put(dsname, pem, (err) => {
301 | if (err) return _error(callback, err)
302 |
303 | this._getKeyInfo(name, callback)
304 | })
305 | } catch (err) {
306 | _error(callback, err)
307 | }
308 | })
309 | })
310 | }
311 |
312 | /**
313 | * Gets the private key as PEM encoded PKCS #8
314 | *
315 | * @param {string} name
316 | * @param {function(Error, string)} callback
317 | */
318 | _getPrivateKey (name, callback) {
319 | const self = this
320 | if (!validateKeyName(name)) {
321 | return _error(callback, `Invalid key name '${name}'`)
322 | }
323 | this.store.get(DsName(name), (err, res) => {
324 | if (err) {
325 | return _error(callback, `Key '${name}' does not exist. ${err.message}`)
326 | }
327 | callback(null, res.toString())
328 | })
329 | }
330 |
331 | _getKeyInfo (name, callback) {
332 | const self = this
333 | if (!validateKeyName(name)) {
334 | return _error(callback, `Invalid key name '${name}'`)
335 | }
336 |
337 | const dsname = DsName(name)
338 | this.store.get(dsname, (err, res) => {
339 | if (err) {
340 | return _error(callback, `Key '${name}' does not exist. ${err.message}`)
341 | }
342 | const pem = res.toString()
343 | try {
344 | const privateKey = forge.pki.decryptRsaPrivateKey(pem, this._())
345 | util.keyId(privateKey, (err, kid) => {
346 | if (err) return _error(callback, err)
347 |
348 | const info = {
349 | name: name,
350 | id: kid
351 | }
352 | return callback(null, info)
353 | })
354 | } catch (e) {
355 | _error(callback, e)
356 | }
357 | })
358 | }
359 |
360 | _encrypt (name, plain, callback) {
361 | if (!validateKeyName(name)) {
362 | return _error(callback, `Invalid key name '${name}'`)
363 | }
364 |
365 | if (!Buffer.isBuffer(plain)) {
366 | return _error(callback, 'Data is required')
367 | }
368 |
369 | const dsname = DsName(name)
370 | this.store.get(dsname, (err, res) => {
371 | if (err) {
372 | return _error(callback, `Key '${name}' does not exist. ${err.message}`)
373 | }
374 | const pem = res.toString()
375 | try {
376 | const privateKey = {
377 | key: pem,
378 | passphrase: this._(),
379 | padding: crypto.constants.RSA_PKCS1_PADDING
380 | }
381 | const res = {
382 | algorithm: 'RSA_PKCS1_PADDING',
383 | cipherData: crypto.publicEncrypt(privateKey, plain)
384 | }
385 | callback(null, res)
386 | } catch (err) {
387 | _error(callback, err)
388 | }
389 | })
390 | }
391 |
392 | _decrypt (name, cipher, callback) {
393 | if (!validateKeyName(name)) {
394 | return _error(callback, `Invalid key name '${name}'`)
395 | }
396 |
397 | if (!Buffer.isBuffer(cipher)) {
398 | return _error(callback, 'Data is required')
399 | }
400 |
401 | const dsname = DsName(name)
402 | this.store.get(dsname, (err, res) => {
403 | if (err) {
404 | return _error(callback, `Key '${name}' does not exist. ${err.message}`)
405 | }
406 | const pem = res.toString()
407 | try {
408 | const privateKey = {
409 | key: pem,
410 | passphrase: this._(),
411 | padding: crypto.constants.RSA_PKCS1_PADDING
412 | }
413 | callback(null, crypto.privateDecrypt(privateKey, cipher))
414 | } catch (err) {
415 | _error(callback, err)
416 | }
417 | })
418 | }
419 |
420 | }
421 |
422 | module.exports = Keychain
423 |
--------------------------------------------------------------------------------
/src/keychain.md:
--------------------------------------------------------------------------------
1 | A secure key chain implemented in JS
2 |
3 | # Features
4 |
5 | - Manages the lifecycle of a key
6 | - Keys are encrypted at rest
7 | - Enforces the use of safe key names
8 | - Uses encrypted PKCS 8 for key storage
9 | - Uses PBKDF2 for a "stetched" key encryption key
10 | - Enforces NIST SP 800-131A and NIST SP 800-132
11 | - Uses PKCS 7: CMS (aka RFC 5652) to provide cryptographically protected messages
12 | - Delays reporting errors to slow down brute force attacks
13 |
14 | # Usage
15 |
16 | const datastore = new FsStore('./a-keystore')
17 | const opts = {
18 | passPhrase: 'some long easily remembered phrase'
19 | }
20 | const keychain = new Keychain(datastore, opts)
21 |
22 | # API
23 |
24 | Managing a key
25 |
26 | - `createKey (name, type, size, callback)`
27 | - `renameKey (oldName, newName, callback)`
28 | - `removeKey (name, callback)`
29 | - `exportKey (name, password, callback)`
30 | - `importKey (name, pem, password, callback)`
31 | - `importPeer (name, peer, callback)`
32 |
33 | A naming service for a key
34 |
35 | - `listKeys (callback)`
36 | - `findKeyById (id, callback)`
37 | - `findKeyByName (name, callback)`
38 |
39 | Cryptographically protected messages
40 |
41 | - `cms.createAnonymousEncryptedData (name, plain, callback)`
42 | - `cms.readData (cmsData, callback)`
43 |
44 | ## KeyInfo
45 |
46 | 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.
47 |
48 | ```
49 | {
50 | name: 'rsa-key',
51 | id: 'QmYWYSUZ4PV6MRFYpdtEDJBiGs4UrmE6g8wmAWSePekXVW'
52 | }
53 | ```
54 |
55 | 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).
56 |
57 | ## Private key storage
58 |
59 | 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`.
60 |
61 | See [details](https://github.com/richardschneider/ipfs-encryption/issues/10) for an in-depth discussion.
62 |
63 | The default options for generating the derived encryption key are in the `dek` object
64 | ```
65 | const defaultOptions = {
66 | createIfNeeded: true,
67 |
68 | //See https://cryptosense.com/parameter-choice-for-pbkdf2/
69 | dek: {
70 | keyLength: 512 / 8,
71 | iterationCount: 10000,
72 | salt: 'you should override this value with a crypto secure random number',
73 | hash: 'sha512'
74 | }
75 | }
76 | ```
77 |
78 | 
79 |
80 | ### Physical storage
81 |
82 | 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.
83 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const forge = require('node-forge')
4 | const pki = forge.pki
5 | const multihash = require('multihashes')
6 | const rsaUtils = require('libp2p-crypto/src/keys/rsa-utils')
7 | const rsaClass = require('libp2p-crypto/src/keys/rsa-class')
8 |
9 | exports = module.exports
10 |
11 | // Create an IPFS key id; the SHA-256 multihash of a public key.
12 | // See https://github.com/richardschneider/ipfs-encryption/issues/16
13 | exports.keyId = (privateKey, callback) => {
14 | try {
15 | const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e)
16 | const spki = pki.publicKeyToSubjectPublicKeyInfo(publicKey)
17 | const der = new Buffer(forge.asn1.toDer(spki).getBytes(), 'binary')
18 | const jwk = rsaUtils.pkixToJwk(der)
19 | const rsa = new rsaClass.RsaPublicKey(jwk)
20 | rsa.hash((err, kid) => {
21 | if (err) return callback(err)
22 |
23 | const kids = multihash.toB58String(kid)
24 | return callback(null, kids)
25 | })
26 | } catch (err) {
27 | callback(err)
28 | }
29 | }
30 |
31 | exports.certificateForKey = (privateKey, callback) => {
32 | exports.keyId(privateKey, (err, kid) => {
33 | if (err) return callback(err)
34 |
35 | const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e)
36 | const cert = pki.createCertificate();
37 | cert.publicKey = publicKey;
38 | cert.serialNumber = '01';
39 | cert.validity.notBefore = new Date();
40 | cert.validity.notAfter = new Date();
41 | cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
42 | var attrs = [{
43 | name: 'organizationName',
44 | value: 'ipfs'
45 | }, {
46 | shortName: 'OU',
47 | value: 'keystore'
48 | }, {
49 | name: 'commonName',
50 | value: kid
51 | }];
52 | cert.setSubject(attrs);
53 | cert.setIssuer(attrs);
54 | cert.setExtensions([{
55 | name: 'basicConstraints',
56 | cA: true
57 | }, {
58 | name: 'keyUsage',
59 | keyCertSign: true,
60 | digitalSignature: true,
61 | nonRepudiation: true,
62 | keyEncipherment: true,
63 | dataEncipherment: true
64 | }, {
65 | name: 'extKeyUsage',
66 | serverAuth: true,
67 | clientAuth: true,
68 | codeSigning: true,
69 | emailProtection: true,
70 | timeStamping: true
71 | }, {
72 | name: 'nsCertType',
73 | client: true,
74 | server: true,
75 | email: true,
76 | objsign: true,
77 | sslCA: true,
78 | emailCA: true,
79 | objCA: true
80 | }]);
81 | // self-sign certificate
82 | cert.sign(privateKey)
83 |
84 | return callback(null, cert)
85 | })
86 | }
87 |
--------------------------------------------------------------------------------
/test/browser.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | 'use strict'
3 |
4 | const async = require('async')
5 | const LevelStore = require('datastore-level')
6 |
7 | // use in the browser with level.js
8 | const browserStore = new LevelStore('my/db/name', {db: require('level-js')})
9 |
10 | describe('browser', () => {
11 | const datastore1 = new LevelStore('test-keystore-1', {db: require('level-js')})
12 | const datastore2 = new LevelStore('test-keystore-2', {db: require('level-js')})
13 |
14 | before((done) => {
15 | async.series([
16 | (cb) => datastore1.open(cb),
17 | (cb) => datastore2.open(cb)
18 | ], done)
19 | })
20 |
21 | after((done) => {
22 | async.series([
23 | (cb) => datastore1.close(cb),
24 | (cb) => datastore2.close(cb)
25 | ], done)
26 | })
27 |
28 | require('./keychain.spec')(datastore1, datastore2)
29 | require('./peerid')
30 | })
31 |
--------------------------------------------------------------------------------
/test/keychain.spec.js:
--------------------------------------------------------------------------------
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 | chai.use(require('chai-string'))
9 | const Keychain = require('..').Keychain
10 | const PeerId = require('peer-id')
11 |
12 | module.exports = (datastore1, datastore2) => {
13 | describe('keychain', () => {
14 | const passPhrase = 'this is not a secure phrase'
15 | const rsaKeyName = 'tajné jméno'
16 | const renamedRsaKeyName = 'ชื่อลับ'
17 | let rsaKeyInfo
18 | let emptyKeystore
19 | let ks
20 |
21 | before((done) => {
22 | emptyKeystore = new Keychain(datastore1, { passPhrase: passPhrase })
23 | ks = new Keychain(datastore2, { passPhrase: passPhrase })
24 | done()
25 | })
26 |
27 | it('needs a pass phrase to encrypt a key', () => {
28 | expect(() => new Keychain(datastore2)).to.throw()
29 | })
30 |
31 | it ('needs a NIST SP 800-132 non-weak pass phrase', () => {
32 | expect(() => new Keychain(datastore2, { passPhrase: '< 20 character'})).to.throw()
33 | })
34 |
35 | it('needs a store to persist a key', () => {
36 | expect(() => new Keychain(null, { passPhrase: passPhrase})).to.throw()
37 | })
38 |
39 | it('has default options', () => {
40 | expect(Keychain.options).to.exist()
41 | })
42 |
43 | describe('key name', () => {
44 | it('is a valid filename and non-ASCII', () => {
45 | ks.removeKey('../../nasty', (err) => {
46 | expect(err).to.exist()
47 | expect(err).to.have.property('message', 'Invalid key name \'../../nasty\'')
48 | })
49 | ks.removeKey('', (err) => {
50 | expect(err).to.exist()
51 | expect(err).to.have.property('message', 'Invalid key name \'\'')
52 | })
53 | ks.removeKey(' ', (err) => {
54 | expect(err).to.exist()
55 | expect(err).to.have.property('message', 'Invalid key name \' \'')
56 | })
57 | ks.removeKey(null, (err) => {
58 | expect(err).to.exist()
59 | expect(err).to.have.property('message', 'Invalid key name \'null\'')
60 | })
61 | ks.removeKey(undefined, (err) => {
62 | expect(err).to.exist()
63 | expect(err).to.have.property('message', 'Invalid key name \'undefined\'')
64 | })
65 | })
66 | })
67 |
68 | describe('key', () => {
69 | it('can be an RSA key', function (done) {
70 | this.timeout(20 * 1000)
71 | ks.createKey(rsaKeyName, 'rsa', 2048, (err, info) => {
72 | expect(err).to.not.exist()
73 | expect(info).exist()
74 | rsaKeyInfo = info
75 | done()
76 | })
77 | })
78 |
79 | it('has a name and id', () => {
80 | expect(rsaKeyInfo).to.have.property('name', rsaKeyName)
81 | expect(rsaKeyInfo).to.have.property('id')
82 | })
83 |
84 | it('is encrypted PEM encoded PKCS #8', (done) => {
85 | ks._getPrivateKey(rsaKeyName, (err, pem) => {
86 | expect(err).to.not.exist()
87 | expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----')
88 | done()
89 | })
90 | })
91 |
92 | it('does not overwrite existing key', (done) => {
93 | ks.createKey(rsaKeyName, 'rsa', 2048, (err) => {
94 | expect(err).to.exist()
95 | done()
96 | })
97 | })
98 |
99 | it('cannot create the "self" key', (done) => {
100 | ks.createKey('self', 'rsa', 2048, (err) => {
101 | expect(err).to.exist()
102 | done()
103 | })
104 | })
105 |
106 | describe('implements NIST SP 800-131A', () => {
107 | it('disallows RSA length < 2048', (done) => {
108 | ks.createKey('bad-nist-rsa', 'rsa', 1024, (err) => {
109 | expect(err).to.exist()
110 | expect(err).to.have.property('message', 'Invalid RSA key size 1024')
111 | done()
112 | })
113 | })
114 | })
115 |
116 | })
117 |
118 | describe('query', () => {
119 | it('finds all existing keys', (done) => {
120 | ks.listKeys((err, keys) => {
121 | expect(err).to.not.exist()
122 | expect(keys).to.exist()
123 | const mykey = keys.find((k) => k.name === rsaKeyName)
124 | expect(mykey).to.exist()
125 | done()
126 | })
127 | })
128 |
129 | it('finds a key by name', (done) => {
130 | ks.findKeyByName(rsaKeyName, (err, key) => {
131 | expect(err).to.not.exist()
132 | expect(key).to.exist()
133 | expect(key).to.deep.equal(rsaKeyInfo)
134 | done()
135 | })
136 | })
137 |
138 | it('finds a key by id', (done) => {
139 | ks.findKeyById(rsaKeyInfo.id, (err, key) => {
140 | expect(err).to.not.exist()
141 | expect(key).to.exist()
142 | expect(key).to.deep.equal(rsaKeyInfo)
143 | done()
144 | })
145 | })
146 |
147 | it('returns the key\'s name and id', (done) => {
148 | ks.listKeys((err, keys) => {
149 | expect(err).to.not.exist()
150 | expect(keys).to.exist()
151 | keys.forEach((key) => {
152 | expect(key).to.have.property('name')
153 | expect(key).to.have.property('id')
154 | })
155 | done()
156 | })
157 | })
158 | })
159 |
160 | describe('encryption', () => {
161 | const plainData = Buffer.from('This a message from Alice to Bob')
162 |
163 | it('requires a known key name', (done) => {
164 | ks._encrypt('not-there', plainData, (err) => {
165 | expect(err).to.exist()
166 | done()
167 | })
168 | })
169 |
170 | it('requires some data', (done) => {
171 | ks._encrypt(rsaKeyName, null, (err) => {
172 | expect(err).to.exist()
173 | done()
174 | })
175 | })
176 |
177 | it('generates encrypted data and encryption algorithm', (done) => {
178 | ks._encrypt(rsaKeyName, plainData, (err, res) => {
179 | expect(err).to.not.exist()
180 | expect(res).to.have.property('cipherData')
181 | expect(res).to.have.property('algorithm')
182 | done()
183 | })
184 | })
185 |
186 | it('decrypts', (done) => {
187 | ks._encrypt(rsaKeyName, plainData, (err, res) => {
188 | expect(err).to.not.exist()
189 | expect(res).to.have.property('cipherData')
190 | ks._decrypt(rsaKeyName, res.cipherData, (err, plain) => {
191 | expect(err).to.not.exist()
192 | expect(plain.toString()).to.equal(plainData.toString())
193 | done()
194 | })
195 | })
196 | })
197 |
198 | })
199 |
200 | describe('CMS protected data', () => {
201 | const plainData = Buffer.from('This is a message from Alice to Bob')
202 | let cms
203 |
204 | it('service is available', (done) => {
205 | expect(ks).to.have.property('cms')
206 | done()
207 | })
208 |
209 | it('is anonymous', (done) => {
210 | ks.cms.createAnonymousEncryptedData(rsaKeyName, plainData, (err, msg) => {
211 | expect(err).to.not.exist()
212 | expect(msg).to.exist()
213 | expect(msg).to.be.instanceOf(Buffer)
214 | cms = msg
215 | done()
216 | })
217 | })
218 |
219 | it('is a PKCS #7 message', (done) => {
220 | ks.cms.readData("not CMS", (err) => {
221 | expect(err).to.exist()
222 | done()
223 | })
224 | })
225 |
226 | it('is a PKCS #7 binary message', (done) => {
227 | ks.cms.readData(plainData, (err) => {
228 | expect(err).to.exist()
229 | done()
230 | })
231 | })
232 |
233 | it('cannot be read without the key', (done) => {
234 | emptyKeystore.cms.readData(cms, (err, plain) => {
235 | expect(err).to.exist()
236 | done()
237 | })
238 | })
239 |
240 | it('can be read with the key', (done) => {
241 | ks.cms.readData(cms, (err, plain) => {
242 | expect(err).to.not.exist()
243 | expect(plain).to.exist()
244 | expect(plain.toString()).to.equal(plainData.toString())
245 | done()
246 | })
247 | })
248 |
249 | })
250 |
251 | describe('exported key', () => {
252 | let pemKey
253 |
254 | it('is a PKCS #8 encrypted pem', (done) => {
255 | ks.exportKey(rsaKeyName, 'password', (err, pem) => {
256 | expect(err).to.not.exist()
257 | expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----')
258 | pemKey = pem
259 | done()
260 | })
261 | })
262 |
263 | it('can be imported', (done) => {
264 | ks.importKey('imported-key', pemKey, 'password', (err, key) => {
265 | expect(err).to.not.exist()
266 | expect(key.name).to.equal('imported-key')
267 | expect(key.id).to.equal(rsaKeyInfo.id)
268 | done()
269 | })
270 | })
271 |
272 | it('cannot be imported as an existing key name', (done) => {
273 | ks.importKey(rsaKeyName, pemKey, 'password', (err, key) => {
274 | expect(err).to.exist()
275 | done()
276 | })
277 | })
278 |
279 | it('cannot be imported with the wrong password', function (done) {
280 | this.timeout(5 * 1000)
281 | ks.importKey('a-new-name-for-import', pemKey, 'not the password', (err, key) => {
282 | expect(err).to.exist()
283 | done()
284 | })
285 | })
286 | })
287 |
288 | describe('peer id', () => {
289 | 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=='
290 | let alice
291 |
292 | before(function (done) {
293 | const encoded = Buffer.from(alicePrivKey, 'base64')
294 | PeerId.createFromPrivKey(encoded, (err, id) => {
295 | alice = id
296 | done()
297 | })
298 | })
299 |
300 | it('private key can be imported', (done) => {
301 | ks.importPeer('alice', alice, (err, key) => {
302 | expect(err).to.not.exist()
303 | expect(key.name).to.equal('alice')
304 | expect(key.id).to.equal(alice.toB58String())
305 | done()
306 | })
307 | })
308 | })
309 |
310 | describe('rename', () => {
311 | it('requires an existing key name', (done) => {
312 | ks.renameKey('not-there', renamedRsaKeyName, (err) => {
313 | expect(err).to.exist()
314 | done()
315 | })
316 | })
317 |
318 | it('requires a valid new key name', (done) => {
319 | ks.renameKey(rsaKeyName, '..\not-valid', (err) => {
320 | expect(err).to.exist()
321 | done()
322 | })
323 | })
324 |
325 | it('does not overwrite existing key', (done) => {
326 | ks.renameKey(rsaKeyName, rsaKeyName, (err) => {
327 | expect(err).to.exist()
328 | done()
329 | })
330 | })
331 |
332 | it('cannot create the "self" key', (done) => {
333 | ks.renameKey(rsaKeyName, 'self', (err) => {
334 | expect(err).to.exist()
335 | done()
336 | })
337 | })
338 |
339 | it('removes the existing key name', (done) => {
340 | ks.renameKey(rsaKeyName, renamedRsaKeyName, (err, key) => {
341 | expect(err).to.not.exist()
342 | expect(key).to.exist()
343 | expect(key).to.have.property('name', renamedRsaKeyName)
344 | expect(key).to.have.property('id', rsaKeyInfo.id)
345 | ks.findKeyByName(rsaKeyName, (err, key) => {
346 | expect(err).to.exist()
347 | done()
348 | })
349 | })
350 | })
351 |
352 | it('creates the new key name', (done) => {
353 | ks.findKeyByName(renamedRsaKeyName, (err, key) => {
354 | expect(err).to.not.exist()
355 | expect(key).to.exist()
356 | expect(key).to.have.property('name', renamedRsaKeyName)
357 | done()
358 | })
359 | })
360 |
361 | it('does not change the key ID', (done) => {
362 | ks.findKeyByName(renamedRsaKeyName, (err, key) => {
363 | expect(err).to.not.exist()
364 | expect(key).to.exist()
365 | expect(key).to.have.property('name', renamedRsaKeyName)
366 | expect(key).to.have.property('id', rsaKeyInfo.id)
367 | done()
368 | })
369 | })
370 | })
371 |
372 | describe('key removal', () => {
373 | it('cannot remove the "self" key', (done) => {
374 | ks.removeKey('self', (err) => {
375 | expect(err).to.exist()
376 | done()
377 | })
378 | })
379 |
380 | it('cannot remove an unknown key', (done) => {
381 | ks.removeKey('not-there', (err) => {
382 | expect(err).to.exist()
383 | done()
384 | })
385 | })
386 |
387 | it('can remove a known key', (done) => {
388 | ks.removeKey(renamedRsaKeyName, (err) => {
389 | expect(err).to.not.exist()
390 | done()
391 | })
392 | })
393 | })
394 |
395 | })
396 | }
397 |
--------------------------------------------------------------------------------
/test/node.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | 'use strict'
3 |
4 | const os = require('os')
5 | const path = require('path')
6 | const rimraf = require('rimraf')
7 | const async = require('async')
8 | const FsStore = require('datastore-fs')
9 |
10 | describe('node', () => {
11 | const store1 = path.join(os.tmpdir(), 'test-keystore-1')
12 | const store2 = path.join(os.tmpdir(), 'test-keystore-2')
13 | const datastore1 = new FsStore(store1)
14 | const datastore2 = new FsStore(store2)
15 |
16 | before((done) => {
17 | async.series([
18 | (cb) => datastore1.open(cb),
19 | (cb) => datastore2.open(cb)
20 | ], done)
21 | })
22 |
23 | after((done) => {
24 | async.series([
25 | (cb) => datastore1.close(cb),
26 | (cb) => datastore2.close(cb),
27 | (cb) => rimraf(store1, cb),
28 | (cb) => rimraf(store2, cb)
29 | ], done)
30 | })
31 |
32 | require('./keychain.spec')(datastore1, datastore2)
33 | require('./peerid')
34 | })
35 |
--------------------------------------------------------------------------------
/test/peerid.js:
--------------------------------------------------------------------------------
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(function (done) {
25 | const encoded = Buffer.from(sample.privKey, 'base64')
26 | PeerId.createFromPrivKey(encoded, (err, id) => {
27 | peer = id
28 | done()
29 | })
30 | })
31 |
32 | it('decoded public key', (done) => {
33 | // console.log('peer id', peer.toJSON())
34 | // console.log('id', peer.toB58String())
35 | // console.log('id decoded', multihash.decode(peer.id))
36 |
37 | // get protobuf version of the public key
38 | const publicKeyProtobuf = peer.marshalPubKey()
39 | const publicKey = crypto.keys.unmarshalPublicKey(publicKeyProtobuf)
40 | // console.log('public key', publicKey)
41 | publicKeyDer = publicKey.marshal()
42 | // console.log('public key der', publicKeyDer.toString('base64'))
43 |
44 | // get protobuf version of the private key
45 | const privateKeyProtobuf = peer.marshalPrivKey()
46 | crypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => {
47 | // console.log('private key', key)
48 | // console.log('\nprivate key der', key.marshal().toString('base64'))
49 | done()
50 | })
51 | })
52 |
53 | it('encoded public key with DER', (done) => {
54 | const jwk = rsaUtils.pkixToJwk(publicKeyDer)
55 | // console.log('jwk', jwk)
56 | const rsa = new rsaClass.RsaPublicKey(jwk)
57 | // console.log('rsa', rsa)
58 | rsa.hash((err, keyId) => {
59 | // console.log('err', err)
60 | // console.log('keyId', keyId)
61 | // console.log('id decoded', multihash.decode(keyId))
62 | const kids = multihash.toB58String(keyId)
63 | // console.log('id', kids)
64 | expect(kids).to.equal(peer.toB58String())
65 | done()
66 | })
67 | })
68 |
69 | it('encoded public key with JWT', (done) => {
70 | const jwk = {
71 | kty: 'RSA',
72 | n: 'tkiqPxzBWXgZpdQBd14o868a30F3Sc43jwWQG3caikdTHOo7kR14o-h12D45QJNNQYRdUty5eC8ItHAB4YIH-Oe7DIOeVFsnhinlL9LnILwqQcJUeXENNtItDIM4z1ji1qta7b0mzXAItmRFZ-vkNhHB6N8FL1kbS3is_g2UmX8NjxAwvgxjyT5e3_IO85eemMpppsx_ZYmSza84P6onaJFL-btaXRq3KS7jzXkzg5NHKigfjlG7io_RkoWBAghI2smyQ5fdu-qGpS_YIQbUnhL9tJLoGrU72MufdMBZSZJL8pfpz8SB9BBGDCivV0VpbvV2J6En26IsHL_DN0pbIw',
73 | e: 'AQAB',
74 | alg: 'RS256',
75 | kid: '2011-04-29'
76 | }
77 | // console.log('jwk', jwk)
78 | const rsa = new rsaClass.RsaPublicKey(jwk)
79 | // console.log('rsa', rsa)
80 | rsa.hash((err, keyId) => {
81 | // console.log('err', err)
82 | // console.log('keyId', keyId)
83 | // console.log('id decoded', multihash.decode(keyId))
84 | const kids = multihash.toB58String(keyId)
85 | // console.log('id', kids)
86 | expect(kids).to.equal(peer.toB58String())
87 | done()
88 | })
89 | })
90 |
91 | it('decoded private key', (done) => {
92 | // console.log('peer id', peer.toJSON())
93 | // console.log('id', peer.toB58String())
94 | // console.log('id decoded', multihash.decode(peer.id))
95 |
96 | // get protobuf version of the private key
97 | const privateKeyProtobuf = peer.marshalPrivKey()
98 | crypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => {
99 | // console.log('private key', key)
100 | //console.log('\nprivate key der', key.marshal().toString('base64'))
101 | done()
102 | })
103 | })
104 |
105 | })
106 |
--------------------------------------------------------------------------------