├── contracts ├── .gitkeep ├── Migrations.sol ├── auth │ ├── Roles.sol │ └── Attributes.sol ├── ContractRegistry.sol ├── Specification.sol └── Authorization.sol ├── babel.config.js ├── public ├── favicon.ico └── index.html ├── src ├── assets │ ├── logo.png │ └── spinner.gif ├── views │ ├── Home.vue │ ├── AddSensor.vue │ ├── Sensor.vue │ ├── Components.vue │ ├── CreateTwin.vue │ ├── Sources.vue │ ├── Account.vue │ ├── Sensors.vue │ ├── Specification.vue │ ├── Documents.vue │ └── Roles.vue ├── store │ ├── index.js │ ├── state.js │ ├── mutations.js │ └── actions.js ├── components │ ├── Spinner.vue │ └── Index.vue ├── plugins │ ├── utils.js │ ├── crypto.js │ └── swarm.js ├── main.js ├── router.js └── App.vue ├── misc ├── Screenshots │ ├── Screenshot_Home.PNG │ ├── Screenshot_Users.PNG │ ├── Screenshot_Account.PNG │ ├── Screenshot_Sensors.PNG │ ├── Screenshot_Components.PNG │ ├── Screenshot_Documents.PNG │ ├── Screenshot_ShareTwin.PNG │ ├── Screenshot_Specification.PNG │ ├── Screenshot_ChangesUserRole.PNG │ └── Screenshot_ChangesUserAttributes.PNG └── logs.xml.example ├── migrations ├── 1_initial_migration.js ├── 3_deploy_rest.js └── 2_deploy_auth.js ├── config.json.example ├── ethertwin.iml ├── .gitignore ├── agent ├── provider-engine.js └── agent.js ├── LICENSE ├── package.json ├── test └── contract_test.js ├── README.md └── truffle-config.js /contracts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma67/ethertwin/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma67/ethertwin/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma67/ethertwin/HEAD/src/assets/spinner.gif -------------------------------------------------------------------------------- /misc/Screenshots/Screenshot_Home.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma67/ethertwin/HEAD/misc/Screenshots/Screenshot_Home.PNG -------------------------------------------------------------------------------- /misc/Screenshots/Screenshot_Users.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma67/ethertwin/HEAD/misc/Screenshots/Screenshot_Users.PNG -------------------------------------------------------------------------------- /misc/Screenshots/Screenshot_Account.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma67/ethertwin/HEAD/misc/Screenshots/Screenshot_Account.PNG -------------------------------------------------------------------------------- /misc/Screenshots/Screenshot_Sensors.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma67/ethertwin/HEAD/misc/Screenshots/Screenshot_Sensors.PNG -------------------------------------------------------------------------------- /misc/Screenshots/Screenshot_Components.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma67/ethertwin/HEAD/misc/Screenshots/Screenshot_Components.PNG -------------------------------------------------------------------------------- /misc/Screenshots/Screenshot_Documents.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma67/ethertwin/HEAD/misc/Screenshots/Screenshot_Documents.PNG -------------------------------------------------------------------------------- /misc/Screenshots/Screenshot_ShareTwin.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma67/ethertwin/HEAD/misc/Screenshots/Screenshot_ShareTwin.PNG -------------------------------------------------------------------------------- /misc/Screenshots/Screenshot_Specification.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma67/ethertwin/HEAD/misc/Screenshots/Screenshot_Specification.PNG -------------------------------------------------------------------------------- /misc/Screenshots/Screenshot_ChangesUserRole.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma67/ethertwin/HEAD/misc/Screenshots/Screenshot_ChangesUserRole.PNG -------------------------------------------------------------------------------- /misc/Screenshots/Screenshot_ChangesUserAttributes.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigma67/ethertwin/HEAD/misc/Screenshots/Screenshot_ChangesUserAttributes.PNG -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import state from './state' 4 | import mutations from './mutations' 5 | import actions from './actions' 6 | 7 | Vue.use(Vuex); 8 | 9 | const store = new Vuex.Store({ 10 | state: state, 11 | mutations: mutations, 12 | actions: actions 13 | }); 14 | 15 | export default store; -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "swarm": "http://132.199.123.236:5000", 3 | "ethereum": { 4 | "rpc": "ws://localhost:7545", 5 | "registry": "0x98abd356b17C8CD88d2321F219C04B825746B29D", 6 | "authorization": "0x9BDbCe5F41648d0C5F8ef2D05BfB8A52997f4e21" 7 | } 8 | "agent_key": "90cecdf9597eb555edce7a4fbf780e8e7b1bd3123a13b8acb045ff9641752eea" 9 | } 10 | -------------------------------------------------------------------------------- /ethertwin.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/store/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | contracts: {}, 3 | balance: -1, 4 | addresses: { 5 | ContractRegistryAddress: "", 6 | AuthorizationAddress: "" 7 | }, 8 | specificationAbi: {}, 9 | user: { 10 | address: "", 11 | isDeviceAgent: false 12 | }, 13 | users: ['0x0'], 14 | selectedTwin: 0, 15 | spinner: false, 16 | twins: [] 17 | } 18 | -------------------------------------------------------------------------------- /migrations/3_deploy_rest.js: -------------------------------------------------------------------------------- 1 | var Authorization = artifacts.require("./Authorization.sol"); 2 | var Specification = artifacts.require("./Specification.sol"); 3 | var ContractRegistry = artifacts.require("./ContractRegistry.sol"); 4 | 5 | module.exports = function(deployer) { 6 | deployer.deploy(Specification, Authorization.address); 7 | deployer.deploy(ContractRegistry, Authorization.address); 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | *.iml 25 | public/contracts/* 26 | /network/ 27 | 28 | config.json 29 | **.xml 30 | -------------------------------------------------------------------------------- /migrations/2_deploy_auth.js: -------------------------------------------------------------------------------- 1 | var Web3 = require("../node_modules/web3/"); 2 | web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); 3 | var Authorization = artifacts.require("./Authorization.sol"); 4 | 5 | module.exports = function(deployer, network, accounts) { 6 | deployer.deploy( 7 | Authorization, 8 | "0x00a329c0648769a73afac7f9381e08fb43dbea72", 9 | { 10 | from: accounts[0], 11 | value: web3.utils.toWei("10000", "ether") 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.13; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | constructor () public { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) external restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) external restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ethertwin 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /misc/logs.xml.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1111 5 | SOFTC1 6 | :SimKey <start> [ bl ] 7 | 0 8 | 9 | 10 | 1112 11 | SOFTC1 12 | :Key <start> 13 | 0 14 | 15 | 16 | 3320 17 | SOFTC1 18 | Redacted 19 | 999.123 20 | > 21 | -------------------------------------------------------------------------------- /agent/provider-engine.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const inherits = require('util').inherits 4 | const HookedWalletEthTxSubprovider = require('web3-provider-engine/subproviders/hooked-wallet-ethtx') 5 | 6 | module.exports = WalletSubprovider 7 | 8 | inherits(WalletSubprovider, HookedWalletEthTxSubprovider) 9 | 10 | function WalletSubprovider (wallet, opts) { 11 | opts = opts || {} 12 | 13 | opts.getAccounts = function (cb) { 14 | cb(null, [ wallet.getAddressString() ]) 15 | } 16 | 17 | opts.getPrivateKey = function (address, cb) { 18 | if (address !== wallet.getAddressString()) { 19 | cb(new Error('Account not found')) 20 | } else { 21 | cb(null, wallet.getPrivateKey()) 22 | } 23 | } 24 | 25 | WalletSubprovider.super_.call(this, opts) 26 | } -------------------------------------------------------------------------------- /src/components/Spinner.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/AddSensor.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 30 | 31 | -------------------------------------------------------------------------------- /contracts/auth/Roles.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.13; 2 | 3 | /** 4 | * @title Roles 5 | * @dev Library for managing addresses assigned to a Role. 6 | */ 7 | library Roles { 8 | struct Role { 9 | mapping (address => bool) bearer; 10 | } 11 | 12 | /** 13 | * @dev Give an account access to this role. 14 | */ 15 | function add(Role storage role, address account) internal { 16 | require(!has(role, account), "Roles: account already has role"); 17 | role.bearer[account] = true; 18 | } 19 | 20 | /** 21 | * @dev Remove an account's access to this role. 22 | */ 23 | function remove(Role storage role, address account) internal { 24 | require(has(role, account), "Roles: account does not have role"); 25 | role.bearer[account] = false; 26 | } 27 | 28 | /** 29 | * @dev Check if an account has this role. 30 | * @return bool 31 | */ 32 | function has(Role storage role, address account) internal view returns (bool) { 33 | require(account != address(0), "Roles: account is the zero address"); 34 | return role.bearer[account]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/plugins/utils.js: -------------------------------------------------------------------------------- 1 | let ROLES = { 2 | DEVICEAGENT: 0, 3 | MANUFACTURER: 1, 4 | OWNER: 2, 5 | DISTRIBUTOR: 3, 6 | MAINTAINER: 4 7 | }; 8 | 9 | export default { 10 | install(Vue) { 11 | 12 | Vue.prototype.$utils = { 13 | enum2String(enumVal) { 14 | switch (enumVal) { 15 | case ROLES.DEVICEAGENT: 16 | return "Device Agent"; 17 | case ROLES.MANUFACTURER: 18 | return "Manufacturer"; 19 | case ROLES.OWNER: 20 | return "Owner"; 21 | case ROLES.DISTRIBUTOR: 22 | return "Distributor"; 23 | case ROLES.MAINTAINER: 24 | return "Maintainer"; 25 | default: 26 | return null; 27 | } 28 | }, 29 | 30 | date(timestamp){ 31 | let locale = window.navigator.language; 32 | return new Date(timestamp*1000).toLocaleString(locale); 33 | }, 34 | 35 | swarmHashToBytes(hash){ 36 | return window.utils.hexToBytes("0x" + hash); 37 | }, 38 | 39 | hexToSwarmHash(hex){ 40 | return hex.substr(2, hex.length) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Benedikt Putz, Marietheres Dietz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contracts/auth/Attributes.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.13; 2 | 3 | /** 4 | * @title Attributes 5 | * @dev Library for managing addresses assigned to a Attribute. 6 | */ 7 | library Attributes { 8 | struct Attribute { 9 | mapping (address => bool) bearer; 10 | } 11 | 12 | /** 13 | * @dev Give an account access to this attribute. 14 | */ 15 | function add(Attribute storage attribute, address account) internal { 16 | require(!has(attribute, account), "Attributes: account already has attribute"); 17 | attribute.bearer[account] = true; 18 | } 19 | 20 | /** 21 | * @dev Remove an account's access to this attribute. 22 | */ 23 | function remove(Attribute storage attribute, address account) internal { 24 | require(has(attribute, account), "Attributes: account does not have attribute"); 25 | attribute.bearer[account] = false; 26 | } 27 | 28 | /** 29 | * @dev Check if an account has this attribute. 30 | * @return bool 31 | */ 32 | function has(Attribute storage attribute, address account) internal view returns (bool) { 33 | require(account != address(0), "Attributes: account is the zero address"); 34 | return attribute.bearer[account]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | contracts(state, contracts){ 4 | state.contracts = contracts; 5 | }, 6 | addresses(state, payload){ 7 | let result = payload.addresses; 8 | state.addresses = result; 9 | if (payload.callback) payload.callback(state) 10 | }, 11 | account(state, account){ 12 | state.user.wallet = account; 13 | state.user.address = account.getAddressString(); 14 | }, 15 | addTwin(state, twin){ 16 | state.twins.push(twin); 17 | }, 18 | addTwinComponents(state, data){ 19 | state.twins[data.twin].components = data.components; 20 | state.twins[data.twin].aml = data.aml; 21 | }, 22 | balance(state, balance){ 23 | state.balance = balance; 24 | }, 25 | selectTwin(state, id){ 26 | state.selectedTwin = id; 27 | }, 28 | users(state, users){ 29 | state.users = users; 30 | }, 31 | twins(state, twins){ 32 | state.twins = twins; 33 | }, 34 | removeTwin(state, twinAddress){ 35 | state.twins = state.twins.filter(function(v){ 36 | return v.address !== twinAddress; 37 | }); 38 | }, 39 | setSpecificationAbi(state, ABI){ 40 | state.specificationAbi = ABI; 41 | }, 42 | spinner(state, status){ 43 | state.spinner = status; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/plugins/crypto.js: -------------------------------------------------------------------------------- 1 | const ecies = require('eth-ecies'); 2 | const crypto = require('crypto'); 3 | 4 | export default { 5 | 6 | /** 7 | * 8 | * @param publicKey string 9 | * @param data Buffer 10 | * @returns {String} 11 | */ 12 | encryptECIES(publicKey, data) { 13 | let userPublicKey = new Buffer(publicKey, 'hex'); 14 | return ecies.encrypt(userPublicKey, data).toString('base64'); 15 | }, 16 | 17 | /** 18 | * 19 | * @param privateKey string 20 | * @param encryptedData base64 string 21 | * @returns {Buffer} 22 | */ 23 | decryptECIES(privateKey, encryptedData) { 24 | let bufferData = new Buffer(encryptedData, 'base64') 25 | return ecies.decrypt(privateKey, bufferData); 26 | }, 27 | 28 | encryptAES(text, key) { 29 | const iv = crypto.randomBytes(16); 30 | //ISO/IEC 10116:2017 31 | let cipher = crypto.createCipheriv('aes-256-ctr', Buffer.from(key), iv); 32 | let encrypted = cipher.update(text); 33 | encrypted = Buffer.concat([encrypted, cipher.final()]); 34 | return {iv: iv.toString('hex'), encryptedData: encrypted.toString('base64')}; 35 | }, 36 | 37 | decryptAES(text, key, init_vector) { 38 | let iv = Buffer.from(init_vector, 'hex'); 39 | let encryptedText = Buffer.from(text, 'base64'); 40 | let decipher = crypto.createDecipheriv('aes-256-ctr', Buffer.from(key), iv); 41 | let decrypted = decipher.update(encryptedText); 42 | decrypted = Buffer.concat([decrypted, decipher.final()]); 43 | return decrypted; 44 | }, 45 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | //VUE 2 | import Vue from 'vue' 3 | import App from './App.vue' 4 | import router from './router' 5 | import store from './store' 6 | 7 | //UI 8 | import VueSweetalert2 from 'vue-sweetalert2'; 9 | import 'bootstrap' 10 | import { library } from '@fortawesome/fontawesome-svg-core' 11 | import {faUserSecret, faTrash, faShareAlt, faSearch, faPlusSquare, faSave, faFileAlt, faFileUpload, faFileDownload, faWifi, faSitemap, faDatabase, faAddressCard, faHistory, faUserTag, faUserCircle, faProjectDiagram, faLockOpen, faLock, faSyncAlt} from '@fortawesome/free-solid-svg-icons' 12 | import {faEthereum} from '@fortawesome/free-brands-svg-icons' 13 | import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome' 14 | library.add(faUserSecret, faTrash, faShareAlt, faSearch, faPlusSquare, faSave, faFileAlt, faFileUpload, faFileDownload, faWifi, faSitemap, faDatabase, faAddressCard, faEthereum, faHistory, faUserTag, faUserCircle, faProjectDiagram, faLockOpen, faLock, faSyncAlt) 15 | Vue.component('font-awesome-icon', FontAwesomeIcon) 16 | 17 | //plugins 18 | import utils from './plugins/utils' 19 | import swarm from './plugins/swarm' 20 | 21 | //VUE setup 22 | Vue.use(utils) 23 | Vue.use(VueSweetalert2); 24 | Vue.config.productionTip = false 25 | 26 | new Vue({ 27 | router, 28 | store, 29 | beforeCreate: function() { 30 | this.$store.dispatch('setup') 31 | }, 32 | render: h => h(App) 33 | }).$mount('#app') 34 | 35 | //Swarm Plugin depends on initialized Ethereum account in store, 36 | //which happens during Vue instantiation 37 | Vue.use(swarm, store) 38 | -------------------------------------------------------------------------------- /contracts/ContractRegistry.sol: -------------------------------------------------------------------------------- 1 | pragma experimental ABIEncoderV2; 2 | pragma solidity 0.5.13; 3 | 4 | import "./auth/Roles.sol"; 5 | import "./Specification.sol"; 6 | import "./Authorization.sol"; 7 | 8 | contract ContractRegistry { 9 | event TwinCreated(address contractAddress, address owner, bytes32 aml, address deviceAgent); 10 | address[] public contracts; 11 | 12 | Authorization internal auth; 13 | Specification internal spec; 14 | 15 | constructor (address payable _auth) public { 16 | auth = Authorization(_auth); 17 | } 18 | 19 | function registerContract(string calldata _deviceName, bytes32 _deviceAML, address _deviceAgent) external returns(address) { 20 | 21 | //require RBAC.DEVICEAGENT PRIVILEGES --> device agent has value 0 22 | //require(auth.getRole(msg.sender, address(this)) == 0, "Your account has no privileges of device agent!"); 23 | spec = new Specification(address(auth)); 24 | 25 | //get address of new specification instance 26 | address contractAddress = address(spec); 27 | 28 | // set role for contract owner 29 | auth.initializeDevice(contractAddress, msg.sender, _deviceAgent); 30 | 31 | //set params in the specification contract 32 | spec.updateTwin(_deviceName, _deviceAgent, msg.sender); 33 | 34 | //add AML to specification contract 35 | spec._addNewAMLVersion(_deviceAML, msg.sender); 36 | 37 | //add contract to all contracts 38 | contracts.push(contractAddress); 39 | 40 | emit TwinCreated(contractAddress, msg.sender, _deviceAML, _deviceAgent); 41 | 42 | return contractAddress; 43 | } 44 | 45 | function getContracts() external view returns (address[] memory){ 46 | return contracts; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethertwin", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "agent": "node agent/agent.js", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "@erebos/bzz-browser": "^0.13.0", 13 | "@erebos/bzz-feed": "^0.13.2", 14 | "@erebos/bzz-node": "^0.13.0", 15 | "@erebos/secp256k1": "^0.10.0", 16 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 17 | "@fortawesome/free-brands-svg-icons": "^5.15.1", 18 | "@fortawesome/free-solid-svg-icons": "^5.15.1", 19 | "@fortawesome/vue-fontawesome": "^0.1.10", 20 | "@truffle/contract": "^4.2.29", 21 | "@truffle/hdwallet-provider": "^1.2.0", 22 | "bootstrap": "^4.5.3", 23 | "canvas": "^2.6.1", 24 | "core-js": "^3.7.0", 25 | "eth-ecies": "^1.0.5", 26 | "ethereumjs-wallet": "^1.0.1", 27 | "jazzicon": "^1.5.0", 28 | "jquery": "^3.5.1", 29 | "rxjs": "^6.6.3", 30 | "vue": "^2.6.12", 31 | "vue-json-pretty": "^1.7.1", 32 | "vue-router": "^3.4.9", 33 | "vue-sweetalert2": "^2.1.5", 34 | "vuex": "^3.5.1", 35 | "web3": "^1.3.0", 36 | "web3-provider-engine": "^16.0.1", 37 | "web3-utils": "^1.3.0", 38 | "xml2js": "^0.4.23" 39 | }, 40 | "devDependencies": { 41 | "@vue/cli-plugin-babel": "^4.5.4", 42 | "@vue/cli-plugin-eslint": "^4.5.4", 43 | "@vue/cli-service": "^4.5.4", 44 | "babel-eslint": "^10.1.0", 45 | "elliptic": "^6.5.3", 46 | "eslint": "^6.8.0", 47 | "eslint-plugin-vue": "^6.2.2", 48 | "popper.js": "^1.16.1", 49 | "vue-template-compiler": "^2.6.12" 50 | }, 51 | "eslintConfig": { 52 | "root": true, 53 | "env": { 54 | "node": true 55 | }, 56 | "extends": [ 57 | "plugin:vue/essential", 58 | "eslint:recommended" 59 | ], 60 | "rules": {}, 61 | "parserOptions": { 62 | "parser": "babel-eslint" 63 | } 64 | }, 65 | "postcss": { 66 | "plugins": { 67 | "autoprefixer": {} 68 | } 69 | }, 70 | "browserslist": [ 71 | "> 1%", 72 | "last 2 versions" 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './views/Home.vue' 4 | import Account from "./views/Account"; 5 | import CreateTwin from './views/CreateTwin.vue' 6 | import Specification from './views/Specification.vue' 7 | import Sensors from './views/Sensors.vue' 8 | import Sensor from './views/Sensor.vue' 9 | import Documents from './views/Documents.vue' 10 | import Roles from './views/Roles.vue' 11 | import AddSensor from "./views/AddSensor"; 12 | import Components from "./views/Components"; 13 | import Sources from "./views/Sources"; 14 | 15 | Vue.use(Router); 16 | 17 | export default new Router({ 18 | mode: 'history', 19 | base: process.env.BASE_URL, 20 | linkActiveClass: "active", 21 | routes: [ 22 | { 23 | path: '/', 24 | name: 'home', 25 | component: Home 26 | }, 27 | { 28 | path: '/account', 29 | name: 'account', 30 | component: Account, 31 | props: true 32 | }, 33 | { 34 | path: '/twin/:twin/users', 35 | name: 'roles', 36 | component: Roles, 37 | props: true 38 | }, 39 | { 40 | path: '/twin/:twin/components', 41 | name: 'components', 42 | component: Components, 43 | props: true 44 | }, 45 | { 46 | path: '/twin/create', 47 | name: 'twin-create', 48 | component: CreateTwin, 49 | props: true 50 | }, 51 | { 52 | path: '/twin/:twin/specification', 53 | name: 'twin-spec', 54 | component: Specification, 55 | props: true 56 | }, 57 | { 58 | path: '/twin/:twin/sensors/add', 59 | name: 'sensor-add', 60 | component: AddSensor, 61 | props: true 62 | }, 63 | { 64 | path: '/twin/:twin/sensors', 65 | name: 'sensors', 66 | component: Sensors, 67 | props: true 68 | }, 69 | { 70 | path: '/twin/:twin/sensors/:component/:sensor', 71 | name: 'sensor', 72 | component: Sensor, 73 | props: true 74 | }, 75 | { 76 | path: '/twin/:twin/documents', 77 | name: 'documents', 78 | component: Documents, 79 | props: true 80 | }, 81 | { 82 | path: '/twin/:twin/sources', 83 | name: 'sources', 84 | component: Sources, 85 | props: true 86 | }, 87 | ] 88 | }) 89 | -------------------------------------------------------------------------------- /src/views/Sensor.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 95 | 96 | 99 | -------------------------------------------------------------------------------- /src/views/Components.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 90 | 91 | 94 | -------------------------------------------------------------------------------- /test/contract_test.js: -------------------------------------------------------------------------------- 1 | const ContractRegistry = artifacts.require("ContractRegistry"); 2 | const Authorization = artifacts.require("Authorization"); 3 | const Specification = artifacts.require("Specification"); 4 | 5 | 6 | contract("ContractRegistry", accounts => { 7 | let c, a, s, hash, hashBytes; 8 | const component = "068ec45a-1002-4a75-8e27-21d8e0da6e3d"; 9 | const componentHash = web3.utils.sha3(component) 10 | 11 | before(async () => { 12 | c = await ContractRegistry.deployed(); 13 | a = await Authorization.deployed(); 14 | hash = "0x40b46874c3ac6b4bfb7ed00aba8c0d0fbf9b3c8fece226f8f27b6f8446b6a0ee"; 15 | hashBytes = web3.utils.hexToBytes(hash) 16 | }); 17 | 18 | it("should register a new user and verify funds", async() => { 19 | await a.send(web3.utils.toWei("10000", "ether")); 20 | let before = await web3.eth.getBalance(a.address); 21 | let tx = await a.register({from: accounts[4]}); 22 | let after = await web3.eth.getBalance(a.address); 23 | }) 24 | 25 | it("should create a new twin", async () => { 26 | let tx = await c.registerContract("My Twin", hashBytes, accounts[1]); 27 | let contracts = await c.getContracts(); 28 | s = await Specification.at(contracts[0]); 29 | assert.equal(contracts.length, 1, "Twin Specification Contract not deployed"); 30 | let deviceAgent = await s.deviceAgent(); 31 | assert.equal(deviceAgent, accounts[1], "Device Agent incorrect") 32 | }); 33 | 34 | it("should add a new AML version", async () => { 35 | await s.addNewAMLVersion(hashBytes); 36 | let aml = await s.getAMLHistory(); 37 | assert.equal(aml.length, 2, "Doc Version not created"); 38 | assert.equal(aml[1].hash, hash, "Doc hash not equal"); 39 | }); 40 | 41 | it("should add component attribute", async() => { 42 | await a.addAttributes(accounts[0], [componentHash], s.address) 43 | let authorized = await a.hasAttributes(accounts[0], [componentHash], s.address) 44 | assert.equal(authorized[0], true, "Attribute not added") 45 | }); 46 | 47 | it("should create a new document", async () => { 48 | await s.addDocument(component, "manual.pdf", "This is the asset manual.", hashBytes); 49 | let docs = await s.getDocument(component, 0); 50 | assert.equal(docs.versions[0].hash, hash, "Doc not created"); 51 | let docCount = await s.getDocumentCount(component); 52 | assert.equal(docCount, 1, "Not enough documents") 53 | }); 54 | 55 | it("should add a new document version", async () => { 56 | await s.addDocumentVersion(component, 0, hashBytes); 57 | let docs = await s.getDocument(component, 0); 58 | assert.equal(docs.versions[1].hash, hash, "Doc Version not created"); 59 | }); 60 | 61 | it("should create a new sensor", async () => { 62 | await s.addSensor(component, "My Sensor", hashBytes); 63 | let sensors = await s.getSensor(component, 0); 64 | assert.equal(sensors.hash, hash, "Sensor not created"); 65 | }); 66 | 67 | it("should remove a sensor", async () => { 68 | //add two more sensors 69 | await s.addSensor(component, "My Sensor 2", hashBytes); 70 | await s.addSensor(component, "My Sensor 3", hashBytes); 71 | //remove middle one 72 | await s.removeSensor(component, 1); 73 | let sensor = await s.getSensor(component, 1); 74 | assert.equal(sensor.name, "My Sensor 3", "Sensor not removed"); 75 | }); 76 | 77 | it("should create a new external source", async () => { 78 | let myUri = "http://my-url.com:8080"; 79 | await s.addExternalSource(myUri, "test"); 80 | let sources = await s.getExternalSource(0); 81 | assert.equal(sources.URI, myUri, "Source not created"); 82 | }); 83 | 84 | it("should add a program call", async () => { 85 | let call = "ConveyorVelocity(220)"; 86 | await s.addProgramCall(component, call); 87 | let calls = await s.getProgramCallQueue(component); 88 | assert.equal(calls[0].call, call, "Call not created"); 89 | }); 90 | 91 | it("should update the program counter", async () => { 92 | await s.updateProgramCounter(component, 1); 93 | let counter = await s.getProgramCounter(component); 94 | assert.equal(counter.toNumber(), 1, "Counter not updated"); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/views/CreateTwin.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 108 | 109 | 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EtherTwin 2 | 3 | *This repository is part of a research project on blockchain-based Digital Twins: https://doi.org/10.1016/j.ipm.2020.102425* 4 | 5 | The **EtherTwin** prototype originates from a research approach to share Digital Twin data over its lifecycle. 6 | To allow the participation of the multiple lifecycle parties without relying on trusted third parties (TTPs), **EtherTwin** relies on a distributed approach by integrating the [Ethereum](ethereum.org) blockchain and the distributed hash table (DHT) [Swarm](swarm.ethereum.org). 7 | On the basis of [AutomationML](https://www.automationml.org/) (AML) files that specify assets, a Digital Twin can be created and shared with the twin's lifecycle parties. 8 | The **EtherTwin** prototype allows to: 9 | - create Digital Twins 10 | - share each twin 11 | - upload documents 12 | - update twin specification and documents (versioning) 13 | - create sensor data feeds 14 | - control access of users by lifecycle roles and asset attributes 15 | - list asset components (specification parsing) 16 | 17 | An exemplary use case is given in the following [video](https://drive.google.com/open?id=1Bq8xNVj2TEluJ3_-DK3eLaDQvqv9rLQ8). The video is based on a slightly outdated version of EtherTwin, but it illustrates the core functionality well. 18 | 19 | **Live demo**: For a live demonstration of the prototype using a private Ethereum blockchain, visit http://ethertwin.ur.de/. 20 | An Ethereum account will be automatically created for you with the private key stored in your browser's local storage. Before you can issue transactions, you'll need to request some Ether on our test blockchain at http://ethertwin.ur.de:3333/0x0 (replace 0x0 with your Ethereum account). 21 | ## Project setup 22 | ``` 23 | npm install 24 | ``` 25 | 26 | ### Prerequisites 27 | 28 | Download [Parity](https://github.com/paritytech/parity-ethereum/releases) to set up your Ethereum blockchain. Create a folder `network`, add the `parity.exe` and `password.txt`. 29 | Then run the following console command in the `network` folder to start your blockchain: 30 | ``` 31 | parity --config dev --unlock "0x00a329c0648769a73afac7f9381e08fb43dbea72" --password .\password.txt --unsafe-expose --jsonrpc-cors=all 32 | ``` 33 | 34 | Download [Swarm](https://swarm.ethereum.org/downloads/) to set up the DHT. 35 | Then run the following console command: 36 | ``` 37 | swarm --bzzaccount --config ./config.toml --datadir . --httpaddr=0.0.0.0 --corsdomain '*' --password password.txt``` 38 | ``` 39 | 40 | Install [Truffle](https://github.com/trufflesuite/truffle) to initialize the Smart Contracts: 41 | ``` 42 | npm -i truffle 43 | truffle migrate --network parity 44 | ``` 45 | 46 | ### Configuration 47 | Add `config.json` with the corresponding values (IP) for the Swarm DHT etc. 48 | Note that the values for the `registry` and `authorization` are optional: 49 | ``` 50 | { 51 | "swarm": "http://:5000", 52 | "ethereum": { 53 | "rpc": "ws://:8546", 54 | "registry": "", 55 | "authorization": "" 56 | } 57 | "agent_key": "" 58 | } 59 | ``` 60 | 61 | You also need to add an appropriate log file in the `misc` folder, for example by renaming `logs.xml.example` to `logs.xml`. 62 | 63 | ### Run 64 | Run the device agent 65 | ``` 66 | npm run agent 67 | ``` 68 | 69 | Run the main web app for development with hot reloads 70 | ``` 71 | npm run serve 72 | ``` 73 | 74 | Build static files for deployment 75 | ``` 76 | npm run build 77 | ``` 78 | 79 | ## Usage 80 | An exemplary specification for the twin creation can be found at `misc/CandyFactory.aml`, which originates from the [CPS Twinning](https://github.com/sbaresearch/cps-twinning) prototype. 81 | Various screenshots showing the **EtherTwin** prototype functionality can be found at `misc/Screenshots`. The following screenshot illustrates the Home site of our 82 | **EtherTwin** prototype - showing all twins of the user. 83 | ![Start page of the **EtherTwin** prototype](./misc/Screenshots/Screenshot_Home.PNG "Start page of the **EtherTwin** prototype") 84 | 85 | 86 | ## Research and Citation 87 | Please consider citing our publication if you are using our **EtherTwin** prototype for your research: https://doi.org/10.1016/j.ipm.2020.102425 88 | 89 | ``` 90 | B. Putz, M. Dietz, P. Empl, and G. Pernul, "EtherTwin: Blockchain-based Secure Digital Twin Information Management", Information Processing & Management, vol. 58, no. 1, 2021. 91 | ``` 92 | -------------------------------------------------------------------------------- /src/views/Sources.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 117 | 118 | 127 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * truffleframework.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | // const HDWallet = require('truffle-hdwallet-provider'); 22 | // const infuraKey = "fj4jll3k....."; 23 | // 24 | // const fs = require('fs'); 25 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 26 | 27 | module.exports = { 28 | /** 29 | * Networks define how you connect to your ethereum client and let you set the 30 | * defaults web3 uses to send transactions. If you don't specify one truffle 31 | * will spin up a development blockchain for you on port 9545 when you 32 | * run `develop` or `test`. You can ask a truffle command to use a specific 33 | * network from the command line, e.g 34 | * 35 | * $ truffle test --network 36 | */ 37 | contracts_build_directory: "public/contracts", 38 | plugins: [ "truffle-security" ], 39 | networks: { 40 | // Useful for testing. The `development` name is special - truffle uses it by default 41 | // if it's defined here and no other network is specified at the command line. 42 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 43 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 44 | // options below to some value. 45 | // 46 | development: { 47 | host: "127.0.0.1", 48 | port: 8501, 49 | network_id: "8995" // Match any network id 50 | }, 51 | parity: { 52 | host: '127.0.0.1', 53 | port: 8545, 54 | from: '0x00a329c0648769a73afac7f9381e08fb43dbea72', 55 | network_id: '*' 56 | }, 57 | ganache: { 58 | host: '127.0.0.1', 59 | port: 7545, 60 | network_id: '*' 61 | }, 62 | pi: { 63 | host: "132.199.123.240", 64 | port: "7545", 65 | network_id: "66" 66 | } 67 | 68 | 69 | // Another network with more advanced options... 70 | // advanced: { 71 | // port: 8777, // Custom port 72 | // network_id: 1342, // Custom network 73 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 74 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 75 | // from:
, // Account to send txs from (default: accounts[0]) 76 | // websockets: true // Enable EventEmitter interface for web3 (default: false) 77 | // }, 78 | 79 | // Useful for deploying to a public network. 80 | // NB: It's important to wrap the provider as a function. 81 | // ropsten: { 82 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 83 | // network_id: 3, // Ropsten's id 84 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 85 | // confirmations: 2, // # of confs to wait between deployments. (default: 0) 86 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 87 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 88 | // }, 89 | 90 | // Useful for private networks 91 | // private: { 92 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 93 | // network_id: 2111, // This network is yours, in the cloud. 94 | // production: true // Treats this network as if it was a public net. (default: false) 95 | // } 96 | }, 97 | 98 | // Set default mocha options here, use special reporters etc. 99 | mocha: { 100 | // timeout: 100000 101 | }, 102 | // Configure your compilers 103 | compilers: { 104 | solc: { 105 | version: "0.5.13", 106 | optimizer: { 107 | enabled: true, 108 | runs: 200, 109 | }, 110 | }, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/views/Account.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 184 | 185 | 188 | -------------------------------------------------------------------------------- /src/plugins/swarm.js: -------------------------------------------------------------------------------- 1 | import { BzzFeed } from '@erebos/bzz-feed' 2 | import { BzzBrowser } from '@erebos/bzz-browser' 3 | import { createKeyPair, sign } from '@erebos/secp256k1' 4 | import config from '../../config' 5 | import crypto from './crypto' 6 | let c = require('crypto'); 7 | 8 | export default { 9 | install(Vue, store) { 10 | 11 | // Set up client based on app wallet 12 | let user = store.state.user; 13 | let keyPair = createKeyPair(user.wallet.getPrivateKey().toString('hex')); 14 | let client = new BzzBrowser({ url: config.swarm }); 15 | let feed = new BzzFeed({ 16 | bzz: client, 17 | signBytes: bytes => Promise.resolve(sign(bytes, keyPair)), 18 | }); 19 | 20 | Vue.prototype.$swarm = { 21 | 22 | async updateFeedSimple(feedParams, update){ 23 | await feed.createManifest(feedParams); 24 | return feed.setContent(feedParams, JSON.stringify(update), {contentType: "application/json"}) 25 | }, 26 | 27 | async updateFeedText(feedParams, update){ 28 | return feed.setContent(feedParams, update, {contentType: "text/plain"}) 29 | }, 30 | 31 | async uploadDoc(content, contentType) { 32 | return new Promise((resolve, reject) => { 33 | try { 34 | client 35 | .uploadFile(content, {contentType: contentType}) 36 | .then(hash => { 37 | resolve(hash); 38 | }); 39 | } catch (err) { 40 | reject(err); 41 | } 42 | }); 43 | }, 44 | 45 | async downloadDoc(hash, type = "text") { 46 | return new Promise((resolve, reject) => { 47 | try { 48 | client.download(hash) 49 | .then(response => { 50 | switch(type){ 51 | case "text": 52 | resolve(response.text()); 53 | break; 54 | case "json": 55 | resolve(response.json()); 56 | break; 57 | case "file": 58 | resolve(response) 59 | } 60 | }); 61 | } catch (err) { 62 | reject(err); 63 | } 64 | }); 65 | }, 66 | 67 | /** 68 | * Create a new feed 69 | * @param device valid Ethereum address 70 | * @param topic 32 byte hash (i.e. web3.utils.sha3) 71 | * @returns {Promise} 72 | */ 73 | async createFeed(device, topic) { 74 | try { 75 | return await feed.createManifest({ 76 | user: device, 77 | topic: topic, 78 | }); 79 | } catch (err) { 80 | alert(err); 81 | } 82 | }, 83 | 84 | /** 85 | * Adds a new entry to the feed 86 | * @param feedHash Feed manifest hash 87 | * @param contents Content string for JSON 88 | * @returns {Promise} 89 | */ 90 | async updateFeed(feedHash, contents) { 91 | try { 92 | let options = {contentType: "application/json"}; 93 | let meta = await feed.getMetadata(feedHash); 94 | let update = { 95 | time: meta.epoch.time, 96 | content: contents 97 | }; 98 | let hash = await client.uploadFile(JSON.stringify(update), options); 99 | await feed.postChunk(meta, `0x${hash}`, options); 100 | } catch (err) { 101 | alert(err); 102 | } 103 | }, 104 | 105 | /** Retrieve past feed updates 106 | * 107 | * @param feedHash Feed manifest hash 108 | * @param count Number of updates to get 109 | * @returns {Promise<*>} 110 | */ 111 | async getFeedUpdates(feedHash, count) { 112 | let updates = Array(); 113 | try { 114 | //only retrieves latest content 115 | let content = await this.getFeedItemJson(feedHash); 116 | updates.push(content); 117 | let lastTime = content.time - 1; 118 | 119 | //get past n updates 120 | while (updates.length < count) { 121 | let content = await this.getFeedItemJson(feedHash, lastTime); 122 | lastTime = lastTime === content.time ? 123 | content.time - 1 : 124 | content.time; 125 | updates.push(content); 126 | } 127 | } catch (err) { 128 | console.log(err.toString()); 129 | } 130 | 131 | return updates; 132 | }, 133 | 134 | /** 135 | * Retrieves a JSON item based on a feed hash and timestamp 136 | * @param feedHash Feed manifest hash 137 | * @param time Feed time 138 | * @returns {Promise} 139 | */ 140 | async getFeedItemJson(feedHash, time = (new Date()) / 1000) { 141 | let meta = await feed.getMetadata(feedHash); 142 | let content = await feed.getContent({ 143 | user: meta.feed.user, 144 | topic: meta.feed.topic, 145 | time: time 146 | }); 147 | return await content.json(); 148 | }, 149 | 150 | async getUserFeedLatest(user, topic) { 151 | let content = await feed.getContent({ 152 | user: user, 153 | topic: topic 154 | }); 155 | return content.json(); 156 | }, 157 | 158 | async getUserFeedText(user){ 159 | let content = await feed.getContent({user: user}); 160 | return content.text(); 161 | }, 162 | 163 | /** Functions related to uploading and downloading encrypted files **/ 164 | createFileKey() { 165 | return c.randomBytes(32); 166 | }, 167 | 168 | encryptKeyForSelf(key){ 169 | let ownPublicKey = store.state.user.wallet.getPublicKey().toString('hex'); 170 | let ciphertext = crypto.encryptECIES(ownPublicKey, key); 171 | return [{address: user.address, fileKey: ciphertext}]; 172 | }, 173 | 174 | //share an existing key on the user's own feed with another user 175 | async getAndShareFileKey(user, topic, userAddress) { 176 | //get existing keys and decrypt key 177 | let fileKeys = await this.getUserFeedLatest(user, topic); 178 | let keyObject = fileKeys.filter(f => f.address === user)[0]; 179 | let privateKey = store.state.user.wallet.getPrivateKey().toString('hex'); 180 | let plainKey = crypto.decryptECIES(privateKey, keyObject.fileKey); 181 | return this.shareFileKey(user, topic, userAddress, plainKey, fileKeys) 182 | }, 183 | 184 | async shareFileKey(user, topic, shareAddress, key, fileKeys = []){ 185 | let sharePublicKey = await this.getUserFeedText(shareAddress); 186 | let ciphertext = crypto.encryptECIES(sharePublicKey, key); 187 | fileKeys.push({address: shareAddress, fileKey: ciphertext}) 188 | return this.updateFeedSimple({user: user, topic: topic}, fileKeys); 189 | }, 190 | 191 | //gets from any feed and decrypts a file key, which was encrypted for the current user 192 | async getFileKey(user, topic) { 193 | let fileKeys = await this.getUserFeedLatest(user, topic); 194 | let keyObject = fileKeys.filter(f => f.address.toLowerCase() === store.state.user.address)[0]; 195 | let privateKey = store.state.user.wallet.getPrivateKey().toString('hex'); 196 | let plainKey = crypto.decryptECIES(privateKey, keyObject.fileKey); 197 | return new Buffer(plainKey, 'base64'); 198 | }, 199 | 200 | async uploadEncryptedDoc (content, contentType, user, topic) { 201 | let key = await this.getFileKey(user, topic); 202 | return this.encryptAndUpload(content, contentType, key) 203 | }, 204 | 205 | //content: buffer 206 | async encryptAndUpload(content, contentType, key) { 207 | let cipherText = crypto.encryptAES(content, key); 208 | cipherText.contentType = contentType; 209 | return this.uploadDoc(JSON.stringify(cipherText), "application/json"); 210 | //return client.uploadData(cipherText) 211 | }, 212 | 213 | async downloadEncryptedDoc(user, topic, hash) { 214 | let key = await this.getFileKey(user, topic); 215 | let response = await client.download(hash); 216 | let res = await response.json(); 217 | return { 218 | content: crypto.decryptAES(res.encryptedData, key, res.iv), 219 | type: res.type 220 | }; 221 | }, 222 | } 223 | } 224 | }; 225 | -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | import TruffleContract from '@truffle/contract' 3 | import config from '../../config.json' 4 | const HDWalletProvider = require("@truffle/hdwallet-provider"); 5 | import Wallet from 'ethereumjs-wallet' 6 | import utils from 'web3-utils' 7 | let web3; 8 | 9 | function setupWeb3(){ 10 | let wallet; 11 | let privateKey = localStorage.getItem('privateKey'); 12 | if(!privateKey) { 13 | wallet = Wallet.generate(); 14 | privateKey = wallet.getPrivateKey().toString('hex'); 15 | localStorage.setItem('privateKey', wallet.getPrivateKey().toString('hex')) 16 | } 17 | else{ 18 | let privateKeyBuffer = new Buffer(privateKey, "hex"); 19 | wallet = Wallet.fromPrivateKey(privateKeyBuffer) 20 | } 21 | let webSocketProvider = new Web3.providers.WebsocketProvider(config.ethereum.rpc); 22 | let provider = new HDWalletProvider([privateKey], webSocketProvider, 0, 1); 23 | window.web3 = new Web3(provider); 24 | web3 = window.web3; 25 | window.utils = utils; 26 | window.web3.eth.defaultAccount = wallet.getAddressString(); 27 | return wallet; 28 | } 29 | 30 | async function getSpecification(address, state){ 31 | //check role of user 32 | return new Promise((resolve) => { 33 | let vm = state; 34 | let utils = state.utils; 35 | state.contracts.Authorization.getRole(vm.user.address, address) 36 | .then(function (roleNo) { 37 | let role = utils.enum2String(Number(roleNo)); 38 | 39 | if (role !== null && roleNo < 5) { 40 | let twin = {}; 41 | twin.roleNo = roleNo; 42 | twin.role = role; 43 | 44 | vm.contracts.SpecificationContract = TruffleContract(state.specificationAbi); 45 | vm.contracts.SpecificationContract.setProvider(web3.currentProvider); 46 | vm.contracts.SpecificationContract.at(address).then(function (instance1) { 47 | twin.specification = instance1; 48 | twin.address = instance1.address; 49 | return instance1.getTwin(function(err,res){ 50 | twin.deviceName = res[0]; 51 | twin.deviceAgent = res[1]; 52 | twin.owner = res[2]; 53 | }); 54 | }).then(function () { 55 | resolve(twin); 56 | }); 57 | } 58 | else{ 59 | resolve(null); 60 | } 61 | }); 62 | }); 63 | } 64 | 65 | export default{ 66 | async setup({ commit }){ 67 | let wallet = setupWeb3(); 68 | commit('account', wallet); 69 | }, 70 | 71 | async initContracts({ commit }, ABIs){ 72 | let contracts = {}; 73 | let addresses = {}; 74 | let instances = {}; 75 | return new Promise((resolve, reject) => { 76 | contracts.Authorization = TruffleContract(ABIs.authorization); 77 | contracts.Authorization.setProvider(web3.currentProvider); 78 | 79 | contracts.ContractRegistry = TruffleContract(ABIs.registry); 80 | contracts.ContractRegistry.setProvider(web3.currentProvider); 81 | 82 | let registry = (config.ethereum.registry) ? 83 | contracts.ContractRegistry.at(config.ethereum.registry) : 84 | contracts.ContractRegistry.deployed(); 85 | 86 | registry.then(function (instance1) { 87 | instances.ContractRegistry = instance1; 88 | addresses.ContractRegistryAddress = instance1.address; 89 | }) 90 | .then(function () { 91 | let auth = (config.ethereum.authorization) ? 92 | contracts.Authorization.at(config.ethereum.authorization) : 93 | contracts.Authorization.deployed(); 94 | 95 | auth.then(function (instance2) { 96 | instances.Authorization = instance2; 97 | addresses.AuthorizationAddress = instance2.address; 98 | commit('contracts', instances); 99 | commit('addresses', 100 | { 101 | addresses: addresses, 102 | callback: (state) => { 103 | resolve({state}) 104 | } 105 | }); 106 | }); 107 | }).catch(function (err) { 108 | reject(err) 109 | }); 110 | }); 111 | }, 112 | 113 | async loadTwins({ commit, state }) { 114 | let vm = this; 115 | return new Promise((resolve, reject) => { 116 | state.contracts.ContractRegistry.getContracts() 117 | .then(function (contracts) { 118 | //iteration through all elements 119 | if (contracts.length > 0) { 120 | state.utils = vm._vm.$utils; 121 | Promise.all(contracts.map(c => getSpecification(c, state))).then(function(twins) { 122 | twins = twins.filter(result => (result !== null)); 123 | commit('twins', twins); 124 | resolve(); 125 | }); 126 | } 127 | else{ 128 | resolve(); 129 | } 130 | }) 131 | .catch(function (error) { 132 | reject(error); 133 | }) 134 | }); 135 | }, 136 | 137 | async updateBalance({commit, state}){ 138 | let balanceTenEightteen = await window.web3.eth.getBalance(state.user.address); 139 | let balance = (balanceTenEightteen/Math.pow(10,18)); 140 | commit('balance', balance) 141 | }, 142 | 143 | async register({state, dispatch}, vm){ 144 | await Promise.all([ 145 | state.contracts.Authorization.register({from: state.user.address}), 146 | //If not yet published, publish user public key to feed 147 | vm.$swarm.updateFeedText( 148 | {user: state.user.address}, 149 | state.user.wallet.getPublicKey().toString('hex') 150 | ) 151 | ]); 152 | dispatch('loadUsers') 153 | }, 154 | 155 | async loadUsers({commit, state}){ 156 | let users = await state.contracts.Authorization.getUsers(); 157 | return new Promise((resolve) => { 158 | commit('users', users.map(u => u.toLowerCase())); 159 | resolve(users) 160 | }) 161 | }, 162 | 163 | async parseAML({commit, state}, { twinAddress, vm }) { 164 | if (twinAddress != null) { 165 | commit('selectTwin', twinAddress); 166 | let twinIndex = state.twins.findIndex(f => f.address === twinAddress) 167 | let twin = state.twins[twinIndex] 168 | if ('components' in twin) return; 169 | commit('spinner', true); 170 | let length = await twin.specification.getAMLCount(); 171 | let index = length.toNumber() - 1; 172 | 173 | //get latest version of specification-AML 174 | let amlInfo = await twin.specification.getAML(index); 175 | //get AML from Swarm using aml-hash: amlInfo.hash 176 | let aml = (await vm.$swarm.downloadEncryptedDoc(twin.owner, window.web3.utils.sha3(twinAddress), vm.$utils.hexToSwarmHash(amlInfo.hash))).content; 177 | //parse aml to get the relevant components: CAEXFile -> InstanceHierarchy -> InternalElement (=Array with all components) 178 | // InternalElement.[0] ._Name ._ID ._RefBaseSystemUnitPath 179 | let parser = new DOMParser(); 180 | let amlDoc = parser.parseFromString(aml, "text/xml"); 181 | let instanceHierarchy = amlDoc.documentElement.getElementsByTagName("InstanceHierarchy"); 182 | 183 | //all child nodes are high-level components 184 | let childNodes = instanceHierarchy[0].children; 185 | 186 | let components = []; 187 | for (let i = 0; i < childNodes.length; i++) { 188 | //all children of type "InternalElement" are high-level components 189 | if (childNodes[i].nodeName === "InternalElement") { 190 | let id = childNodes[i].getAttribute("ID"); 191 | let name = childNodes[i].getAttribute("Name"); 192 | let hash = window.web3.utils.sha3(id); 193 | // add parsed components to the components array 194 | components.push({id: id, name: name, hash: hash}); 195 | } 196 | } 197 | 198 | //filter by attributes 199 | if (twin.role !== "Owner") { 200 | let a = state.contracts.Authorization; 201 | let componentsBytes = components.map(c => window.web3.utils.hexToBytes(c.hash)); 202 | let c = await a.hasAttributes.call( 203 | vm.account, 204 | componentsBytes, 205 | twin.specification.address 206 | ); 207 | components = components.filter((d, ind) => c[ind]); 208 | } 209 | commit('addTwinComponents', {twin: twinIndex, aml: aml, components: components}); 210 | commit('spinner', false); 211 | } 212 | } 213 | 214 | } 215 | -------------------------------------------------------------------------------- /contracts/Specification.sol: -------------------------------------------------------------------------------- 1 | pragma experimental ABIEncoderV2; 2 | pragma solidity 0.5.13; 3 | 4 | import "./Authorization.sol"; 5 | 6 | contract Specification { 7 | 8 | Authorization internal auth; 9 | address internal contractRegistry; 10 | 11 | string public deviceName; 12 | address public deviceAgent; 13 | address public owner; 14 | 15 | constructor (address payable _authAddress) public { 16 | auth = Authorization(_authAddress); 17 | contractRegistry = msg.sender; 18 | } 19 | 20 | //generic version blueprint for file stored on DHT 21 | struct Version { 22 | uint timestamp; 23 | address author; 24 | bytes32 hash; 25 | } 26 | 27 | struct Document { 28 | string name; 29 | string description; 30 | Version[] versions; 31 | } 32 | 33 | struct Sensor { 34 | string name; 35 | bytes32 hash; 36 | } 37 | 38 | struct ExternalSource { 39 | string URI; 40 | string description; 41 | address owner; 42 | } 43 | 44 | struct ProgramCall { 45 | uint timestamp; 46 | address author; 47 | string call; 48 | } 49 | 50 | Version[] public AML; 51 | ExternalSource[] public sources; 52 | 53 | //map hash of component to its documents 54 | mapping(bytes32 => Document[]) public documents; 55 | //map hash of component to its sensors 56 | mapping(bytes32 => Sensor[]) public sensors; 57 | //program calls for specific component 58 | mapping(bytes32 => ProgramCall[]) public programCallQueue; 59 | //array index of the most recently run program 60 | mapping(bytes32 => uint) public programCounter; 61 | 62 | function getTwin() external view returns (string memory, address, address){ 63 | return (deviceName, deviceAgent, owner); 64 | } 65 | 66 | function updateTwin(string calldata _deviceName, address _deviceAgent, address _owner) external 67 | { 68 | require(msg.sender == contractRegistry || auth.hasPermission(msg.sender, Authorization.PERMISSION.TWIN_UPDATE, address(this))); 69 | deviceName = _deviceName; 70 | deviceAgent = _deviceAgent; 71 | owner = _owner; 72 | } 73 | 74 | //******* AML *******// 75 | 76 | //overload function with msg.sender variant, since Solidity does not support optional parameters 77 | function _addNewAMLVersion(bytes32 _newAMLVersion, address sender) external { 78 | // AML can be inserted by each role except unauthorized accounts 79 | require(!(auth.getRole(sender, address(this)) == 404), "Your account has no privileges"); 80 | AML.push(Version(now, sender, _newAMLVersion)); 81 | } 82 | 83 | function addNewAMLVersion(bytes32 _newAMLVersion) external { 84 | // AML can be inserted by each role except unauthorized accounts 85 | require(!(auth.getRole(msg.sender, address(this)) == 404), "Your account has no privileges"); 86 | AML.push(Version(now, msg.sender, _newAMLVersion)); 87 | } 88 | 89 | function getAML(uint id) external view returns (Version memory){ 90 | return AML[id]; 91 | } 92 | 93 | function getAMLCount() external view returns (uint){ 94 | return AML.length; 95 | } 96 | 97 | function getAMLHistory() external view returns (Version[] memory){ 98 | return AML; 99 | } 100 | 101 | //******* DOCUMENTS *******// 102 | 103 | //register a new document (always appends to the end) 104 | function addDocument(string calldata componentId, string calldata name, string calldata description, bytes32 docHash) external { 105 | bytes32 id = keccak256(bytes(componentId)); 106 | require(auth.hasPermissionAndAttribute(msg.sender, Authorization.PERMISSION.DOC_CREATE, id, address(this))); 107 | 108 | documents[id].length++; 109 | Document storage doc = documents[id][documents[id].length - 1]; 110 | doc.name = name; 111 | doc.description = description; 112 | doc.versions.push(Version(now, msg.sender, docHash)); 113 | 114 | documents[id].push(doc); 115 | documents[id].length--; 116 | } 117 | 118 | //update Document storage metadata 119 | function updateDocument(string calldata componentId, uint documentId, string calldata name, string calldata description) external { 120 | bytes32 id = keccak256(bytes(componentId)); 121 | require(auth.hasPermissionAndAttribute(msg.sender, Authorization.PERMISSION.DOC_UPDATE, id, address(this))); 122 | documents[id][documentId].name = name; 123 | documents[id][documentId].description = description; 124 | } 125 | 126 | //add new document version 127 | function addDocumentVersion(string calldata componentId, uint documentId, bytes32 docHash) external { 128 | bytes32 id = keccak256(bytes(componentId)); 129 | require(auth.hasPermissionAndAttribute(msg.sender, Authorization.PERMISSION.DOC_UPDATE, id, address(this))); 130 | Version memory updated = Version(now, msg.sender, docHash); 131 | documents[id][documentId].versions.push(updated); 132 | } 133 | 134 | function getDocument(string calldata componentId, uint index) external view returns (Document memory){ 135 | bytes32 id = keccak256(bytes(componentId)); 136 | return documents[id][index]; 137 | } 138 | 139 | function getDocumentCount(string calldata componentId) external view returns (uint){ 140 | bytes32 id = keccak256(bytes(componentId)); 141 | return documents[id].length; 142 | } 143 | 144 | function removeDocument(string calldata componentId, uint index) external { 145 | bytes32 id = keccak256(bytes(componentId)); 146 | require(auth.hasPermissionAndAttribute(msg.sender, Authorization.PERMISSION.DOC_DELETE, id, address(this))); 147 | require(index < documents[id].length); 148 | documents[id][index] = documents[id][documents[id].length-1]; 149 | delete documents[id][documents[id].length-1]; 150 | documents[id].pop(); 151 | } 152 | 153 | //******* SENSORS *******// 154 | 155 | function addSensor(string calldata componentId, string calldata name, bytes32 hash) external { 156 | bytes32 id = keccak256(bytes(componentId)); 157 | require(auth.hasPermissionAndAttribute(msg.sender, Authorization.PERMISSION.SENSOR_CREATE, id, address(this))); 158 | sensors[id].push(Sensor(name, hash)); 159 | } 160 | 161 | function getSensor(string calldata componentId, uint index) external view returns (Sensor memory){ 162 | bytes32 id = keccak256(bytes(componentId)); 163 | return sensors[id][index]; 164 | } 165 | 166 | function getSensorCount(string calldata componentId) external view returns (uint){ 167 | bytes32 id = keccak256(bytes(componentId)); 168 | return sensors[id].length; 169 | } 170 | 171 | function removeSensor(string calldata componentId, uint index) external { 172 | bytes32 id = keccak256(bytes(componentId)); 173 | require(auth.hasPermissionAndAttribute(msg.sender, Authorization.PERMISSION.SENSOR_DELETE, id, address(this))); 174 | require(index < sensors[id].length); 175 | sensors[id][index] = sensors[id][sensors[id].length-1]; 176 | delete sensors[id][sensors[id].length-1]; 177 | sensors[id].pop(); 178 | } 179 | 180 | //******* EXTERNAL SOURCES *******// 181 | 182 | function addExternalSource(string calldata URI, string calldata description) external { 183 | sources.push(ExternalSource(URI, description, msg.sender)); 184 | } 185 | 186 | function getExternalSource(uint index) external view returns (ExternalSource memory){ 187 | return sources[index]; 188 | } 189 | 190 | function getExternalSourceHistory() external view returns (ExternalSource[] memory){ 191 | return sources; 192 | } 193 | 194 | function removeExternalSource(uint index) external { 195 | //todo permission check 196 | require(index < sources.length); 197 | sources[index] = sources[sources.length-1]; 198 | delete sources[sources.length-1]; 199 | sources.pop(); 200 | } 201 | 202 | //******* PROGRAM CALLS *******// 203 | function addProgramCall(string calldata componentId, string calldata call) external { 204 | bytes32 id = keccak256(bytes(componentId)); 205 | //todo permission check 206 | programCallQueue[id].push(ProgramCall(now, msg.sender, call)); 207 | } 208 | 209 | function getProgramCallQueue(string calldata componentId) external view returns (ProgramCall[] memory){ 210 | bytes32 id = keccak256(bytes(componentId)); 211 | return programCallQueue[id]; 212 | } 213 | 214 | function getProgramCounter(string calldata componentId) external view returns (uint){ 215 | bytes32 id = keccak256(bytes(componentId)); 216 | return programCounter[id]; 217 | } 218 | 219 | function updateProgramCounter(string calldata componentId, uint counter) external { 220 | bytes32 id = keccak256(bytes(componentId)); 221 | //todo permission check 222 | programCounter[id] = counter; 223 | } 224 | 225 | } 226 | -------------------------------------------------------------------------------- /contracts/Authorization.sol: -------------------------------------------------------------------------------- 1 | pragma experimental ABIEncoderV2; 2 | 3 | import "./auth/Roles.sol"; 4 | import "./auth/Attributes.sol"; 5 | import "./Specification.sol"; 6 | import "./ContractRegistry.sol"; 7 | 8 | contract Authorization { 9 | using Roles for Roles.Role; 10 | using Attributes for Attributes.Attribute; 11 | 12 | //RBAC roles 13 | enum RBAC {DEVICEAGENT, MANUFACTURER, OWNER, DISTRIBUTOR, MAINTAINER} 14 | 15 | enum PERMISSION { TWIN_CREATE, TWIN_UPDATE, TWIN_DELETE, 16 | DOC_CREATE, DOC_READ, DOC_UPDATE, DOC_DELETE, 17 | SENSOR_CREATE, SENSOR_READ, SENSOR_UPDATE, SENSOR_DELETE, SPECIFICATION_UPDATE} 18 | 19 | //events for removing and adding roles 20 | event RoleChanged(address indexed twin, address indexed operator, uint role, bool added); 21 | event AttributesChanged(address indexed twin, address indexed operator, bytes32[] attributes, bool added); 22 | event DeviceAgentChanged(address indexed operator); 23 | 24 | // local storage of the device agent address 25 | address public deviceAgentAddress; 26 | 27 | mapping(address => bool) public userRegistered; 28 | address[] public users; 29 | 30 | //map twin to mapping(role => users) 31 | mapping(address => mapping(uint => Roles.Role)) internal roleMapping; 32 | 33 | //on-chain permissions 34 | //map role to mapping(permission => bool) 35 | mapping(address => mapping(uint => mapping(uint => bool))) internal permissions; 36 | 37 | //on-chain permissions for components 38 | //map twin to mapping(attribute => users) 39 | mapping(address => mapping(bytes32 => Attributes.Attribute)) internal attributes; 40 | 41 | //this constructor defines the static address of the device agent at deployment 42 | constructor (address _deviceAgent) public payable 43 | { 44 | deviceAgentAddress = _deviceAgent; 45 | } 46 | 47 | function () external payable {} 48 | 49 | // is called by the ContractRegistry.sol --> initial step to register a device 50 | function initializeDevice(address _contract, address _operator, address _deviceAgent) external { 51 | //require(_operator == deviceAgentAddress, "You are not authorized to register devices."); 52 | roleMapping[_contract][uint(RBAC.OWNER)].add(_operator); 53 | roleMapping[_contract][uint(RBAC.DEVICEAGENT)].add(_deviceAgent); 54 | 55 | //initialize permissions - only true must be defined, default value is false 56 | permissions[_contract][uint(RBAC.DEVICEAGENT)][uint(PERMISSION.SENSOR_READ)] = true; 57 | permissions[_contract][uint(RBAC.DEVICEAGENT)][uint(PERMISSION.DOC_READ)] = true; 58 | permissions[_contract][uint(RBAC.DEVICEAGENT)][uint(PERMISSION.SENSOR_UPDATE)] = true; 59 | 60 | permissions[_contract][uint(RBAC.OWNER)][uint(PERMISSION.TWIN_CREATE)] = true; 61 | permissions[_contract][uint(RBAC.OWNER)][uint(PERMISSION.TWIN_UPDATE)] = true; 62 | permissions[_contract][uint(RBAC.OWNER)][uint(PERMISSION.TWIN_DELETE)] = true; 63 | permissions[_contract][uint(RBAC.OWNER)][uint(PERMISSION.DOC_CREATE)] = true; 64 | permissions[_contract][uint(RBAC.OWNER)][uint(PERMISSION.DOC_READ)] = true; 65 | permissions[_contract][uint(RBAC.OWNER)][uint(PERMISSION.DOC_UPDATE)] = true; 66 | permissions[_contract][uint(RBAC.OWNER)][uint(PERMISSION.DOC_DELETE)] = true; 67 | permissions[_contract][uint(RBAC.OWNER)][uint(PERMISSION.SENSOR_CREATE)] = true; 68 | permissions[_contract][uint(RBAC.OWNER)][uint(PERMISSION.SENSOR_READ)] = true; 69 | permissions[_contract][uint(RBAC.OWNER)][uint(PERMISSION.SENSOR_DELETE)] = true; 70 | permissions[_contract][uint(RBAC.OWNER)][uint(PERMISSION.SPECIFICATION_UPDATE)] = true; 71 | 72 | permissions[_contract][uint(RBAC.MANUFACTURER)][uint(PERMISSION.DOC_CREATE)] = true; 73 | permissions[_contract][uint(RBAC.MANUFACTURER)][uint(PERMISSION.DOC_READ)] = true; 74 | permissions[_contract][uint(RBAC.MANUFACTURER)][uint(PERMISSION.DOC_UPDATE)] = true; 75 | permissions[_contract][uint(RBAC.MANUFACTURER)][uint(PERMISSION.SENSOR_READ)] = true; 76 | 77 | permissions[_contract][uint(RBAC.MAINTAINER)][uint(PERMISSION.DOC_CREATE)] = true; 78 | permissions[_contract][uint(RBAC.MAINTAINER)][uint(PERMISSION.DOC_READ)] = true; 79 | permissions[_contract][uint(RBAC.MAINTAINER)][uint(PERMISSION.DOC_UPDATE)] = true; 80 | permissions[_contract][uint(RBAC.MAINTAINER)][uint(PERMISSION.SENSOR_CREATE)] = true; 81 | permissions[_contract][uint(RBAC.MAINTAINER)][uint(PERMISSION.SENSOR_READ)] = true; 82 | permissions[_contract][uint(RBAC.MAINTAINER)][uint(PERMISSION.SENSOR_UPDATE)] = true; 83 | 84 | permissions[_contract][uint(RBAC.DISTRIBUTOR)][uint(PERMISSION.DOC_CREATE)] = true; 85 | permissions[_contract][uint(RBAC.DISTRIBUTOR)][uint(PERMISSION.DOC_READ)] = true; 86 | permissions[_contract][uint(RBAC.DISTRIBUTOR)][uint(PERMISSION.DOC_UPDATE)] = true; 87 | } 88 | 89 | //registers user address and pays out Ether to conduct transactions 90 | function register() external { 91 | require(!userRegistered[msg.sender]); 92 | users.push(msg.sender); 93 | userRegistered[msg.sender] = true; 94 | //msg.sender.transfer(50 ether); 95 | } 96 | 97 | ////////// 98 | // ROLES 99 | ////////// 100 | 101 | //checks if an user has the given role for a specific contract 102 | function hasRole(address _operator, uint _role, address _contract) external view returns (bool){ 103 | return roleMapping[_contract][_role].has(_operator); 104 | } 105 | 106 | // adds a role for an user for a specific specification contract 107 | function addRole(address _operator, uint _role, address _contract) public onlyOwner(_contract) { 108 | roleMapping[_contract][_role].add(_operator); 109 | emit RoleChanged(_contract, _operator, _role, true); 110 | } 111 | 112 | //update a user's own role 113 | function updateRole(uint _role, address _contract) external onlyOwner(_contract) { 114 | removeRole(msg.sender, _role, _contract); 115 | addRole(msg.sender, _role, _contract); 116 | } 117 | 118 | //removes an user of a role for a specific specification contract 119 | function removeRole(address _operator, uint _role, address _contract) public { 120 | require(roleMapping[_contract][uint(RBAC.OWNER)].has(msg.sender) || 121 | roleMapping[_contract][_role].has(_operator), "You are not authorized to remove this role!"); 122 | roleMapping[_contract][_role].remove(_operator); 123 | emit RoleChanged(_contract, _operator, _role, false); 124 | } 125 | 126 | // return the role of an user for a specific contract 127 | function getRole(address _operator, address _contract) public view returns (uint){ 128 | if (roleMapping[_contract][uint(RBAC.OWNER)].has(_operator)) { 129 | return uint(RBAC.OWNER); 130 | } else if (roleMapping[_contract][uint(RBAC.MANUFACTURER)].has(_operator)) { 131 | return uint(RBAC.MANUFACTURER); 132 | } else if (roleMapping[_contract][uint(RBAC.MAINTAINER)].has(_operator)) { 133 | return uint(RBAC.MAINTAINER); 134 | } else if (roleMapping[_contract][uint(RBAC.DISTRIBUTOR)].has(_operator)) { 135 | return uint(RBAC.DISTRIBUTOR); 136 | } else if (roleMapping[_contract][uint(RBAC.DEVICEAGENT)].has(_operator)) { 137 | return uint(RBAC.DEVICEAGENT); 138 | } 139 | else { 140 | return 404; 141 | } 142 | } 143 | 144 | //return all users 145 | function getUsers() external view returns (address[] memory){ 146 | return users; 147 | } 148 | 149 | 150 | /////////////// 151 | // PERMISSIONS 152 | /////////////// 153 | 154 | function hasPermission(address _user, PERMISSION permission, address _contract) external view returns (bool){ 155 | uint role = getRole(_user, _contract); 156 | return permissions[_contract][role][uint(permission)]; 157 | } 158 | 159 | function hasPermissionAndAttribute(address _user, PERMISSION permission, bytes32 _component, address _contract) external view returns (bool){ 160 | uint role = getRole(_user, _contract); 161 | //including the owner here removes the need to set all attributes for the owner 162 | return role == uint(RBAC.OWNER) || (permissions[_contract][role][uint(permission)] && attributes[_contract][_component].has(_user)); 163 | } 164 | 165 | /////////////// 166 | // ATTRIBUTES 167 | /////////////// 168 | 169 | function addAttributes(address _user, bytes32[] calldata _components, address _contract) external onlyOwner(_contract) { 170 | require(_components.length > 0, "No attributes supplied"); 171 | uint len = _components.length; 172 | for (uint i=0; i < len; i++) { 173 | attributes[_contract][_components[i]].add(_user); 174 | } 175 | emit AttributesChanged(_contract, _user, _components, true); 176 | } 177 | 178 | function removeAttributes(address _user, bytes32[] calldata _components, address _contract) external onlyOwner(_contract) { 179 | require(_components.length > 0, "No attributes supplied"); 180 | uint len = _components.length; 181 | for (uint i=0; i < len; i++) { 182 | attributes[_contract][_components[i]].remove(_user); 183 | } 184 | emit AttributesChanged(_contract, _user, _components, false); 185 | } 186 | 187 | //check if a user has the given attribute for a specific contract 188 | function hasAttributes(address _user, bytes32[] calldata _components, address _contract) external view returns (bool[] memory){ 189 | require(_components.length > 0, "No attributes supplied"); 190 | bool[] memory checks = new bool[](_components.length); 191 | uint len = _components.length; 192 | for (uint i = 0; i < len; i++) { 193 | checks[i] = attributes[_contract][_components[i]].has(_user); 194 | } 195 | return checks; 196 | } 197 | 198 | // if the address of the device agent is changed, the old device agent can call this method to change the address 199 | function changeDeviceAgent(address _operator, address _contract) external onlyDeviceAgent(_contract) { 200 | addRole(_operator, uint(RBAC.DEVICEAGENT), _contract); 201 | removeRole(msg.sender, uint(RBAC.DEVICEAGENT), _contract); 202 | deviceAgentAddress = _operator; 203 | emit DeviceAgentChanged(_operator); 204 | } 205 | 206 | modifier onlyOwner(address _contract){ 207 | require(roleMapping[_contract][uint(RBAC.OWNER)].has(msg.sender), "You are not authorized to change Twin roles."); 208 | _; 209 | } 210 | 211 | //modifier used for functions: only device agent can call method 212 | modifier onlyDeviceAgent(address _contract){ 213 | require(isDeviceAgent() == true, "You do not have the permissions."); 214 | _; 215 | } 216 | 217 | // checks if the sender of this request is the device agent 218 | function isDeviceAgent() public view returns (bool) 219 | { 220 | return deviceAgentAddress == msg.sender; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/views/Documents.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 290 | 291 | 299 | -------------------------------------------------------------------------------- /src/components/Index.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 257 | 258 | 266 | 267 | 286 | -------------------------------------------------------------------------------- /agent/agent.js: -------------------------------------------------------------------------------- 1 | const config = require('../config.json'); 2 | //swarm 3 | const secp256k1 = require("@erebos/secp256k1"); 4 | const bzzfeed = require('@erebos/bzz-feed'); 5 | const bzznode = require('@erebos/bzz-node'); 6 | //web3 7 | const Wallet = require('ethereumjs-wallet') 8 | const WalletSubprovider = require('./provider-engine'); 9 | const ProviderEngine = require('web3-provider-engine'); 10 | const FixtureSubprovider = require('web3-provider-engine/subproviders/fixture.js'); 11 | const FilterSubprovider = require('web3-provider-engine/subproviders/filters.js'); 12 | const WebsocketSubprovider = require('web3-provider-engine/subproviders/websocket.js'); 13 | //contracts 14 | const Web3 = require('web3'); 15 | const TruffleContract = require('@truffle/contract'); 16 | const ContractRegistry = require('../public/contracts/ContractRegistry.json'); 17 | const Authorization = require('../public/contracts/Authorization.json'); 18 | const Specification = require('../public/contracts/Specification.json'); 19 | //crypto 20 | const c = require('crypto'); 21 | const ecies = require('eth-ecies'); 22 | //sensor data 23 | const fs = require('fs'); 24 | const xml2js = require('xml2js'); 25 | 26 | /** Config **/ 27 | let privateKey = config.agent_key 28 | let wallet = Wallet.default.fromPrivateKey(Buffer.from(privateKey, 'hex')); 29 | let publicKey = wallet.getPublicKey().toString('hex'); 30 | let address = wallet.getAddressString(); 31 | 32 | /** Swarm Setup **/ 33 | let keyPair = secp256k1.createKeyPair(privateKey); 34 | const client = new bzznode.BzzNode({ url: config.swarm }); 35 | const feed = new bzzfeed.BzzFeed({ 36 | bzz: client, 37 | signBytes: bytes => Promise.resolve(secp256k1.sign(bytes, keyPair)), 38 | }); 39 | 40 | //If not yet published, publish user public key to feed 41 | feed.getContent({user: address}).catch(() => { 42 | feed.setContent( 43 | {user: address}, 44 | publicKey, 45 | {contentType: "text/plain"} 46 | ); 47 | }); 48 | 49 | /** Web3 and Contracts Setup **/ 50 | var engine = new ProviderEngine(); 51 | let web3 = new Web3(engine); 52 | engine.addProvider(new FixtureSubprovider()); 53 | engine.addProvider(new FilterSubprovider()); 54 | engine.addProvider(new WalletSubprovider(wallet)); 55 | engine.addProvider(new WebsocketSubprovider({rpcUrl: config.ethereum.rpc})); 56 | engine.start(); 57 | 58 | let registryContract = TruffleContract(ContractRegistry); 59 | registryContract.setProvider(web3.currentProvider); 60 | let authContract = TruffleContract(Authorization); 61 | authContract.setProvider(web3.currentProvider); 62 | 63 | subscribe(); 64 | pushSensorData() 65 | 66 | async function pushSensorData(){ 67 | let samples = await getSamples(); 68 | let feedHash = "8ff71c988265ccdb70841eebf26690dc7f0fdda234bfc4d72fd8cf5613c4ae90"; 69 | while(1){ 70 | let logs = [] 71 | let totalDuration = 0 72 | for await (log of samples.logset.log) { 73 | logs.push(log) 74 | let duration = Number(log.duration) 75 | log.duration = Number(log.duration).toFixed(1) 76 | 77 | //action takes more than 1s -> directly send 78 | if(duration >= 1000 && totalDuration === 0){ 79 | await updateFeed(feedHash, log) 80 | console.log(duration) 81 | await sleep(duration) 82 | } 83 | //action takes less than 1s -> batch until > 1s 84 | else if(totalDuration > 1000){ 85 | await updateFeed(feedHash, logs) 86 | await sleep(totalDuration) 87 | console.log(totalDuration) 88 | totalDuration = 0 89 | logs = [] 90 | } 91 | else { 92 | totalDuration = totalDuration + duration; 93 | logs.push(log) 94 | await sleep(100) 95 | } 96 | } 97 | } 98 | } 99 | 100 | function sleep(ms) { 101 | return new Promise((resolve) => { 102 | setTimeout(resolve, ms); 103 | }); 104 | } 105 | 106 | async function subscribe() { 107 | await web3.eth.net.getId(); 108 | let registry = await ((config.ethereum.registry) ? 109 | registryContract.at(config.ethereum.registry) : 110 | registryContract.deployed()); 111 | let auth = await ((config.ethereum.authorization) ? 112 | authContract.at(config.ethereum.authorization) : 113 | authContract.deployed()); 114 | 115 | registry.TwinCreated({}, (error, data) => { 116 | if (error) 117 | console.log("Error: " + error); 118 | else { 119 | if(data.returnValues.deviceAgent.toLowerCase() === address) 120 | createKeys(data) 121 | } 122 | }); 123 | auth.RoleChanged({}, (error, data) => {updateKeys(error, data, auth)}); 124 | auth.AttributesChanged({}, (error, data) => {updateKeys(error, data, auth)}); 125 | } 126 | 127 | async function createKeys(data){ 128 | await sleep(1000) //wait for key update by client 129 | let before = new Date() 130 | let components = await getComponents(data.returnValues.contractAddress, data.returnValues.aml, data.returnValues.owner); 131 | let users = await getUsers(); 132 | //get user role 133 | let a = await authContract.deployed(); 134 | let usersRoles = await Promise.all(users.map(u => a.getRole(u.toLowerCase(), data.returnValues.contractAddress))); 135 | users = users.filter((u, ind) => usersRoles[ind] < 5); 136 | 137 | let usersPublicKeys = await Promise.all(users.map(getUserFeedText)); 138 | //add own address and publicKey if not included 139 | if(!users.map(u => u.toLowerCase()).includes(address)){ 140 | users.push(address); 141 | usersPublicKeys.push(publicKey); 142 | } 143 | 144 | //check for each valid user and each component if the user has the attribute 145 | let promises = 146 | components.map(component => createComponentKeys(data.returnValues.contractAddress, component, "doc", users, usersPublicKeys)); 147 | promises.push(... 148 | components.map(component => createComponentKeys(data.returnValues.contractAddress, component, "sensor", users, usersPublicKeys))); 149 | await Promise.all(promises); 150 | console.log("File keys created. " + components.length * 2 + " feeds updated in " + (new Date() - before) + "ms.") 151 | } 152 | 153 | async function createComponentKeys(twinAddress, component, type, users, usersPublicKeys){ 154 | let update = createSymmetricKeys( 155 | users, 156 | usersPublicKeys 157 | ); 158 | await updateFeedSimple( 159 | { 160 | user: address, 161 | topic: web3.utils.sha3(twinAddress + component.id + type) 162 | }, update); 163 | return web3.utils.sha3(twinAddress + component.id + type); 164 | } 165 | 166 | function createSymmetricKeys(shareAddresses, usersPublicKeys) { 167 | let key = c.randomBytes(32); 168 | return shareAddresses.map((d, ind) => makeFileKey(key, d.toLowerCase(), usersPublicKeys[ind])); 169 | } 170 | 171 | function makeFileKey(key, shareAddress, sharePublicKey){ 172 | let ciphertext = encryptECIES(sharePublicKey, key); 173 | return {address: shareAddress, fileKey: ciphertext}; 174 | } 175 | 176 | async function updateKeys(error, data, auth){ 177 | if (error) 178 | console.log("Error: " + error); 179 | else { 180 | //check if deviceAgent for this twin 181 | let before = new Date(); 182 | let twin = await getSpecification(data.returnValues.twin); 183 | let deviceAgent = await twin.deviceAgent(); 184 | if(deviceAgent.toLowerCase() === address) { 185 | let [ twinData, amlHistory, publicKey ] = await Promise.all([ 186 | twin.getTwin(), 187 | twin.getAMLHistory(), 188 | getUserFeedText(data.returnValues.operator) 189 | ]); 190 | let aml = amlHistory[amlHistory.length - 1]; 191 | let components = await getComponents(data.returnValues.twin, aml.hash, twinData[2]); 192 | 193 | //add keys for all components if owner, else where attribute is present 194 | let [permissionsDocuments, permissionsSensors] = await Promise.all([ 195 | getPermissions(auth, twin.address, data.returnValues.operator, components, 5), 196 | getPermissions(auth, twin.address, data.returnValues.operator, components, 9) 197 | ]); 198 | 199 | //documents 200 | let componentsFiltered = components.filter((c, ind) => permissionsDocuments[ind]); 201 | let updateKeyCount = componentsFiltered.length; 202 | let updateFunc = data.returnValues.added ? shareFileKey : removeFileKey; 203 | let promises = componentsFiltered.map(component => updateFunc( 204 | address, 205 | web3.utils.sha3(data.returnValues.twin + component.id + "doc"), 206 | data.returnValues.operator, 207 | publicKey 208 | )); 209 | 210 | //sensors 211 | componentsFiltered = components.filter((c, ind) => permissionsSensors[ind]); 212 | updateKeyCount += componentsFiltered.length; 213 | promises.push(... 214 | componentsFiltered.map(component => updateFunc( 215 | address, 216 | web3.utils.sha3(data.returnValues.twin + component.id + "sensor"), 217 | data.returnValues.operator, 218 | publicKey 219 | )) 220 | ); 221 | await Promise.all(promises); 222 | console.log("File keys updated. " + updateKeyCount + " feeds updated in " + (new Date() - before) + "ms.") 223 | } 224 | } 225 | } 226 | 227 | async function getPermissions(auth, twin, user, components, permission){ 228 | return Promise.all(components.map(c => auth.hasPermissionAndAttribute( 229 | user, 230 | permission, 231 | web3.utils.hexToBytes(c.hash), 232 | twin 233 | ))); 234 | } 235 | 236 | async function getComponents(twinAddress, amlHash, owner){ 237 | let aml = (await downloadEncryptedDoc(owner, web3.utils.sha3(twinAddress), amlHash.substr(2, amlHash.length))) 238 | .content.toString(); 239 | let amlDoc = await parseXML(aml) 240 | let components = []; 241 | for (log of amlDoc.CAEXFile.InstanceHierarchy[0].InternalElement) { 242 | let id = log.$.ID 243 | let name = log.$.Name 244 | let hash = web3.utils.sha3(id); 245 | // add parsed components to the components array 246 | components.push({id: id, name: name, hash: hash}); 247 | } 248 | 249 | return components; 250 | } 251 | 252 | async function getUsers(){ 253 | let auth = await authContract.deployed(); 254 | return auth.getUsers(); 255 | } 256 | 257 | /** 258 | * HELPERS 259 | */ 260 | 261 | async function getSpecification(address){ 262 | let truffle = TruffleContract(Specification); 263 | truffle.setProvider(web3.currentProvider); 264 | return await truffle.at(address) 265 | } 266 | 267 | async function getSamples(){ 268 | return new Promise((resolve, reject) => { 269 | fs.readFile('misc/logs.xml', function(err, data) { 270 | resolve(parseXML(data)) 271 | }); 272 | }); 273 | } 274 | 275 | async function parseXML(xml){ 276 | let parser = new xml2js.Parser(); 277 | return new Promise((resolve, reject) => { 278 | parser.parseString(xml, function (err, result) { 279 | resolve(result); 280 | }); 281 | }); 282 | } 283 | 284 | /** 285 | * SWARM + CRYPTO HELPERS 286 | */ 287 | 288 | async function downloadEncryptedDoc(user, topic, hash) { 289 | let key = await getFileKey(user, topic); 290 | let response = await client.download(hash); 291 | let res = await response.json(); 292 | return { 293 | content: decryptAES(res.encryptedData, key, res.iv), 294 | type: res.type 295 | }; 296 | } 297 | 298 | async function getUserFeedText(user){ 299 | let content = await feed.getContent({user: user}); 300 | return await content.text(); 301 | } 302 | 303 | async function getUserFeedLatest(user, topic) { 304 | let content = await feed.getContent({ 305 | user: user, 306 | topic: topic 307 | }); 308 | return content.json(); 309 | } 310 | 311 | async function updateFeedSimple(feedParams, update){ 312 | return feed.setContent(feedParams, JSON.stringify(update), {contentType: "application/json"}) 313 | } 314 | 315 | async function updateFeed(feedHash, contents) { 316 | let options = {contentType: "application/json"}; 317 | let meta = await feed.getMetadata(feedHash); 318 | let update = { 319 | time: meta.epoch.time, 320 | content: contents 321 | }; 322 | let hash = await client.uploadFile(JSON.stringify(update), options); 323 | try { 324 | return await feed.postChunk(meta, `0x${hash}`, options); 325 | } 326 | catch(err){console.log(err)} 327 | } 328 | 329 | async function getFileKey(user, topic) { 330 | let content = await feed.getContent({ 331 | user: user, 332 | topic: topic 333 | }); 334 | let fileKeys = await content.json(); 335 | let keyObject = fileKeys.filter(f => f.address.toLowerCase() === address.toLowerCase())[0]; 336 | let plainKey = decryptECIES(privateKey, keyObject.fileKey); 337 | return Buffer.from(plainKey, 'base64'); 338 | } 339 | 340 | //share an existing key on the user's own feed with another user 341 | async function shareFileKey(user, topic, shareAddress, publicKey = "") { 342 | //get existing keys and decrypt key 343 | let fileKeys = await getUserFeedLatest(user, topic);//encrypt for new user 344 | if(!Array.isArray(fileKeys) || fileKeys.includes(shareAddress)) return; 345 | publicKey = publicKey === "" ? await getUserFeedText(shareAddress) : publicKey; 346 | let keyObject = fileKeys.filter(f => f.address === user)[0]; 347 | let plainKey = decryptECIES(privateKey, keyObject.fileKey); 348 | let newKey = encryptECIES(publicKey, plainKey); 349 | fileKeys.push({address: shareAddress, fileKey: newKey}); 350 | await updateFeedSimple({user: user, topic: topic}, fileKeys); 351 | } 352 | 353 | //remove a user's file key 354 | async function removeFileKey(user, topic, userAddress) { 355 | let fileKeys = await getUserFeedLatest(user, topic); 356 | fileKeys = fileKeys.filter(f => f.address !== userAddress); 357 | await updateFeedSimple({user: user, topic: topic}, fileKeys); 358 | } 359 | 360 | /** 361 | * 362 | * @param publicKey string 363 | * @param data Buffer 364 | * @returns {String} 365 | */ 366 | function encryptECIES(publicKey, data) { 367 | let userPublicKey = Buffer.from(publicKey, 'hex'); 368 | return ecies.encrypt(userPublicKey, data).toString('base64'); 369 | } 370 | 371 | /** 372 | * 373 | * @param privateKey string 374 | * @param encryptedData base64 string 375 | * @returns {Buffer} 376 | */ 377 | function decryptECIES(privateKey, encryptedData) { 378 | let bufferData = Buffer.from(encryptedData, 'base64'); 379 | return ecies.decrypt(privateKey, bufferData); 380 | } 381 | 382 | function decryptAES(text, key, init_vector) { 383 | let iv = Buffer.from(init_vector, 'hex'); 384 | let encryptedText = Buffer.from(text, 'base64'); 385 | let decipher = c.createDecipheriv('aes-256-ctr', Buffer.from(key), iv); 386 | let decrypted = decipher.update(encryptedText); 387 | decrypted = Buffer.concat([decrypted, decipher.final()]); 388 | return decrypted; 389 | } 390 | -------------------------------------------------------------------------------- /src/views/Roles.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 287 | 288 | 296 | 297 | 307 | --------------------------------------------------------------------------------