├── .travis.yml ├── .npmignore ├── .gitignore ├── src ├── index.js ├── browser.js ├── config.js ├── test-utils.js ├── keygen.test.js ├── validate.test.js ├── validate.js ├── uri-rules.test.js ├── keypath-utils.js ├── uri-rules.js ├── keygen.js ├── jsdoc-types.js ├── keystore.test.js └── keystore.js ├── package.json ├── README.md └── API.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | coverage 3 | sandbox 4 | src 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | coverage 4 | dist 5 | lib 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const Keystore = require('./keystore') 2 | const Keygen = require('./keygen') 3 | 4 | const ecc = require('eosjs-ecc') 5 | 6 | module.exports = { 7 | Keystore, 8 | Keygen, 9 | modules: { 10 | ecc 11 | } 12 | } -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | const Keystore = require('./keystore') 2 | const Keygen = require('./keygen') 3 | const ecc = require('eosjs-ecc') 4 | 5 | const createHistory = require('history').createBrowserHistory 6 | const config = require('./config') 7 | 8 | config.history = createHistory() 9 | config.localStorage = localStorage 10 | 11 | module.exports = { 12 | Keystore, 13 | Keygen, 14 | modules: { 15 | ecc 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | let historyInstance, localStorageInstance 5 | 6 | module.exports = { 7 | 8 | /** Set a browser friendly or custom implemention */ 9 | set history(h) { 10 | historyInstance = h 11 | }, 12 | 13 | /** @return node or react native friendly implemention (unless setter was called) */ 14 | get history() { 15 | if(historyInstance) { 16 | return historyInstance 17 | } 18 | const createHistory = require('history').createMemoryHistory 19 | historyInstance = createHistory() 20 | return historyInstance 21 | }, 22 | 23 | /** Set a browser friendly or custom implemention */ 24 | set localStorage(ls) { 25 | localStorageInstance = ls 26 | }, 27 | 28 | /** @return node or react native friendly implemention (unless setter was called) */ 29 | get localStorage() { 30 | if(localStorageInstance) { 31 | return localStorageInstance 32 | } 33 | localStorageInstance = require('localStorage') 34 | return localStorageInstance 35 | }, 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/test-utils.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | const accountPermissions = [{ 4 | perm_name: 'active', 5 | parent: 'owner', 6 | required_auth: { 7 | threshold: 1, 8 | keys: [{ 9 | key: 'EOS7vgT3ZsuUxWH1tWyqw6cyKqKhPjUFbonZjyrrXqDauty61SrYe', 10 | weight: 1 11 | } 12 | ], 13 | accounts: [] 14 | } 15 | },{ 16 | perm_name: 'mypermission', 17 | parent: 'active', 18 | required_auth: { 19 | threshold: 1, 20 | keys: [{ 21 | key: 'EOS5MiUJEXxjJw6wUcE6yUjxpATaWetubAGUJ1nYLRSHYPpGCJ8ZU', 22 | weight: 1 23 | } 24 | ], 25 | accounts: [] 26 | } 27 | },{ 28 | perm_name: 'owner', 29 | parent: '', 30 | required_auth: { 31 | threshold: 1, 32 | keys: [{ 33 | key: 'EOS8jJUMo67w6tYBhzjZqyzq5QyL7pH7jVTmv1xoakXmkkgLrfTTx', 34 | weight: 1 35 | } 36 | ], 37 | accounts: [] 38 | } 39 | }] 40 | 41 | function checkKeySet(keys) { 42 | assert.equal(typeof keys.masterPrivateKey, 'string', 'keys.masterPrivateKey') 43 | 44 | assert.equal(typeof keys.privateKeys, 'object', 'keys.privateKeys') 45 | assert.equal(typeof keys.privateKeys.owner, 'string', 'keys.privateKeys.owner') 46 | assert.equal(typeof keys.privateKeys.active, 'string', 'keys.privateKeys.active') 47 | 48 | assert.equal(typeof keys.publicKeys, 'object', 'keys.publicKeys') 49 | assert.equal(typeof keys.publicKeys.owner, 'string', 'keys.publicKeys.owner') 50 | assert.equal(typeof keys.publicKeys.active, 'string', 'keys.publicKeys.active') 51 | } 52 | 53 | module.exports = { 54 | accountPermissions, 55 | checkKeySet 56 | } -------------------------------------------------------------------------------- /src/keygen.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const assert = require('assert') 3 | const {accountPermissions, checkKeySet} = require('./test-utils.js') 4 | 5 | const {PrivateKey} = require('eosjs-ecc') 6 | const Keygen = require('./keygen') 7 | 8 | describe('Keygen', () => { 9 | it('initialize', () => PrivateKey.initialize()) 10 | 11 | it('generateMasterKeys (create)', () => { 12 | return Keygen.generateMasterKeys().then(keys => { 13 | checkKeySet(keys) 14 | }) 15 | }) 16 | 17 | it('generateMasterKeys (re-construct)', () => { 18 | const master = 'PW5JMx76CTUTXxpAbwAqGMMVzSeJaP5UVTT5c2uobcpaMUdLAphSp' 19 | return Keygen.generateMasterKeys(master).then(keys => { 20 | assert.equal(keys.masterPrivateKey, master, 'masterPrivateKey') 21 | checkKeySet(keys) 22 | }) 23 | }) 24 | 25 | it('authsByPath', () => { 26 | const paths = Keygen.authsByPath(accountPermissions) 27 | assert.deepEqual( 28 | ['active', 'active/mypermission', 'owner'], 29 | Object.keys(paths) 30 | ) 31 | }) 32 | 33 | it('deriveKeys', () => { 34 | const master = 'PW5JMx76CTUTXxpAbwAqGMMVzSeJaP5UVTT5c2uobcpaMUdLAphSp' 35 | return Keygen.generateMasterKeys(master).then(keys => { 36 | const wifsByPath = { 37 | owner: keys.privateKeys.owner, 38 | active: keys.privateKeys.active, 39 | } 40 | 41 | const derivedKeys = Keygen.deriveKeys('active/mypermission', wifsByPath) 42 | const active = PrivateKey(keys.privateKeys.active) 43 | const checkKey = active.getChildKey('mypermission').toWif() 44 | 45 | assert.equal(derivedKeys.length, 1, 'derived key count') 46 | assert.equal(derivedKeys[0].path, 'active/mypermission') 47 | assert.equal(derivedKeys[0].privateKey.toWif(), checkKey, 'wrong private key') 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/validate.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const assert = require('assert') 3 | 4 | const validate = require('./validate') 5 | 6 | describe('Validate', () => { 7 | 8 | it('path', () => { 9 | validate.path('owner') // better error than doesNotThrow 10 | assert.doesNotThrow(() => validate.path('owner')) 11 | assert.doesNotThrow(() => validate.path('active')) 12 | assert.doesNotThrow(() => validate.path('active/mypermission')) 13 | assert.doesNotThrow(() => validate.path('active')) 14 | assert.doesNotThrow(() => validate.path('active/mykey')) 15 | 16 | assert.throws(() => validate.path('active/mykey/active'), /duplicate/) 17 | assert.throws(() => validate.path('owner/active'), /owner is implied, juse use active/) 18 | assert.throws(() => validate.path('joe/active'), /path should start with owner or active/) 19 | assert.throws(() => validate.path('owner/mykey/active'), /active is always first/) 20 | assert.throws(() => validate.path('active/mykey/owner'), /owner is always first/) 21 | assert.throws(() => validate.path('active/owner'), /owner is always first/) 22 | }) 23 | 24 | it('keyType', () => { 25 | const testPubkey = 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV' 26 | const testMasterPass = 'PW5JMx76CTUTXxpAbwAqGMMVzSeJaP5UVTT5c2uobcpaMUdLAphSp' 27 | const testPrivate = testMasterPass.substring(2) 28 | 29 | assert.equal(validate.keyType(testPubkey), 'pubkey') 30 | assert.equal(validate.keyType(testMasterPass), 'master') 31 | assert.equal(validate.keyType(testPrivate), 'wif') 32 | assert.equal(validate.keyType(testPrivate.substring(1)), null) 33 | }) 34 | 35 | it('isMasterKey', () => { 36 | const testMasterPass = 'PW5JMx76CTUTXxpAbwAqGMMVzSeJaP5UVTT5c2uobcpaMUdLAphSp' 37 | assert(validate.isMasterKey(testMasterPass)) 38 | assert(!validate.isMasterKey(testMasterPass + 'a')) 39 | }) 40 | 41 | }) -------------------------------------------------------------------------------- /src/validate.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | const {PrivateKey, PublicKey} = require('eosjs-ecc') 4 | 5 | module.exports = { 6 | keyType, 7 | path, 8 | isPath, 9 | isMasterKey 10 | } 11 | 12 | function isMasterKey(key) { 13 | return /^PW/.test(key) && PrivateKey.isWif(key.substring(2)) 14 | } 15 | 16 | function keyType(key) { 17 | return isMasterKey(key) ? 'master' : 18 | PrivateKey.isWif(key) ? 'wif' : 19 | PrivateKey.isValid(key) ? 'privateKey' : 20 | PublicKey.isValid(key) ? 'pubkey' : 21 | null 22 | } 23 | 24 | function isPath(txt) { 25 | try { 26 | path(txt) 27 | return true 28 | } catch(e) { 29 | return false 30 | } 31 | } 32 | 33 | /** 34 | Static validation of a keyPath. Protect against common mistakes. 35 | @see [validate.test.js](./validate.test.js) 36 | 37 | @arg {keyPath} path 38 | 39 | @example path('owner') 40 | @example path('active') 41 | @example path('active/mypermission') 42 | */ 43 | function path(path) { 44 | assert.equal(typeof path, 'string', 'path') 45 | assert(path !== '', 'path should not be empty') 46 | assert(path.indexOf(' ') === -1, 'remove spaces') 47 | assert(path.indexOf('\\') === -1, 'use forward slash') 48 | assert(path[0] !== '/', 'remove leading slash') 49 | assert(path[path.length - 1] !== '/', 'remove ending slash') 50 | assert(!/[A-Z]/.test(path), 'path should not have uppercase letters') 51 | 52 | assert(path !== 'owner/active', 'owner is implied, juse use active') 53 | 54 | const el = Array.from(path.split('/')) 55 | 56 | const unique = new Set() 57 | el.forEach(e => {unique.add(e)}) 58 | assert(unique.size === el.length, 'duplicate path element') 59 | 60 | assert(el[0] === 'owner' || el[0] === 'active', 61 | 'path should start with owner or active') 62 | 63 | assert(!el.includes('owner') || el.indexOf('owner') === 0, 64 | 'owner is always first') 65 | 66 | assert(!el.includes('active') || el.indexOf('active') === 0, 67 | 'active is always first') 68 | } 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eosjs-keygen", 3 | "version": "1.3.2", 4 | "description": "General purpose library for private key storage and key management.", 5 | "main": "lib/index.js", 6 | "browser": "lib/browser.js", 7 | "node": "", 8 | "scripts": { 9 | "test": "mocha --exit --use_strict src/*.test.js", 10 | "coverage": "istanbul cover _mocha -- --exit -R spec src/*.test.js", 11 | "coveralls": "npm run coverage && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls", 12 | "prepublishOnly": "npm run build_lib && npm run prepublishOnly_test && npm run docs", 13 | "prepublishOnly_test": "mocha --exit --use_strict lib/*.test.js", 14 | "build": "npm run docs && npm run build_browser", 15 | "docs": "jsdoc2md src/keystore.js src/keygen.js src/jsdoc-types.js > API.md", 16 | "build_lib": "babel src --out-dir lib", 17 | "build_browser": "npm run build_lib && mkdir -p dist && browserify -o dist/eosjs-keygen.js -s kos lib/index.js", 18 | "build_browser_test": "npm run build_lib && mkdir -p dist && browserify -o dist/mocha-test.js lib/*.test.js" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/EOSIO/eosjs-keygen.git" 23 | }, 24 | "keywords": [ 25 | "EOS", 26 | "Blockchain" 27 | ], 28 | "author": "jamesc", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/EOSIO/eosjs-keygen/issues" 32 | }, 33 | "homepage": "https://github.com/EOSIO/eosjs-keygen#readme", 34 | "devDependencies": { 35 | "babel-cli": "^6.26.0", 36 | "babel-core": "^6.26.0", 37 | "babel-preset-es2015": "^6.24.1", 38 | "browserify": "^14.4.0", 39 | "coveralls": "^3.0.0", 40 | "eosjs": "^4.0.2", 41 | "istanbul": "^1.1.0-alpha.1", 42 | "jsdoc-to-markdown": "^3.0.4", 43 | "mocha": "^5.2.0" 44 | }, 45 | "dependencies": { 46 | "eosjs-ecc": "^4.0.1", 47 | "history": "^4.7.2", 48 | "localStorage": "^1.0.3", 49 | "minimatch": "^3.0.4" 50 | }, 51 | "babel": { 52 | "presets": [ 53 | "es2015" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![NPM](https://img.shields.io/npm/v/eosjs-keygen.svg)](https://www.npmjs.org/package/eosjs-keygen) 3 | 4 | # Repository 5 | 6 | The purpose of this library is for managing keys in local storage. This is designed to derive and cache keys but also needs a password manager to store a "root" key. This library does not have secure or password protected storage. It does however figure out permission hierarchies and is configurable enough to only store keys you feel are safe to store. 7 | 8 | General purpose cryptography is found in [eosjs-ecc](http://github.com/eosio/eosjs-ecc) library. Hierarchical 9 | deterministic key generation uses PrivateKey.getChildKey in eosjs-ecc. 10 | 11 | ### Usage 12 | 13 | ```javascript 14 | let {Keystore, Keygen} = require('eosjs-keygen') 15 | Eos = require('eosjs') 16 | 17 | sessionConfig = { 18 | timeoutInMin: 30, 19 | uriRules: { 20 | 'owner' : '/account_recovery', 21 | 'active': '/(transfer|contracts)', 22 | 'active/**': '/producers' 23 | } 24 | } 25 | 26 | keystore = Keystore('myaccount', sessionConfig) 27 | eos = Eos.Testnet({keyProvider: keystore.keyProvider}) 28 | 29 | Keygen.generateMasterKeys().then(keys => { 30 | // create blockchain account called 'myaccount' 31 | console.log(keys) 32 | 33 | eos.getAccount('myaccount').then(account => { 34 | keystore.deriveKeys({ 35 | parent: keys.masterPrivateKey, 36 | accountPermissions: account.permissions 37 | }) 38 | }) 39 | 40 | }) 41 | ``` 42 | 43 | See [./API](./API.md) 44 | 45 | # Development 46 | 47 | ```javascript 48 | let {Keystore, Keygen} = require('./src') 49 | ``` 50 | 51 | Use Node v8+ (updates `package-lock.json`) 52 | 53 | # Browser 54 | 55 | ```bash 56 | git clone https://github.com/EOSIO/eosjs-keygen.git 57 | cd eosjs-keygen 58 | npm install 59 | npm run build 60 | # builds: ./dist/eosjs-keygen.js 61 | ``` 62 | 63 | ```html 64 | 65 | 70 | ``` 71 | 72 | # Runtime Environment 73 | 74 | Node 6+ and browser (browserify, webpack, etc) 75 | 76 | Built with React Native in mind, create an issue if you find a bug. 77 | -------------------------------------------------------------------------------- /src/uri-rules.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const assert = require('assert') 3 | 4 | const UriRules = require('./uri-rules') 5 | 6 | describe('Path Rules', () => { 7 | 8 | const uriRules = UriRules({ 9 | 'owner' : '/account_recovery', 10 | 'active': '/(transfers|contracts)', 11 | 'active/**': '/contract' 12 | }) 13 | 14 | const fixtures = [ 15 | {// 1 16 | uri: '/account_recovery', 17 | paths: ['owner', 'active', 'active/mypermission'], 18 | allow: ['owner', 'active', 'active/mypermission'], deny: [] 19 | }, 20 | {// 2 21 | uri: '/transfers', 22 | paths: ['active', 'active/mypermission', 'owner'], 23 | allow: ['active', 'active/mypermission'], deny: ['owner'] 24 | }, 25 | {// 3 26 | uri: '/contract', 27 | paths: ['active', 'active/mypermission', 'owner'], 28 | allow: ['active/mypermission'], deny: ['active', 'owner'] 29 | }, 30 | {// 4 31 | uri: '/', 32 | paths: ['active', 'active/mypermission', 'owner'], 33 | allow: [], deny: ['active', 'active/mypermission', 'owner'], 34 | } 35 | ] 36 | 37 | let fixtureIndex = 1 38 | for(const test of fixtures) { 39 | const {paths, uri, allow, deny} = test 40 | 41 | it(`Test ${fixtureIndex} Path Rules: ` + JSON.stringify(test), () => { 42 | assert.deepEqual(uriRules.check(uri, paths), {allow, deny}) 43 | assert.deepEqual(uriRules.allow(uri, paths), allow) 44 | assert.deepEqual(uriRules.deny(uri, paths), deny) 45 | }) 46 | fixtureIndex++ 47 | } 48 | }) 49 | 50 | describe('Uri Rules', () => { 51 | const fixtures = [ 52 | { 53 | rule: 'start-with', 54 | allow: [ 55 | 'start-with', 'start-with/', 56 | 'start-with#hp1', 'start-with?qp1', 57 | 'start-with/#hp1', 'start-with/?qp1' 58 | ], 59 | deny: [ 60 | 'start-with-not', 'not-start-with', 61 | '/start-with', 'not/start-with' 62 | ] 63 | }, 64 | { 65 | rule: 'end-with$', 66 | allow: ['end-with'], 67 | deny: [ 68 | 'not-end-with', 69 | '/end-with', 'end-with/', 'mypath/end-with', 70 | 'end-with?', 'end-with?qp', 'end-with#hp=1' 71 | ] 72 | }, 73 | ] 74 | 75 | const keyPath = 'active/other' 76 | 77 | let fixtureIndex = 1 78 | for(const test of fixtures) { 79 | const {rule, allow, deny} = test 80 | const uriRules = UriRules({[keyPath]: rule}) 81 | 82 | for(const path of allow) { 83 | it(`Test ${fixtureIndex} Uri rule '${rule}' allows '${path}'`, () => { 84 | assert.deepEqual([keyPath], uriRules.allow(path, [keyPath])) 85 | }) 86 | } 87 | for(const path of deny) { 88 | it(`Test ${fixtureIndex} Uri rule '${rule}' denies '${path}'`, () => { 89 | assert.deepEqual([keyPath], uriRules.deny(path, [keyPath])) 90 | }) 91 | } 92 | fixtureIndex++ 93 | } 94 | }) 95 | -------------------------------------------------------------------------------- /src/keypath-utils.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | module.exports = Storage 4 | 5 | /** 6 | Generic array key based storage. 7 | 8 | - Allows null or undefined 9 | - Uses a prefix-friendly key encoding for searching 10 | - Serilization is handled externally 11 | */ 12 | function Storage(namespace) { 13 | 14 | function createKey(...elements) { 15 | const key = JSON.stringify([namespace, ...elements]) 16 | // remove [ and ] so key is prefix-friendly for searching 17 | const keyTrim = key.substring(1, key.length - 1) 18 | return Buffer.from(keyTrim).toString('hex') 19 | } 20 | 21 | /** 22 | Save a value to state. 23 | 24 | @return {boolean} dirty 25 | @throws {AssertionError} immutable 26 | */ 27 | function save(state, key, value, {immutable = true, clobber = true} = {}) { 28 | assert.equal(typeof state, 'object', 'state') 29 | 30 | key = Array.isArray(key) ? createKey(...key) : key 31 | assert.equal(typeof key, 'string', 'key') 32 | 33 | assert(value == null || typeof value === 'string' || typeof value === 'object', 34 | 'value should be null, a string, or a serializable object') 35 | 36 | const existing = deNull(state[key]) // convert 'null' string back into null 37 | 38 | if(value == null && existing != null && !clobber) { 39 | // don't erase it, keep the existing value 40 | value = existing 41 | } 42 | 43 | const dirty = existing !== value 44 | assert(existing == null || !(dirty && immutable), 'immutable') 45 | 46 | if(dirty) { 47 | state[key] = value 48 | } 49 | 50 | return dirty 51 | } 52 | 53 | /** 54 | @arg {object} state 55 | @arg {string|Array} key 56 | @arg {string} [defaultValue] 57 | 58 | @return {string} 59 | */ 60 | function get(state, key) { 61 | assert.equal(typeof state, 'object', 'state') 62 | 63 | key = Array.isArray(key) ? createKey(...key) : key 64 | assert.equal(typeof key, 'string', 'key') 65 | 66 | return deNull(state[key]) 67 | } 68 | 69 | /** 70 | @arg {object} state - localStorage, etc 71 | @arg {Array} keyPrefix - index, if partial path, the rest of the 72 | key elements end up in keySuffix. 73 | 74 | @arg {function} callback(keySuffix = [], value) return false to stop looping 75 | */ 76 | function query(state, keyPrefix, callback) { 77 | assert.equal(typeof state, 'object', 'state') 78 | assert(Array.isArray(keyPrefix), 'keyPrefix is an array') 79 | assert.equal(typeof callback, 'function', 'callback') 80 | 81 | const prefix = createKey(...keyPrefix) 82 | for(const key of Object.keys(state)) { 83 | if(key.indexOf(prefix) !== 0) { 84 | continue 85 | } 86 | const decodedKeys = JSON.parse('[' + Buffer.from(key, 'hex') + ']') 87 | const ret = callback(decodedKeys.slice(keyPrefix.length + 1), deNull(state[key])) 88 | if(ret === false) { 89 | break 90 | } 91 | } 92 | } 93 | 94 | return { 95 | createKey, 96 | save, 97 | get, 98 | query 99 | } 100 | } 101 | 102 | const deNull = value => 103 | value === 'null' ? null : 104 | value === 'undefined' ? undefined : 105 | value 106 | -------------------------------------------------------------------------------- /src/uri-rules.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const minimatch = require("minimatch") 3 | 4 | const validate = require('./validate') 5 | 6 | module.exports = UriRules 7 | 8 | /** 9 | @arg {uriRules} 10 | */ 11 | function UriRules(rules) { 12 | assert.equal(typeof rules, 'object', 'rules') 13 | rules = Object.assign({}, rules) 14 | 15 | for(const path in rules) { 16 | const uriMatchers = rules[path] 17 | const rePatterns = createUrlRules(uriMatchers) 18 | rules[path] = rePatterns 19 | } 20 | 21 | /** 22 | Separate out paths into Allow and Deny. 23 | 24 | @arg {uriData} 25 | 26 | @arg {Set|Array} paths - key paths: owner, active, 27 | active/mypermission, etc.. These paths are created from blockchain 28 | account.permissions and gathered in the keystore.login function. 29 | 30 | @return {{allow: Array, deny: Array}} - paths allowed or 31 | denied under current Uri. This tells the keystore, according to the 32 | Uri rules to generate, save, or remove private keys only for these paths. 33 | */ 34 | function check(uri, paths) { 35 | return checkUrl(uri, paths, rules) 36 | } 37 | 38 | /** Just allowed paths */ 39 | function allow(uri, paths) { 40 | return checkUrl(uri, paths, rules).allow 41 | } 42 | 43 | /** Just deny paths */ 44 | function deny(uri, paths) { 45 | return checkUrl(uri, paths, rules).deny 46 | } 47 | 48 | return { 49 | check, 50 | allow, 51 | deny 52 | } 53 | } 54 | 55 | function createUrlRules(uriMatchers) { 56 | if(typeof uriMatchers === 'string') { 57 | uriMatchers = [uriMatchers] 58 | } 59 | return uriMatchers.map(uriPattern => { 60 | assert.equal(typeof uriPattern, 'string', uriPattern) 61 | 62 | uriPattern = uriPattern.trim() 63 | assert.notEqual(uriPattern.charAt(0), '^', 'uriPattern') 64 | 65 | const prefix = '^' 66 | 67 | // Allow: /contracts, /contracts/abc, /contracts#hp=1, /contracts?qp=1 68 | // Do not allow: /contracts2 69 | const suffix = uriPattern.charAt(uriPattern.length - 1) === '$' ? '' : '\/?([#\?].*)?$' 70 | 71 | uriPattern = new RegExp(prefix + uriPattern + suffix, 'i') 72 | return uriPattern 73 | }) 74 | } 75 | 76 | /** @private */ 77 | function checkUrl(uri, paths, rules) { 78 | assert.equal(typeof uri, 'string', 'uri') 79 | 80 | if(typeof paths === 'string') { 81 | paths = [paths] 82 | } 83 | 84 | assert(paths instanceof Array || paths instanceof Set, 85 | 'paths is a Set or Array') 86 | 87 | for(const path of paths) { 88 | validate.path(path) 89 | } 90 | 91 | /** 92 | Get uri rules (minimatch pattern) for a path (string). 93 | 94 | @arg {string} path 95 | 96 | @return {Array} from rules[path] or null 97 | */ 98 | function fullUrlPathSet(path) { 99 | const uriPaths = [] 100 | for(const rulePath in rules) { 101 | let match 102 | // active key is derived from owner (but this is implied) 103 | if(minimatch(rulePath, 'owner') && minimatch(path, 'active{,/**}')) { 104 | match = true 105 | } else { 106 | // Paths are derivied, so if any root part of the path matches the 107 | // minimatch, all the children (being derived) are an implied match too. 108 | 109 | // Check the rule as we re-build the path .. 110 | const accumulativePath = [] 111 | for(const part of path.split('/')) { 112 | accumulativePath.push(part) 113 | match = minimatch(accumulativePath.join('/'), rulePath) 114 | if(match) { 115 | break 116 | } 117 | } 118 | } 119 | // console.log('fullUrlPathSet', path, match ? '==' : '!=', rulePath) 120 | if(match) { 121 | uriPaths.push(rules[rulePath]) 122 | } 123 | } 124 | return uriPaths.length ? [].concat.apply([], uriPaths) : null 125 | } 126 | 127 | const allow = [], deny = [] 128 | for(const path of paths) { 129 | const uriPathSet = fullUrlPathSet(path) 130 | if(uriPathSet) { 131 | let oneMatches = false 132 | for(const uriPathRegExp of uriPathSet) { 133 | oneMatches = uriPathRegExp.test(uri) 134 | // console.log('uriPathRegExp', uriPathRegExp, uri, oneMatches) 135 | if(oneMatches) { 136 | allow.push(path) 137 | break 138 | } 139 | } 140 | if(!oneMatches) { 141 | deny.push(path) 142 | } 143 | } else { 144 | deny.push(path) 145 | // console.log('Missing uriRule for: ' + uri, path) 146 | } 147 | } 148 | assert.equal(paths.length, allow.length + deny.length, 'missing path(s)') 149 | return {allow, deny} 150 | } 151 | -------------------------------------------------------------------------------- /src/keygen.js: -------------------------------------------------------------------------------- 1 | /** @module Keygen */ 2 | 3 | const assert = require('assert') 4 | 5 | const {PrivateKey} = require('eosjs-ecc') 6 | const validate = require('./validate') 7 | 8 | module.exports = { 9 | generateMasterKeys, 10 | authsByPath, 11 | deriveKeys 12 | } 13 | 14 | /** 15 | New accounts will call this to create a new keyset.. 16 | 17 | A password manager or backup should save (at the very minimum) the returned 18 | {masterPrivateKey} for later login. The owner and active can be re-created 19 | from the masterPrivateKey. It is still a good idea to save all information 20 | in the backup for easy reference. 21 | 22 | @arg {masterPrivateKey} [masterPrivateKey = null] When null, a new random key 23 | is created. 24 | 25 | @return {Promise} masterKeys 26 | 27 | @example 28 | masterKeys = { 29 | masterPrivateKey, // <= place in a password input field (password manager) 30 | privateKeys: {owner, active}, // <= derived from masterPrivateKey 31 | publicKeys: {owner, active} // <= derived from masterPrivateKey 32 | } 33 | */ 34 | function generateMasterKeys(masterPrivateKey = null) { 35 | let master 36 | if(masterPrivateKey == null) { 37 | master = PrivateKey.randomKey() 38 | } else { 39 | assert(validate.isMasterKey(masterPrivateKey), 'masterPrivateKey') 40 | master = PrivateKey(masterPrivateKey.substring('PW'.length)) 41 | } 42 | 43 | return Promise.resolve(master).then(master => { 44 | const ownerPrivate = master.getChildKey('owner') 45 | const activePrivate = ownerPrivate.getChildKey('active') 46 | 47 | return { 48 | masterPrivateKey: `PW${master.toWif()}`, 49 | privateKeys: { 50 | owner: ownerPrivate.toWif(), 51 | active: activePrivate.toWif() 52 | }, 53 | publicKeys: { 54 | owner: ownerPrivate.toPublic().toString(), 55 | active: activePrivate.toPublic().toString() 56 | } 57 | } 58 | }) 59 | } 60 | 61 | /** @typedef {Object} keyPathAuth */ 62 | 63 | /** 64 | @private 65 | 66 | Recursively create keyPath using the parent links in the blockchain 67 | account's permission object. Under this keyPath, store the full 68 | required_auth structure for later inspection. 69 | 70 | @arg {accountPermissions} 71 | @return {object} 72 | */ 73 | function authsByPath(accountPermissions) { 74 | assert(Array.isArray(accountPermissions), 'accountPermissions is an array') 75 | accountPermissions.forEach(perm => assert.equal(typeof perm, 'object', 76 | 'accountPermissions is an array of objects')) 77 | 78 | const byName = {} // Index by permission name 79 | accountPermissions.forEach(perm => { 80 | byName[perm.perm_name] = perm 81 | }) 82 | 83 | function parentPath(perm, stack = []) { 84 | stack.push(perm.parent) 85 | const parent = byName[perm.parent] 86 | if(parent) { 87 | return parentPath(parent, stack) 88 | } 89 | return stack 90 | } 91 | 92 | const auths = {} 93 | accountPermissions.forEach(perm => { 94 | if(perm.parent === '') { 95 | auths[perm.perm_name] = perm.required_auth 96 | } else { 97 | let pathStr = parentPath(perm).reverse().join('/') 98 | if(pathStr.charAt(0) === '/') { 99 | pathStr = pathStr.substring(1) 100 | } 101 | pathStr = `${pathStr}/${perm.perm_name}` 102 | if(pathStr.indexOf('owner/active/') === 0) { 103 | // active is always a child of owner 104 | pathStr = pathStr.substring('owner/'.length) 105 | } else if(pathStr === 'owner/active') { 106 | // owner is implied, juse use active 107 | pathStr = 'active' 108 | } 109 | auths[pathStr] = perm.required_auth 110 | } 111 | }) 112 | 113 | return auths 114 | } 115 | 116 | /** 117 | @private see keystore.deriveKeys 118 | 119 | Derive missing intermediate keys and paths for the given path. 120 | 121 | @return {Array} [{path, privateKey}] newly derived keys or empty array (keys already 122 | exist or can't be derived). 123 | */ 124 | function deriveKeys(path, wifsByPath) { 125 | validate.path(path) 126 | assert.equal(typeof wifsByPath, 'object', 'wifsByPath') 127 | 128 | if(wifsByPath[path]) { 129 | return [] 130 | } 131 | 132 | let maxLen = 0 133 | let bestPath = 0 134 | 135 | const paths = Object.keys(wifsByPath) 136 | for(const wifPath in wifsByPath) { 137 | if(path.indexOf(`${wifPath}/`) === 0) { 138 | if(wifPath.length > maxLen){ 139 | maxLen = wifPath.length 140 | bestPath = wifPath 141 | } 142 | } 143 | } 144 | 145 | if(!bestPath) { 146 | return [] 147 | } 148 | 149 | const newKeys = [] 150 | let extendedPath = bestPath 151 | const wif = wifsByPath[bestPath] 152 | assert(!!wif, 'wif') 153 | let extendedPrivate = PrivateKey(wif) 154 | for(const extend of path.substring(bestPath.length + '/'.length).split('/')) { 155 | extendedPrivate = extendedPrivate.getChildKey(extend) 156 | extendedPath += `/${extend}` 157 | newKeys.push({ 158 | path: extendedPath, 159 | privateKey: extendedPrivate 160 | }) 161 | } 162 | 163 | return newKeys 164 | } 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /src/jsdoc-types.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | Public Key (EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV) 4 | 5 | @typedef {string} pubkey 6 | */ 7 | 8 | /** 9 | [Wallet Import Format](https://en.bitcoin.it/wiki/Wallet_import_format) 10 | (5JMx76CTUTXxpAbwAqGMMVzSeJaP5UVTT5c2uobcpaMUdLAphSp) 11 | 12 | @typedef {string} wif 13 | */ 14 | 15 | /** 16 | Private key object from eosjs-ecc. 17 | 18 | @typedef {object} privateKey 19 | */ 20 | 21 | /** 22 | Master Private Key. Strong random key used to derive all other key types. 23 | Has a 'PW' prefix followed by a valid wif. (`'PW' + wif === 24 | 'PW5JMx76CTUTXxpAbwAqGMMVzSeJaP5UVTT5c2uobcpaMUdLAphSp'`) 25 | 26 | @typedef {string} masterPrivateKey 27 | */ 28 | 29 | /** 30 | Cold storage / recovery key. Has authoritiy to do everything including 31 | account recovery. 32 | 33 | @typedef {wif} owner 34 | */ 35 | 36 | /** 37 | Spending key. Has the authority to do everything except account recovery. 38 | 39 | @typedef {wif} active 40 | */ 41 | 42 | /** 43 | Master private key or one of its derived private keys. 44 | 45 | @typedef {masterPrivateKey|wif} parentPrivateKey 46 | */ 47 | 48 | /** 49 | Signing Keys and(or) Accounts each having a weight that when matched in 50 | the signatures should accumulate to meet or exceed the auth's total threshold. 51 | 52 | @typedef {object} auth 53 | 54 | @example required_auth: { 55 | threshold: 1, 56 | keys: [{ 57 | key: 'EOS78Cs5HPKY7HKHrSMnR76uj7yeajPuNwSH1Fsria3sJuufwE3Zd', 58 | weight: 1 59 | } 60 | ], 61 | accounts: [] 62 | } 63 | */ 64 | 65 | /** 66 | Permissions object from Eos blockchain obtained via get_account. 67 | 68 | See chain API get_account => account.permissions. 69 | 70 | @typedef {object} accountPermissions 71 | 72 | @example const accountPermissions = [{ 73 | perm_name: 'active', 74 | parent: 'owner', 75 | required_auth: { 76 | threshold: 1, 77 | keys: [{ 78 | key: 'EOS78Cs5HPKY7HKHrSMnR76uj7yeajPuNwSH1Fsria3sJuufwE3Zd', 79 | weight: 1 80 | } 81 | ], 82 | accounts: [] 83 | } 84 | },{ 85 | perm_name: 'mypermission', 86 | parent: 'active', 87 | required_auth: { 88 | threshold: 1, 89 | keys: [{ 90 | key: 'EOS78Cs5HPKY7HKHrSMnR76uj7yeajPuNwSH1Fsria3sJuufwE3Zd', 91 | weight: 1 92 | } 93 | ], 94 | accounts: [] 95 | } 96 | },{ 97 | perm_name: 'owner', 98 | parent: '', 99 | required_auth: { 100 | threshold: 1, 101 | keys: [{ 102 | key: 'EOS78Cs5HPKY7HKHrSMnR76uj7yeajPuNwSH1Fsria3sJuufwE3Zd', 103 | weight: 1 104 | } 105 | ], 106 | accounts: [] 107 | } 108 | }] 109 | */ 110 | 111 | /** 112 | @see [validate.path(keyPath)](./validate.js) 113 | 114 | @typedef {string} keyPath 115 | @example 'owner', 'active', 'active/mypermission' 116 | */ 117 | 118 | /** 119 | An expanded version of a private key, a keypath ('active/mypermission'), 120 | and its calculated public key (for performance reasons). 121 | 122 | @typedef {object} keyPathPrivate 123 | @property {wif} wif 124 | @property {pubkey} pubkey 125 | @property {keyPath} path 126 | */ 127 | 128 | /** 129 | Glob matching expressions (`active`, `active/**`, `owner/*`). 130 | 131 | @see https://www.npmjs.com/package/glob#glob-primer - syntax 132 | @see https://www.npmjs.com/package/minimatch - implementation 133 | 134 | @typedef {string} minimatch 135 | */ 136 | 137 | /** 138 | Key derviation path (`owner`, `active/*`, `active/**`, `active/mypermission`) 139 | @typedef {minimatch} keyPathMatcher 140 | */ 141 | 142 | /** 143 | A URI without the prefixing scheme, host, port. 144 | 145 | @typedef {string} uriData 146 | @example '/producers', '/account_recovery#name=..' 147 | */ 148 | 149 | /** 150 | A valid regular expression string. The provided string is modified when 151 | it is converted to a RegExp object: 152 | 153 | - A start of line match is implied (`^` is always added, do not add one) 154 | - Unless the uriPath ends with `$`, automatically matches query parameters 155 | and fragment (hash tag info). 156 | - The RegExp that is created is always case-insensitive to help a 157 | non-canonical path match. Uri paths should be canonical. 158 | 159 | @typedef {string} uriMatcher 160 | 161 | @example '/(transfer|contracts)', '/bare-uri$' 162 | @example function createPathMatcher(path) { 163 | // Ensure match starts at the begining 164 | const prefix = '^' 165 | 166 | // If path matcher does not end with $, allow Uri query and fragment 167 | const suffix = path.charAt(path.length - 1) === '$' ? '' : '\/?([\?#].*)?$' 168 | 169 | // Path matches are case in-sensitive 170 | return new RegExp(prefix + path + suffix, 'i') 171 | } 172 | */ 173 | 174 | /** 175 | @typedef {uriMatcher|Array} uriMatchers 176 | */ 177 | 178 | /** 179 | @typedef {Object} uriRule 180 | @example { 181 | 'owner': '/account_recovery$', // <= $ prevents query or fragment params 182 | 'active': ['/transfer', '/contracts'] 183 | } 184 | */ 185 | 186 | /** 187 | @typedef {Object} uriRules 188 | 189 | Define rules that say which private keys may exist within given locations 190 | of the application. If a rule is not found or does not match, the keystore 191 | will remove the key. The UI can prompt the user to obtain the needed key 192 | again. 193 | 194 | For any non-trivial configuration, implementions should create a unit test 195 | that will test the actual configuration used in the application 196 | (see `./uri-rules.test.js` for a template). 197 | 198 | Paths imply that active is always derived from owner. So, instead of writing 199 | `owner/active/**` the path must be written as `active/**`. 200 | 201 | @example uriRules = { // Hypothetical examples 202 | // Allow owner and all derived keys (including active) 203 | 'owner': '/account_recovery', 204 | 205 | // Allow active key (and any derived child) 206 | 'active': '/(transfer|contracts)', 207 | 208 | // Allow keys derived from active (but not active itself) 209 | 'active/**': '/producers', 210 | 211 | // If user-provided or unaudited content could be loaded in a given 212 | // page, make sure the root active key is not around on these pages. 213 | 'active/**': '/@[\\w\\.]' 214 | } 215 | */ 216 | -------------------------------------------------------------------------------- /src/keystore.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const assert = require('assert') 3 | const {accountPermissions, checkKeySet} = require('./test-utils.js') 4 | const {PrivateKey, Signature} = require('eosjs-ecc') 5 | const ecc = require('eosjs-ecc') 6 | const config = require('./config') 7 | 8 | const Keystore = require('./keystore.js') 9 | 10 | let pathname 11 | let historyListener 12 | 13 | config.history = { 14 | get location() { 15 | return { pathname, search: '', hash: '' } 16 | }, 17 | get listen() { 18 | return callback => { 19 | historyListener = callback 20 | } 21 | } 22 | } 23 | 24 | let keystore 25 | 26 | function reset() { 27 | if(keystore) { 28 | keystore.logout() 29 | } 30 | Keystore.wipeAll() 31 | } 32 | 33 | describe('Keystore', () => { 34 | before(() => PrivateKey.initialize()) 35 | const master = 'PW5JMx76CTUTXxpAbwAqGMMVzSeJaP5UVTT5c2uobcpaMUdLAphSp' 36 | const master2 = 'PW5JKvXxVvnFgyHZSmGASQfnmya3QrgdQ46ydQn7CzVB6RNT3nCnu' 37 | 38 | beforeEach(() => { 39 | pathname = '/' 40 | reset() 41 | }) 42 | 43 | afterEach(() => { 44 | reset() 45 | }) 46 | 47 | it('create', () => { 48 | Keystore('uid') 49 | }) 50 | 51 | it('initialize from disk', async function() { 52 | keystore = Keystore('myaccount') 53 | 54 | const privateKey = await PrivateKey.randomKey() 55 | const wif = privateKey.toWif() 56 | const pubkey = privateKey.toPublic().toString() 57 | 58 | keystore.addKey('active/mypermission', wif, true/*disk*/) 59 | 60 | keystore = Keystore('myaccount') 61 | assert.deepEqual(keystore.getKeyPaths(), { 62 | pubkey: ['active/mypermission'], 63 | wif: ['active/mypermission'] 64 | }) 65 | }) 66 | 67 | it('saveKeyMatches', () => { 68 | keystore = Keystore('myaccount') 69 | 70 | keystore.deriveKeys({ 71 | parent: master, 72 | accountPermissions, 73 | saveKeyMatches: 'active{,/**}' 74 | }) 75 | 76 | keystore = Keystore('myaccount') 77 | assert.deepEqual(keystore.getKeyPaths(), { 78 | pubkey: ['active', 'active/mypermission'], 79 | wif: ['active', 'active/mypermission'] 80 | }) 81 | }) 82 | 83 | describe('login', () => { 84 | it('active key (without blockchain permission)', async function() { 85 | keystore = Keystore('uid') 86 | const privateKey = await PrivateKey.randomKey() 87 | const wif = privateKey.toWif() 88 | 89 | keystore.deriveKeys({parent: wif}) 90 | 91 | const keyPaths = ['active'] 92 | 93 | assert.deepEqual( 94 | keystore.getKeyPaths(), 95 | {pubkey: keyPaths, wif: keyPaths} 96 | ) 97 | }) 98 | 99 | it('master key (without blockchain permission)', () => { 100 | keystore = Keystore('uid') 101 | 102 | keystore.deriveKeys({parent: master}) 103 | 104 | const keyPaths = ['active'] 105 | 106 | assert.deepEqual( 107 | keystore.getKeyPaths(), 108 | {pubkey: keyPaths, wif: keyPaths} 109 | ) 110 | }) 111 | 112 | it('login changed', () => { 113 | keystore = Keystore('uid') 114 | keystore.deriveKeys({parent: master}) 115 | keystore.deriveKeys({parent: master2}) 116 | }) 117 | 118 | it('saved login changed', () => { 119 | keystore = Keystore('uid') 120 | keystore.deriveKeys({parent: master, saveKeyMatches: 'active'}) 121 | keystore.deriveKeys({parent: master2}) 122 | }) 123 | }) 124 | 125 | describe('invalid login', () => { 126 | it('account permissions', () => { 127 | keystore = Keystore('uid') 128 | assert.throws( 129 | () => {keystore.deriveKeys({parent: master2, accountPermissions})}, 130 | /invalid login/ 131 | ) 132 | }) 133 | 134 | it('account permissions early', () => { 135 | keystore = Keystore('uid') 136 | keystore.deriveKeys({parent: master, accountPermissions}) 137 | assert.throws(() => {keystore.deriveKeys({parent: master2})}, /invalid login/) 138 | }) 139 | 140 | it('account permissions later', () => { 141 | keystore = Keystore('uid') 142 | keystore.deriveKeys({parent: master}) 143 | assert.throws( 144 | () => {keystore.deriveKeys({parent: master2, accountPermissions})}, 145 | /invalid login/ 146 | ) 147 | }) 148 | }) 149 | 150 | for(const role of ['active', 'owner']) { 151 | it(`block ${role} key re-use`, () => { 152 | keystore = Keystore('uid') 153 | const perm = JSON.parse(JSON.stringify(accountPermissions)) 154 | 155 | const rolePos = role === 'active' ? 0 : role === 'owner' ? 2 : -1 156 | const wif = perm[rolePos].required_auth.keys[0].key 157 | 158 | perm[(rolePos + 1) % perm.length].required_auth.keys[0].key = wif 159 | 160 | assert.throws(() => { 161 | keystore.deriveKeys({parent: master, accountPermissions: perm}) 162 | }, / key reused in authority/) 163 | // }, new RegExp(`${role} key reused in authority`)) 164 | }) 165 | } 166 | 167 | it('derive all active permisison keys', () => { 168 | keystore = Keystore('uid') 169 | keystore.deriveKeys({parent: master, accountPermissions}) 170 | 171 | const keyPaths = ['active', 'active/mypermission'] 172 | assert.deepEqual(keystore.getKeyPaths(), {pubkey: keyPaths, wif: keyPaths}) 173 | }) 174 | 175 | it('get derived active public keys', () => { 176 | keystore = Keystore('uid') 177 | keystore.deriveKeys({parent: master, accountPermissions}) 178 | 179 | assert.deepEqual(keystore.getPublicKeys(), [ 180 | 'EOS7vgT3ZsuUxWH1tWyqw6cyKqKhPjUFbonZjyrrXqDauty61SrYe', 181 | 'EOS5MiUJEXxjJw6wUcE6yUjxpATaWetubAGUJ1nYLRSHYPpGCJ8ZU' 182 | ]) 183 | }) 184 | 185 | it('low permission page master login', () => { 186 | const uriRules = { 187 | 'active/mypermission': '/' 188 | } 189 | 190 | keystore = Keystore('uid', {uriRules}) 191 | keystore.deriveKeys({parent: master, accountPermissions}) 192 | 193 | // Make sure "active" is not avabile, only active/mypermisison 194 | const keyPaths = ['active/mypermission'] 195 | assert.deepEqual( 196 | keystore.getKeyPaths(), 197 | {pubkey: keyPaths, wif: keyPaths} 198 | ) 199 | }) 200 | 201 | it('low permission page login', () => { 202 | const uriRules = { 203 | 'active/mypermission': '/' 204 | } 205 | 206 | const mypermission = 207 | PrivateKey(master.substring(2)) 208 | .getChildKey('owner') 209 | .getChildKey('active') 210 | .getChildKey('mypermission') 211 | 212 | keystore = Keystore('uid', {uriRules}) 213 | 214 | // Active key is not required, just the lower mypermission key 215 | keystore.deriveKeys({parent: mypermission, accountPermissions}) 216 | 217 | const keyPaths = ['active/mypermission'] 218 | assert.deepEqual( 219 | keystore.getKeyPaths(), 220 | {pubkey: keyPaths, wif: keyPaths} 221 | ) 222 | }) 223 | 224 | it('uri rules history', () => { 225 | const uriRules = { 226 | 'owner': '/account_recovery', 227 | 'active': '/transfers' 228 | } 229 | 230 | keystore = Keystore('uid', {uriRules}) 231 | 232 | pathname = '/' 233 | assert.throws(() => 234 | keystore.deriveKeys({parent: master, accountPermissions}), 235 | /invalid login for page/ 236 | ) 237 | 238 | pathname = '/account_recovery' 239 | 240 | keystore.deriveKeys({parent: master, accountPermissions}) 241 | 242 | assert.deepEqual(keystore.getKeyPaths(), { 243 | pubkey: ['active', 'active/mypermission', 'owner'], 244 | wif: ['active', 'active/mypermission', 'owner'] 245 | }) 246 | 247 | pathname = '/transfers' 248 | historyListener() // trigger history change event 249 | assert.deepEqual(keystore.getKeyPaths(), { 250 | pubkey: ['active', 'active/mypermission', 'owner'], 251 | wif: ['active', 'active/mypermission'] 252 | }) 253 | }) 254 | 255 | it('timeout', (done) => { 256 | const config = { 257 | uriRules: {'**': '.*'}, 258 | timeoutInMin: .0001, 259 | timeoutKeyPaths: ['owner', 'owner/**'] 260 | } 261 | 262 | keystore = Keystore('myaccount', config) 263 | keystore.deriveKeys({parent: master, accountPermissions}) 264 | 265 | const before = ['active', 'active/mypermission', 'owner'] 266 | assert.deepEqual(keystore.getKeyPaths(), {pubkey: before, wif: before}) 267 | 268 | function timeout() { 269 | const after = ['active', 'active/mypermission'] 270 | assert.deepEqual(keystore.getKeyPaths(), {pubkey: before, wif: after}) 271 | done() 272 | } 273 | 274 | setTimeout(() => {timeout()}, .003 * min) 275 | }) 276 | 277 | it('saveKeyMatches disk security', () => { 278 | keystore = Keystore('myaccount') 279 | assert.throws(() => 280 | keystore.deriveKeys({parent: master, saveKeyMatches: 'owner'}), 281 | /do not save owner key to disk/ 282 | ) 283 | }) 284 | 285 | it('addKey disk security', async function() { 286 | keystore = Keystore('myaccount') 287 | 288 | const disk = true 289 | const privateKey = await PrivateKey.randomKey() 290 | const save = path => keystore.addKey(path, privateKey, disk) 291 | 292 | assert.throws(() => {save('owner')}, /not be stored on disk/) 293 | assert.throws(() => {save('owner/cold')}, /not be stored on disk/) 294 | 295 | assert.doesNotThrow(() => {save('active')}) 296 | assert.doesNotThrow(() => {save('active/mypermission')}) 297 | }) 298 | 299 | it('save key', async function() { 300 | keystore = Keystore('myaccount') 301 | const save = key => keystore.addKey('active', key) 302 | 303 | const privateKey = await PrivateKey.randomKey() 304 | const wif = privateKey.toWif() 305 | const publicKey = privateKey.toPublic() 306 | const pubkey = publicKey.toString() 307 | 308 | assert.deepEqual(save(privateKey), {wif, pubkey, dirty: true}) 309 | assert.deepEqual(save(wif), {wif, pubkey, dirty: false}) 310 | assert.deepEqual(save(publicKey), {pubkey, dirty: false}) 311 | assert.deepEqual(save(pubkey), {pubkey, dirty: false}) 312 | }) 313 | 314 | it('save and get keys', async function() { 315 | keystore = Keystore('myaccount', { 316 | uriRules: {'**': '.*'} // allow owner key 317 | }) 318 | 319 | const privateKey = await PrivateKey.randomKey() 320 | const wif = privateKey.toWif() 321 | const pubkey = privateKey.toPublic().toString() 322 | 323 | assert.deepEqual(keystore.addKey('owner', wif), { 324 | wif, 325 | pubkey, 326 | dirty: true 327 | }) 328 | 329 | assert.deepEqual(keystore.getKeyPaths(), { 330 | pubkey: ['owner'], 331 | wif: ['owner'] 332 | }) 333 | 334 | assert.deepEqual(keystore.getPublicKeys(), [pubkey]) 335 | assert.deepEqual(keystore.getPublicKeys('owner'), [pubkey]) 336 | 337 | assert.equal(keystore.getPublicKey('owner'), pubkey) 338 | assert.equal(keystore.getPrivateKey('owner'), wif) 339 | 340 | const cold = privateKey.getChildKey('cold') 341 | assert.equal(keystore.getPublicKey('owner/cold'), cold.toPublic().toString()) 342 | assert.equal(keystore.getPrivateKey('owner/cold'), cold.toWif()) 343 | 344 | // keep the owner key above, add public key active/other 345 | assert.deepEqual(keystore.addKey('active/other', pubkey), { 346 | pubkey, 347 | dirty: true 348 | }) 349 | 350 | assert.deepEqual(keystore.getKeyPaths(), { 351 | pubkey: ['active/other', 'owner'], 352 | wif: ['owner'] 353 | }) 354 | 355 | // add the private key for active/mypermission 356 | assert.deepEqual(keystore.addKey('active/mypermission', wif), { 357 | dirty: true, 358 | pubkey, 359 | wif 360 | }) 361 | 362 | // now we have everything: owner, active/mypermission 363 | assert.deepEqual(keystore.getKeyPaths(), { 364 | pubkey: ['active/mypermission', 'active/other', 'owner'], 365 | wif: ['active/mypermission', 'owner'] 366 | }) 367 | }) 368 | 369 | it('removeKeys', async function() { 370 | keystore = Keystore('myaccount') 371 | 372 | const privateKey = await PrivateKey.randomKey() 373 | const wif = privateKey.toWif() 374 | const pubkey = privateKey.toPublic().toString() 375 | 376 | assert.deepEqual(keystore.addKey('active', wif), {wif, pubkey, dirty: true}) 377 | 378 | keystore.removeKeys('active', true/*keepPublicKeys*/) 379 | assert.deepEqual(keystore.getKeyPaths(), { 380 | pubkey: ['active'], 381 | wif: [] 382 | }) 383 | 384 | keystore.removeKeys(new Set(['active']), false/*keepPublicKeys*/) 385 | assert.deepEqual(keystore.getKeyPaths(), {pubkey: [], wif: []}) 386 | }) 387 | 388 | it('signSharedSecret', async function() { 389 | // server creates "one time" random key pairs 390 | 391 | const oneTimeServerPrivate = await PrivateKey.unsafeRandomKey() // server 392 | const oneTimeServerPublic = ecc.privateToPublic(oneTimeServerPrivate) // server 393 | 394 | const clientKeystore = Keystore('myaccount', {uriRules: {'**': '.*'}}) 395 | 396 | clientKeystore.deriveKeys({ 397 | parent: master, 398 | accountPermissions // .. all 3 keys 399 | }) 400 | 401 | // client receives oneTimeServerPublic 402 | 403 | // client creates "one time" random key pairs (in signSharedSecret) 404 | const clientProof = await clientKeystore.signSharedSecret(oneTimeServerPublic) 405 | 406 | // server receives clientProof 407 | 408 | // clientProof is a collection of signatures and a one time public 409 | const sharedSecret = oneTimeServerPrivate.getSharedSecret(clientProof.oneTimePublic) 410 | 411 | const recoveredPubkeys = clientProof.signatures.map(signature => 412 | ecc.recover(signature, sharedSecret) // server 413 | ) 414 | 415 | assert.equal(recoveredPubkeys.length, 3, 'expecting 3 keys') 416 | assert.deepEqual( 417 | clientKeystore.getPublicKeys().sort(), 418 | recoveredPubkeys.sort() 419 | ) 420 | 421 | Keystore.wipeAll() 422 | }) 423 | 424 | it('keyProvider', () => { 425 | keystore = Keystore('myaccount') 426 | keystore.deriveKeys({parent: master}) 427 | 428 | const pubkeys = keystore.keyProvider({publicKeyPathMatcher: 'active'}) 429 | 430 | assert.equal(pubkeys.length, 1, 'pubkeys.length') 431 | 432 | const wifs = keystore.keyProvider({pubkeys}) 433 | assert.equal(wifs.length, 1, 'pubkeys.length') 434 | assert.equal(ecc.privateToPublic(wifs[0]), pubkeys[0]) 435 | 436 | keystore.removeKeys('active') 437 | assert.throws(() => {keystore.keyProvider({pubkeys})}, 438 | /login with your 'active' key/) 439 | 440 | keystore.removeKeys('active', false /* keepPublicKeys */) 441 | assert.throws(() => {keystore.keyProvider({pubkeys})}, 442 | /missing public key EOS.*/) 443 | }) 444 | 445 | it('wipe all', async function() { 446 | keystore = Keystore('myaccount') 447 | keystore.addKey('active/mypermission', await PrivateKey.randomKey(), true/*disk*/) 448 | 449 | Keystore.wipeAll() 450 | 451 | keystore = Keystore('myaccount') 452 | assert.deepEqual(keystore.getKeyPaths(), {pubkey: [], wif: []}) 453 | }) 454 | 455 | it('logout', async function() { 456 | keystore = Keystore('myaccount') 457 | 458 | const privateKey = await PrivateKey.randomKey() 459 | const wif = privateKey.toWif() 460 | const pubkey = privateKey.toPublic().toString() 461 | 462 | // saves the public keys 463 | keystore.deriveKeys({parent: `PW${wif}`}) 464 | keystore.logout() 465 | assert.equal(keystore.getKeys().length, 0, 'getKeys().length') 466 | 467 | // use a new password 468 | keystore.deriveKeys({parent: master}) 469 | assert.equal(keystore.getKeys().length, 1, 'getKeys().length') 470 | 471 | const keyPathStore2 = Keystore('myaccount') 472 | assert.deepEqual(keyPathStore2.getKeyPaths(), { 473 | pubkey: [], 474 | wif: [] 475 | }) 476 | }) 477 | }) 478 | 479 | const sec = 1000, min = 60 * sec 480 | -------------------------------------------------------------------------------- /src/keystore.js: -------------------------------------------------------------------------------- 1 | /** @module Keystore */ 2 | 3 | const assert = require('assert') 4 | const {PrivateKey, Signature} = require('eosjs-ecc') 5 | const ecc = require('eosjs-ecc') 6 | const minimatch = require('minimatch') 7 | 8 | const Keygen = require('./keygen') 9 | const UriRules = require('./uri-rules') 10 | const validate = require('./validate') 11 | const globalConfig = require('./config') 12 | 13 | const {localStorage} = require('./config') 14 | const userStorage = require('./keypath-utils')('kstor') 15 | 16 | module.exports = Keystore 17 | 18 | /** 19 | Provides private key management and storage and tooling to limit exposure 20 | of private keys as much as possible. 21 | 22 | Although multiple root keys may be stored, this key store was designed with 23 | the idea that all keys for a given `accountName` are derive from a single 24 | root key (the master private key). 25 | 26 | This keystore does not query the blockchain or any external services. 27 | Removing keys here does not affect the blockchain. 28 | 29 | @arg {string} accountName - Blockchain account name that will act as the 30 | container for a key and all derived child keys. 31 | 32 | @arg {object} [config] 33 | 34 | @arg {number} [config.timeoutInMin = 10] - upon timeout, remove keys 35 | matching timeoutKeyPaths. 36 | 37 | @arg {number} [config.timeoutKeyPaths = ['owner', 'owner/**']] - by default, 38 | expire only owner and owner derived children. If the default uriRules are 39 | used this actually has nothing to delete. 40 | 41 | @arg {uriRules} [config.uriRules] - Specify which type of private key will 42 | be available on certain pages of the application. Lock it down as much as 43 | possible and later re-prompt the user if a key is needed. Default is to 44 | allow active (`active`) and all active derived keys (`active/**`) everywhere 45 | (`.*`). 46 | 47 | @arg {boolean} [keepPublicKeys = true] - Enable for better UX; show users keys they 48 | have access too without requiring them to login. Logging in brings a 49 | private key online which is not necessary to see public information. 50 | 51 | The UX should implement this behavior in a way that is clear public keys 52 | are cached before enabling this feature. 53 | @example config = { 54 | uriRules: { 55 | 'active': '.*', 56 | 'active/**': '.*' 57 | }, 58 | timeoutInMin: 10, 59 | timeoutKeyPaths: [ 60 | 'owner', 61 | 'owner/**' 62 | ], 63 | keepPublicKeys: true 64 | } 65 | */ 66 | function Keystore(accountName, config = {}) { 67 | assert.equal(typeof accountName, 'string', 'accountName') 68 | assert.equal(typeof config, 'object', 'config') 69 | 70 | const configDefaults = { 71 | uriRules: { 72 | 'active': '.*', 73 | 'active/**': '.*' 74 | }, 75 | timeoutInMin: 10, 76 | timeoutKeyPaths: [ 77 | 'owner', 78 | 'owner/**' 79 | ], 80 | keepPublicKeys: true 81 | } 82 | 83 | config = Object.assign({}, configDefaults, config) 84 | 85 | const uriRules = UriRules(config.uriRules) 86 | 87 | /** @private */ 88 | const state = {} 89 | 90 | let expireAt, expireInterval 91 | let unlistenHistory 92 | 93 | // Initialize state from localStorage 94 | userStorage.query(localStorage, [accountName, 'kpath'], ([path, pubkey], wif) => { 95 | const storageKey = userStorage.createKey(accountName, 'kpath', path, pubkey) 96 | state[storageKey] = wif 97 | }) 98 | 99 | /** 100 | Login or derive and save private keys. This may be called from a login 101 | action. Keys may be removed as during Uri navigation or when calling 102 | logout. 103 | 104 | @arg {object} params 105 | @arg {parentPrivateKey} params.parent - Master password (masterPrivateKey), 106 | active, owner, or other permission key. 107 | 108 | @arg {Array} [params.saveKeyMatches] - These private 109 | keys will be saved to disk. (example: `active`). 110 | 111 | @arg {accountPermissions} [params.accountPermissions] - Permissions object 112 | from Eos blockchain via get_account. This is used to validate the parent 113 | and derive additional permission keys. This allows this keystore to detect 114 | incorrect passwords early before trying to sign a transaction. 115 | 116 | See Chain API `get_account => account.permissions`. 117 | 118 | @throws {Error} 'invalid login' 119 | */ 120 | function deriveKeys({ 121 | parent, 122 | saveKeyMatches = [], 123 | accountPermissions 124 | }) { 125 | keepAlive() 126 | 127 | assert(parent != null, 'parent is a master password or private key') 128 | 129 | const keyType = validate.keyType(parent) 130 | assert(/master|wif|privateKey/.test(keyType), 131 | 'parentPrivateKey is a masterPrivateKey or private key') 132 | 133 | if(typeof saveKeyMatches === 'string') { 134 | saveKeyMatches = [saveKeyMatches] 135 | } 136 | 137 | saveKeyMatches.forEach(m => { 138 | if(minimatch('owner', m)) { 139 | throw new Error('do not save owner key to disk') 140 | } 141 | // if(minimatch('active', m)) { 142 | // throw new Error('do not save active key to disk') 143 | // } 144 | }) 145 | 146 | assert(typeof accountPermissions === 'object' || accountPermissions == null, 147 | 'accountPermissions is an optional object') 148 | 149 | if(!unlistenHistory) { 150 | unlistenHistory = globalConfig.history.listen(() => { 151 | keepAlive() 152 | 153 | // Prevent certain private keys from being available to high-risk pages. 154 | const paths = getKeyPaths().wif 155 | const pathsToPurge = uriRules.check(currentUriPath(), paths).deny 156 | removeKeys(pathsToPurge) 157 | }) 158 | } 159 | 160 | if(!expireInterval) { 161 | if(config.timeoutInMin != null) { 162 | function tick() { 163 | if(timeUntilExpire() === 0) { 164 | removeKeys(config.timeoutKeyPaths) 165 | clearInterval(expireInterval) 166 | expireInterval = null 167 | } 168 | } 169 | 170 | expireInterval = setInterval(tick, config.timeoutInMin * min) 171 | } 172 | } 173 | 174 | // cache 175 | if(!accountPermissions) { 176 | const permissions = 177 | userStorage.get(localStorage, [accountName, 'permissions']) 178 | 179 | if(permissions) { 180 | accountPermissions = JSON.parse(permissions) 181 | } 182 | } 183 | 184 | // cache pubkey (that is a slow calculation) 185 | const Keypair = privateKey => ({ 186 | privateKey, 187 | pubkey: privateKey.toPublic().toString() 188 | }) 189 | 190 | // blockchain permission format 191 | const perm = (parent, perm_name, pubkey) => ({ 192 | perm_name, parent, required_auth: {keys: [{key: pubkey}]} 193 | }) 194 | 195 | // Know if this is stubbed in next (don't cache later) 196 | const isPermissionStub = accountPermissions == null 197 | 198 | const parentKeys = {} 199 | if(keyType === 'master') { 200 | const masterPrivateKey = PrivateKey(parent.substring(2)) 201 | parentKeys.owner = Keypair(masterPrivateKey.getChildKey('owner')) 202 | parentKeys.active = Keypair(parentKeys.owner.privateKey.getChildKey('active')) 203 | if(!accountPermissions) { 204 | accountPermissions = [ 205 | perm('owner', 'active', parentKeys.active.pubkey), 206 | perm('', 'owner', parentKeys.owner.pubkey) 207 | ] 208 | } 209 | } else { 210 | if(accountPermissions) { 211 | // unknown for now.. 212 | parentKeys.other = Keypair(PrivateKey(parent)) 213 | } else { 214 | parentKeys.active = Keypair(PrivateKey(parent)) 215 | accountPermissions = [ 216 | perm('owner', 'active', parentKeys.active.pubkey) 217 | ] 218 | } 219 | } 220 | 221 | assert(accountPermissions, 'accountPermissions is required at this point') 222 | 223 | const authsByPath = Keygen.authsByPath(accountPermissions) 224 | 225 | // Don't allow key re-use 226 | function uniqueKeyByRole(role) { 227 | const auth = authsByPath[role] 228 | if(auth == null) { 229 | return 230 | } 231 | auth.keys.forEach(rolePub => { 232 | for(const other in authsByPath) { 233 | if(other === role) { 234 | continue 235 | } 236 | authsByPath[other].keys.forEach(otherPub => { 237 | if(otherPub.key === rolePub.key) { 238 | throw new Error(role + ' key reused in authority: ' + other) 239 | } 240 | }) 241 | } 242 | }) 243 | } 244 | uniqueKeyByRole('active') 245 | uniqueKeyByRole('owner') 246 | 247 | if(!isPermissionStub) { 248 | // cache 249 | userStorage.save( 250 | localStorage, 251 | [accountName, 'permissions'], 252 | JSON.stringify(accountPermissions), 253 | {immutable: false} 254 | ) 255 | } 256 | 257 | let keyUpdates = [], allow = false 258 | 259 | // check existing keys.. 260 | for(const path in authsByPath) { 261 | const auth = authsByPath[path] 262 | for(const parentPath in parentKeys) { 263 | const parentKey = parentKeys[parentPath] // owner, active, other 264 | if(auth.keys.find(k => k.key === parentKey.pubkey) != null) { 265 | keyUpdates.push({path, privateKey: parentKey.privateKey}) 266 | } 267 | } 268 | } 269 | 270 | if(keyUpdates.length === 0) { 271 | throw new Error('invalid login') 272 | } 273 | 274 | // Sync keyUpdates with storage .. 275 | function saveKeyUpdates() { 276 | // sort key updates so removeKeys will only remove children 277 | for(const {path, privateKey} of keyUpdates.sort()) { 278 | const disk = saveKeyMatches.find(m => minimatch(path, m)) != null 279 | const update = addKey(path, privateKey, disk) 280 | if(update) { 281 | allow = true 282 | if(update.dirty) { // blockchain key changed 283 | // remove so these will be re-derived 284 | const children = getKeys(`${path}/**`).map(k => k.path) 285 | removeKeys(children, false/*keepPublicKeys*/) 286 | } 287 | } 288 | } 289 | } 290 | 291 | saveKeyUpdates() 292 | 293 | // Gather up all known keys then derive children 294 | const wifsByPath = {} 295 | 296 | // After saveKeyUpdates, fetch the remaining allowed and valid private keys 297 | getKeys().filter(k => !!k.wif).forEach(k => { 298 | // getKeys => {path, pubkey, wif} 299 | wifsByPath[k.path] = k.wif 300 | }) 301 | 302 | // Combine existing keys in the keystore with any higher permission keys 303 | // in wifsByPath that may not exist after this function call. 304 | for(const {path, privateKey} of keyUpdates) { 305 | if(!wifsByPath[path]) { 306 | // These more secure keys could be used to derive less secure 307 | // child keys below. 308 | wifsByPath[path] = privateKey.toWif() 309 | } 310 | } 311 | 312 | keyUpdates = [] 313 | 314 | // Use all known keys in wifsByPath to derive all known children. 315 | 316 | // Why? As the user navigates any parent could get removed but the child 317 | // could still be allowed. Good thing we saved the children while we could. 318 | for(const path in authsByPath) { 319 | if(!wifsByPath[path]) { 320 | const keys = Keygen.deriveKeys(path, wifsByPath) 321 | if(keys.length) { 322 | const authorizedKeys = authsByPath[path].keys.map(k => k.key) 323 | for(const key of keys) { // {path, privateKey} 324 | const pubkey = key.privateKey.toPublic().toString() 325 | const inAuth = !!authorizedKeys.find(k => k === pubkey) 326 | if(inAuth) { // if user did not change this key 327 | wifsByPath[key.path] = key.privateKey.toWif() 328 | keyUpdates.push(key) 329 | } 330 | } 331 | } 332 | } 333 | } 334 | 335 | // save allowed children 336 | saveKeyUpdates() 337 | keyUpdates = [] 338 | 339 | if(!allow) { 340 | // uri rules blocked every key 341 | throw new Error('invalid login for page') 342 | } 343 | } 344 | 345 | /** 346 | @private see: keystore.deriveKeys 347 | 348 | Save a private or public key to the store in either RAM only or RAM and 349 | disk. Typically deriveKeys is used instead. 350 | 351 | @arg {keyPath} path - active/mypermission, owner, active, .. 352 | @arg {string} key - wif, pubkey, or privateKey 353 | @arg {boolean} toDisk - save to persistent storage (localStorage) 354 | 355 | @throws {AssertionError} path error or active, owner/* toDisk save attempted 356 | 357 | @return {object} {[wif], pubkey, dirty} or null (denied by uriRules) 358 | */ 359 | function addKey(path, key, toDisk = false) { 360 | validate.path(path) 361 | keepAlive() 362 | 363 | const keyType = validate.keyType(key) 364 | assert(/^wif|pubkey|privateKey$/.test(keyType), 365 | 'key should be a wif, public key string, or privateKey object') 366 | 367 | if(toDisk) { 368 | assert(path !== 'owner', 'owner key should not be stored on disk') 369 | assert(path.indexOf('owner/') !== 0, 370 | 'owner derived keys should not be stored on disk') 371 | 372 | // assert(path !== 'active', 'active key should not be stored on disk') 373 | } 374 | 375 | if(uriRules.deny(currentUriPath(), path).length) { 376 | // console.log('Keystore addKey denied: ', currentUriPath(), path); 377 | return null 378 | } 379 | 380 | const wif = 381 | keyType === 'wif' ? key : 382 | keyType === 'privateKey' ? ecc.PrivateKey(key).toWif() : 383 | null 384 | 385 | const pubkey = 386 | keyType === 'pubkey' ? ecc.PublicKey(key).toString() : 387 | keyType === 'privateKey' ? key.toPublic().toString() : 388 | ecc.privateToPublic(wif) 389 | 390 | assert(!!pubkey, 'pubkey') 391 | 392 | const storageKey = userStorage.createKey(accountName, 'kpath', path, pubkey) 393 | 394 | let dirty = userStorage.save(state, storageKey, wif, {clobber: false}) 395 | 396 | if(toDisk) { 397 | const saved = userStorage.save(localStorage, storageKey, wif, {clobber: false}) 398 | dirty = dirty || saved 399 | } 400 | 401 | return wif == null ? {pubkey, dirty} : {wif, pubkey, dirty} 402 | } 403 | 404 | /** 405 | Return paths for all available keys. Empty array is used if there are 406 | no keys. 407 | 408 | @return {object} {pubkey: Array, wif: Array} 409 | */ 410 | function getKeyPaths() { 411 | keepAlive() 412 | 413 | const pubs = new Set() 414 | const wifs = new Set() 415 | 416 | function query(store) { 417 | userStorage.query(store, [accountName, 'kpath'], ([path, pubkey], wif) => { 418 | pubs.add(path) 419 | if(wif != null) { 420 | wifs.add(path) 421 | } 422 | }) 423 | } 424 | query(state) 425 | query(localStorage) 426 | 427 | return {pubkey: Array.from(pubs).sort(), wif: Array.from(wifs).sort()} 428 | } 429 | 430 | /** 431 | Fetch or derive a public key. 432 | 433 | @arg {keyPath} 434 | @return {pubkey} or null 435 | */ 436 | function getPublicKey(path) { 437 | validate.path(path) 438 | const [key] = getKeys(path) 439 | return key ? key.pubkey : null 440 | } 441 | 442 | /** 443 | Return public keys for a path or path matcher. 444 | 445 | @arg {keyPath|keyPathMatcher} [keyPathMatcher = '**'] return all keys 446 | @return {Array} public keys or empty array 447 | */ 448 | function getPublicKeys(keyPathMatcher = '**') { 449 | return getKeys(keyPathMatcher).map(key => key.pubkey) 450 | } 451 | 452 | /** 453 | Fetch or derive a private key. 454 | @arg {keyPath} path 455 | @return {wif} or null (missing or not available for location) 456 | */ 457 | function getPrivateKey(path) { 458 | validate.path(path) 459 | const [key] = getKeys(path) 460 | return key ? key.wif : undefined 461 | } 462 | 463 | /** 464 | Return private keys for a path matcher or for a list of public keys. If a 465 | list of public keys is provided they will be validated ensuring they all 466 | have private keys to return. 467 | 468 | @arg {keyPathMatcher} [keyPathMatcher = '**'] default is to match all 469 | @arg {Array} [pubkeys = null] if specified, filter and require all 470 | 471 | @throws Error `login with your ${key.pubkey} key` 472 | @throws Error `missing public key ${key}` 473 | 474 | @return {Array} wifs or empty array 475 | */ 476 | function getPrivateKeys(keyPathMatcher = '**', pubkeys) { 477 | if(!pubkeys) { 478 | return getKeys(keyPathMatcher) 479 | .filter(key => key.wif != null) 480 | .map(key => key.wif) 481 | } 482 | 483 | if(pubkeys instanceof Array) { 484 | pubkeys = new Set(pubkeys) 485 | } 486 | 487 | assert(pubkeys instanceof Set, 'pubkeys should be a Set or Array') 488 | 489 | const keys = new Map() 490 | 491 | getKeys(keyPathMatcher).filter(key => pubkeys.has(key.pubkey)).forEach(key => { 492 | if(key.wif == null) { 493 | throw new Error(`login with your '${key.path}' key`) 494 | } 495 | keys.set(key.pubkey, key.wif) 496 | }) 497 | 498 | pubkeys.forEach(key => { 499 | if(!keys.has(key)) { 500 | // Was keepPublicKeys true? 501 | throw new Error(`missing public key ${key}`) 502 | } 503 | }) 504 | 505 | return Array.from(keys.values()) 506 | } 507 | 508 | /** 509 | Fetch or derive a key pairs. 510 | 511 | @arg {keyPath|keyPathMatcher} keyPathMatcher 512 | 513 | @return {Array} {path, pubkey, deny, wif} or empty array. 514 | Based on the Uri rules and current location, the deny could be set to true 515 | and the wif will be null. 516 | */ 517 | function getKeys(keyPathMatcher = '**') { 518 | keepAlive() 519 | 520 | const keys = new Map() 521 | 522 | // if we try to derive it below 523 | const wifsByPath = {} 524 | 525 | const isPath = validate.isPath(keyPathMatcher) 526 | 527 | function query(store) { 528 | userStorage.query(store, [accountName, 'kpath'], ([path, pubkey], wif) => { 529 | if(wif == null) { 530 | wif = wifsByPath[path] 531 | } else { 532 | wifsByPath[path] = wif 533 | } 534 | if(minimatch(path, keyPathMatcher)) { 535 | const result = {path, pubkey} 536 | result.deny = uriRules.deny(currentUriPath(), path).length !== 0 537 | result.wif = result.deny ? null : wif 538 | keys.set(path, result) 539 | if(isPath) { 540 | return false // break 541 | } 542 | } 543 | }) 544 | } 545 | 546 | query(state) 547 | if(isPath && keys.size) { 548 | // A path can match only one, found so no need to query localStorage 549 | return Array.from(keys.values()) 550 | } 551 | 552 | query(localStorage) 553 | if(!isPath) { 554 | // keyPathMatcher can not derive keys 555 | // .. the search is complete (found or not) 556 | return Array.from(keys.values()) 557 | } 558 | 559 | assert(isPath, 'keyPathMatcher should be a path at this point') 560 | 561 | let key = null 562 | 563 | // derive children (path) 564 | const path = keyPathMatcher 565 | const deriveKeys = Keygen.deriveKeys(path, wifsByPath) 566 | if(deriveKeys.length) { 567 | for(const derivedKey of deriveKeys) { // {path, privateKey} 568 | if(derivedKey.path === path) {// filter intermediate children 569 | const deny = uriRules.deny(currentUriPath(), path).length !== 0 570 | key = { 571 | path, 572 | pubkey: derivedKey.privateKey.toPublic().toString(), 573 | wif: deny ? null : derivedKey.privateKey.toWif(), 574 | deny 575 | } 576 | break 577 | } 578 | } 579 | } 580 | 581 | return key ? [key] : [] 582 | } 583 | 584 | /** 585 | @private Remove a key or keys from this key store (ram and disk). Typically 586 | logout is used instead. 587 | 588 | @arg {keyPathMatcher|Array|Set} 589 | 590 | @arg {boolean} keepPublicKeys 591 | */ 592 | function removeKeys(paths, keepPublicKeys = config.keepPublicKeys) { 593 | assert(paths != null, 'paths') 594 | if(typeof paths === 'string') { 595 | paths = [paths] 596 | } 597 | assert(paths instanceof Array || paths instanceof Set, 'paths is a Set or Array') 598 | for(const path of paths) { 599 | validate.path(path) 600 | } 601 | 602 | function clean(store, prefix) { 603 | for(const key in store) { 604 | if(key.indexOf(prefix) === 0) { 605 | if(keepPublicKeys) { 606 | store[key] = null 607 | } else { 608 | delete store[key] 609 | } 610 | } 611 | } 612 | } 613 | 614 | for(const path of paths) { 615 | const prefix = userStorage.createKey(accountName, 'kpath', path) 616 | clean(state, prefix) 617 | clean(localStorage, prefix) 618 | } 619 | } 620 | 621 | /** 622 | @typedef {object} oneTimeSignatures 623 | @property {Array} signatures - in hex 624 | @property {pubkey} oneTimePublic 625 | */ 626 | /** 627 | @arg {pubkey} otherPubkey 628 | @arg {keyPathMatcher} keyPathMatcher 629 | @return {Promise} 630 | */ 631 | function signSharedSecret(otherPubkey, keyPathMatcher = '**') { 632 | assert(/pubkey|PublicKey/.test(validate.keyType(otherPubkey)), 'otherPubkey') 633 | assert.equal(typeof keyPathMatcher, 'string', 'keyPathMatcher') 634 | 635 | return PrivateKey.randomKey().then(oneTimePrivate => { 636 | const sharedSecret = oneTimePrivate.getSharedSecret(otherPubkey) 637 | const signatures = getPrivateKeys(keyPathMatcher).map(wif => 638 | ecc.sign(sharedSecret, wif) 639 | ) 640 | const oneTimePublic = ecc.privateToPublic(oneTimePrivate) 641 | return { 642 | signatures, 643 | oneTimePublic 644 | } 645 | }) 646 | } 647 | 648 | /** 649 | Removes all saved keys on disk and clears keys in memory. Call only when 650 | the user chooses "logout." Do not call when the application exits. 651 | 652 | Forgets everything allowing the user to use a new password next time. 653 | */ 654 | function logout() { 655 | for(const key in state) { 656 | delete state[key] 657 | } 658 | 659 | const prefix = userStorage.createKey(accountName) 660 | for(const key in localStorage) { 661 | if(key.indexOf(prefix) === 0) { 662 | delete localStorage[key] 663 | } 664 | } 665 | 666 | clearInterval(expireInterval) 667 | expireInterval = null 668 | 669 | if(unlistenHistory) { 670 | unlistenHistory() 671 | unlistenHistory = null 672 | } 673 | 674 | expireAt = null 675 | } 676 | 677 | /** 678 | @return {number} 0 (expired) or milliseconds until expire 679 | */ 680 | function timeUntilExpire() { 681 | return ( 682 | expireAt === 0 ? 0 : 683 | expireAt == null ? 0 : 684 | Math.max(0, expireAt - Date.now()) 685 | ) 686 | } 687 | 688 | /** 689 | Keep alive (prevent expiration). Called automatically if Uri navigation 690 | happens or keys are required. It may be necessary to call this manually. 691 | */ 692 | function keepAlive() { 693 | expireAt = Date.now() + config.timeoutInMin * min 694 | } 695 | 696 | /** 697 | Integration for 'eosjs' .. 698 | 699 | Call keyProvider with no parameters or with a specific keyPathMatcher 700 | pattern to get an array of public keys in this key store. A library 701 | like eosjs may be provided these available public keys to eosd 702 | get_required_keys for filtering and to determine which private keys are 703 | needed to sign a given transaction. 704 | 705 | Call again with the get_required_keys pubkeys array to get the required 706 | private keys returned (or an error if any are missing). 707 | 708 | @throws Error `login with your ${path} key` 709 | @throws Error `missing public key ${key}` 710 | 711 | @arg {object} param 712 | @arg {string} [param.keyPathMatcher = '**'] - param.keyPathMatcher for public keys 713 | @arg {Array|Set} [param.pubkeys] for fetching private keys 714 | 715 | @return {Array} available pubkeys in the keystore or matching 716 | wif private keys for the provided pubkeys argument (also filtered using 717 | keyPathMatcher). 718 | 719 | @see https://github.com/eosio/eosjs 720 | */ 721 | function keyProvider({keyPathMatcher = '**', pubkeys} = {}) { 722 | keepAlive() 723 | 724 | if(pubkeys) { 725 | return getPrivateKeys(keyPathMatcher, pubkeys) 726 | } 727 | 728 | if(keyPathMatcher) { 729 | // For `login with your xxx key` below, get all keys even if a 730 | // wif is not available. 731 | return getPublicKeys(keyPathMatcher) 732 | } 733 | } 734 | 735 | return { 736 | deriveKeys, 737 | addKey, 738 | getKeys, 739 | getKeyPaths, 740 | getPublicKey, 741 | getPublicKeys, 742 | getPrivateKey, 743 | getPrivateKeys, 744 | removeKeys, 745 | signSharedSecret, 746 | logout, 747 | timeUntilExpire, 748 | keepAlive, 749 | keyProvider 750 | } 751 | } 752 | 753 | /** @private */ 754 | function currentUriPath() { 755 | const {location} = globalConfig.history 756 | return `${location.pathname}${location.search}${location.hash}` 757 | } 758 | 759 | /** Erase all traces of this keystore (for all users). */ 760 | Keystore.wipeAll = function() { 761 | const prefix = userStorage.createKey() 762 | for(const key in localStorage) { 763 | if(key.indexOf(prefix) === 0) { 764 | delete localStorage[key] 765 | } 766 | } 767 | } 768 | 769 | // used to convert milliseconds 770 | const sec = 1000, min = 60 * sec 771 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | ## Modules 2 | 3 |
4 |
Keystore
5 |
6 |
Keygen
7 |
8 |
9 | 10 | ## Typedefs 11 | 12 |
13 |
pubkey : string
14 |

Public Key (EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV)

15 |
16 |
wif : string
17 |

Wallet Import Format 18 | (5JMx76CTUTXxpAbwAqGMMVzSeJaP5UVTT5c2uobcpaMUdLAphSp)

19 |
20 |
privateKey : object
21 |

Private key object from eosjs-ecc.

22 |
23 |
masterPrivateKey : string
24 |

Master Private Key. Strong random key used to derive all other key types. 25 | Has a 'PW' prefix followed by a valid wif. ('PW' + wif === 26 | 'PW5JMx76CTUTXxpAbwAqGMMVzSeJaP5UVTT5c2uobcpaMUdLAphSp')

27 |
28 |
owner : wif
29 |

Cold storage / recovery key. Has authoritiy to do everything including 30 | account recovery.

31 |
32 |
active : wif
33 |

Spending key. Has the authority to do everything except account recovery.

34 |
35 |
parentPrivateKey : masterPrivateKey | wif
36 |

Master private key or one of its derived private keys.

37 |
38 |
auth : object
39 |

Signing Keys and(or) Accounts each having a weight that when matched in 40 | the signatures should accumulate to meet or exceed the auth's total threshold.

41 |
42 |
accountPermissions : object
43 |

Permissions object from Eos blockchain obtained via get_account.

44 |

See chain API get_account => account.permissions.

45 |
46 |
keyPath : string
47 |
48 |
keyPathPrivate : object
49 |

An expanded version of a private key, a keypath ('active/mypermission'), 50 | and its calculated public key (for performance reasons).

51 |
52 |
minimatch : string
53 |

Glob matching expressions (active, active/**, owner/*).

54 |
55 |
keyPathMatcher : minimatch
56 |

Key derviation path (owner, active/*, active/**, active/mypermission)

57 |
58 |
uriData : string
59 |

A URI without the prefixing scheme, host, port.

60 |
61 |
uriMatcher : string
62 |

A valid regular expression string. The provided string is modified when 63 | it is converted to a RegExp object:

64 |
    65 |
  • A start of line match is implied (^ is always added, do not add one)
  • 66 |
  • Unless the uriPath ends with $, automatically matches query parameters 67 | and fragment (hash tag info).
  • 68 |
  • The RegExp that is created is always case-insensitive to help a 69 | non-canonical path match. Uri paths should be canonical.
  • 70 |
71 |
72 |
uriMatchers : uriMatcher | Array.<uriMatcher>
73 |
74 |
uriRule : Object.<keyPathMatcher, uriMatchers>
75 |
76 |
uriRules : Object.<uriRule>
77 |

Define rules that say which private keys may exist within given locations 78 | of the application. If a rule is not found or does not match, the keystore 79 | will remove the key. The UI can prompt the user to obtain the needed key 80 | again.

81 |

For any non-trivial configuration, implementions should create a unit test 82 | that will test the actual configuration used in the application 83 | (see ./uri-rules.test.js for a template).

84 |

Paths imply that active is always derived from owner. So, instead of writing 85 | owner/active/** the path must be written as active/**.

86 |
87 |
88 | 89 | 90 | 91 | ## Keystore 92 | 93 | * [Keystore](#module_Keystore) 94 | * [~Keystore(accountName, [config], [keepPublicKeys])](#module_Keystore..Keystore) 95 | * _static_ 96 | * [.wipeAll()](#module_Keystore..Keystore.wipeAll) 97 | * _inner_ 98 | * [~deriveKeys(params)](#module_Keystore..Keystore..deriveKeys) 99 | * [~getKeyPaths()](#module_Keystore..Keystore..getKeyPaths) ⇒ object 100 | * [~getPublicKey(path)](#module_Keystore..Keystore..getPublicKey) ⇒ [pubkey](#pubkey) 101 | * [~getPublicKeys([keyPathMatcher])](#module_Keystore..Keystore..getPublicKeys) ⇒ [Array.<pubkey>](#pubkey) 102 | * [~getPrivateKey(path)](#module_Keystore..Keystore..getPrivateKey) ⇒ [wif](#wif) 103 | * [~getPrivateKeys([keyPathMatcher], [pubkeys])](#module_Keystore..Keystore..getPrivateKeys) ⇒ [Array.<wif>](#wif) 104 | * [~getKeys(keyPathMatcher)](#module_Keystore..Keystore..getKeys) ⇒ [Array.<keyPathPrivate>](#keyPathPrivate) 105 | * [~signSharedSecret(otherPubkey, keyPathMatcher)](#module_Keystore..Keystore..signSharedSecret) ⇒ Promise.<oneTimeSignatures> 106 | * [~logout()](#module_Keystore..Keystore..logout) 107 | * [~timeUntilExpire()](#module_Keystore..Keystore..timeUntilExpire) ⇒ number 108 | * [~keepAlive()](#module_Keystore..Keystore..keepAlive) 109 | * [~keyProvider(param)](#module_Keystore..Keystore..keyProvider) ⇒ Array.<(pubkey\|wif)> 110 | * [~oneTimeSignatures](#module_Keystore..oneTimeSignatures) : object 111 | 112 | 113 | 114 | ### Keystore~Keystore(accountName, [config], [keepPublicKeys]) 115 | Provides private key management and storage and tooling to limit exposure 116 | of private keys as much as possible. 117 | 118 | Although multiple root keys may be stored, this key store was designed with 119 | the idea that all keys for a given `accountName` are derive from a single 120 | root key (the master private key). 121 | 122 | This keystore does not query the blockchain or any external services. 123 | Removing keys here does not affect the blockchain. 124 | 125 | **Kind**: inner method of [Keystore](#module_Keystore) 126 | 127 | | Param | Type | Default | Description | 128 | | --- | --- | --- | --- | 129 | | accountName | string | | Blockchain account name that will act as the container for a key and all derived child keys. | 130 | | [config] | object | | | 131 | | [config.timeoutInMin] | number | 10 | upon timeout, remove keys matching timeoutKeyPaths. | 132 | | [config.timeoutKeyPaths] | number | ['owner', 'owner/**'] | by default, expire only owner and owner derived children. If the default uriRules are used this actually has nothing to delete. | 133 | | [config.uriRules] | [uriRules](#uriRules) | | Specify which type of private key will be available on certain pages of the application. Lock it down as much as possible and later re-prompt the user if a key is needed. Default is to allow active (`active`) and all active derived keys (`active/**`) everywhere (`.*`). | 134 | | [keepPublicKeys] | boolean | true | Enable for better UX; show users keys they have access too without requiring them to login. Logging in brings a private key online which is not necessary to see public information. The UX should implement this behavior in a way that is clear public keys are cached before enabling this feature. | 135 | 136 | **Example** 137 | ```js 138 | config = { 139 | uriRules: { 140 | 'active': '.*', 141 | 'active/**': '.*' 142 | }, 143 | timeoutInMin: 10, 144 | timeoutKeyPaths: [ 145 | 'owner', 146 | 'owner/**' 147 | ], 148 | keepPublicKeys: true 149 | } 150 | ``` 151 | 152 | * [~Keystore(accountName, [config], [keepPublicKeys])](#module_Keystore..Keystore) 153 | * _static_ 154 | * [.wipeAll()](#module_Keystore..Keystore.wipeAll) 155 | * _inner_ 156 | * [~deriveKeys(params)](#module_Keystore..Keystore..deriveKeys) 157 | * [~getKeyPaths()](#module_Keystore..Keystore..getKeyPaths) ⇒ object 158 | * [~getPublicKey(path)](#module_Keystore..Keystore..getPublicKey) ⇒ [pubkey](#pubkey) 159 | * [~getPublicKeys([keyPathMatcher])](#module_Keystore..Keystore..getPublicKeys) ⇒ [Array.<pubkey>](#pubkey) 160 | * [~getPrivateKey(path)](#module_Keystore..Keystore..getPrivateKey) ⇒ [wif](#wif) 161 | * [~getPrivateKeys([keyPathMatcher], [pubkeys])](#module_Keystore..Keystore..getPrivateKeys) ⇒ [Array.<wif>](#wif) 162 | * [~getKeys(keyPathMatcher)](#module_Keystore..Keystore..getKeys) ⇒ [Array.<keyPathPrivate>](#keyPathPrivate) 163 | * [~signSharedSecret(otherPubkey, keyPathMatcher)](#module_Keystore..Keystore..signSharedSecret) ⇒ Promise.<oneTimeSignatures> 164 | * [~logout()](#module_Keystore..Keystore..logout) 165 | * [~timeUntilExpire()](#module_Keystore..Keystore..timeUntilExpire) ⇒ number 166 | * [~keepAlive()](#module_Keystore..Keystore..keepAlive) 167 | * [~keyProvider(param)](#module_Keystore..Keystore..keyProvider) ⇒ Array.<(pubkey\|wif)> 168 | 169 | 170 | 171 | #### Keystore.wipeAll() 172 | Erase all traces of this keystore (for all users). 173 | 174 | **Kind**: static method of [Keystore](#module_Keystore..Keystore) 175 | 176 | 177 | #### Keystore~deriveKeys(params) 178 | Login or derive and save private keys. This may be called from a login 179 | action. Keys may be removed as during Uri navigation or when calling 180 | logout. 181 | 182 | **Kind**: inner method of [Keystore](#module_Keystore..Keystore) 183 | **Throws**: 184 | 185 | - Error 'invalid login' 186 | 187 | 188 | | Param | Type | Description | 189 | | --- | --- | --- | 190 | | params | object | | 191 | | params.parent | [parentPrivateKey](#parentPrivateKey) | Master password (masterPrivateKey), active, owner, or other permission key. | 192 | | [params.saveKeyMatches] | [Array.<keyPathMatcher>](#keyPathMatcher) | These private keys will be saved to disk. (example: `active`). | 193 | | [params.accountPermissions] | [accountPermissions](#accountPermissions) | Permissions object from Eos blockchain via get_account. This is used to validate the parent and derive additional permission keys. This allows this keystore to detect incorrect passwords early before trying to sign a transaction. See Chain API `get_account => account.permissions`. | 194 | 195 | 196 | 197 | #### Keystore~getKeyPaths() ⇒ object 198 | Return paths for all available keys. Empty array is used if there are 199 | no keys. 200 | 201 | **Kind**: inner method of [Keystore](#module_Keystore..Keystore) 202 | **Returns**: object - {pubkey: Array, wif: Array} 203 | 204 | 205 | #### Keystore~getPublicKey(path) ⇒ [pubkey](#pubkey) 206 | Fetch or derive a public key. 207 | 208 | **Kind**: inner method of [Keystore](#module_Keystore..Keystore) 209 | **Returns**: [pubkey](#pubkey) - or null 210 | 211 | | Param | Type | 212 | | --- | --- | 213 | | path | [keyPath](#keyPath) | 214 | 215 | 216 | 217 | #### Keystore~getPublicKeys([keyPathMatcher]) ⇒ [Array.<pubkey>](#pubkey) 218 | Return public keys for a path or path matcher. 219 | 220 | **Kind**: inner method of [Keystore](#module_Keystore..Keystore) 221 | **Returns**: [Array.<pubkey>](#pubkey) - public keys or empty array 222 | 223 | | Param | Type | Default | Description | 224 | | --- | --- | --- | --- | 225 | | [keyPathMatcher] | [keyPath](#keyPath) \| [keyPathMatcher](#keyPathMatcher) | '**' | return all keys | 226 | 227 | 228 | 229 | #### Keystore~getPrivateKey(path) ⇒ [wif](#wif) 230 | Fetch or derive a private key. 231 | 232 | **Kind**: inner method of [Keystore](#module_Keystore..Keystore) 233 | **Returns**: [wif](#wif) - or null (missing or not available for location) 234 | 235 | | Param | Type | 236 | | --- | --- | 237 | | path | [keyPath](#keyPath) | 238 | 239 | 240 | 241 | #### Keystore~getPrivateKeys([keyPathMatcher], [pubkeys]) ⇒ [Array.<wif>](#wif) 242 | Return private keys for a path matcher or for a list of public keys. If a 243 | list of public keys is provided they will be validated ensuring they all 244 | have private keys to return. 245 | 246 | **Kind**: inner method of [Keystore](#module_Keystore..Keystore) 247 | **Returns**: [Array.<wif>](#wif) - wifs or empty array 248 | **Throws**: 249 | 250 | - key.pubkey Error `login with your $ key` 251 | - key Error `missing public key $` 252 | 253 | 254 | | Param | Type | Default | Description | 255 | | --- | --- | --- | --- | 256 | | [keyPathMatcher] | [keyPathMatcher](#keyPathMatcher) | '**' | default is to match all | 257 | | [pubkeys] | [Array.<pubkey>](#pubkey) | | if specified, filter and require all | 258 | 259 | 260 | 261 | #### Keystore~getKeys(keyPathMatcher) ⇒ [Array.<keyPathPrivate>](#keyPathPrivate) 262 | Fetch or derive a key pairs. 263 | 264 | **Kind**: inner method of [Keystore](#module_Keystore..Keystore) 265 | **Returns**: [Array.<keyPathPrivate>](#keyPathPrivate) - {path, pubkey, deny, wif} or empty array. 266 | Based on the Uri rules and current location, the deny could be set to true 267 | and the wif will be null. 268 | 269 | | Param | Type | Default | 270 | | --- | --- | --- | 271 | | keyPathMatcher | [keyPath](#keyPath) \| [keyPathMatcher](#keyPathMatcher) | ** | 272 | 273 | 274 | 275 | #### Keystore~signSharedSecret(otherPubkey, keyPathMatcher) ⇒ Promise.<oneTimeSignatures> 276 | **Kind**: inner method of [Keystore](#module_Keystore..Keystore) 277 | 278 | | Param | Type | Default | 279 | | --- | --- | --- | 280 | | otherPubkey | [pubkey](#pubkey) | | 281 | | keyPathMatcher | [keyPathMatcher](#keyPathMatcher) | ** | 282 | 283 | 284 | 285 | #### Keystore~logout() 286 | Removes all saved keys on disk and clears keys in memory. Call only when 287 | the user chooses "logout." Do not call when the application exits. 288 | 289 | Forgets everything allowing the user to use a new password next time. 290 | 291 | **Kind**: inner method of [Keystore](#module_Keystore..Keystore) 292 | 293 | 294 | #### Keystore~timeUntilExpire() ⇒ number 295 | **Kind**: inner method of [Keystore](#module_Keystore..Keystore) 296 | **Returns**: number - 0 (expired) or milliseconds until expire 297 | 298 | 299 | #### Keystore~keepAlive() 300 | Keep alive (prevent expiration). Called automatically if Uri navigation 301 | happens or keys are required. It may be necessary to call this manually. 302 | 303 | **Kind**: inner method of [Keystore](#module_Keystore..Keystore) 304 | 305 | 306 | #### Keystore~keyProvider(param) ⇒ Array.<(pubkey\|wif)> 307 | Integration for 'eosjs' .. 308 | 309 | Call keyProvider with no parameters or with a specific keyPathMatcher 310 | pattern to get an array of public keys in this key store. A library 311 | like eosjs may be provided these available public keys to eosd 312 | get_required_keys for filtering and to determine which private keys are 313 | needed to sign a given transaction. 314 | 315 | Call again with the get_required_keys pubkeys array to get the required 316 | private keys returned (or an error if any are missing). 317 | 318 | **Kind**: inner method of [Keystore](#module_Keystore..Keystore) 319 | **Returns**: Array.<(pubkey\|wif)> - available pubkeys in the keystore or matching 320 | wif private keys for the provided pubkeys argument (also filtered using 321 | keyPathMatcher). 322 | **Throws**: 323 | 324 | - path Error `login with your $ key` 325 | - key Error `missing public key $` 326 | 327 | **See**: https://github.com/eosio/eosjs 328 | 329 | | Param | Type | Default | Description | 330 | | --- | --- | --- | --- | 331 | | param | object | | | 332 | | [param.keyPathMatcher] | string | "'**'" | param.keyPathMatcher for public keys | 333 | | [param.pubkeys] | [Array.<pubkey>](#pubkey) \| [Set.<pubkey>](#pubkey) | | for fetching private keys | 334 | 335 | 336 | 337 | ### Keystore~oneTimeSignatures : object 338 | **Kind**: inner typedef of [Keystore](#module_Keystore) 339 | **Properties** 340 | 341 | | Name | Type | Description | 342 | | --- | --- | --- | 343 | | signatures | Array.<string> | in hex | 344 | | oneTimePublic | [pubkey](#pubkey) | | 345 | 346 | 347 | 348 | ## Keygen 349 | 350 | * [Keygen](#module_Keygen) 351 | * [~generateMasterKeys([masterPrivateKey])](#module_Keygen..generateMasterKeys) ⇒ Promise.<object> 352 | * [~keyPathAuth](#module_Keygen..keyPathAuth) : Object.<keyPath, auth> 353 | 354 | 355 | 356 | ### Keygen~generateMasterKeys([masterPrivateKey]) ⇒ Promise.<object> 357 | New accounts will call this to create a new keyset.. 358 | 359 | A password manager or backup should save (at the very minimum) the returned 360 | {masterPrivateKey} for later login. The owner and active can be re-created 361 | from the masterPrivateKey. It is still a good idea to save all information 362 | in the backup for easy reference. 363 | 364 | **Kind**: inner method of [Keygen](#module_Keygen) 365 | **Returns**: Promise.<object> - masterKeys 366 | 367 | | Param | Type | Default | Description | 368 | | --- | --- | --- | --- | 369 | | [masterPrivateKey] | [masterPrivateKey](#masterPrivateKey) | | When null, a new random key is created. | 370 | 371 | **Example** 372 | ```js 373 | masterKeys = { 374 | masterPrivateKey, // <= place in a password input field (password manager) 375 | privateKeys: {owner, active}, // <= derived from masterPrivateKey 376 | publicKeys: {owner, active} // <= derived from masterPrivateKey 377 | } 378 | ``` 379 | 380 | 381 | ### Keygen~keyPathAuth : Object.<keyPath, auth> 382 | **Kind**: inner typedef of [Keygen](#module_Keygen) 383 | 384 | 385 | ## pubkey : string 386 | Public Key (EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV) 387 | 388 | **Kind**: global typedef 389 | 390 | 391 | ## wif : string 392 | [Wallet Import Format](https://en.bitcoin.it/wiki/Wallet_import_format) 393 | (5JMx76CTUTXxpAbwAqGMMVzSeJaP5UVTT5c2uobcpaMUdLAphSp) 394 | 395 | **Kind**: global typedef 396 | 397 | 398 | ## privateKey : object 399 | Private key object from eosjs-ecc. 400 | 401 | **Kind**: global typedef 402 | 403 | 404 | ## masterPrivateKey : string 405 | Master Private Key. Strong random key used to derive all other key types. 406 | Has a 'PW' prefix followed by a valid wif. (`'PW' + wif === 407 | 'PW5JMx76CTUTXxpAbwAqGMMVzSeJaP5UVTT5c2uobcpaMUdLAphSp'`) 408 | 409 | **Kind**: global typedef 410 | 411 | 412 | ## owner : [wif](#wif) 413 | Cold storage / recovery key. Has authoritiy to do everything including 414 | account recovery. 415 | 416 | **Kind**: global typedef 417 | 418 | 419 | ## active : [wif](#wif) 420 | Spending key. Has the authority to do everything except account recovery. 421 | 422 | **Kind**: global typedef 423 | 424 | 425 | ## parentPrivateKey : [masterPrivateKey](#masterPrivateKey) \| [wif](#wif) 426 | Master private key or one of its derived private keys. 427 | 428 | **Kind**: global typedef 429 | 430 | 431 | ## auth : object 432 | Signing Keys and(or) Accounts each having a weight that when matched in 433 | the signatures should accumulate to meet or exceed the auth's total threshold. 434 | 435 | **Kind**: global typedef 436 | **Example** 437 | ```js 438 | required_auth: { 439 | threshold: 1, 440 | keys: [{ 441 | key: 'EOS78Cs5HPKY7HKHrSMnR76uj7yeajPuNwSH1Fsria3sJuufwE3Zd', 442 | weight: 1 443 | } 444 | ], 445 | accounts: [] 446 | } 447 | ``` 448 | 449 | 450 | ## accountPermissions : object 451 | Permissions object from Eos blockchain obtained via get_account. 452 | 453 | See chain API get_account => account.permissions. 454 | 455 | **Kind**: global typedef 456 | **Example** 457 | ```js 458 | const accountPermissions = [{ 459 | perm_name: 'active', 460 | parent: 'owner', 461 | required_auth: { 462 | threshold: 1, 463 | keys: [{ 464 | key: 'EOS78Cs5HPKY7HKHrSMnR76uj7yeajPuNwSH1Fsria3sJuufwE3Zd', 465 | weight: 1 466 | } 467 | ], 468 | accounts: [] 469 | } 470 | },{ 471 | perm_name: 'mypermission', 472 | parent: 'active', 473 | required_auth: { 474 | threshold: 1, 475 | keys: [{ 476 | key: 'EOS78Cs5HPKY7HKHrSMnR76uj7yeajPuNwSH1Fsria3sJuufwE3Zd', 477 | weight: 1 478 | } 479 | ], 480 | accounts: [] 481 | } 482 | },{ 483 | perm_name: 'owner', 484 | parent: '', 485 | required_auth: { 486 | threshold: 1, 487 | keys: [{ 488 | key: 'EOS78Cs5HPKY7HKHrSMnR76uj7yeajPuNwSH1Fsria3sJuufwE3Zd', 489 | weight: 1 490 | } 491 | ], 492 | accounts: [] 493 | } 494 | }] 495 | ``` 496 | 497 | 498 | ## keyPath : string 499 | **Kind**: global typedef 500 | **See**: [validate.path(keyPath)](./validate.js) 501 | **Example** 502 | ```js 503 | 'owner', 'active', 'active/mypermission' 504 | ``` 505 | 506 | 507 | ## keyPathPrivate : object 508 | An expanded version of a private key, a keypath ('active/mypermission'), 509 | and its calculated public key (for performance reasons). 510 | 511 | **Kind**: global typedef 512 | **Properties** 513 | 514 | | Name | Type | 515 | | --- | --- | 516 | | wif | [wif](#wif) | 517 | | pubkey | [pubkey](#pubkey) | 518 | | path | [keyPath](#keyPath) | 519 | 520 | 521 | 522 | ## minimatch : string 523 | Glob matching expressions (`active`, `active/**`, `owner/*`). 524 | 525 | **Kind**: global typedef 526 | **See** 527 | 528 | - https://www.npmjs.com/package/glob#glob-primer - syntax 529 | - https://www.npmjs.com/package/minimatch - implementation 530 | 531 | 532 | 533 | ## keyPathMatcher : [minimatch](#minimatch) 534 | Key derviation path (`owner`, `active/*`, `active/**`, `active/mypermission`) 535 | 536 | **Kind**: global typedef 537 | 538 | 539 | ## uriData : string 540 | A URI without the prefixing scheme, host, port. 541 | 542 | **Kind**: global typedef 543 | **Example** 544 | ```js 545 | '/producers', '/account_recovery#name=..' 546 | ``` 547 | 548 | 549 | ## uriMatcher : string 550 | A valid regular expression string. The provided string is modified when 551 | it is converted to a RegExp object: 552 | 553 | - A start of line match is implied (`^` is always added, do not add one) 554 | - Unless the uriPath ends with `$`, automatically matches query parameters 555 | and fragment (hash tag info). 556 | - The RegExp that is created is always case-insensitive to help a 557 | non-canonical path match. Uri paths should be canonical. 558 | 559 | **Kind**: global typedef 560 | **Example** 561 | ```js 562 | '/(transfer|contracts)', '/bare-uri$' 563 | 564 | ``` 565 | **Example** 566 | ```js 567 | function createPathMatcher(path) { 568 | // Ensure match starts at the begining 569 | const prefix = '^' 570 | 571 | // If path matcher does not end with $, allow Uri query and fragment 572 | const suffix = path.charAt(path.length - 1) === '$' ? '' : '\/?([\?#].*)?$' 573 | 574 | // Path matches are case in-sensitive 575 | return new RegExp(prefix + path + suffix, 'i') 576 | } 577 | ``` 578 | 579 | 580 | ## uriMatchers : [uriMatcher](#uriMatcher) \| [Array.<uriMatcher>](#uriMatcher) 581 | **Kind**: global typedef 582 | 583 | 584 | ## uriRule : Object.<keyPathMatcher, uriMatchers> 585 | **Kind**: global typedef 586 | **Example** 587 | ```js 588 | { 589 | 'owner': '/account_recovery$', // <= $ prevents query or fragment params 590 | 'active': ['/transfer', '/contracts'] 591 | } 592 | ``` 593 | 594 | 595 | ## uriRules : [Object.<uriRule>](#uriRule) 596 | Define rules that say which private keys may exist within given locations 597 | of the application. If a rule is not found or does not match, the keystore 598 | will remove the key. The UI can prompt the user to obtain the needed key 599 | again. 600 | 601 | For any non-trivial configuration, implementions should create a unit test 602 | that will test the actual configuration used in the application 603 | (see `./uri-rules.test.js` for a template). 604 | 605 | Paths imply that active is always derived from owner. So, instead of writing 606 | `owner/active/**` the path must be written as `active/**`. 607 | 608 | **Kind**: global typedef 609 | **Example** 610 | ```js 611 | uriRules = { // Hypothetical examples 612 | // Allow owner and all derived keys (including active) 613 | 'owner': '/account_recovery', 614 | 615 | // Allow active key (and any derived child) 616 | 'active': '/(transfer|contracts)', 617 | 618 | // Allow keys derived from active (but not active itself) 619 | 'active/**': '/producers', 620 | 621 | // If user-provided or unaudited content could be loaded in a given 622 | // page, make sure the root active key is not around on these pages. 623 | 'active/**': '/@[\\w\\.]' 624 | } 625 | ``` 626 | --------------------------------------------------------------------------------