├── fixtures └── keyring.pub ├── .eslintignore ├── .npmrc ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md ├── workflows │ └── tests.yml ├── COMMIT_FORMAT_EXAMPLES.md ├── COMMIT_FORMAT.md └── CONTRIBUTING.md ├── .eslintrc ├── .npmignore ├── test ├── fixtures │ └── keyring.pub ├── .ararc ├── list.js ├── ethereum │ ├── entropy.js │ ├── wallet.js │ ├── keystore.js │ ├── recover.js │ └── account.js ├── replicate.js ├── key-path.js ├── resolve.js ├── archive.js ├── update.js ├── create.js ├── revoke.js └── recover.js ├── truffle.js ├── truffle-config.js ├── ethereum ├── index.js ├── entropy.js ├── wallet.js ├── account.js └── keystore.js ├── rc.js ├── protobuf ├── index.js ├── schema.proto └── messages.js ├── Makefile ├── save.js ├── ddo.js ├── scripts ├── fix-ethereumjs-wallet ├── install.sh └── package.sh ├── verify.js ├── sign.js ├── key-path.js ├── update.js ├── index.js ├── .gitignore ├── recover.js ├── whoami.js ├── list.js ├── did.js ├── revoke.js ├── share.js ├── dns.js ├── package.json ├── util.js ├── replicate.js ├── LICENSE ├── fs.js ├── resolve.js ├── archive.js ├── create.js ├── docs └── cli.md └── README.md /fixtures/keyring.pub: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | protobuf/messages.js 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | message="chore(release): %s :tada:" 3 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | ## Proposed Changes 4 | 5 | - 6 | - 7 | - -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "ara", 3 | "rules": { 4 | "no-warnings-comments": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .ararc 2 | .github/ 3 | build/ 4 | scripts/ 5 | docs/ 6 | test/ 7 | *.tgz 8 | .nyc_output/ 9 | -------------------------------------------------------------------------------- /test/fixtures/keyring.pub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AraBlocks/ara-identity/HEAD/test/fixtures/keyring.pub -------------------------------------------------------------------------------- /test/.ararc: -------------------------------------------------------------------------------- 1 | [data] 2 | root = "../tmp" 3 | 4 | [web3] 5 | provider = "infuraRopsten" 6 | 7 | [network.dns] 8 | server[] = "1.1.1.1" 9 | server[] = "8.8.8.8" 10 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /* 3 | * See 4 | * to customize your Truffle configuration! 5 | */ 6 | } 7 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /* 3 | * See 4 | * to customize your Truffle configuration! 5 | */ 6 | } 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 2. 11 | 3. 12 | 13 | ## Specifications 14 | 15 | - Version: 16 | - Platform: 17 | - Subsystem: 18 | -------------------------------------------------------------------------------- /ethereum/index.js: -------------------------------------------------------------------------------- 1 | const { entropy } = require('./entropy') 2 | const keystore = require('./keystore') 3 | const account = require('./account') 4 | const wallet = require('./wallet') 5 | 6 | module.exports = { 7 | keystore, 8 | account, 9 | entropy, 10 | wallet, 11 | } 12 | -------------------------------------------------------------------------------- /rc.js: -------------------------------------------------------------------------------- 1 | const extend = require('extend') 2 | const rc = require('ara-runtime-configuration') 3 | 4 | const defaults = () => ({ 5 | network: { 6 | identity: { 7 | archiver: { }, 8 | resolver: { }, 9 | }, 10 | } 11 | }) 12 | 13 | module.exports = (conf) => rc(extend(true, {}, defaults(), conf)) 14 | -------------------------------------------------------------------------------- /protobuf/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const messages = require('./messages') 3 | 4 | // eslint-disable-next-line prefer-template,no-path-concat 5 | const kProtocolBufferSchema = fs.readFileSync(__dirname + '/schema.proto', 'utf8') 6 | 7 | module.exports = { 8 | kProtocolBufferSchema, 9 | messages, 10 | } 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RM ?= $(shell which rm) 2 | CWD ?= $(shell pwd) 3 | NPM ?= $(shell which npm) 4 | BUILD ?= $(CWD)/build 5 | 6 | .PHONY: default install uninstall clean 7 | 8 | default: build 9 | 10 | build: node_modules 11 | ./scripts/package.sh 12 | 13 | node_modules: package.json 14 | 15 | package.json: 16 | $(NPM) install 17 | 18 | clean: 19 | $(RM) -rf $(BUILD) 20 | -------------------------------------------------------------------------------- /save.js: -------------------------------------------------------------------------------- 1 | const isBrowser = require('is-browser') 2 | const { writeIdentity } = require('./util') 3 | 4 | /** 5 | * High level function to save an identity to disk 6 | */ 7 | async function save(identity) { 8 | // @TODO(werle): Handle browser case for saving 9 | if (false === isBrowser) { 10 | await writeIdentity(identity) 11 | return true 12 | } 13 | 14 | return false 15 | } 16 | 17 | module.exports = { 18 | save 19 | } 20 | -------------------------------------------------------------------------------- /ddo.js: -------------------------------------------------------------------------------- 1 | const { DIDDocument } = require('did-document') 2 | 3 | /** 4 | * Creates a DIDDocument from a DID for a DID. 5 | * @public 6 | * @param {Object} opts 7 | * @param {Object} opts.id 8 | * @return {DIDDocument} 9 | * @throws TypeError 10 | */ 11 | function create(opts) { 12 | if (null == opts) { 13 | throw new TypeError('Expecting options object.') 14 | } 15 | 16 | if (null == opts.id) { 17 | throw new TypeError('Expecting DID identifier.') 18 | } 19 | 20 | return new DIDDocument(opts) 21 | } 22 | 23 | module.exports = { 24 | create 25 | } 26 | -------------------------------------------------------------------------------- /ethereum/entropy.js: -------------------------------------------------------------------------------- 1 | const randombytes = require('randombytes') 2 | const { toHex } = require('../util') 3 | 4 | const kDefaultEntropySize = 32 5 | const kMinEntropySize = 16 6 | 7 | async function entropy(size) { 8 | let entropySize = size 9 | if (null == entropySize) { entropySize = kDefaultEntropySize } 10 | if (!entropySize || entropySize < kMinEntropySize) { 11 | throw new TypeError(`Invalid entropy size. Must be larger than ${kMinEntropySize}.`) 12 | } 13 | 14 | return toHex(randombytes(entropySize)) 15 | } 16 | 17 | module.exports = { 18 | entropy 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12, 13, 14] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm i 23 | npm test 24 | npm run lint 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /scripts/fix-ethereumjs-wallet: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable */ 3 | const filename = require.resolve('ethereumjs-wallet/package.json') 4 | const json = require(filename) 5 | const fs = require('fs') 6 | 7 | let needsWrite = false 8 | 9 | if (json && Array.isArray(json.files)) { 10 | for (let i = 0 ; i < json.files.length; ++i) { 11 | const file = json.files[i] 12 | 13 | if ('/' == file[0]) { 14 | json.files[i] = `.${file}` 15 | needsWrite = true 16 | } 17 | } 18 | 19 | if (needsWrite) { 20 | fs.writeFileSync(filename, JSON.stringify(json, null, ' ')) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /verify.js: -------------------------------------------------------------------------------- 1 | const { DID } = require('did-uri') 2 | const crypto = require('ara-crypto') 3 | const { resolve } = require('./resolve') 4 | 5 | const OWNER = 'owner' 6 | 7 | async function verify(uri, signature, message, opts) { 8 | const ddo = await resolve(uri, opts) 9 | let publicKey = null 10 | for (const pk of ddo.publicKey) { 11 | const { fragment } = new DID(pk.id) 12 | if (OWNER === fragment) { 13 | publicKey = Buffer.from(pk.publicKeyHex, 'hex') 14 | } 15 | } 16 | return crypto.ed25519.verify(signature, Buffer.from(message), publicKey) 17 | } 18 | 19 | module.exports = { 20 | verify 21 | } 22 | -------------------------------------------------------------------------------- /test/list.js: -------------------------------------------------------------------------------- 1 | const context = require('ara-context')() 2 | const test = require('ava') 3 | 4 | const { create } = require('../create') 5 | const { list } = require('../list') 6 | const util = require('../util') 7 | 8 | test('list()', async (t) => { 9 | const identity = await create({ context, password: 'password123' }) 10 | await util.writeIdentity(identity) 11 | const identities = await list() 12 | t.true(null !== identities) 13 | t.true('object' === typeof identities) 14 | 15 | await t.throwsAsync( 16 | list('identities'), 17 | { instanceOf: Error }, 18 | 'Cannot read directory identities' 19 | ) 20 | }) 21 | -------------------------------------------------------------------------------- /test/ethereum/entropy.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { entropy } = require('../../ethereum/entropy') 3 | 4 | test('ethereum.entropy(size)', async (t) => { 5 | t.plan(6) 6 | await t.throwsAsync(() => entropy(1), { instanceOf: TypeError }, 'too small') 7 | await t.throwsAsync(() => entropy(1), { instanceOf: TypeError }, 'too small') 8 | await t.throwsAsync(() => entropy(-1), { instanceOf: TypeError }, 'too small') 9 | await t.throwsAsync(() => entropy(15), { instanceOf: TypeError }, 'too small') 10 | // minimum 11 | await t.true(32 === (await entropy(16)).length) 12 | // default 13 | await t.true(64 === (await entropy()).length) 14 | }) 15 | -------------------------------------------------------------------------------- /test/replicate.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { replicate } = require('../replicate') 3 | 4 | test('replicate() invalid opts', async (t) => { 5 | await t.throwsAsync( 6 | replicate({}, {}), 7 | { instanceOf: TypeError }, 8 | 'Expecting a valid identity string to replicate.' 9 | ) 10 | 11 | await t.throwsAsync( 12 | replicate('abcd12345', {}), 13 | { instanceOf: TypeError }, 14 | 'Invalid DID identifier length.' 15 | ) 16 | 17 | const id = '488651f5767e73cc426b2eeb5d7e1e6e54a473e4394ab8a6417391986dd1bf8d' 18 | 19 | await t.throwsAsync( 20 | replicate(id, {}), 21 | { instanceOf: Error }, 22 | 'Could not replicate DID from peer. Request Timed out' 23 | ) 24 | }) 25 | -------------------------------------------------------------------------------- /test/key-path.js: -------------------------------------------------------------------------------- 1 | const context = require('ara-context')() 2 | const test = require('ava') 3 | 4 | const { createIdentityKeyPath } = require('../key-path') 5 | const { create } = require('../create') 6 | 7 | test('createIdentityKeyPath(identity) invalid identity', (t) => { 8 | t.throws(() => createIdentityKeyPath(), { instanceOf: TypeError }) 9 | t.throws(() => createIdentityKeyPath('did:ara:1234'), { instanceOf: TypeError }) 10 | }) 11 | 12 | test('createIdentityKeyPath(identity) valid identity', async (t) => { 13 | // create test identity 14 | const password = 'myPass' 15 | const identity = await create({ context, password }) 16 | 17 | const path = createIdentityKeyPath(identity) 18 | t.true(path && 'string' === typeof path) 19 | }) 20 | -------------------------------------------------------------------------------- /test/ethereum/wallet.js: -------------------------------------------------------------------------------- 1 | const crypto = require('ara-crypto') 2 | const bip39 = require('bip39') 3 | const test = require('ava') 4 | const { load } = require('../../ethereum/wallet') 5 | 6 | test('ethereum.wallet.load(opts)', async (t) => { 7 | await t.throwsAsync(load(), { instanceOf: TypeError }) 8 | await t.throwsAsync(load(null), { instanceOf: TypeError }) 9 | await t.throwsAsync(load(0), { instanceOf: TypeError }) 10 | await t.throwsAsync(load(true), { instanceOf: TypeError }) 11 | await t.throwsAsync(load(NaN), { instanceOf: TypeError }) 12 | await t.throwsAsync(load({}), { instanceOf: TypeError }) 13 | 14 | let seed = await bip39.mnemonicToSeed(bip39.generateMnemonic()) 15 | seed = crypto.blake2b(seed) 16 | t.true(null != load({ seed })) 17 | }) 18 | -------------------------------------------------------------------------------- /sign.js: -------------------------------------------------------------------------------- 1 | const { DID } = require('did-uri') 2 | const crypto = require('ara-crypto') 3 | const ss = require('ara-secret-storage') 4 | const { resolve } = require('./resolve') 5 | const fs = require('./fs') 6 | 7 | async function sign(uri, message, opts) { 8 | const ddo = await resolve(uri, opts) 9 | const did = new DID(ddo.id) 10 | 11 | const buffer = await fs.readFile(did.identifier, 'keystore/ara', { 12 | cache: opts.cache, 13 | network: opts.network, 14 | }) 15 | 16 | const keystore = JSON.parse(buffer) 17 | const password = crypto.blake2b(Buffer.from(opts.password)) 18 | const secretKey = ss.decrypt(keystore, { key: password.slice(0, 16) }) 19 | return crypto.ed25519.sign(Buffer.from(message), secretKey) 20 | } 21 | 22 | module.exports = { 23 | sign 24 | } 25 | -------------------------------------------------------------------------------- /key-path.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const crypto = require('ara-crypto') 3 | const rc = require('./rc')() 4 | 5 | /** 6 | * Generate path to stored local identity 7 | * @param {Object} identity 8 | * @return {String} 9 | * @throws {TypeError} 10 | */ 11 | function createIdentityKeyPath(identity) { 12 | if (null == identity || 'object' !== typeof identity) { 13 | throw new TypeError('util.createIdentityKeyPath: Expecting identity object') 14 | } 15 | 16 | const { root } = rc.network.identity 17 | let { publicKey } = identity 18 | 19 | if (Array.isArray(publicKey) && 0 < publicKey.length) { 20 | const { publicKeyHex } = publicKey[0] 21 | publicKey = Buffer.from(publicKeyHex, 'hex') 22 | } 23 | 24 | const hash = crypto.blake2b(publicKey).toString('hex') 25 | return resolve(root, hash) 26 | } 27 | 28 | module.exports = { 29 | createIdentityKeyPath 30 | } 31 | -------------------------------------------------------------------------------- /update.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('ara:identity:update') 2 | const fs = require('./fs') 3 | 4 | const { create } = require('./create') 5 | 6 | /** 7 | * Updates an ARA identity. 8 | */ 9 | async function update(identifier, opts) { 10 | if (null == opts || 'object' !== typeof opts) { 11 | throw new TypeError('Expecting object.') 12 | } 13 | 14 | if (opts.ddo) { 15 | opts.created = opts.ddo.created 16 | opts.updated = new Date() 17 | } 18 | 19 | if (!opts.keystore) { 20 | opts.keystore = {} 21 | 22 | try { 23 | opts.keystore.ara = JSON.parse(await fs.readFile(identifier, 'keystore/ara')) 24 | } catch (err) { 25 | debug(err) 26 | } 27 | 28 | try { 29 | opts.keystore.eth = JSON.parse(await fs.readFile(identifier, 'keystore/eth')) 30 | } catch (err) { 31 | debug(err) 32 | } 33 | } 34 | 35 | return create(opts) 36 | } 37 | 38 | module.exports = { 39 | update 40 | } 41 | -------------------------------------------------------------------------------- /test/resolve.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { resolve } = require('../resolve') 3 | 4 | const id = '0488542b72a639dc99e9de57cf9ef64911a1f146d29149f4e81690d963c99175' 5 | 6 | test('resolve(uri)', async (t) => { 7 | t.plan(8) 8 | 9 | await t.throwsAsync(resolve(), { instanceOf: TypeError }, 'empty uri') 10 | await t.throwsAsync(resolve({}), { instanceOf: TypeError }, 'invalid argument (object)') 11 | await t.throwsAsync(resolve([]), { instanceOf: TypeError }, 'invalid argument (array)') 12 | await t.throwsAsync(resolve(true), { instanceOf: TypeError }, 'invalid argument (boolean)') 13 | await t.throwsAsync(resolve(1234), { instanceOf: TypeError }, 'invalid argument (number)') 14 | await t.throwsAsync(resolve(() => {}), { instanceOf: TypeError }, 'invalid argument (function)') 15 | await t.throwsAsync(resolve('did:foo:1234'), { instanceOf: TypeError }, 'invalid did method') 16 | await t.throwsAsync(resolve(`did:ara:${id.slice(0, 32)}`), { instanceOf: TypeError }, 'invalid did identifier') 17 | }) 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { createIdentityKeyPath } = require('./key-path') 2 | const { archive } = require('./archive') 3 | const { resolve } = require('./resolve') 4 | const { recover } = require('./recover') 5 | const { create } = require('./create') 6 | const { revoke } = require('./revoke') 7 | const { update } = require('./update') 8 | const { verify } = require('./verify') 9 | const { whoami } = require('./whoami') 10 | const { share } = require('./share') 11 | const { save } = require('./save') 12 | const { sign } = require('./sign') 13 | const ethereum = require('./ethereum') 14 | const { list } = require('./list') 15 | const util = require('./util') 16 | const ddo = require('./ddo') 17 | const dns = require('./dns') 18 | const did = require('./did') 19 | const fs = require('./fs') 20 | 21 | module.exports = { 22 | createIdentityKeyPath, 23 | ethereum, 24 | archive, 25 | recover, 26 | resolve, 27 | create, 28 | revoke, 29 | update, 30 | verify, 31 | whoami, 32 | share, 33 | save, 34 | sign, 35 | list, 36 | util, 37 | dns, 38 | ddo, 39 | did, 40 | fs, 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | package-lock.json 63 | build/ 64 | .DS_Store 65 | .ararc 66 | tmp 67 | -------------------------------------------------------------------------------- /protobuf/schema.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message KeyStore { 4 | uint32 version = 1; 5 | string address = 2; 6 | string id = 3; 7 | Crypto crypto = 4; 8 | 9 | message Crypto { 10 | string cipher = 1; 11 | string ciphertext = 2; 12 | CipherParams cipherparams = 3; 13 | string mac = 4; 14 | string kdf = 5; 15 | KDFParams kdfparams = 6; 16 | string digest = 7; 17 | 18 | message CipherParams { 19 | string iv = 1; 20 | } 21 | 22 | message KDFParams { 23 | uint64 dklen = 1; 24 | uint64 n = 2; 25 | uint64 r = 3; 26 | uint64 p = 4; 27 | uint64 c = 5; 28 | string prf = 6; 29 | string salt = 7; 30 | } 31 | } 32 | } 33 | 34 | message Identity { 35 | string did = 1; 36 | bytes key = 2; 37 | Proof proof = 3; 38 | repeated File files = 4; 39 | 40 | message Proof { 41 | bytes signature = 1; 42 | } 43 | 44 | message File { 45 | string path = 1; 46 | bytes buffer = 2; 47 | } 48 | } 49 | 50 | message Archive { 51 | bytes key = 1; 52 | bool shallow = 2; 53 | bytes signature = 10; 54 | } 55 | 56 | message Keys { 57 | bytes signature = 1; 58 | repeated KeyPair keys = 2; 59 | } 60 | 61 | message KeyPair { 62 | bytes publicKey = 1; 63 | bytes secretKey = 2; 64 | } 65 | -------------------------------------------------------------------------------- /ethereum/wallet.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | const { hdkey, default: Wallet } = require('ethereumjs-wallet') 3 | const isZeroBuffer = require('is-zero-buffer') 4 | const isBuffer = require('is-buffer') 5 | 6 | const DERIVATION_PATH = 'm/44\'/60\'/0\'/0/' 7 | 8 | async function load(opts) { 9 | if (!opts || 'object' !== typeof opts) { 10 | throw new TypeError('Expecting object.') 11 | } 12 | 13 | if (!opts.seed) { 14 | throw new TypeError('Expecting seed to create wallet.') 15 | } 16 | 17 | const index = 0 < opts.index ? opts.index : 0 18 | 19 | if (false === isBuffer(opts.seed) || true === isZeroBuffer(opts.seed)) { 20 | throw new TypeError('Expecting seed to be a non-zero buffer.') 21 | } 22 | 23 | const { seed } = opts 24 | const hdWallet = hdkey.fromMasterSeed(seed) 25 | 26 | return hdWallet.derivePath(`${DERIVATION_PATH}${index}`).getWallet() 27 | } 28 | 29 | async function create(opts) { 30 | if (opts && opts.publicKey && !opts.privateKey) { 31 | return Wallet.fromPublicKey(opts.publicKey) 32 | } 33 | 34 | if (opts && opts.privateKey) { 35 | return Wallet.fromPrivateKey(opts.privateKey) 36 | } 37 | 38 | return new Wallet(opts.privateKey, opts.publicKey) 39 | } 40 | 41 | module.exports = { 42 | create, 43 | load 44 | } 45 | -------------------------------------------------------------------------------- /test/ethereum/keystore.js: -------------------------------------------------------------------------------- 1 | const isBuffer = require('is-buffer') 2 | const crypto = require('ara-crypto') 3 | const bip39 = require('bip39') 4 | const test = require('ava') 5 | 6 | const { toBuffer } = require('../../util') 7 | const keystore = require('../../ethereum/keystore') 8 | const wallet = require('../../ethereum/wallet') 9 | 10 | test('create(opts)', async (t) => { 11 | t.true('function' === typeof keystore.create) 12 | const ks = await keystore.create() 13 | // sanity checks 14 | t.true('object' === typeof ks) 15 | t.true(isBuffer(ks.iv)) 16 | t.true(isBuffer(ks.privateKey)) 17 | t.true(isBuffer(ks.salt)) 18 | }) 19 | 20 | test('dump(opts)', async (t) => { 21 | let seed = await bip39.mnemonicToSeed(bip39.generateMnemonic()) 22 | seed = crypto.blake2b(seed) 23 | const wal = await wallet.load({ seed }) 24 | const ks = await keystore.create() 25 | const ko = await keystore.dump({ 26 | password: 'test', 27 | privateKey: wal.getPrivateKey(), 28 | salt: ks.salt, 29 | iv: ks.iv 30 | }) 31 | 32 | t.true('object' === typeof ko) 33 | t.true(0 === Buffer.compare( 34 | toBuffer(ko.crypto.cipherparams.iv), 35 | toBuffer(ks.iv) 36 | )) 37 | 38 | t.true(0 === Buffer.compare( 39 | toBuffer(ko.crypto.kdfparams.salt), 40 | toBuffer(ks.salt) 41 | )) 42 | }) 43 | -------------------------------------------------------------------------------- /test/archive.js: -------------------------------------------------------------------------------- 1 | const context = require('ara-context')() 2 | const test = require('ava') 3 | 4 | const { archive } = require('../archive') 5 | const { create } = require('../create') 6 | 7 | test('archive() Invalid opts', async (t) => { 8 | const identity = await create({ context, password: 'password' }) 9 | 10 | await t.throwsAsync( 11 | archive(), 12 | { instanceOf: TypeError }, 13 | 'Expecting identity to be an object.' 14 | ) 15 | 16 | // TODO: Find a way to ignore $HOME/.ararc when runnnig locally 17 | // await t.throwsAsync(archive(identity), TypeError, 'Expecting options to be an object.') 18 | // await t.throwsAsync(archive(identity, {}), TypeError, 'Shared secret cannot be empty.') 19 | 20 | await t.throwsAsync( 21 | archive(identity, { secret: 1234 }), 22 | { instanceOf: TypeError }, 23 | 'Expecting shared secret to be a string or buffer.' 24 | ) 25 | 26 | await t.throwsAsync( 27 | archive(identity, { secret: 'test-node', keyring: './fixtures/keyring.pub' }), 28 | { instanceOf: TypeError }, 29 | 'Expecting network name for the archiver.' 30 | ) 31 | 32 | await t.throwsAsync( 33 | archive(identity, { secret: 'test-node', keyring: './fixtures/keyring.pub', network: 'archiver' }), 34 | { instanceOf: Error }, 35 | 'Archiver request timed out.' 36 | ) 37 | }) 38 | -------------------------------------------------------------------------------- /recover.js: -------------------------------------------------------------------------------- 1 | const bip39 = require('bip39') 2 | const { create } = require('./create') 3 | 4 | /** 5 | * Recover an Identity using a bip39 mnemonic 6 | * @public 7 | * @param {object} opts 8 | * @param {object} opts.context 9 | * @param {string} opts.password 10 | * @param {string} opts.mnemonic 11 | * @param {array} opts.ddo (optional) 12 | * @throws TypeError 13 | * @return {object} 14 | */ 15 | 16 | async function recover(opts) { 17 | if (null == opts || 'object' !== typeof opts) { 18 | throw new TypeError('Expecting opts to be an object.') 19 | } 20 | 21 | if (null == opts.password) { 22 | throw new TypeError('Expecting password for recovery.') 23 | } 24 | 25 | if (opts.context && 'object' !== typeof opts.context) { 26 | throw new TypeError('Expecting context object.') 27 | } 28 | 29 | if (opts.context && 'object' !== typeof opts.context.web3) { 30 | throw new TypeError('Expecting web3 to be in context.') 31 | } 32 | 33 | if (null == opts.mnemonic) { 34 | throw new TypeError('Expecting mnemonic for recovery.') 35 | } else if (opts.mnemonic && 'string' !== typeof opts.mnemonic) { 36 | throw new TypeError('Expecting mnemonic to be a string.') 37 | } 38 | 39 | if (!bip39.validateMnemonic(opts.mnemonic)) { 40 | throw new TypeError('Expecting a valid bip39 mnemonic for recovery.') 41 | } 42 | 43 | const identity = await create(opts) 44 | return identity 45 | } 46 | 47 | module.exports = { 48 | recover 49 | } 50 | -------------------------------------------------------------------------------- /test/update.js: -------------------------------------------------------------------------------- 1 | const { Ed25519VerificationKey2018 } = require('ld-cryptosuite-registry') 2 | const { PublicKey } = require('did-document/public-key') 3 | const crypto = require('ara-crypto') 4 | const test = require('ava') 5 | const { update } = require('../update') 6 | const { create } = require('../create') 7 | 8 | test('update() valid ARA id', async (t) => { 9 | const password = 'password' 10 | const identity = await create({ password }) 11 | 12 | t.true('object' === typeof identity) 13 | t.true(null !== identity.mnemonic) 14 | 15 | const keyPair = crypto.ed25519.keyPair() 16 | const publicKey = new PublicKey({ 17 | id: `${identity.did.did}#public-key`, 18 | type: Ed25519VerificationKey2018, 19 | owner: identity.did.did, 20 | publicKeyHex: keyPair.publicKey.toString('hex') 21 | }) 22 | 23 | identity.ddo.addPublicKey(publicKey) 24 | 25 | const updated = await update(identity.did.did, { 26 | password, 27 | publicKey: identity.publicKey, 28 | secretKey: identity.secretKey, 29 | ddo: identity.ddo, 30 | keystore: { 31 | ara: identity.files.find(({ path }) => 'keystore/ara' === path), 32 | eth: identity.files.find(({ path }) => 'keystore/eth' === path), 33 | } 34 | }) 35 | 36 | let added = false 37 | for (const pk of updated.ddo.publicKey) { 38 | if (pk.id === publicKey.id && pk.publicKeyHex === publicKey.publicKeyHex) { 39 | added = true 40 | break 41 | } 42 | } 43 | 44 | t.true(added, 'public key added to ddo and updated') 45 | }) 46 | -------------------------------------------------------------------------------- /whoami.js: -------------------------------------------------------------------------------- 1 | const isDomainName = require('is-domain-name') 2 | 3 | const { resolveDNS } = require('./util') 4 | const { normalize } = require('./did') 5 | const { resolve } = require('./resolve') 6 | const rc = require('./rc')() 7 | 8 | async function whoami(opts) { 9 | const defaults = { 10 | cache: true, 11 | fast: false, 12 | network: rc.network.identity.resolver.network, 13 | keyring: ( 14 | rc.network.identity.resolver.keyring 15 | || rc.network.identity.keyring 16 | || rc.network.keyring 17 | ), 18 | 19 | secret: ( 20 | rc.network.identity.resolver.secret 21 | || rc.network.identity.secret 22 | || rc.network.secret 23 | ) 24 | } 25 | 26 | const ropts = Object.assign(defaults, opts) 27 | 28 | const identifier = ( 29 | ropts.identifier 30 | || rc.network.identity.whoami 31 | || rc.network.whoami 32 | ) 33 | 34 | if (!identifier) { 35 | throw new Error('Could not determine identifier in `whoami`.') 36 | } 37 | 38 | if (true === ropts.fast) { 39 | if (isDomainName(identifier)) { 40 | return normalize(await resolveDNS(identifier)) 41 | } 42 | 43 | return normalize(identifier) 44 | } 45 | 46 | let ddo = null 47 | 48 | try { 49 | ddo = await resolve(identifier, ropts) 50 | } catch (err) { 51 | throw new Error(`Unable to resolve identity: ${identifier}.`) 52 | } 53 | 54 | if (!ddo) { 55 | throw new Error('Missing or malformed identity document (ddo.json).') 56 | } 57 | 58 | return ddo.id 59 | } 60 | 61 | module.exports = { 62 | whoami 63 | } 64 | -------------------------------------------------------------------------------- /test/create.js: -------------------------------------------------------------------------------- 1 | const context = require('ara-context')() 2 | const test = require('ava') 3 | 4 | const { create } = require('../create') 5 | 6 | test('create() valid ARA id', async (t) => { 7 | const identity = await create({ context, password: 'password' }) 8 | t.true('object' === typeof identity) 9 | t.true(null !== identity.mnemonic) 10 | }) 11 | 12 | test('create() using invalid mnemonic', async (t) => { 13 | await t.throwsAsync( 14 | create({ context, password: 'password', mnemonic: 'exhaust' }), 15 | { instanceOf: TypeError }, 16 | 'Expecting a valid bip39 mnemonic' 17 | ) 18 | }) 19 | 20 | test('create() valid ARA id using mnemonic', async (t) => { 21 | const identity = await create({ 22 | context, 23 | password: 'password', 24 | mnemonic: 'exhaust rescue vapor misery spot domain pink dice frown occur ice code' 25 | }) 26 | 27 | t.true('object' === typeof identity) 28 | }) 29 | 30 | test('create() valid ARA id with service endpoints', async (t) => { 31 | t.plan(2) 32 | const service = [] 33 | service.push({ 34 | id: 'arasite', 35 | type: 'ara-site.Service', 36 | serviceEndpoint: 'http://www.ara.one', 37 | description: 'This is our project site' 38 | }) 39 | service.push({ 40 | id: 'aradev', 41 | type: 'ara-dev.Service', 42 | serviceEndpoint: 'http://www.ara.one/dev', 43 | description: 'This is our developer page', 44 | price: '10 Ara' 45 | }) 46 | const identity = await create({ context, password: 'password', ddo: { service } }) 47 | t.true(true === Array.isArray(identity.ddo.service)) 48 | t.true(2 === identity.ddo.service.length) 49 | }) 50 | -------------------------------------------------------------------------------- /test/revoke.js: -------------------------------------------------------------------------------- 1 | const context = require('ara-context')() 2 | const bip39 = require('bip39') 3 | const test = require('ava') 4 | 5 | const { revoke } = require('../revoke') 6 | 7 | test('revoke() invalid opts', async (t) => { 8 | t.plan(9) 9 | const mnemonic = bip39.generateMnemonic() 10 | 11 | await t.throwsAsync( 12 | revoke(), 13 | { instanceOf: TypeError }, 14 | 'Expecting opts to be an object.' 15 | ) 16 | 17 | await t.throwsAsync( 18 | revoke({ }), 19 | { instanceOf: TypeError }, 20 | 'Expecting web3 context object.' 21 | ) 22 | 23 | await t.throwsAsync( 24 | revoke({ context: 'web3' }), 25 | { instanceOf: TypeError }, 26 | 'Expecting web3 context object.' 27 | ) 28 | 29 | await t.throwsAsync( 30 | revoke({ context }), 31 | { instanceOf: TypeError }, 32 | 'Expecting mnemonic for revoking.' 33 | ) 34 | 35 | await t.throwsAsync( 36 | revoke({ context, mnemonic: 1234 }), 37 | { instanceOf: TypeError }, 38 | 'Expecting mnemonic to be a string.' 39 | ) 40 | 41 | await t.throwsAsync( 42 | revoke({ context, mnemonic }), 43 | { instanceOf: TypeError }, 44 | 'Expecting password.' 45 | ) 46 | 47 | await t.throwsAsync( 48 | revoke({ context, mnemonic: 'hello' }), 49 | { instanceOf: TypeError }, 50 | 'Expecting a valid bip39 mnemonic for revoking.' 51 | ) 52 | 53 | await t.throwsAsync( 54 | revoke({ context, mnemonic, password: 1234 }), 55 | { instanceOf: TypeError }, 56 | 'Expecting password to be a string.' 57 | ) 58 | 59 | await t.throwsAsync( 60 | revoke({ context, mnemonic, password: 'test' }), 61 | { instanceOf: Error }, 62 | 'Could not resolve DID for the provided mnemonic' 63 | ) 64 | }) 65 | -------------------------------------------------------------------------------- /list.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | const { resolve } = require('path') 3 | const debug = require('debug')('ara:identity:list') 4 | const pify = require('pify') 5 | const fs = require('fs') 6 | const rc = require('./rc')() 7 | 8 | /** 9 | * Fetch a list of DID identities for ara stored locally on 10 | * the file system. 11 | * 12 | * @public 13 | * @param {?(String)} path 14 | * @return {Promise} 15 | * @throws TypeError 16 | */ 17 | async function list(path) { 18 | const identities = [] 19 | const visits = [] 20 | let entries = [] 21 | 22 | if (undefined === path) { 23 | // eslint-disable-next-line no-param-reassign 24 | path = resolve(rc.network.identity.root) 25 | } 26 | 27 | try { 28 | entries = await pify(fs.readdir)(path) 29 | } catch (err) { 30 | throw new Error(`Cannot read identities directory '${path}'.`) 31 | } 32 | 33 | for (const entry of entries) { 34 | visits.push(pify(visit)(resolve(path, entry))) 35 | } 36 | 37 | // wait for all visits to resolve and then resolve the identities 38 | // array that each visit may append to 39 | await Promise.all(visits) 40 | return identities 41 | 42 | function visit(entry, cb) { 43 | const file = resolve(entry, 'ddo.json') 44 | debug('visit', file) 45 | fs.access(file, onaccess) 46 | function onaccess(err) { 47 | if (null === err) { 48 | fs.readFile(file, onread) 49 | } else { 50 | cb(null) 51 | } 52 | } 53 | 54 | function onread(err, buf) { 55 | if (err) { 56 | cb(err) 57 | } else { 58 | try { 59 | const { id } = JSON.parse(buf) 60 | 61 | if ('string' === typeof id) { 62 | identities.push(id) 63 | } 64 | 65 | cb(null) 66 | } catch (err2) { 67 | debug('list: visit: onread: error:', err2) 68 | cb(null) 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | module.exports = { 76 | list 77 | } 78 | -------------------------------------------------------------------------------- /test/ethereum/recover.js: -------------------------------------------------------------------------------- 1 | const context = require('ara-context')() 2 | const test = require('ava') 3 | 4 | const { create } = require('../../create') 5 | const keystore = require('../../ethereum/keystore') 6 | 7 | test('recover(opts)', async (t) => { 8 | const mnemonic1 = ( 9 | 'delay blanket scene cactus ' 10 | + 'rare bicycle embark wheel ' 11 | + 'swallow laptop predict moral' 12 | ) 13 | 14 | const password1 = 'hello123' 15 | const identity1 = await create({ 16 | context, 17 | mnemonic: mnemonic1, 18 | password: password1 19 | }) 20 | 21 | const files1 = identity1.files 22 | 23 | let keys 24 | let encryptedKS 25 | 26 | files1.forEach((file) => { 27 | if ('keystore/eth' === file.path) { 28 | encryptedKS = file.buffer.toString('utf8') 29 | } 30 | if ('keystore/ara' === file.path) { 31 | keys = file.buffer.toString('utf8') 32 | } 33 | }) 34 | 35 | const { web3 } = context 36 | const privateKey1 = await keystore.recover(password1, keys, encryptedKS) 37 | t.is(identity1.account.privateKey, web3.utils.bytesToHex(privateKey1)) 38 | 39 | const mnemonic2 = ( 40 | 'delay blanket scene cactus ' 41 | + 'rare bicycle embark wheel ' 42 | + 'swallow laptop predict moral' 43 | ) 44 | 45 | const password2 = 'hello23456' 46 | const identity2 = await create({ 47 | context, 48 | mnemonic: mnemonic2, 49 | password: password2 50 | }) 51 | 52 | const files2 = identity2.files // eslint-disable-line prefer-destructuring 53 | 54 | files2.forEach((file) => { 55 | if ('keystore/eth' === file.path) { 56 | encryptedKS = file.buffer.toString('utf8') 57 | } 58 | if ('keystore/ara' === file.path) { 59 | keys = file.buffer.toString('utf8') 60 | } 61 | }) 62 | const privateKey2 = await keystore.recover(password2, keys, encryptedKS) 63 | t.is(identity2.account.privateKey, web3.utils.bytesToHex(privateKey2)) 64 | 65 | t.true(0 === Buffer.compare(privateKey1, privateKey2)) 66 | }) 67 | -------------------------------------------------------------------------------- /.github/COMMIT_FORMAT_EXAMPLES.md: -------------------------------------------------------------------------------- 1 | Commit Message Examples 2 | ======================= 3 | 4 | ## Chores 5 | 6 | A `chore` type is a task not directly tied to a feature, fix, or test. It is 7 | often work that requires no change to production code. 8 | 9 | ``` 10 | chore(scripts/): Moved extraneous scripts into scripts/ directory 11 | ``` 12 | 13 | ## Documentation 14 | 15 | A `docs` type is a task that directly effects documentation that is 16 | constructed manually, programmitcally, or through a third-party. This 17 | can include typos, additions, deletions, and examples. 18 | 19 | ``` 20 | docs(resolve.js): Describe new resolve API 21 | ``` 22 | 23 | ## Features 24 | 25 | A `feat` type is a task that introduces a new feature. The new feature 26 | may introduce a breaking change to production code. 27 | 28 | ``` 29 | feat(create.js): Introduce new identity creation 30 | ``` 31 | 32 | ## Fixes/bugs 33 | 34 | A `fix` type is a task that addresses a bug in production code, build 35 | scripts, compilation steps, or anything that directly or indiretly breaks or 36 | impacts production. 37 | 38 | ``` 39 | fix(buffer.js): Fix the freeing of buffer resources 40 | ``` 41 | 42 | ## Refactoring 43 | 44 | A `refactor` type is a task that changes existing code. A refactor 45 | should be an improvement to the existing production code. 46 | 47 | ``` 48 | refactor(platform.js): Simplify platform logic 49 | ``` 50 | 51 | ## Code style 52 | 53 | A `style` type is a task that addresses code formatting such as missing 54 | semicolons, converting tabs to spaces, or removing extra newlines. There 55 | should not be any code changes. 56 | 57 | ``` 58 | style(drive.js): Convert tabs to spaces 59 | ``` 60 | 61 | ## Tests 62 | 63 | A `test` type is a task that addresses the testing of production code. 64 | This may include adding a new or missing test, refactoring existing 65 | tests, or removing useless tests. There should not be any code changes. 66 | 67 | ``` 68 | test(buffer.js): Fix broken buffer alloc logic 69 | ``` 70 | -------------------------------------------------------------------------------- /test/ethereum/account.js: -------------------------------------------------------------------------------- 1 | const context = require('ara-context')() 2 | const Web3 = require('web3') 3 | const test = require('ava') 4 | 5 | const { create: createIdentity } = require('../../create') 6 | const { writeIdentity } = require('../../util') 7 | const { create, load } = require('../../ethereum/account') 8 | 9 | const web3 = new Web3('http://127.0.0.1:9545') 10 | const password = 'password' 11 | 12 | test('ethereum.create({entropy})', async (t) => { 13 | await t.throwsAsync(create({ web3, privateKey: 0 }), { instanceOf: TypeError }, 'ethereum.account.create: Expecting privateKey to be a buffer') 14 | const account = await create({ web3, privateKey: Buffer.from('b06c21d8e52ce9f89c40695ce79c3349bacf90418f84137220c503d14e2b5e36') }) 15 | t.true(null != account) 16 | t.true('object' === typeof account) 17 | t.true('string' === typeof account.address) 18 | t.true('string' === typeof account.privateKey) 19 | }) 20 | 21 | test('ethereum.load({opts}) invalid args', async (t) => { 22 | await t.throwsAsync(load(), { instanceOf: TypeError }, 'Expecting opts object') 23 | await t.throwsAsync(load({ web3: null }), { instanceOf: TypeError }, 'Expecting web3 object') 24 | await t.throwsAsync(load({ web3, publicKey: 1234 }), { instanceOf: TypeError }, 'Expecting publicKey to be non-empty string') 25 | await t.throwsAsync(load({ 26 | web3, 27 | publicKey: 'b06c21d8e52ce9f89c40695ce79c3349bacf90418f84137220c503d14e2b5e36', 28 | password: 1234 29 | }), { instanceOf: TypeError }, 'Expecting password to be a string') 30 | }) 31 | 32 | test('ethereum.load({opts}) valid args', async (t) => { 33 | // create account 34 | const identity = await createIdentity({ context, password }) 35 | await writeIdentity(identity) 36 | const { publicKey, account } = identity 37 | 38 | // reload account 39 | const loadedAccount = await load({ 40 | web3, 41 | publicKey, 42 | password 43 | }) 44 | t.is(account.address, loadedAccount.address) 45 | t.is(account.privateKey, loadedAccount.privateKey) 46 | }) 47 | -------------------------------------------------------------------------------- /did.js: -------------------------------------------------------------------------------- 1 | const { DID, parse } = require('did-uri') 2 | const isDomainName = require('is-domain-name') 3 | const isBuffer = require('is-buffer') 4 | const { toHex } = require('./util') 5 | 6 | const DID_ARA_METHOD = 'ara' 7 | const IDENTIFIER_LENGTH = 64 8 | 9 | /** 10 | * Creates a DID document (DDO) from an identifier. 11 | * @public 12 | * @param {String|Buffer} identifier 13 | * @param {?(String)} [method] 14 | * @return {Object} 15 | * @throws TypeError 16 | */ 17 | function create(identifier, method) { 18 | if (!identifier) { 19 | throw new TypeError('Expecting identifier.') 20 | } 21 | 22 | if ('string' !== typeof identifier && false === isBuffer(identifier)) { 23 | throw new TypeError('Expecting identifier to be a string or buffer.') 24 | } 25 | 26 | if (method && 'string' !== typeof method) { 27 | throw new TypeError('Expecting method to be a string.') 28 | } 29 | 30 | const id = toHex(identifier) 31 | const didMethod = method || DID_ARA_METHOD 32 | const uri = `did:${didMethod}:${id}` 33 | return new DID(uri) 34 | } 35 | 36 | /** 37 | * Normalizes a given DID URI to Prefix & Identifier 38 | * @public 39 | * @return {String} 40 | * @throws TypeError 41 | */ 42 | function normalize(uri, method) { 43 | if (!uri) { 44 | throw new TypeError('Expecting URI.') 45 | } 46 | 47 | if ('string' !== typeof uri) { 48 | throw new TypeError('Expecting URI to be a string or buffer.') 49 | } 50 | 51 | if (isDomainName(uri)) { 52 | throw new Error('DNS resolvable names are not allowed') 53 | } 54 | 55 | if (method && 'string' !== typeof method) { 56 | throw new TypeError('Expecting method to be a string.') 57 | } 58 | 59 | method = method || DID_ARA_METHOD 60 | 61 | const prefix = `did:${method}:` 62 | 63 | if (prefix !== uri.slice(0, prefix.length)) { 64 | if (DID_ARA_METHOD === method && uri.length !== IDENTIFIER_LENGTH) { 65 | throw new Error(`Expecting Ara URI to be of length ${IDENTIFIER_LENGTH}. Got ${uri}. Ensure Ara URI is a valid hex string.`) 66 | } 67 | return prefix + uri 68 | } 69 | 70 | return uri 71 | } 72 | 73 | module.exports = { 74 | normalize, 75 | create, 76 | parse, 77 | DID, 78 | } 79 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PREFIX="${PREFIX-/usr/local}" 4 | ARA_DIR="${ARA_DIR-$HOME/.ara}" 5 | BIN="$ARA_DIR/bin" 6 | AID="${AID:-$BIN/ara-identity}" 7 | OS=$(uname) 8 | 9 | function ontrap { 10 | if [ -f $BASHRC.bak ]; then 11 | mv -f $BASHRC.bak $BASHRC 12 | fi 13 | } 14 | 15 | trap ontrap EXIT 16 | 17 | echo " mkdirp: $ARA_DIR" 18 | mkdir -p $ARA_DIR 19 | 20 | echo " mkdirp: $BIN" 21 | mkdir -p $BIN 22 | 23 | echo " mkdirp: $AID" 24 | mkdir -p $AID 25 | 26 | echo " install: $(ls *.node --color=none | tr '\n' ' ')" 27 | cp *.node $AID 28 | 29 | echo " install: $AID" 30 | cp aid $AID 31 | 32 | if test -f aid.exe; then 33 | echo " error: TODO: aid.exe" 34 | exit 1 35 | else 36 | ln -sf $AID/aid $BIN/aid 37 | if ! test -f $PREFIX/bin/aid; then 38 | ln -sf $AID/aid $PREFIX/bin/aid 39 | echo " install: $PREFIX/bin/aid" 40 | fi 41 | fi 42 | 43 | if [ "--completions" = "$1" ]; then 44 | if [ -z "$BASHRC" ]; then 45 | if [[ "Darwin" == "$OS" ]] || ! [ -f "$HOME/.bashrc" ]; then 46 | BASHRC="$HOME/.bash_profile" 47 | else 48 | BASHRC="$HOME/.bashrc" 49 | fi 50 | fi 51 | 52 | if ! [ -f "$BASHRC" ] || [ -z "$BASHRC" ]; then 53 | echo >&2 " error: Unable to determine .bashrc or .bash_profile" 54 | exit 1 55 | fi 56 | 57 | TMPBASHRC="$BASHRC.tmp" 58 | PATTERN='s/###\-begin\-aid\-completions\-###.*###-end-aid-completions-###//' 59 | 60 | rm -f $TMPBASHRC 61 | 62 | echo " backup: $BASHRC" 63 | cp -f $BASHRC $BASHRC.bak 64 | 65 | cat $BASHRC | \ 66 | tr '\n' '\r' | \ 67 | sed $PATTERN | \ 68 | tr '\r' '\n' > $TMPBASHRC 69 | 70 | echo " diff: (before) $BASHRC <> $TMPBASHRC" 71 | diff $BASHRC $TMPBASHRC 72 | 73 | echo " install: aid completions " 74 | $BIN/aid __completions | \ 75 | sed 's/_yargs/_aid/g' | \ 76 | sed 's/begin-yargs/begin-aid/g' | \ 77 | sed 's/end-yargs/end-aid/g' >> $TMPBASHRC 78 | 79 | echo " diff: (after) $BASHRC <> $TMPBASHRC" 80 | diff $BASHRC $TMPBASHRC 81 | 82 | echo " install: $BASHRC (aid completions)" 83 | mv -f $TMPBASHRC $BASHRC 84 | 85 | echo " cleanup: $TMPBASHRC $BASHRC.bak" 86 | rm -f $TMPBASHRC $BASHRC.bak 87 | 88 | fi 89 | 90 | echo 91 | echo " ok!" 92 | -------------------------------------------------------------------------------- /revoke.js: -------------------------------------------------------------------------------- 1 | const crypto = require('ara-crypto') 2 | const bip39 = require('bip39') 3 | const { normalize } = require('./did') 4 | const { resolve } = require('./resolve') 5 | const { create } = require('./create') 6 | 7 | /** 8 | * Revoke an identity using a bip39 mnemonic 9 | * @public 10 | * @param {object} opts 11 | * @param {object} opts.context 12 | * @param {string} opts.mnemonic 13 | * @param {string} opts.password 14 | * @throws TypeError 15 | * @return {object} 16 | */ 17 | 18 | async function revoke(opts) { 19 | if (null == opts || 'object' !== typeof opts) { 20 | throw new TypeError('Expecting opts to be an object.') 21 | } 22 | 23 | if (opts.context && 'object' !== typeof opts.context) { 24 | throw new TypeError('Expecting context object.') 25 | } 26 | 27 | if (opts.context && 'object' !== typeof opts.context.web3) { 28 | throw new TypeError('Expecting web3 to be in context.') 29 | } 30 | 31 | if (null == opts.mnemonic) { 32 | throw new TypeError('Expecting mnemonic for revoking.') 33 | } else if (opts.mnemonic && 'string' !== typeof opts.mnemonic) { 34 | throw new TypeError('Expecting mnemonic to be a string.') 35 | } 36 | 37 | if (!bip39.validateMnemonic(opts.mnemonic)) { 38 | throw new TypeError('Expecting a valid bip39 mnemonic for revoking.') 39 | } 40 | 41 | if (null == opts.password) { 42 | throw new TypeError('Expecting password.') 43 | } else if (opts.password && 'string' !== typeof opts.password) { 44 | throw new TypeError('Expecting password to be a string.') 45 | } 46 | 47 | const seed = crypto.blake2b(await bip39.mnemonicToSeed(opts.mnemonic)) 48 | const { publicKey } = crypto.keyPair(seed) 49 | 50 | let ddo 51 | try { 52 | ddo = await resolve(normalize(publicKey.toString('hex'))) 53 | } catch (err) { 54 | throw new Error('Could not resolve DID for the provided mnemonic') 55 | } 56 | 57 | if (ddo.revoked && 'string' === typeof ddo.revoked) { 58 | throw new Error('DID for the provided mnemonic has already been revoked') 59 | } 60 | 61 | opts.created = ddo.created 62 | opts.revoked = true 63 | opts.ddo = ddo 64 | 65 | const identity = await create(opts) 66 | return identity 67 | } 68 | 69 | module.exports = { 70 | revoke 71 | } 72 | -------------------------------------------------------------------------------- /test/recover.js: -------------------------------------------------------------------------------- 1 | const context = require('ara-context')() 2 | const bip39 = require('bip39') 3 | const test = require('ava') 4 | 5 | const { recover } = require('../recover') 6 | const { create } = require('../create') 7 | 8 | test('recover() invalid opts', async (t) => { 9 | await t.throwsAsync( 10 | recover(), 11 | { instanceOf: TypeError }, 12 | 'Expecting opts to be an object.' 13 | ) 14 | 15 | await t.throwsAsync( 16 | recover({ context }), 17 | { instanceOf: TypeError }, 18 | 'Expecting password for recovery.' 19 | ) 20 | 21 | await t.throwsAsync( 22 | recover({ context, password: 123 }), 23 | { instanceOf: TypeError }, 24 | 'Expecting mnemonic for recovery.' 25 | ) 26 | 27 | await t.throwsAsync( 28 | recover({ context, password: 123, mnemonic: 1234 }), 29 | { instanceOf: TypeError }, 30 | 'Expecting mnemonic to be a string.' 31 | ) 32 | 33 | await t.throwsAsync( 34 | recover({ context, password: 123, mnemonic: 'hello how' }), 35 | { instanceOf: TypeError }, 36 | 'Expecting a valid bip39 mnemonic for recovery.' 37 | ) 38 | }) 39 | 40 | test('recover() using valid mnemonic', async (t) => { 41 | const mnemonic = bip39.generateMnemonic() 42 | const identity = await recover({ context, password: 'password', mnemonic }) 43 | t.true('object' === typeof identity) 44 | t.true(mnemonic === identity.mnemonic) 45 | }) 46 | 47 | test('recover() compare recovered identity', async (t) => { 48 | const identity = await create({ context, password: '123' }) 49 | // eslint-disable-next-line prefer-destructuring 50 | const mnemonic = identity.mnemonic 51 | const recoveredID = await recover({ context, password: 'hello', mnemonic }) 52 | t.true(identity.account.privateKey === recoveredID.account.privateKey) 53 | t.true(identity.mnemonic === recoveredID.mnemonic) 54 | t.true(0 === Buffer.compare(identity.publicKey, recoveredID.publicKey)) 55 | t.true(0 === Buffer.compare(identity.secretKey, recoveredID.secretKey)) 56 | 57 | t.true(0 === Buffer.compare( 58 | identity.wallet.getPrivateKey(), 59 | recoveredID.wallet.getPrivateKey() 60 | )) 61 | 62 | t.true(0 === Buffer.compare( 63 | identity.wallet.getPublicKey(), 64 | recoveredID.wallet.getPublicKey() 65 | )) 66 | }) 67 | -------------------------------------------------------------------------------- /scripts/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CWD="$(pwd)" 4 | DIR="$(cd `dirname ${BASH_SOURCE[0]}` > /dev/null && pwd)" 5 | PKG=${PKG:-$(which pkg)} 6 | BUILD=${BUILD:-build} 7 | TARGET=${TARGET:-bin/aid} 8 | 9 | if [ -z "$PKG" ]; then 10 | PKG="node_modules/.bin/pkg" 11 | fi 12 | 13 | rm -rf $BUILD 14 | mkdir -p $BUILD 15 | 16 | build() { 17 | printf "> build: %s\n" $TARGET 18 | $PKG --output --public --out-path $BUILD $TARGET \ 19 | | while read line; do \ 20 | if $(echo $line | grep $CWD >/dev/null) && \ 21 | $(echo $line | grep '\.node' >/dev/null); then \ 22 | local path="$line"; \ 23 | printf "> dep: %s\n" $(basename $path); \ 24 | cp $path $BUILD; \ 25 | else \ 26 | echo $line; \ 27 | fi; \ 28 | done 29 | 30 | local rc=$? 31 | return $rc 32 | } 33 | 34 | 35 | build 36 | 37 | mkdir $BUILD/{macos,linux,win} 38 | 39 | mv $BUILD/aid-macos $BUILD/macos/aid 40 | mv $BUILD/aid-linux $BUILD/linux/aid 41 | mv $BUILD/aid-win.exe $BUILD/win/aid.exe 42 | 43 | cp $BUILD/*.node $BUILD/macos 44 | cp $BUILD/*.node $BUILD/linux 45 | cp $BUILD/*.node $BUILD/win 46 | 47 | rm -f $BUILD/*.node 48 | 49 | cp "$DIR/install.sh" $BUILD/macos 50 | cp "$DIR/install.sh" $BUILD/linux 51 | cp "$DIR/install.sh" $BUILD/win 52 | 53 | cp "$DIR/../README.md" $BUILD/macos 54 | cp "$DIR/../README.md" $BUILD/linux 55 | cp "$DIR/../README.md" $BUILD/win 56 | 57 | cp "$DIR/../LICENSE" $BUILD/macos 58 | cp "$DIR/../LICENSE" $BUILD/linux 59 | cp "$DIR/../LICENSE" $BUILD/win 60 | 61 | cp "$DIR/../CHANGELOG.md" $BUILD/macos 62 | cp "$DIR/../CHANGELOG.md" $BUILD/linux 63 | cp "$DIR/../CHANGELOG.md" $BUILD/win 64 | 65 | zip -r $BUILD/macos.zip $BUILD/macos 66 | zip -r $BUILD/linux.zip $BUILD/linux 67 | zip -r $BUILD/win.zip $BUILD/win 68 | 69 | if test -f $HOME/.bashrc; then 70 | BASHRC="$HOME/.bashrc" 71 | elif test -f $HOME/.bash_profile; then 72 | BASHRC="$HOME/.bash_profile" 73 | elif test -f $HOME/.zshrc; then 74 | BASHRC="$HOME/.zshrc" 75 | fi 76 | 77 | if test -f $BASHRC; then 78 | if ! $(cat $BASHRC | grep 'PATH=' | grep '.ara/bin' >/dev/null); then 79 | { 80 | echo '## Ara bin path :]'; 81 | echo 'export PATH="$HOME/.ara/bin:$PATH"'; 82 | } >> $BASHRC 83 | fi 84 | fi 85 | exit $? 86 | -------------------------------------------------------------------------------- /ethereum/account.js: -------------------------------------------------------------------------------- 1 | const isBuffer = require('is-buffer') 2 | const { toHex, toBuffer } = require('../util') 3 | const { recover } = require('./keystore') 4 | const fs = require('../fs') 5 | 6 | /** 7 | * Creates an Ethereum account with web3 specified by 8 | * an optional entropy value. 9 | * 10 | * @public 11 | * @param {Object} opts 12 | * @param {Object} opts.web3 13 | * @param {Number} opts.entropy 14 | * @return {Object} 15 | * @throws TypeError 16 | */ 17 | async function create(opts) { 18 | if (!opts || 'object' !== typeof opts) { 19 | throw new TypeError('Expecting object.') 20 | } 21 | 22 | if (!opts.web3 || 'object' !== typeof opts.web3) { 23 | throw new TypeError('Expecting web3 to be an object.') 24 | } 25 | 26 | if (!opts.privateKey || false === isBuffer(opts.privateKey)) { 27 | throw new TypeError('Expecting privateKey to be a buffer') 28 | } 29 | 30 | const { web3 } = opts 31 | const privateKey = web3.utils.bytesToHex(opts.privateKey) 32 | const account = web3.eth.accounts.privateKeyToAccount(privateKey) 33 | return account 34 | } 35 | 36 | /** 37 | * Loads an Ethereum account based on a publicKey 38 | * and password. 39 | * 40 | * @public 41 | * @param {Object} opts 42 | * @param {Object} opts.web3 43 | * @param {Object} opts.publicKey 44 | * @param {Object} opts.password 45 | * @returns {Object} 46 | * @throws {TypeError} 47 | */ 48 | async function load(opts) { 49 | if (!opts || 'object' !== typeof opts) { 50 | throw new TypeError('Expecting object') 51 | } 52 | 53 | if (!opts.web3 || 'object' !== typeof opts.web3) { 54 | throw new TypeError('Expecting web3 object') 55 | } 56 | 57 | if (!opts.publicKey || ('string' !== typeof opts.publicKey 58 | && !isBuffer(opts.publicKey))) { 59 | throw new TypeError('Expecting public key to be string or buffer') 60 | } 61 | 62 | if (!opts.password || 'string' !== typeof opts.password) { 63 | throw new TypeError('Expecting password to be non-empty string') 64 | } 65 | 66 | const { web3 } = opts 67 | 68 | const publicKey = toBuffer(opts.publicKey) 69 | const identifier = toHex(publicKey) 70 | const secretKey = await fs.readFile(identifier, 'keystore/ara') 71 | const encryptedKeystore = await fs.readFile(identifier, 'keystore/eth') 72 | 73 | const buf = await recover(opts.password, secretKey, encryptedKeystore) 74 | const privateKey = web3.utils.bytesToHex(buf) 75 | 76 | return web3.eth.accounts.privateKeyToAccount(privateKey) 77 | } 78 | 79 | module.exports = { 80 | create, 81 | load, 82 | } 83 | -------------------------------------------------------------------------------- /share.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require('events') 2 | const { createSwarm } = require('ara-network/discovery/swarm') 3 | const { createCFS } = require('cfsnet/create') 4 | const debug = require('debug')('ara:identity:share') 5 | const path = require('path') 6 | const pump = require('pump') 7 | const raf = require('random-access-file') 8 | 9 | const { createIdentityKeyPath } = require('./key-path') 10 | const { toHex } = require('./util') 11 | const did = require('./did') 12 | 13 | async function share(identity, opts = {}) { 14 | // DID? 15 | if ('string' === typeof identity) { 16 | const ddo = did.parse(did.normalize(identity)) 17 | const publicKey = Buffer.from(ddo.identifier, 'hex') 18 | identity = { publicKey } 19 | } 20 | 21 | const { publicKey } = identity 22 | const cfs = await createCFS({ 23 | shallow: true, 24 | key: publicKey, 25 | id: toHex(publicKey), 26 | 27 | storage(filename, drive, dir) { 28 | const root = createIdentityKeyPath({ publicKey }) 29 | if ('function' === typeof opts.storage) { 30 | return opts.storage(filename, drive, root) 31 | } 32 | 33 | if ('home' === path.basename(dir)) { 34 | return raf(path.resolve(root, 'home', filename)) 35 | } 36 | 37 | return raf(path.resolve(root, filename)) 38 | } 39 | }) 40 | 41 | const swarms = Object.assign(new EventEmitter(), { 42 | stream: createSwarm({ stream: onstream }), 43 | connection: createSwarm({ }).on('connection', onconnection), 44 | 45 | close() { 46 | try { swarms.stream.close() } catch (err) { 47 | swarms.stream.destroy() 48 | } 49 | 50 | try { swarms.connection.close() } catch (err) { 51 | swarms.connection.destroy() 52 | } 53 | } 54 | }) 55 | 56 | proxyEvents(swarms.stream) 57 | proxyEvents(swarms.connection) 58 | 59 | swarms.stream.join(cfs.discoveryKey, { announce: true }) 60 | swarms.connection.join(cfs.discoveryKey, { announce: true }) 61 | 62 | return swarms 63 | 64 | function proxyEvents(src) { 65 | src.on('peer', (...args) => swarms.emit('peer', ...args)) 66 | src.on('error', (...args) => swarms.emit('error', ...args)) 67 | } 68 | 69 | function createStream() { 70 | return cfs.replicate({ 71 | download: false, 72 | upload: true, 73 | live: true 74 | }) 75 | } 76 | 77 | function onstream() { 78 | return createStream() 79 | } 80 | 81 | function onconnection(connection) { 82 | const stream = createStream() 83 | 84 | pump(stream, connection, stream, (err) => { 85 | if (err) { 86 | debug(err) 87 | } 88 | }) 89 | } 90 | } 91 | 92 | module.exports = { 93 | share 94 | } 95 | -------------------------------------------------------------------------------- /dns.js: -------------------------------------------------------------------------------- 1 | const isDomainName = require('is-domain-name') 2 | const { DID } = require('did-uri') 3 | const dns = require('ara-identity-dns') 4 | const { resolve } = require('./resolve') 5 | const { update } = require('./update') 6 | 7 | const RFC4501 = 'RFC4501' 8 | const DNS_SERVICE_TYPE = 'DNSURIRecord' 9 | 10 | async function query(domain, opts) { 11 | const resolutions = await dns.resolve(domain, opts) 12 | return resolutions 13 | } 14 | 15 | async function bind(identifier, domain, opts) { 16 | if (!isDomainName(domain)) { 17 | throw new TypeError(`Expecting '${domain}' to be a valid domain name.`) 18 | } 19 | 20 | const ddo = opts.ddo || await resolve(identifier, opts) 21 | let isBound = false 22 | 23 | for (const service of ddo.service) { 24 | if (DNS_SERVICE_TYPE === service.type || RFC4501 === service.type) { 25 | const p = service.id.split(';') 26 | const { did } = new DID(p[0]) 27 | const name = p[1] 28 | if (did === ddo.id && name === domain) { 29 | isBound = true 30 | break 31 | } 32 | } 33 | } 34 | 35 | if (isBound) { 36 | throw new Error(`Domain '${domain}' is already bound to identity.`) 37 | } 38 | 39 | const authority = opts.authority ? `//${opts.authority}/` : '' 40 | 41 | ddo.service.push({ 42 | id: `${ddo.id};${domain}`, 43 | type: DNS_SERVICE_TYPE, 44 | serviceEndpoint: `dns://${authority}${domain}?type=${opts.type || 'TXT'}` 45 | }) 46 | 47 | return update(ddo.id, { 48 | password: opts.password, 49 | publicKey: opts.publicKey, 50 | secretKey: opts.secretKey, 51 | ddo, 52 | }) 53 | } 54 | 55 | async function unbind(identifier, domain, opts) { 56 | if (!isDomainName(domain)) { 57 | throw new TypeError(`Expecting '${domain}' to be a valid domain name.`) 58 | } 59 | 60 | const ddo = opts.ddo || await resolve(identifier, opts) 61 | let wasBound = false 62 | 63 | for (let i = 0; i < ddo.service.length; ++i) { 64 | const service = ddo.service[i] 65 | if (DNS_SERVICE_TYPE === service.type || RFC4501 === service.type) { 66 | const p = service.id.split(';') 67 | const { did } = new DID(p[0]) 68 | const name = p[1] 69 | 70 | if (did === ddo.id && name === domain) { 71 | ddo.service.splice(i, 1) 72 | wasBound = true 73 | break 74 | } 75 | } 76 | } 77 | 78 | if (wasBound) { 79 | return update(ddo.id, { 80 | password: opts.password, 81 | publicKey: opts.publicKey, 82 | secretKey: opts.secretKey, 83 | ddo, 84 | }) 85 | } 86 | 87 | throw new Error(`Domain '${domain}' not bound to identity.`) 88 | } 89 | 90 | module.exports = { 91 | resolve, 92 | unbind, 93 | query, 94 | bind, 95 | } 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ara-identity", 3 | "version": "0.65.0", 4 | "description": "Create and resolve decentralized identity based Ara identifiers.", 5 | "main": "index.js", 6 | "bin": { 7 | "aid": "bin/aid" 8 | }, 9 | "scripts": { 10 | "truffle": "truffle develop", 11 | "test": "ava test/*", 12 | "lint": "eslint .", 13 | "lint-fix": "eslint . --quiet --fix", 14 | "pkg": "./scripts/package.sh", 15 | "changelog": "conventional-changelog --same-file --preset angular --release-count 0 --infile CHANGELOG.md", 16 | "version": "npm run changelog && git add CHANGELOG.md" 17 | }, 18 | "browserify": { 19 | "transform": [ 20 | "brfs" 21 | ] 22 | }, 23 | "keywords": [ 24 | "ara", 25 | "identity", 26 | "decentralized", 27 | "identity", 28 | "did" 29 | ], 30 | "author": "Joseph Werle ", 31 | "license": "LGPL-3.0", 32 | "dependencies": { 33 | "ara-console": "^0.3.0", 34 | "ara-context": "^1.0.5", 35 | "ara-crypto": "^0.9.4", 36 | "ara-identity-dns": "^0.3.0", 37 | "ara-network": "^1.4.1", 38 | "ara-runtime-configuration": "^2.0.1", 39 | "ara-secret-storage": "^0.2.1", 40 | "async-exit-hook": "^2.0.1", 41 | "bip39": "^3.0.4", 42 | "brfs": "^2.0.2", 43 | "cfsnet": "^0.20.0", 44 | "debug": "^4.3.1", 45 | "did-document": "^0.6.2", 46 | "did-uri": "^0.5.1", 47 | "ethereumjs-wallet": "^1.0.1", 48 | "extend": "^3.0.1", 49 | "inquirer": "^8.0.0", 50 | "ip": "^1.1.5", 51 | "is-browser": "^2.1.0", 52 | "is-buffer": "^2.0.2", 53 | "is-domain-name": "^1.0.1", 54 | "is-zero-buffer": "^1.0.1", 55 | "keythereum": "^1.0.4", 56 | "ld-cryptosuite-registry": "^0.3.2", 57 | "mirror-folder": "^3.0.0", 58 | "mkdirp": "^1.0.4", 59 | "node-fetch": "^2.3.0", 60 | "pify": "^5.0.0", 61 | "protocol-buffers": "^4.0.4", 62 | "protocol-buffers-encodings": "^1.1.0", 63 | "pump": "^3.0.0", 64 | "random-access-file": "^2.1.1", 65 | "random-access-memory": "^3.0.0", 66 | "randombytes": "^2.1.0", 67 | "rimraf": "^3.0.2", 68 | "table": "^6.6.0", 69 | "yargs": "^16.2.0" 70 | }, 71 | "devDependencies": { 72 | "ava": "^3.15.0", 73 | "conventional-changelog-cli": "^2.1.1", 74 | "eslint": "^7.25.0", 75 | "eslint-config-ara": "github:arablocks/eslint-config-ara#semver:^3.0.x", 76 | "eslint-plugin-import": "^2.22.1", 77 | "pkg": "^5.1.0", 78 | "truffle": "^5.3.4", 79 | "web3": "^1.3.5" 80 | }, 81 | "peerDependencies": { 82 | "ara-context": "^1.0.5", 83 | "ara-network": "^1.4.1", 84 | "ara-runtime-configuration": "^2.0.1" 85 | }, 86 | "directories": { 87 | "test": "test" 88 | }, 89 | "repository": { 90 | "type": "git", 91 | "url": "git+https://github.com/AraBlocks/ara-identity.git" 92 | }, 93 | "bugs": { 94 | "url": "https://github.com/AraBlocks/ara-identity/issues" 95 | }, 96 | "homepage": "https://github.com/AraBlocks/ara-identity#readme", 97 | "pkg": { 98 | "assets": [ 99 | "protobuf/schema.proto" 100 | ] 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /.github/COMMIT_FORMAT.md: -------------------------------------------------------------------------------- 1 | Commit Message Formatting 2 | ========================= 3 | 4 | *Based on the [Git Commit Msg][karma-git-format] format described by Karma.* 5 | 6 | ## Format of a commit message: 7 | 8 | First line cannot be longer than 70 characters, second line is always 9 | blank and other lines should be wrapped at 80 characters. 10 | 11 | ``` 12 | (): 13 | 14 | 15 | 16 |