├── 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 |
2 |
3 |
4 |
5 |
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 |
12 | We're sorry but ethertwin doesn't work properly without JavaScript enabled. Please enable it to continue.
13 |
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 |
2 |
3 |
4 |
5 |
13 |
14 |
--------------------------------------------------------------------------------
/src/views/AddSensor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Add Sensor
6 |
7 |
20 |
21 |
22 |
23 |
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 |
2 |
3 |
4 |
5 |
Sensor Feed: {{ sensorData.name }}
6 |
7 |
11 |
12 |
13 | Refresh
14 |
15 |
16 |
17 |
18 |
19 | Timestamp
20 | Values
21 |
22 |
23 |
24 | No updates found
25 |
26 | {{ $utils.date(update.time) }}
27 |
28 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
95 |
96 |
99 |
--------------------------------------------------------------------------------
/src/views/Components.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Components of Twin: {{ twinObject.deviceName }}
4 |
5 |
6 |
7 |
8 |
19 |
20 |
21 | ID : {{ component.id }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
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 |
2 |
3 |
4 |
5 |
Add Twin
6 |
7 |
35 |
36 |
37 |
38 |
39 |
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 | 
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 |
2 |
3 |
External Data Sources for Twin: {{ twinObject.deviceName }}
4 |
Add new Data Source
5 |
6 |
16 |
17 |
18 |
19 | Add Data Source
20 |
21 |
22 |
23 |
24 |
Existing Data Sources
25 |
26 |
27 |
28 | URI
29 | Description
30 | Actions
31 |
32 |
33 |
34 |
35 | {{ source[0] }}
36 | {{ source[1] }}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
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 |
2 |
3 |
4 |
Account Information
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Network
14 |
15 |
16 |
17 |
19 | {{ this.network }}
20 |
21 |
22 |
23 |
24 |
25 | Registry Contract:
26 | {{ this.$store.state.addresses.ContractRegistryAddress }}
27 | Authorization Contract:
28 | {{ this.$store.state.addresses.AuthorizationAddress }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Account
38 |
39 | {{ this.account }}
40 |
41 |
42 |
43 |
44 |
45 |
46 | Balance
47 |
48 | {{ this.$store.state.balance }} ETH
49 |
50 |
51 |
52 |
53 |
54 |
55 | Public key
56 |
57 |
58 | {{ this.pubKey }}
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | Private key
67 |
68 | {{ this.privKey }}
69 |
70 |
71 |
72 |
73 |
74 |
75 | Register
76 |
77 | Please register your address for other users
78 |
79 |
80 |
81 |
Update private key
82 |
83 |
84 |
85 |
86 | save changes
87 |
88 |
89 |
90 |
91 |
145 |
146 |
149 |
--------------------------------------------------------------------------------
/src/views/Sensors.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Sensors for Twin: {{ twinObject.deviceName }}
4 |
Add new sensors
5 |
Select a component and enter the name of the sensor.
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ component.name }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | Add Sensor
25 |
26 |
27 |
28 |
29 |
Existing sensors
30 |
31 |
32 |
33 | Component
34 | Name
35 | Actions
36 |
37 |
38 |
39 |
40 | {{ sensor.componentName }}
41 | {{ sensor.name }}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
173 |
174 |
180 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Ether Twin
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Twins
20 |
28 |
29 |
30 |
31 |
32 | Specification
33 |
34 |
35 |
36 | Components
37 |
38 |
39 |
40 | Documents
41 |
42 |
43 |
44 | Sensors
45 |
46 |
47 |
48 | Data sources
49 |
50 |
51 |
54 |
55 |
56 |
57 | Users
58 |
59 |
60 |
61 |
62 |
63 |
64 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
151 |
152 |
153 |
163 |
--------------------------------------------------------------------------------
/src/views/Specification.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Specification for Twin: {{ twinObject.deviceName }}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Latest Version
13 | Author
14 |
15 |
16 | {{ versions.length }}
17 | {{ author }}
18 |
19 |
20 |
21 |
22 |
37 |
38 |
39 |
40 |
42 | Save
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
62 |
63 |
64 |
66 | Upload
67 |
68 |
69 |
70 |
71 |
72 |
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 |
2 |
3 |
Documents for Twin: {{ twinObject.deviceName }}
4 |
Add new documents
5 |
Select a component and choose a file to add a new document.
6 |
7 |
8 |
9 |
10 |
11 | {{
12 | component.name }}
13 |
14 |
15 |
16 |
23 |
24 |
30 |
31 |
32 |
33 |
35 | Upload
36 |
37 |
38 |
39 |
40 |
41 |
Existing Documents
42 |
43 |
44 |
45 |
46 |
47 | {{ component.name }}
48 |
49 |
50 |
51 |
52 |
{{ document[0] }}
53 |
54 |
55 |
{{ document[1] }}
56 |
57 | Version {{
58 | document[2].length - j }} ({{$utils.date(version[0])}})
59 |
60 |
61 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
290 |
291 |
299 |
--------------------------------------------------------------------------------
/src/components/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Your account has insufficient funds. Please get some Ether before proceeding.
5 |
Reload
6 |
7 |
8 | You must register this user with the blockchain before you can create a twin:
9 |
Register
10 |
11 |
12 |
13 |
Digital Twin Overview
14 |
15 |
16 |
17 |
18 |
Add a new twin or select a twin by clicking on an action.
19 |
20 |
21 |
22 |
24 | Refresh
25 |
26 |
27 |
28 |
30 | Add Twin
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Name
42 |
43 |
44 | Contract Address
45 |
46 |
47 | Role
48 |
49 |
50 | Actions
51 |
52 |
53 |
54 |
55 |
57 | {{ twin.deviceName }}
58 | {{ twin.address }}
59 | {{ twin.role }}
60 |
61 |
63 |
65 |
66 |
68 |
70 |
71 |
73 |
74 |
75 |
78 |
79 |
80 |
81 |
83 |
84 |
85 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | You are not authorized for any device at this time.
95 |
96 |
97 |
98 |
99 |
100 |
101 |
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 |
2 |
3 |
Current users of: {{ twinObject.deviceName }}
4 |
5 |
6 |
7 |
8 | User address
9 | Role
10 | Attribute(s)
11 | Actions
12 |
13 |
14 |
15 |
16 | {{ user.address }}
17 | {{ user.role }}
18 | {{ user.attribute.join(", ") }}
19 |
20 |
21 |
22 |
23 |
24 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
287 |
288 |
296 |
297 |
307 |
--------------------------------------------------------------------------------