├── .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 | ![key storage](../doc/private-key.png?raw=true) 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 | --------------------------------------------------------------------------------