├── .eslintrc ├── .gitignore ├── .gitmodules ├── .travis.yml ├── COPYING ├── Makefile ├── README.md ├── build_manifest.py ├── extension.zip ├── extension ├── icon128.png ├── icon16.png └── icon48.png ├── flowtype_definitions └── chrome.js ├── management ├── .bowerrc ├── Gruntfile.js ├── LICENSE ├── Makefile ├── app │ ├── fonts │ │ ├── trezorfont.eot │ │ ├── trezorfont.ttf │ │ └── trezorfont.woff │ ├── index.html │ ├── scripts │ │ ├── app.js │ │ ├── config.original.js │ │ ├── controllers │ │ │ ├── debug.js │ │ │ ├── device │ │ │ │ ├── device.js │ │ │ │ ├── firmware.js │ │ │ │ ├── info.js │ │ │ │ ├── recovery.js │ │ │ │ ├── setup.js │ │ │ │ └── wipe.js │ │ │ ├── error.js │ │ │ ├── main.js │ │ │ ├── modal │ │ │ │ └── pin.js │ │ │ └── navbar.js │ │ ├── directives │ │ │ ├── flashMessages.js │ │ │ └── focus.js │ │ ├── filters.js │ │ ├── filters │ │ │ └── filters.js │ │ ├── services │ │ │ ├── DeviceList.js │ │ │ ├── TrezorDevice.js │ │ │ ├── bip39.js │ │ │ ├── console.js │ │ │ ├── deviceService.js │ │ │ ├── firmwareService.js │ │ │ ├── modals │ │ │ │ ├── modalOpener.js │ │ │ │ ├── passphraseModalService.js │ │ │ │ ├── pinModalService.js │ │ │ │ └── setupModalService.js │ │ │ ├── slider.js │ │ │ ├── udevError.js │ │ │ └── utils.js │ │ ├── templates.error.js │ │ ├── templates.webwallet.js │ │ └── vendor │ │ │ ├── polyfill.js │ │ │ └── vendor.js │ ├── styles │ │ ├── angular-csp.css │ │ └── main.css │ └── views │ │ ├── debug.button.html │ │ ├── debug.link.html │ │ ├── debug.log.html │ │ ├── device │ │ ├── advanced.html │ │ ├── index.basic.html │ │ ├── index.html │ │ ├── index.setup.html │ │ ├── nav.html │ │ ├── recovery.html │ │ └── wipe.html │ │ ├── deviceDetails.link.html │ │ ├── error.html │ │ ├── error.udev.html │ │ ├── main.html │ │ ├── modal │ │ ├── button.html │ │ ├── disconnect.html │ │ ├── disconnect.wipe.html │ │ ├── firmware.html │ │ ├── forget.disconnected.html │ │ ├── forget.requested.html │ │ ├── forget.wipe.html │ │ ├── label.html │ │ ├── passphrase.html │ │ ├── pin.html │ │ ├── setup.html │ │ └── upperheader.html │ │ └── topBars.html ├── bower.json ├── firmware_copy.py └── package.json ├── manifest_no_matches.json ├── package.json ├── src ├── .eslintignore ├── .flowconfig ├── chrome │ ├── platformInfo.js │ └── storage.js └── index.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "ecmaFeatures": { 5 | "experimentalObjectRestSpread": true, 6 | "jsx": true 7 | }, 8 | "sourceType": "module" 9 | }, 10 | "extends": "eslint:recommended", 11 | "parser": "babel-eslint", 12 | "plugins": [ 13 | "standard", 14 | "promise", 15 | "flowtype" 16 | ], 17 | "env": { 18 | "es6": true, 19 | "node": true, 20 | "browser": true 21 | }, 22 | "globals": { 23 | "chrome": false 24 | }, 25 | "rules": { 26 | "flowtype/define-flow-type": 1, 27 | "flowtype/use-flow-type": 1, 28 | "comma-dangle": [2, "always-multiline"], 29 | "no-unused-vars": [2, { 30 | "args": "none", 31 | "vars": "all" 32 | }], 33 | "no-console": 0, 34 | "quotes": [2, "double", "avoid-escape"], 35 | "linebreak-style": [ 36 | 2, 37 | "unix" 38 | ], 39 | "semi": [ 40 | 2, 41 | "always" 42 | ], 43 | "accessor-pairs": 2, 44 | "arrow-spacing": [2, { "before": true, "after": true }], 45 | "block-spacing": [2, "always"], 46 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 47 | "comma-spacing": [2, { "before": false, "after": true }], 48 | "comma-style": [2, "last"], 49 | "constructor-super": 2, 50 | "curly": [2, "multi-line"], 51 | "dot-location": [2, "property"], 52 | "eol-last": 2, 53 | "eqeqeq": [2, "allow-null"], 54 | "generator-star-spacing": [2, { "before": true, "after": true }], 55 | "handle-callback-err": [2, "^(err|error)$" ], 56 | "indent": [2, 2, { "SwitchCase": 1 }], 57 | "jsx-quotes": [2, "prefer-single"], 58 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 59 | "keyword-spacing": [2, {"before": true, "after": true}], 60 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 61 | "new-parens": 2, 62 | "no-array-constructor": 2, 63 | "no-caller": 2, 64 | "no-class-assign": 2, 65 | "no-cond-assign": 2, 66 | "no-const-assign": 2, 67 | "no-control-regex": 2, 68 | "no-debugger": 2, 69 | "no-delete-var": 2, 70 | "no-dupe-args": 2, 71 | "no-dupe-class-members": 2, 72 | "no-dupe-keys": 2, 73 | "no-duplicate-case": 2, 74 | "no-empty-character-class": 2, 75 | "no-empty-pattern": 2, 76 | "no-eval": 2, 77 | "no-ex-assign": 2, 78 | "no-extend-native": 2, 79 | "no-extra-bind": 2, 80 | "no-extra-boolean-cast": 2, 81 | "no-extra-parens": [2, "functions"], 82 | "no-fallthrough": 2, 83 | "no-floating-decimal": 2, 84 | "no-func-assign": 2, 85 | "no-implied-eval": 2, 86 | "no-inner-declarations": [2, "functions"], 87 | "no-invalid-regexp": 2, 88 | "no-irregular-whitespace": 2, 89 | "no-iterator": 2, 90 | "no-label-var": 2, 91 | "no-labels": 2, 92 | "no-lone-blocks": 2, 93 | "no-mixed-spaces-and-tabs": 2, 94 | "no-multi-spaces": 2, 95 | "no-multi-str": 2, 96 | "no-multiple-empty-lines": [2, { "max": 1 }], 97 | "no-native-reassign": 2, 98 | "no-negated-in-lhs": 2, 99 | "no-new": 2, 100 | "no-new-func": 2, 101 | "no-new-object": 2, 102 | "no-new-require": 2, 103 | "no-new-wrappers": 2, 104 | "no-obj-calls": 2, 105 | "no-octal": 2, 106 | "no-octal-escape": 2, 107 | "no-path-concat": 2, 108 | "no-proto": 2, 109 | "no-redeclare": 2, 110 | "no-regex-spaces": 2, 111 | "no-return-assign": [2, "except-parens"], 112 | "no-self-compare": 2, 113 | "no-sequences": 2, 114 | "no-shadow-restricted-names": 2, 115 | "no-spaced-func": 2, 116 | "no-sparse-arrays": 2, 117 | "no-this-before-super": 2, 118 | "no-throw-literal": 2, 119 | "no-trailing-spaces": 2, 120 | "no-undef": 2, 121 | "no-undef-init": 2, 122 | "no-unexpected-multiline": 2, 123 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 124 | "no-unreachable": 2, 125 | "no-useless-call": 2, 126 | "no-use-before-define": [2, {"functions": false, "classes": false}], 127 | "no-var": 2, 128 | "no-with": 2, 129 | "one-var": [2, { "initialized": "never" }], 130 | "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], 131 | "padded-blocks": [2, "never"], 132 | "prefer-const": 2, 133 | "semi-spacing": [2, { "before": false, "after": true }], 134 | "space-before-blocks": [2, "always"], 135 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 136 | "space-in-parens": [2, "never"], 137 | "space-infix-ops": 2, 138 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 139 | "spaced-comment": [2, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], 140 | "use-isnan": 2, 141 | "valid-typeof": 2, 142 | "wrap-iife": [2, "any"], 143 | "yoda": [2, "never"], 144 | 145 | "standard/object-curly-even-spacing": [2, "either"], 146 | "standard/array-bracket-even-spacing": [2, "either"], 147 | "standard/computed-property-even-spacing": [2, "even"], 148 | "promise/param-names": 2, 149 | "promise/always-return": 0 150 | }, 151 | } 152 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | config_proto_compiled.js 3 | extension/index.js 4 | node_modules/ 5 | extension/data/ 6 | extension/management/ 7 | extension/manifest.json 8 | management/app/bower_components/ 9 | management/app/scripts/config.js 10 | management/dist/ 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "trezor-common"] 2 | path = trezor-common 3 | url = https://github.com/trezor/trezor-common.git 4 | [submodule "management/data"] 5 | path = management/data 6 | url = https://github.com/trezor/webwallet-data 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: node_js 3 | 4 | addons: 5 | apt: 6 | packages: 7 | - python3 8 | 9 | node_js: 10 | - "4.4" 11 | 12 | before_script: 13 | - npm install -g flow-bin eslint eslint-plugin-flowtype babel-eslint eslint-plugin-standard eslint-plugin-promise 14 | 15 | env: 16 | - GOAL=zip 17 | - GOAL=flow 18 | - GOAL=eslint 19 | 20 | script: make $GOAL 21 | 22 | notifications: 23 | webhooks: 24 | urls: 25 | - http://ci-bot.satoshilabs.com:5000/travis 26 | on_success: always 27 | on_failure: always 28 | on_start: always 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: clear zip 2 | 3 | flow: 4 | cd src; flow check 5 | 6 | eslint: 7 | cd src; eslint . 8 | 9 | clear: 10 | rm -f extension.zip 11 | rm -f extension/index.js 12 | rm -f extension/manifest.json 13 | rm -rf extension/data 14 | rm -rf extension/management 15 | rm -f src/config_proto_compiled.js 16 | $(MAKE) -C management clear-dist #not clearing dependencies, it would take forever 17 | 18 | zip: dist 19 | zip -r extension extension 20 | 21 | dist: pre-browserify dist-build 22 | 23 | dist-build: 24 | NODE_ENV=dist `npm bin`/browserify src/index.js > extension/index.js 25 | 26 | debug: pre-browserify-debug 27 | NODE_ENV=debug `npm bin`/browserify src/index.js -o extension/index.js 28 | 29 | pre-browserify: check-modules management_make manifest 30 | pre-browserify-debug: check-modules manifest 31 | 32 | management_make: 33 | $(MAKE) -C management 34 | 35 | manifest: 36 | python3 build_manifest.py 37 | 38 | npm-install: 39 | yarn 40 | 41 | check-modules: 42 | git submodule foreach git pull origin master 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TREZOR Chrome Extension 2 | 3 | [![Build Status](https://travis-ci.org/trezor/trezor-chrome-extension.svg?branch=master)](https://travis-ci.org/trezor/trezor-chrome-extension) [![gitter](https://badges.gitter.im/trezor/community.svg)](https://gitter.im/trezor/community) 4 | 5 | Chrome extension for [Bitcoin TREZOR](https://www.bitcointrezor.com/) by [SatoshiLabs](http://satoshilabs.com/). 6 | 7 | ## About 8 | 9 | TREZOR Chrome Extension has two different purposes. 10 | 11 | First, it has a built-in device management functionality, for wiping/recovery/initialization/.... It doesn't connect to any outside sources, all the data (including TREZOR firmware) is bundled with the app, so it works completely offline (if you somehow manage to install Chrome without an internet connection). 12 | 13 | Second, it's a transport layer between websites (such as our own webwallet [TREZOR Wallet](https://wallet.trezor.io)) and TREZOR devices. It's possible to send Chrome messages (see [chrome messages API documentation](https://developer.chrome.com/extensions/messaging)) to the extension, and the extension resends it to TREZOR hardware and back. 14 | 15 | The messages are encoded through [protobuf.js](https://github.com/dcodeIO/ProtoBuf.js/) library and sent to the actual hardware via [Chrome USB HID API](https://developer.chrome.com/apps/hid). 16 | 17 | The API of the extensions is described below. **For development of web apps for TREZOR, it is recommended to use [trezor.js](https://github.com/trezor/trezor.js) javascript API, which has separate javascript calls for most common usecases; or [TREZOR Connect](https://github.com/trezor/connect), which is even more high level.** However, the *end user* still needs to install either the extension or [trezord](https://github.com/trezor/trezord). 18 | 19 | ## Install via Web store 20 | 21 | Extension is available for download [at Google Web store](https://chrome.google.com/webstore/detail/jcjjhjgimijdkoamemaghajlhegmoclj) (and is automatically offered on webwallet [TREZOR Wallet](https://wallet.trezor.io)). 22 | 23 | ## Install via ZIP 24 | 25 | If you don't trust Google Web store (or want to use an offline machine), you can download the ZIP file extension.zip, unzip it, go to [chrome://extensions/](chrome://extensions/) in Chrome, enable "Developer mode", click "Load upacked extension" and find the directory. 26 | 27 | The ZIP file in the repo will be updated simultaneously with the updates in Google Web Store; it might not be up-to-date with the master branch. 28 | 29 | 30 | ## Install from source 31 | 32 | ### Checking out sources 33 | 34 | ``` 35 | git clone --recursive https://github.com/trezor/chrome-extension.git 36 | ``` 37 | 38 | Or, if you already cloned the repository but not the submodules 39 | 40 | ``` 41 | git submodule update --init --recursive 42 | ``` 43 | 44 | ### Building 45 | 46 | Building works on OS X and Linux and uses `make`. 47 | 48 | You need to have [flow](http://flowtype.org) installed for type checking. If you don't want to install it, edit the `flow-check` in Makefile to something like `true`. 49 | 50 | You also need python3 and npm and have them in `$PATH`. 51 | 52 | (You need to be online for the build because of `npm install` and `git update` that happen in the build.) 53 | 54 | ``` 55 | make npm-install # needed only the first time 56 | make clear # if you built before 57 | make zip 58 | ``` 59 | 60 | ## Source 61 | 62 | The source code of the transport layer is using [flow](http://flowtype.org) type annotations and some features of ECMAScript 6. 63 | 64 | Most of the logic is now in trezor-link npm package, the extension just do data validation and so on. 65 | 66 | The source code of the device management is an angular app. If it seems a little "over-blown", it's because it was created as a clone of the whole myTREZOR app, which handles more than device management, and then functionality was stripped off. 67 | 68 | 69 | ## Caveats 70 | 71 | On Mac OS X, Windows and Chrome OS, installing the extension should work without any root privileges. Unfortunately, on GNU/Linux, you have install so-called udev rules as a root. 72 | 73 | If you are using wallet.trezor.io, we are trying to detect the errors and offer you an easy package for the two most popular packaging systems (DEB and RPM). 74 | 75 | If you don't want to or can't install that, please refer to our documentation 76 | 77 | http://doc.satoshilabs.com/trezor-user/settingupchromeonlinux.html 78 | 79 | ## UDP connection 80 | 81 | Connect to UDP TREZOR emulator (will be released soon-ish) by opening extension background page in chrome://extensions and typing into console 82 | 83 | `window.setUdp([21324, ...])` 84 | 85 | with the list of ports of the virtual devices. The devices are immediately added and are registered as connected; if an app (like myTREZOR) is running, it will see them and try to communicate with them. To simulate disconnect, just type 86 | 87 | `window.setUdp([])` 88 | 89 | and the device are marked as disconnected. 90 | 91 | Allowed ports are 21324, 21325 and 21326. 92 | 93 | Note: there is a known bug - if you set up UDP connection, but turn off the emulator, the HID device listing might get stuck on webpage reloads. If you notice this, set `wuindow.setUdp([])` or turn on the emulator. 94 | 95 | ## API 96 | 97 | If installed using some of the described methods, the extension has an id `jcjjhjgimijdkoamemaghajlhegmoclj`. 98 | 99 | You send the messages to the extension using [chrome messages API](https://developer.chrome.com/extensions/messaging) (read the note about whitelisting below). 100 | 101 | The messages are javacript Objects with `type` property and `body` property; `type` is always string, `body` varies depending on the type. 102 | 103 | The response is a javascript Object with `type` property, which is either `"response"` or `"error"`; in the `response` case, the object has `body` with type depending on message; in the `error` case, the object has `message` with error message. 104 | 105 | So, the code, communicating with the extension, might look like this: 106 | 107 | ```` 108 | chrome.runtime.sendMessage('jcjjhjgimijdkoamemaghajlhegmoclj', {type: "info"}, 109 | function(response) { 110 | if (response.type === "error") { 111 | handleError(response.type); 112 | } else { 113 | handleInfo(response.body); 114 | } 115 | } 116 | ); 117 | ```` 118 | 119 | The possible messages are: 120 | 121 | | type | body | response type | description | 122 | |------|------|---------------|-------------| 123 | | `info` | | {`version`: string,
`configured`: boolean} | Returns current version of bridge and info about configuration.
See `configure` for more info. | 124 | | `configure` | config, as hex string | `"Success"` | Before any advanced call, configuration file needs to be loaded to extension.
Configuration file is signed by SatoshiLabs and the validity of the signature is limited.
Current config should be [in this repo](https://github.com/trezor/webwallet-data/blob/master/config_signed.bin), or [on AWS here](http://mytrezor.s3.amazonaws.com/config_signed.bin). | 125 | | `enumerate` | | Array<{`path`: number,
`session`: number | null}> | Lists devices.
`path` uniquely defines device between more connected devices. It usually increases on reconnect until restart of browser, but there is no guarantee.
If `session` is null, nobody else is using the device; if it's number, it identifies who is using it. | 126 | | `listen` | previous, as JSON | like `enumerate` | Listen to changes and returns either on change or after 30 second timeout. Compares change from `previous` that is sent as a parameter. "Change" is both connecting/disconnecting and session change.

`previous` must be exactly the output from the previous `enumerate`, even if the devices have additional properties that are not described above | 127 | | `acquire` | {`path`: path of device,
`previous`: previous session (or `null`) | {`session`: number} | Acquires the device at `path`. By "acquiring" the device, you are claiming the device for yourself.
Before acquiring, checks that the current session is `previous`.
If two applications call `acquire` on a newly connected device at the same time, only one of them succeed. | 128 | | `release` | {`session`: session to release} | `"Success"` | Releases the device with the given session.
By "releasing" the device, you claim that you don't want to use the device anymore. | 129 | | `call` | {`id`: session to call,
`type`: string,
`message`: object} | {`type`: string, `body`: object} | Calls the message and returns the response from TREZOR.
Messages are defined in [this protobuf file](https://github.com/trezor/trezor-common/blob/master/protob/messages.proto).
`type` in request is, for example, `GetFeatures`; `type` in response is, for example, `Features` | 130 | 131 | ### Whitelisting 132 | 133 | You cannot connect to the extension from anywhere on the internet. Your URL needs to be specifically whitelisted; whitelist is baked-in in the extension manifest. 134 | 135 | `localhost` is specifically whitelisted, so you can experiment on `http://localhost`. If you want to add your url in order to make a TREZOR web app, [make a pull request to this file](https://github.com/trezor/trezor-common/blob/master/signer/config.json). 136 | 137 | ## License 138 | 139 | GPLv3 140 | 141 | * (C) 2015 SatoshiLabs 142 | * (C) 2014 Mike Tsao 143 | * (C) 2014 Liz Fong-Jones 144 | * (C) 2015 William Wolf 145 | 146 | some code from [sowbug/trhid](https://github.com/sowbug/trhid) and [throughnothing/chrome-trezor](https://github.com/throughnothing/chrome-trezor) 147 | -------------------------------------------------------------------------------- /build_manifest.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | with open('trezor-common/signer/config.json') as data_file: 5 | data = json.load(data_file) 6 | 7 | whitelist = data["whitelist_urls"] 8 | 9 | def fix(url): 10 | fixed = url.replace("https?", "*").replace("[\w\.-]+","*").replace('\.', ".").replace("(:\d+)?","").replace("(/.*)?","/*") 11 | return fixed 12 | 13 | with open('manifest_no_matches.json') as data_file: 14 | manifest = json.load(data_file) 15 | 16 | trezor_guard_ids = ["imloifkgjagghnncjkhggdhalmcnfklk", "niebkpllfhmpfbffbfifagfgoamhpflf"] 17 | 18 | matches = list(filter(lambda x: x != "null", map(fix, whitelist))) 19 | 20 | manifest["externally_connectable"] = {"matches": matches, "ids": trezor_guard_ids} 21 | 22 | if (os.environ.get("STORE_BETA") == "1"): 23 | manifest["name"] = "BETA - TREZOR Chrome Extension" 24 | manifest.pop("key", None) 25 | 26 | with open('extension/manifest.json', 'w') as outfile: 27 | json.dump(manifest, outfile, indent=4) 28 | -------------------------------------------------------------------------------- /extension.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trezor-graveyard/trezor-chrome-extension/dd7c8859af6ccd7beeed32ada3c5f169cfa12194/extension.zip -------------------------------------------------------------------------------- /extension/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trezor-graveyard/trezor-chrome-extension/dd7c8859af6ccd7beeed32ada3c5f169cfa12194/extension/icon128.png -------------------------------------------------------------------------------- /extension/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trezor-graveyard/trezor-chrome-extension/dd7c8859af6ccd7beeed32ada3c5f169cfa12194/extension/icon16.png -------------------------------------------------------------------------------- /extension/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trezor-graveyard/trezor-chrome-extension/dd7c8859af6ccd7beeed32ada3c5f169cfa12194/extension/icon48.png -------------------------------------------------------------------------------- /flowtype_definitions/chrome.js: -------------------------------------------------------------------------------- 1 | declare class Chrome { 2 | hid: ChromeHid; 3 | runtime: ChromeRuntime; 4 | storage: ChromeStorage; 5 | app: ChromeApp; 6 | browser: ChromeBrowser; 7 | sockets: ChromeSockets; 8 | } 9 | 10 | declare class ChromeSockets { 11 | udp: ChromeUdp; 12 | } 13 | 14 | declare type ChromeUdpProperties = { 15 | persistent?: boolean; 16 | name?: string; 17 | bufferSize?: number; 18 | } 19 | 20 | declare type ChromeUdpCreateInfo = { 21 | socketId: number; 22 | } 23 | 24 | declare type ChromeUdpSendInfo = { 25 | resultCode: number; 26 | bytesSent?: number; 27 | } 28 | 29 | declare class ChromeOnReceive { 30 | addListener: (callback: (info: ChromeUdpReceiveInfo) => void) => void; 31 | } 32 | 33 | declare type ChromeUdpReceiveInfo = { 34 | socketId: number; 35 | data: ArrayBuffer; 36 | remoteAddress: string; 37 | remotePort: number; 38 | } 39 | 40 | declare class ChromeUdp { 41 | create: (properties: ChromeUdpProperties, callback: (info: ChromeUdpCreateInfo) => void) => void; 42 | close: (socketId: number, callback: () => void) => void; 43 | bind: (socketId: number, address: string, port: number, callback: (result: number) => void) => void; 44 | send: (socketId: number, data: ArrayBuffer, address: string, port: number, 45 | callback: (info: ChromeUdpSendInfo) => void 46 | ) => void; 47 | onReceive: ChromeOnReceive; 48 | } 49 | 50 | declare class ChromeHidDeviceInfo { 51 | deviceId: number; 52 | vendorId: number; 53 | productId: number; 54 | collections: Array<{usagePage: number, usage: number, reportIds:Array}>; 55 | maxInputReportSize: number; 56 | maxOutputReportSize: number; 57 | maxFeatureReportSize: number; 58 | reportDescriptor: ArrayBuffer; 59 | } 60 | 61 | declare type ChromeHidGetDevicesOptions = { 62 | vendorId? : number; 63 | productId? : number; 64 | } 65 | 66 | declare class ChromeHid { 67 | getDevices: (options: ChromeHidGetDevicesOptions, 68 | callback: (i: Array) => void) => void; 69 | 70 | send: (connectionId: number, reportId: number, data: ArrayBuffer, callback: () => void) => void; 71 | 72 | receive: (connectionId: number, callback: (reportId: number, data: ArrayBuffer) => void) => void; 73 | 74 | connect: (deviceId: number, callback: (connection: {connectionId: number}) => void) => void; 75 | 76 | disconnect: (connectionId: number, callback: () => void) => void; 77 | } 78 | 79 | declare class ChromePlatformInfo { 80 | os: string; 81 | arch: string; 82 | nacl_arch: string; 83 | } 84 | 85 | declare class ChromeRuntime { 86 | lastError: string; 87 | getPlatformInfo(callback: (platformInfo: ChromePlatformInfo) => void): void, 88 | onMessageExternal: ChromeOnMessage; 89 | onMessage: ChromeOnMessage; 90 | getManifest(): Object; 91 | id: string; 92 | } 93 | 94 | declare type ChromeStorageItems = { [key:string]: any} 95 | declare type ChromeCallback = () => void; 96 | 97 | declare class ChromeStorageArea { 98 | getBytesInUse(callback: (bytesInUse: number) => void): void; 99 | getBytesInUse(keys: string, callback: (bytesInUse: number) => void): void; 100 | getBytesInUse(keys: string[], callback: (bytesInUse: number) => void): void; 101 | clear(callback?: ChromeCallback): void; 102 | set(items: ChromeStorageItems, callback?: ChromeCallback): void; 103 | remove(keys: string, callback?: ChromeCallback): void; 104 | remove(keys: string[], callback?: ChromeCallback): void; 105 | get(callback: (items: ChromeStorageItems) => void): void; 106 | get(keys: string, callback: (items: ChromeStorageItems) => void): void; 107 | get(keys: string[], callback: (items: ChromeStorageItems) => void): void; 108 | 109 | QUOTA_BYTES: number 110 | } 111 | 112 | declare class ChromeStorage { 113 | local: ChromeStorageArea; 114 | } 115 | 116 | declare class ChromeMessageSender { 117 | tabs: ?any; 118 | frameId: ?number; 119 | id: ?string; 120 | url: ?string; 121 | tlsChanelId: ?string; 122 | } 123 | 124 | declare class ChromeOnMessage { 125 | addListener: ( 126 | callback: ( 127 | message: Object, sender:ChromeMessageSender, sendResponse: ( 128 | response: Object 129 | ) => void 130 | ) => boolean 131 | ) => void; 132 | } 133 | 134 | declare class ChromeApp { 135 | runtime: ChromeAppRuntime; 136 | window: ChromeAppWindow; 137 | } 138 | 139 | declare class ChromeAppRuntime { 140 | onLaunched: ChromeAppOnLaunched; 141 | } 142 | 143 | declare class ChromeAppOnLaunched { 144 | addListener: ( 145 | callback: () => void //callback can have more parameters but I am not using them 146 | ) => void; 147 | } 148 | 149 | declare class ChromeBrowser { 150 | // according to specification, callback is optional, but in reality it's mandatory (as of now) 151 | openTab: (options: {url: string}, callback: () => void) => void 152 | } 153 | 154 | declare type ChromeBoundsSpecification = { 155 | left? : number, 156 | top? : number, 157 | width? : number, 158 | height? : number, 159 | minWidth?: number, 160 | minHeight?: number, 161 | maxWidth?: number, 162 | maxHeight?: number 163 | } 164 | 165 | declare class ChromeAppWindowOnClosed { 166 | addListener: ( callback: () => void ) => void 167 | } 168 | 169 | declare class ChromeAppWindow { 170 | create: (url: string, options: {innerBounds: ChromeBoundsSpecification}, callback: (createdWindow: ChromeAppWindow) => void) => void; 171 | onClosed: ChromeAppWindowOnClosed; 172 | } 173 | 174 | declare var chrome: Chrome; 175 | 176 | -------------------------------------------------------------------------------- /management/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /management/Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2013-11-20 using generator-angular 0.6.0-rc.1 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | 10 | module.exports = function (grunt) { 11 | require('load-grunt-tasks')(grunt); 12 | require('time-grunt')(grunt); 13 | 14 | grunt.initConfig({ 15 | yeoman: { 16 | // configurable paths 17 | app: require('./bower.json').appPath || 'app', 18 | dist: 'dist' 19 | }, 20 | watch: { 21 | styles: { 22 | files: ['<%= yeoman.app %>/styles/{,*/}*.css'], 23 | tasks: ['copy:styles', 'autoprefixer'] 24 | }, 25 | livereload: { 26 | options: { 27 | livereload: '<%= connect.options.livereload %>' 28 | }, 29 | files: [ 30 | '<%= yeoman.app %>/{,*/}*.html', 31 | '.tmp/styles/{,*/}*.css', 32 | '{.tmp,<%= yeoman.app %>}/scripts/{,*/}*.js', 33 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 34 | ] 35 | } 36 | }, 37 | autoprefixer: { 38 | options: ['last 1 version'], 39 | dist: { 40 | files: [{ 41 | expand: true, 42 | cwd: '.tmp/styles/', 43 | src: '{,*/}*.css', 44 | dest: '.tmp/styles/' 45 | }] 46 | } 47 | }, 48 | connect: { 49 | options: { 50 | port: 8000, 51 | hostname: '0.0.0.0', 52 | livereload: 35729 53 | }, 54 | livereload: { 55 | options: { 56 | open: false, 57 | base: [ 58 | '.tmp', 59 | '<%= yeoman.app %>' 60 | ] 61 | } 62 | }, 63 | test: { 64 | options: { 65 | port: 9001, 66 | base: [ 67 | '.tmp', 68 | 'test', 69 | '<%= yeoman.app %>' 70 | ] 71 | } 72 | }, 73 | dist: { 74 | options: { 75 | base: '<%= yeoman.dist %>' 76 | } 77 | } 78 | }, 79 | clean: { 80 | dist: { 81 | files: [{ 82 | dot: true, 83 | src: [ 84 | '.tmp', 85 | '<%= yeoman.dist %>/*', 86 | '!<%= yeoman.dist %>/.git*' 87 | ] 88 | }] 89 | }, 90 | server: '.tmp' 91 | }, 92 | jshint: { 93 | options: { 94 | jshintrc: '.jshintrc', 95 | reporter: require('jshint-stylish') 96 | }, 97 | all: [ 98 | 'Gruntfile.js', 99 | '<%= yeoman.app %>/scripts/{,*/}*.js' 100 | ] 101 | }, 102 | rev: { 103 | dist: { 104 | files: { 105 | src: [ 106 | '<%= yeoman.dist %>/scripts/{,*/}*.js', 107 | '<%= yeoman.dist %>/fonts/{,*/}*.js', 108 | '<%= yeoman.dist %>/styles/{,*/}*.css', 109 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 110 | ] 111 | } 112 | } 113 | }, 114 | useminPrepare: { 115 | html: '<%= yeoman.app %>/index.html', 116 | css: ['<%= yeoman.app %>/styles/{,*/}*.css'], 117 | options: { 118 | dest: '<%= yeoman.dist %>' 119 | } 120 | }, 121 | usemin: { 122 | html: ['<%= yeoman.dist %>/index.html'], 123 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 124 | options: { 125 | assetsDirs: ['<%= yeoman.dist %>', '<%= yeoman.dist %>/fonts'] 126 | } 127 | }, 128 | filerev: { 129 | options: { 130 | algorithm: 'md5', 131 | length: 8 132 | }, 133 | dist: { 134 | files: [{ 135 | expand: true, 136 | cwd: '<%= yeoman.app %>/fonts', 137 | src: '{,*/}*.{ttf,eot,woff}', 138 | dest: '<%= yeoman.dist %>/fonts' 139 | }] 140 | } 141 | }, 142 | svgmin: { 143 | dist: { 144 | files: [{ 145 | expand: true, 146 | cwd: '<%= yeoman.app %>/images', 147 | src: '{,*/}*.svg', 148 | dest: '<%= yeoman.dist %>/images' 149 | }] 150 | } 151 | }, 152 | cssmin: { 153 | // By default, your `index.html` will take care of 154 | // minification. This option is pre-configured if you do not wish to use 155 | // Usemin blocks. 156 | // dist: { 157 | // files: { 158 | // '<%= yeoman.dist %>/styles/main.css': [ 159 | // '.tmp/styles/{,*/}*.css', 160 | // '<%= yeoman.app %>/styles/{,*/}*.css' 161 | // ] 162 | // } 163 | // } 164 | }, 165 | htmlmin: { 166 | dist: { 167 | options: {}, 168 | files: [{ 169 | expand: true, 170 | cwd: '<%= yeoman.app %>', 171 | src: ['*.html'], 172 | dest: '<%= yeoman.dist %>' 173 | }] 174 | }, 175 | minify: { 176 | options: { 177 | collapseBooleanAttributes: true, 178 | collapseWhitespace: true, 179 | removeAttributeQuotes: true, 180 | removeComments: true, 181 | removeRedundantAttributes: true, 182 | removeScriptTypeAttributes: true, 183 | removeStyleLinkTypeAttributes: true 184 | }, 185 | files: [{ 186 | expand: true, 187 | cwd: '<%= yeoman.dist %>', 188 | src: ['*.html'], 189 | dest: '<%= yeoman.dist %>' 190 | }] 191 | } 192 | }, 193 | ngtemplates: { 194 | webwalletApp: { 195 | cwd: '<%= yeoman.app %>', 196 | src: ['views/**/*.html'], 197 | dest: '.tmp/scripts/templates.js', 198 | options: { 199 | htmlmin: { 200 | collapseBooleanAttributes: true, 201 | collapseWhitespace: true, 202 | removeAttributeQuotes: true, 203 | removeComments: true, 204 | removeRedundantAttributes: true, 205 | removeScriptTypeAttributes: true, 206 | removeStyleLinkTypeAttributes: true 207 | } 208 | } 209 | } 210 | }, 211 | copy: { 212 | dist: { 213 | files: [{ 214 | expand: true, 215 | dot: true, 216 | cwd: '<%= yeoman.app %>', 217 | dest: '<%= yeoman.dist %>', 218 | src: [ 219 | '*.{ico,png,txt}', 220 | '.htaccess', 221 | 'images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 222 | ] 223 | }, { 224 | expand: true, 225 | cwd: '.tmp/images', 226 | dest: '<%= yeoman.dist %>/images', 227 | src: [ 228 | 'generated/*' 229 | ] 230 | }, { 231 | expand: true, 232 | cwd: '<%= yeoman.app %>/bower_components/bootstrap/dist/fonts', 233 | dest: '<%= yeoman.dist %>/fonts', 234 | src: ['*'] 235 | }] 236 | }, 237 | styles: { 238 | expand: true, 239 | cwd: '<%= yeoman.app %>/styles', 240 | dest: '.tmp/styles/', 241 | src: '{,*/}*.css' 242 | } 243 | }, 244 | concurrent: { 245 | server: [ 246 | 'copy:styles' 247 | ], 248 | test: [ 249 | 'copy:styles' 250 | ], 251 | dist: [ 252 | 'copy:styles', 253 | 'filerev', 254 | 'svgmin', 255 | 'htmlmin' 256 | ] 257 | }, 258 | karma: { 259 | unit: { 260 | configFile: 'karma.conf.js', 261 | singleRun: true 262 | } 263 | }, 264 | ngAnnotate: { 265 | dist: { 266 | files: [{ 267 | expand: true, 268 | cwd: '.tmp/concat/scripts', 269 | src: '*.js', 270 | dest: '.tmp/concat/scripts' 271 | }] 272 | } 273 | }, 274 | uglify: { 275 | dist: { 276 | files: { 277 | '<%= yeoman.dist %>/scripts/scripts.js': [ 278 | '<%= yeoman.dist %>/scripts/scripts.js' 279 | ] 280 | } 281 | } 282 | } 283 | }); 284 | 285 | grunt.registerTask('server', function (target) { 286 | if (target === 'dist') { 287 | return grunt.task.run(['build', 'connect:dist:keepalive']); 288 | } 289 | 290 | grunt.task.run([ 291 | 'clean:server', 292 | 'concurrent:server', 293 | 'autoprefixer', 294 | 'connect:livereload', 295 | 'watch' 296 | ]); 297 | }); 298 | 299 | grunt.registerTask('test', [ 300 | 'clean:server', 301 | 'concurrent:test', 302 | 'autoprefixer', 303 | 'connect:test', 304 | 'karma' 305 | ]); 306 | 307 | grunt.registerTask('build', [ 308 | 'clean:dist', 309 | 'ngtemplates', 310 | 'useminPrepare', 311 | 'concurrent:dist', 312 | 'autoprefixer', 313 | 'concat', 314 | 'ngAnnotate', 315 | 'copy:dist', 316 | 'cssmin', 317 | 'htmlmin:dist', 318 | 'uglify', 319 | 'rev', 320 | 'usemin', 321 | 'htmlmin:minify' 322 | ]); 323 | 324 | grunt.registerTask('default', [ 325 | 'jshint', 326 | 'test', 327 | 'build' 328 | ]); 329 | }; 330 | -------------------------------------------------------------------------------- /management/Makefile: -------------------------------------------------------------------------------- 1 | all: clear-dist node_modules app/bower_components dist copy-dist copy-data 2 | 3 | beta-all: clear-dist node_modules app/bower_components beta-dist copy-dist copy-data 4 | 5 | clear: clear-dist 6 | rm -rf node_modules app/bower_components 7 | 8 | clear-dist: 9 | rm -rf dist .tmp 10 | rm -f app/scripts/config.js 11 | 12 | node_modules: 13 | npm install 14 | 15 | 16 | app/bower_components: 17 | `npm bin`/bower install 18 | 19 | dist: copy-config build 20 | 21 | beta-dist: beta-config build 22 | 23 | copy-config: 24 | cp app/scripts/config.original.js app/scripts/config.js 25 | 26 | beta-config: 27 | cat app/scripts/config.original.js | sed 's/jcjjhjgimijdkoamemaghajlhegmoclj/pciijihalmablemehncipbfpodhnbmkj/' > app/scripts/config.js 28 | 29 | 30 | build: 31 | rm -rf app/bower_components/trezor.js 32 | `npm bin`/bower install 33 | `npm bin`/grunt build 34 | rm -rf .tmp 35 | 36 | copy-dist: 37 | rm -rf ../extension/management 38 | cp -r dist ../extension/management 39 | 40 | copy-data: 41 | rm -rf ../extension/data 42 | python3 firmware_copy.py 43 | -------------------------------------------------------------------------------- /management/app/fonts/trezorfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trezor-graveyard/trezor-chrome-extension/dd7c8859af6ccd7beeed32ada3c5f169cfa12194/management/app/fonts/trezorfont.eot -------------------------------------------------------------------------------- /management/app/fonts/trezorfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trezor-graveyard/trezor-chrome-extension/dd7c8859af6ccd7beeed32ada3c5f169cfa12194/management/app/fonts/trezorfont.ttf -------------------------------------------------------------------------------- /management/app/fonts/trezorfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trezor-graveyard/trezor-chrome-extension/dd7c8859af6ccd7beeed32ada3c5f169cfa12194/management/app/fonts/trezorfont.woff -------------------------------------------------------------------------------- /management/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TREZOR Management 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 26 | 27 | 28 |
30 | 31 | 44 | 45 | 46 |
48 |
50 | 51 |
52 |
53 | 54 | 55 | 56 | 57 | 58 |
61 | 62 |
63 | 64 |
67 |
68 | 69 | 70 |
71 | 72 |
74 |
76 |

{{m.level}}!

77 | {{m.text}} 78 | 79 | 80 |
81 |
83 |

{{m.level}}

84 | {{m.text}} 85 | 86 | 87 |
88 |
89 |
90 | 91 |
92 |
93 |
94 |
95 |
96 |
97 | 98 |
99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /management/app/scripts/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main 3 | * 4 | * - Load Transport. 5 | * - Bootstrap Angular app. 6 | * - Register Bitcoin URI handler. 7 | */ 8 | (function (angular) { 9 | 'use strict'; 10 | 11 | //logging user agent string as soon as possible 12 | console.log("[app] User agent : ", window.navigator.userAgent); 13 | 14 | angular.module('webwalletApp', [ 15 | 'ngRoute', 16 | 'ngSanitize', 17 | 'ui.bootstrap', 18 | 'ja.qr' 19 | ]); 20 | 21 | // Load the Transport and bootstrap the Angular app. 22 | angular.element(document).ready(function () { 23 | init( 24 | loadConfig() 25 | ); 26 | registerUriHandler(); 27 | }); 28 | 29 | 30 | function isIE() { 31 | var myNav = navigator.userAgent.toLowerCase(); 32 | return (myNav.indexOf('msie') != -1) ? parseInt(myNav.split('msie')[1]) : false; 33 | } 34 | 35 | /** 36 | * Try to load the Transport and then bootstrap the Angular app. 37 | * 38 | * @param {Object} config Configuration 39 | * 40 | * @see acquireTransport() 41 | * @see createApp() 42 | */ 43 | function init(config) { 44 | if (isIE()) { 45 | if (isIE() <= 9) { 46 | createApp("MSIE 9 and lower is not supported. Please install a newer browser!"); 47 | return; 48 | } 49 | } 50 | 51 | var configUrl = config.configUrl; 52 | var configPromise = window.trezor.http(configUrl); 53 | var extensionId = config.extensionId; 54 | 55 | acquireTransport(extensionId) 56 | .then(function (transport) { 57 | return configPromise 58 | .then(function (config) { 59 | return transport.configure(config); 60 | }) 61 | .then(function () { 62 | createApp(null, transport); 63 | }); 64 | }) 65 | .catch(function (err) { 66 | createApp(err); 67 | }); 68 | } 69 | 70 | /** 71 | * Inject and return the config service. 72 | * 73 | * @return {Object} Config service 74 | */ 75 | function loadConfig() { 76 | var injector = angular.injector(['webwalletApp']); 77 | return injector.get('config'); 78 | } 79 | 80 | /** 81 | * Acquire Transport 82 | * 83 | * @param {String} extensionId ID of extension 84 | * @return {Promise} If the Transport was loaded, the Promise is 85 | * resolved with an instance of `HttpTransport` 86 | * or `PluginTransport`. If loading failed 87 | * the Promise is failed. 88 | */ 89 | function acquireTransport(extensionId) { 90 | var trezor = window.trezor; 91 | 92 | function loadExtension() { 93 | return trezor.ChromeExtensionTransport.create(extensionId); 94 | } 95 | 96 | 97 | return loadExtension().catch(function (e) { 98 | console.log(e); 99 | throw (e); 100 | }); 101 | } 102 | 103 | /** 104 | * Bootstrap (create and initialize) the Angular app. 105 | * 106 | * Pass to the app the reference to the Transport object. 107 | * 108 | * @param {Error|null} err Error from the Transport loading process 109 | * @param {Object} transport Transport object 110 | */ 111 | function createApp(err, transport) { 112 | // Create module. 113 | var app = angular.module('webwalletApp'); 114 | var container = document.getElementById('webwalletApp-container'); 115 | 116 | // Attach routes. 117 | if (!err) { 118 | app.config(attachRoutes).config( 119 | function ($compileProvider) { 120 | $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|chrome-extension):/); 121 | } 122 | ); 123 | } else { 124 | console.error(err); 125 | } 126 | 127 | // Pass Transport reference. 128 | app 129 | .value('trezorError', err) 130 | .value('trezorApi', window.trezor) 131 | .value('trezor', transport); 132 | 133 | // Initialize Angular.js. 134 | try { 135 | angular.bootstrap(container, ['webwalletApp']); 136 | } catch (err2) { 137 | console.error('[app] Error occured while bootstrapping ' + 138 | 'the Angular.js app.'); 139 | console.error(err2); 140 | container.innerHTML = [ 141 | '
', 142 | '
', 143 | '
', 144 | '
', 145 | '

Plugin loading failed :(

', 146 | ' ', 150 | '
', 151 | '
', 152 | '
', 153 | '
' 154 | ].join(''); 155 | container.removeAttribute('ng-cloak'); 156 | } 157 | } 158 | 159 | /** 160 | * Attach routes to passed $routeProvider. 161 | * 162 | * @param {Object} $routeProvider Angular $routeProvider as returned 163 | * by `app.config()`. 164 | */ 165 | function attachRoutes($routeProvider) { 166 | $routeProvider 167 | .when('/', { 168 | templateUrl: 'views/main.html' 169 | }) 170 | .when('/import', { 171 | templateUrl: 'views/import.html' 172 | }) 173 | .when('/device/:deviceId', { 174 | templateUrl: 'views/device/index.html' 175 | }) 176 | .when('/device/:deviceId/advanced', { 177 | templateUrl: 'views/device/advanced.html' 178 | }) 179 | .when('/device/:deviceId/settings', { 180 | templateUrl: 'views/device/settings.html' 181 | }) 182 | .when('/device/:deviceId/recovery', { 183 | templateUrl: 'views/device/recovery.html' 184 | }) 185 | .when('/device/:deviceId/wipe', { 186 | templateUrl: 'views/device/wipe.html' 187 | }) 188 | .when('/send/:uri*', { 189 | resolve: { 190 | uriRedirect: 'uriRedirect' 191 | } 192 | }) 193 | .otherwise({ 194 | redirectTo: '/' 195 | }); 196 | } 197 | 198 | /** 199 | * Register Bitcoin URI handler 200 | * 201 | * Requests to this URI are then handled by the `uriRedirect` service. 202 | * 203 | * @see services/uriRedirect.js 204 | */ 205 | function registerUriHandler() { 206 | var URI_PROTOCOL = 'bitcoin', 207 | URI_TEMPLATE = '/#/send/%s', 208 | URI_NAME = 'MyTrezor: Send Bitcoins to address', 209 | url; 210 | 211 | url = location.protocol + '//' + location.host + URI_TEMPLATE; 212 | if (navigator.registerProtocolHandler && 213 | (!navigator.isProtocolHandlerRegistered || 214 | !navigator.isProtocolHandlerRegistered(URI_PROTOCOL, url))) { 215 | navigator.registerProtocolHandler( 216 | URI_PROTOCOL, 217 | url, 218 | URI_NAME 219 | ); 220 | } 221 | } 222 | 223 | }(this.angular)); 224 | -------------------------------------------------------------------------------- /management/app/scripts/config.original.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('webwalletApp').constant('config', { 4 | // show debug information in the interface 5 | debug: false, 6 | 7 | // minimal versions of firmare supporting given feature 8 | // use version string or false for disabling everywhere 9 | features: { 10 | getFeatures: '1.3.3', 11 | cacheInFeatures: '1.3.3' 12 | }, 13 | 14 | // address of the bridge configuration file 15 | configUrl: '/data/config_signed.bin', 16 | 17 | 18 | // ID of the extension, given by chrome web store 19 | // Make sure to replicate this in the index.html element! 20 | extensionId: "jcjjhjgimijdkoamemaghajlhegmoclj", 21 | 22 | useBip44: true 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /management/app/scripts/controllers/debug.js: -------------------------------------------------------------------------------- 1 | /*global angular, $*/ 2 | 3 | /** 4 | * Debug Controller 5 | * 6 | * Show the error log when clicked on the link in the footer or on the button 7 | * in an error message. 8 | * 9 | * @see index.html 10 | * @see debug.button.html 11 | * @see debug.link.html 12 | * @see debug.log.html 13 | */ 14 | angular.module('webwalletApp').controller('DebugCtrl', function ( 15 | trezor, 16 | trezorApi, 17 | $rootScope, 18 | $scope, 19 | deviceList 20 | ) { 21 | 22 | 'use strict'; 23 | 24 | $rootScope.debug = {}; 25 | 26 | $rootScope.debug.toggle = function () { 27 | if ($rootScope.debug.visible) { 28 | $rootScope.debug.visible = false; 29 | return; 30 | } 31 | $rootScope.debug.logs = debugLogsString(); 32 | $rootScope.debug.visible = true; 33 | }; 34 | 35 | $rootScope.debug.focus = function (e) { 36 | $(e.target).select(); 37 | }; 38 | 39 | function debugLogsString() { 40 | return (window.console.logs || []) 41 | .map(JSON.stringify) 42 | .join('\n'); 43 | } 44 | 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /management/app/scripts/controllers/device/device.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | /** 4 | * Device Controller 5 | */ 6 | angular.module('webwalletApp').controller('DeviceCtrl', function ( 7 | $scope, 8 | $location, 9 | $routeParams, 10 | $document, 11 | flash, 12 | TrezorDevice, 13 | deviceList, 14 | modalOpener, 15 | setupModalService, 16 | pinModalService, 17 | passphraseModalService, 18 | deviceService) { 19 | 20 | 'use strict'; 21 | 22 | 23 | // Get current device or go to homepage. 24 | $scope.device = deviceList.get($routeParams.deviceId); 25 | if (!$scope.device) { 26 | $location.path('/'); 27 | return; 28 | } 29 | 30 | $scope.isActive = function (path) { 31 | return $location.path().match(path); 32 | }; 33 | 34 | 35 | // Handle device events -- buttons and disconnect. 36 | $scope.$on(TrezorDevice.EVENT_PREFIX + TrezorDevice.EVENT_PIN, 37 | promptPin); 38 | $scope.$on(TrezorDevice.EVENT_PREFIX + TrezorDevice.EVENT_BUTTON, 39 | handleButton); 40 | $scope.$on(TrezorDevice.EVENT_PREFIX + TrezorDevice.EVENT_PASSPHRASE, 41 | promptPassphrase); 42 | $scope.$on(deviceService.EVENT_ASK_FORGET, forgetOnDisconnect); 43 | 44 | 45 | 46 | /** 47 | * Forgetting the device. 48 | * 49 | * @param {Object} e Event object 50 | * @param {TrezorDevice} device Device that was disconnected 51 | */ 52 | function forgetOnDisconnect(e, device) { 53 | deviceList.forget(device); 54 | setupModalService.closeOpen(); 55 | pinModalService.closeOpen(); 56 | passphraseModalService.closeOpen(); 57 | } 58 | 59 | /** 60 | * Ask the user to set the device PIN. 61 | * 62 | * Bind keypress events that allow the user to control the number 63 | * buttons (dial) using a keyboard. 64 | * 65 | * @param {Event} event Event object 66 | * @param {TrezorDevice} dev Device 67 | * @param {String} type Action type. Possible values: 68 | * - 'PinMatrixRequestType_Current' 69 | * - 'PinMatrixRequestType_NewFirst' 70 | * - 'PinMatrixRequestType_NewSecond' 71 | * @param {Function} callback Called as `callback(err, res)` 72 | */ 73 | function promptPin(e, dev, type, callback) { 74 | pinModalService.showModal($scope, e, dev, type, callback); 75 | } 76 | 77 | function promptPassphrase(e, dev, callback) { 78 | passphraseModalService.showModal($scope, e, dev, callback); 79 | } 80 | 81 | function handleButton(event, dev, code) { 82 | var ignore = [ 83 | 'ButtonRequest_ConfirmWord', 84 | 'ButtonRequest_FirmwareCheck' 85 | ]; 86 | 87 | if ((dev.id === $scope.device.id) && 88 | (ignore.indexOf(code) < 0)) { 89 | 90 | promptButton(code); 91 | } 92 | } 93 | 94 | function promptButton(code) { 95 | var modal = modalOpener.openModal($scope, 'button', buttonModalSize(code), { 96 | code: code 97 | }, undefined, code); 98 | $scope.device.once(TrezorDevice.EVENT_RECEIVE, function () { 99 | modal.modal.close(); 100 | }); 101 | $scope.device.once(TrezorDevice.EVENT_ERROR, function () { 102 | modal.modal.close(); 103 | }); 104 | } 105 | 106 | function buttonModalSize(code) { 107 | if (code === 'ButtonRequest_Address') { 108 | return 'md'; 109 | } else { 110 | return 'lg'; 111 | } 112 | } 113 | }); 114 | -------------------------------------------------------------------------------- /management/app/scripts/controllers/device/firmware.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp').controller('FirmwareCtrl', function ( 4 | $modal, 5 | $scope, 6 | $rootScope, 7 | $routeParams, 8 | deviceList, 9 | firmwareService, 10 | modalOpener, 11 | TrezorDevice) { 12 | 13 | 'use strict'; 14 | 15 | var _modal = null, 16 | _modalScope = null, 17 | _state = null, 18 | STATE_INITIAL = 'initial', 19 | STATE_INITIAL_DISCONNECTED = 'initial-disconnected', 20 | STATE_NORMAL = 'device-normal', 21 | STATE_BOOTLOADER = 'device-bootloader', 22 | STATE_UPDATE_DOWNLOADING = 'update-downloading', 23 | STATE_UPDATE_FLASHING = 'update-flashing', 24 | STATE_UPDATE_SUCCESS = 'update-success', 25 | STATE_UPDATE_ERROR = 'update-error', 26 | STATE_UPDATE_CHECK = 'update-check'; 27 | 28 | $scope.$on(firmwareService.EVENT_CONNECT, resetOutdatedFirmwareBar); 29 | $scope.$on(firmwareService.EVENT_DISCONNECT, resetOutdatedFirmwareBar); 30 | $scope.$on(firmwareService.EVENT_DISCONNECT, resetOutdatedFirmwareBar); 31 | 32 | /** 33 | * onDisconnect() has high priority, because this hook must 34 | * execute even if the Firmware modal is opened. 35 | * 36 | * @see firmwareService.firmwareMain() 37 | */ 38 | $scope.$on(firmwareService.EVENT_DISCONNECT, onDisconnect, 10); 39 | 40 | // States 41 | $scope.$on(firmwareService.EVENT_BOOTLOADER, function (e, dev) { 42 | /* 43 | * We need to change the reference to the device in modal's scope, 44 | * because the device changes when switching to the bootloader 45 | * mode. We save a reference to the original device, to forget 46 | * it in case user cancels the update process. 47 | */ 48 | if (_modalScope) { 49 | _modalScope.previousDevice = _modalScope.device; 50 | _modalScope.device = dev; 51 | } 52 | setState(STATE_BOOTLOADER); 53 | }); 54 | $scope.$on(firmwareService.EVENT_NORMAL, function () { 55 | setState(STATE_NORMAL); 56 | }); 57 | 58 | // Modals 59 | $scope.$on(firmwareService.EVENT_CANDIDATE, 60 | function (e, params) { 61 | showCandidateFirmwareModal( 62 | params.dev, 63 | params.firmware 64 | ); 65 | }); 66 | $scope.$on(firmwareService.EVENT_OUTDATED, 67 | function (e, params) { 68 | showOutdatedFirmware( 69 | params.dev, 70 | params.firmware, 71 | params.version 72 | ); 73 | }); 74 | 75 | function showOutdatedFirmware(dev, firmware, version) { 76 | if (firmware.required) { 77 | return showOutdatedFirmwareModal(dev, firmware, version); 78 | } 79 | return showOutdatedFirmwareBar(dev, firmware, version); 80 | } 81 | 82 | function showOutdatedFirmwareBar(dev, firmware, version) { 83 | $rootScope.optionalFirmware = { 84 | device: dev, 85 | firmware: firmware, 86 | version: version, 87 | update: function () { 88 | showOutdatedFirmwareModal(dev, firmware, version); 89 | } 90 | }; 91 | } 92 | 93 | function resetOutdatedFirmwareBar(e, dev) { 94 | if ($rootScope.optionalFirmware && 95 | $rootScope.optionalFirmware.device.id === dev.id) { 96 | delete $rootScope.optionalFirmware; 97 | } 98 | } 99 | 100 | function showOutdatedFirmwareModal(dev, firmware, version) { 101 | _showFirmwareModal(dev, firmware, version, STATE_INITIAL); 102 | } 103 | 104 | function showCandidateFirmwareModal(dev, firmware) { 105 | _showFirmwareModal(dev, firmware, undefined, STATE_BOOTLOADER); 106 | } 107 | 108 | function _showFirmwareModal(dev, firmware, version, state) { 109 | _modalScope = angular.extend($rootScope.$new(), { 110 | firmware: firmware, 111 | version: version, 112 | device: dev, 113 | update: function () { 114 | updateFirmware(firmware); 115 | } 116 | }); 117 | setState(state); 118 | 119 | firmwareService.setModalOpen(true); 120 | _modal = $modal.open({ 121 | templateUrl: 'views/modal/firmware.html', 122 | size: 'lg', 123 | backdrop: 'static', 124 | keyboard: false, 125 | scope: _modalScope 126 | }); 127 | 128 | modalOpener.stopBackpaceOnModal(_modal); 129 | 130 | _modal.result.then(function () { 131 | firmwareService.setModalOpen(false); 132 | }, function () { 133 | firmwareService.setModalOpen(false); 134 | if (firmware.required) { 135 | deviceList.forget(dev, true); 136 | } 137 | }); 138 | 139 | return _modal.result; 140 | } 141 | 142 | function updateFirmware(firmware) { 143 | var deregister; 144 | 145 | _modalScope.firmware = firmware; 146 | setState(STATE_UPDATE_DOWNLOADING); 147 | 148 | firmwareService.download(firmware) 149 | .then(function (data) { 150 | deregister = $rootScope.$on( 151 | TrezorDevice.EVENT_PREFIX + TrezorDevice.EVENT_BUTTON, 152 | promptButton 153 | ); 154 | setState(STATE_UPDATE_FLASHING); 155 | return _modalScope.device.flash(data); 156 | }) 157 | .then( 158 | function () { 159 | setState(STATE_UPDATE_SUCCESS); 160 | deregister(); 161 | }, 162 | function (err) { 163 | setState(STATE_UPDATE_ERROR); 164 | _modalScope.error = err.message; 165 | deregister(); 166 | } 167 | ); 168 | 169 | function promptButton(e, dev, code) { 170 | if (code === TrezorDevice.REQ_BUTTON_FIRMWARE) { 171 | setState(STATE_UPDATE_CHECK); 172 | } 173 | } 174 | } 175 | 176 | /** 177 | * Close the firmware modal if the update is already finished or 178 | * if it failed. 179 | */ 180 | function onDisconnect(e, dev) { 181 | if (_state === STATE_UPDATE_SUCCESS || 182 | _state === STATE_UPDATE_ERROR || 183 | _state === STATE_BOOTLOADER) { 184 | deviceList.forget(dev, true); 185 | if (_modalScope.previousDevice) 186 | deviceList.forget(_modalScope.previousDevice, true); 187 | _modal.close(); 188 | return; 189 | } 190 | if (_state === STATE_INITIAL || 191 | _state === STATE_NORMAL) { 192 | setState(STATE_INITIAL_DISCONNECTED); 193 | return; 194 | } 195 | setState(STATE_INITIAL); 196 | } 197 | 198 | function setState(state) { 199 | _state = state; 200 | if (_modalScope) { 201 | _modalScope.state = state; 202 | } 203 | } 204 | }); 205 | -------------------------------------------------------------------------------- /management/app/scripts/controllers/device/info.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp').controller('DeviceInfoCtrl', function ( 4 | flash, 5 | $scope, 6 | $rootScope, 7 | $timeout, 8 | $document, 9 | selecter, 10 | modalOpener, 11 | deviceList, 12 | firmwareService, 13 | $location) { 14 | 15 | 'use strict'; 16 | 17 | 18 | $scope.learnMorePassDisplayed = false; 19 | $scope.learnMorePass = function () { 20 | $scope.learnMorePassDisplayed = true; 21 | } 22 | 23 | 24 | //hack 25 | $scope._changing = null; 26 | 27 | $rootScope.$on('modal.button.show', modalShown); 28 | 29 | function modalShown(event, code) { 30 | event.targetScope.changing = $scope._changing; 31 | } 32 | 33 | $scope.isLatestFirmware = false; 34 | firmwareService.check($scope.device.features).then(function(firmware) { 35 | if (!firmware) { 36 | $scope.isLatestFirmware = true; 37 | } else { 38 | $scope.isLatestFirmware = false; 39 | } 40 | }); 41 | 42 | /** 43 | * Change device PIN 44 | * 45 | * Ask the user to set the PIN and then save the value. 46 | * 47 | * `remove` false or undefined ==> change 48 | * `remove` true ==> remove PIN 49 | */ 50 | $scope.changePin = function (remove) { 51 | $scope._changing = "pin"; 52 | var text = remove ? "removed" : "changed"; 53 | $scope.device.changePin(remove).then( 54 | function () { 55 | flash.success('PIN was successfully ' + text); 56 | }, 57 | function (err) { 58 | flash.error(err.message || 'PIN change failed'); 59 | } 60 | ).finally(function () { 61 | $scope._changing = null; 62 | }); 63 | }; 64 | 65 | //input=> boolean - true enables pass, false removes pass 66 | $scope.togglePassphrase = function (enable) { 67 | $scope._changing = "passphrase"; 68 | 69 | var text = enable ? "enabled" : "disabled"; 70 | var forgettext = "Disconnect your Trezor to " + (enable ? "enable" : "disable") + " passphrase."; 71 | $scope.device.togglePassphrase(enable).then( 72 | function () { 73 | deviceList.forget($scope.device, true, forgettext); 74 | }, 75 | function (err) { 76 | flash.error(err.message || 'Passphrase toggle failed'); 77 | } 78 | ).finally(function () { 79 | $scope._changing = null; 80 | }); 81 | }; 82 | 83 | 84 | /** 85 | * Ask the user to set the device label and then store filled value. 86 | * 87 | * If he/she fills in an empty value, the default label is used (read from 88 | * `TrezorDevice#DEFAULT_LABEL`). 89 | */ 90 | $scope.changeLabel = function () { 91 | promptLabel() 92 | .then(function (label) { 93 | label = label.trim() || $scope.device.DEFAULT_LABEL; 94 | return $scope.device.changeLabel(label); 95 | }) 96 | .then( 97 | function () { 98 | flash.success('Label was successfully changed'); 99 | }, 100 | function (err) { 101 | /* 102 | * Show error message only if there actually was an 103 | * error. Closing the label modal triggers rejection 104 | * as well, but without an error. 105 | */ 106 | if (err) { 107 | flash.error(err.message || 108 | 'Failed to change the device label'); 109 | } 110 | } 111 | ); 112 | }; 113 | 114 | 115 | 116 | /** 117 | * Ask the user to set the device label. 118 | */ 119 | function promptLabel() { 120 | 121 | return modalOpener.openModal($scope, 'label', 'md', { 122 | label: $scope.device.features.label || '' 123 | }).result; 124 | } 125 | 126 | 127 | }); 128 | -------------------------------------------------------------------------------- /management/app/scripts/controllers/device/recovery.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp').controller('DeviceRecoveryCtrl', function ( 4 | bip39, 5 | flash, 6 | $scope, 7 | $location) { 8 | 9 | 'use strict'; 10 | 11 | $scope.recovering = false; 12 | $scope.seedFocus = false; 13 | $scope.seedWord = ''; 14 | $scope.seedWords = null; 15 | $scope.seedWordlist = bip39.english; 16 | $scope.settings = { 17 | pin_protection: true, 18 | word_count: 24 19 | }; 20 | 21 | $scope.$on('device.word', promptWord); 22 | 23 | $scope.startsWith = function (state, viewValue) { 24 | var prefix = state.substr(0, viewValue.length).toLowerCase(); 25 | return prefix === viewValue.toLowerCase(); 26 | }; 27 | 28 | $scope.recoverDevice = function () { 29 | if ($scope.settings.label) { 30 | $scope.settings.label = $scope.settings.label.trim(); 31 | } 32 | $scope.settings.word_count = +$scope.settings.word_count; 33 | 34 | // Reset previous attempt to restore device. 35 | $scope.seedWords = []; 36 | flash.clear(); 37 | 38 | $scope.recovering = true; 39 | $scope.device.recover($scope.settings).then( 40 | function () { 41 | $scope.recovering = false; 42 | $location.path('/device/' + $scope.device.id + '/'); 43 | }, 44 | function (err) { 45 | $scope.recovering = false; 46 | flash.error(err.message || 'Recovery failed'); 47 | } 48 | ); 49 | }; 50 | 51 | $scope.recoverWord = function () { 52 | $scope.seedWords.push($scope.seedWord); 53 | $scope.wordCallback($scope.seedWord); 54 | }; 55 | 56 | function promptWord(event, dev, callback) { 57 | if (dev.id !== $scope.device.id) { 58 | return; 59 | } 60 | 61 | $scope.seedFocus = true; 62 | $scope.seedWord = ''; 63 | $scope.wordCallback = function (word) { 64 | $scope.seedFocus = false; 65 | $scope.wordCallback = null; 66 | $scope.seedWord = ''; 67 | callback(null, word); 68 | }; 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /management/app/scripts/controllers/device/setup.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp').controller('DeviceSetupCtrl', function ( 4 | utils, 5 | flash, 6 | $scope, 7 | modalOpener, 8 | deviceList, 9 | deviceService, 10 | setupModalService, 11 | $modal) { 12 | 13 | 'use strict'; 14 | 15 | var modal; 16 | 17 | $scope.advanced = false; 18 | $scope.settings = { 19 | strength: 256, 20 | pin_protection: true 21 | }; 22 | 23 | // `recoveryWords` count depends on users choice and gets 24 | // initialized in `setupDevice` 25 | $scope.recoveryStarted = false; 26 | $scope.recoveryWords = null; 27 | $scope.recoveryWordsDone = 0; 28 | $scope.recoveryCurrentWord = 1; 29 | 30 | $scope.$on('device.button', function (event, dev, code) { 31 | if (dev.id === $scope.device.id && 32 | code === 'ButtonRequest_ConfirmWord') { 33 | $scope.setupRecoveryNext(); 34 | } 35 | }); 36 | 37 | /** 38 | * Returns a word count of a BIP39 seed mnemonic, for `bits` of entropy. 39 | * @see https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki 40 | * @summary Number -> Number 41 | */ 42 | function getWordCountFromSeedStrength(bits) { 43 | return (bits + (bits / 32)) / 11; 44 | } 45 | 46 | $scope.setupDevice = function () { 47 | var set = $scope.settings, 48 | dev = $scope.device; 49 | 50 | set.strength = +set.strength; 51 | if (set.label) { 52 | set.label = set.label.trim() || dev.DEFAULT_LABEL; 53 | } else { 54 | set.label = dev.DEFAULT_LABEL; 55 | } 56 | 57 | // Set the total word count so the modal window initialized in 58 | // `setupRecoveryNext` can pick it up 59 | $scope.recoveryWords = getWordCountFromSeedStrength(set.strength); 60 | 61 | dev.reset(set).then( 62 | function () { 63 | utils.redirect('/device/' + dev.id + '/').then(function () { 64 | flash.success('Congratulations! Your device is now ready to use.'); 65 | }); 66 | }, 67 | function (err) { 68 | flash.error(err.message || 'Setup failed'); 69 | } 70 | ); 71 | }; 72 | 73 | 74 | $scope.setupRecoveryNext = function () { 75 | 76 | // First write 77 | if (!$scope.recoveryStarted) { 78 | $scope.recoveryStarted = true; 79 | $scope.stage = 'writeFirst'; 80 | openModal(); 81 | return; 82 | } 83 | 84 | $scope.recoveryWordsDone = $scope.recoveryWordsDone + 1; 85 | $scope.recoveryCurrentWord = $scope.recoveryCurrentWord + 1; 86 | 87 | // Write 88 | if ($scope.recoveryWordsDone < $scope.recoveryWords) { 89 | $scope.stage = 'write'; 90 | 91 | // First check 92 | } else if ($scope.recoveryWordsDone === $scope.recoveryWords) { 93 | $scope.recoveryCurrentWord = 1; 94 | $scope.stage = 'checkFirst'; 95 | 96 | // Check 97 | } else if ($scope.recoveryWordsDone < 2 * $scope.recoveryWords - 1) { 98 | $scope.stage = 'check'; 99 | 100 | // Last check 101 | } else { 102 | $scope.device.once('receive', function () { 103 | closeModal(); 104 | }); 105 | } 106 | }; 107 | 108 | function closeModal() { 109 | setupModalService.closeOpen(); 110 | } 111 | 112 | function openModal() { 113 | setupModalService.showModal($scope); 114 | } 115 | }); 116 | -------------------------------------------------------------------------------- /management/app/scripts/controllers/device/wipe.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp').controller('DeviceWipeCtrl', function ( 4 | $scope, 5 | $rootScope, 6 | flash, 7 | deviceList, 8 | modalOpener 9 | ) { 10 | 11 | 'use strict'; 12 | 13 | var _wipeInProgress = false, 14 | _disconnectModal = null; 15 | 16 | deviceList.registerDisconnectHook(forgetOnDisconnectAfterWipe, 10); 17 | 18 | /** 19 | * Replace default modal asking the user if he/she wants to forget the 20 | * device with a custom one that features a message that the wipe was 21 | * successfully finished. 22 | * 23 | * @param {TrezorDevice} dev Device 24 | */ 25 | function forgetOnDisconnectAfterWipe(dev) { 26 | var hideSuccessMessage; 27 | if (_wipeInProgress) { 28 | _wipeInProgress = false; 29 | hideSuccessMessage = _disconnectModal !== null; 30 | if (_disconnectModal) { 31 | _disconnectModal.close(); 32 | _disconnectModal = null; 33 | } 34 | promptForget(hideSuccessMessage) 35 | .then(function () { 36 | deviceList.forget(dev); 37 | }, function () { 38 | deviceList.navigateTo(dev, true); 39 | }); 40 | deviceList.abortHook(); 41 | } 42 | } 43 | 44 | /** 45 | * Wipe the device 46 | * 47 | * Then ask the user if he/she wants to forget it -- this happens 48 | * automatically, because the disconnect event is fired while wiping the 49 | * device (the HID ID changes). 50 | */ 51 | $scope.wipeDevice = function () { 52 | _wipeInProgress = true; 53 | $scope.device.wipe().then( 54 | function () { 55 | promptDisconnect(); 56 | }, 57 | function (err) { 58 | _wipeInProgress = false; 59 | flash.error(err.message || 'Wiping failed'); 60 | } 61 | ); 62 | }; 63 | 64 | /** 65 | * Ask user to disconnect the device. 66 | */ 67 | function promptDisconnect() { 68 | var modal = modalOpener.openModal($scope, 'disconnect.wipe', 'sm'); 69 | _disconnectModal = modal.modal; 70 | return modal.result; 71 | } 72 | 73 | /** 74 | * Ask user if he/she wants to forget the device. 75 | * 76 | * @param {Boolean} hideSuccessMsg Hide the success message (it is not 77 | * necessary if the modal asking to 78 | * disconnect the device was already 79 | * shown). 80 | */ 81 | function promptForget(hideSuccessMsg) { 82 | return modalOpener.openModal($scope, 'forget.wipe', null, { 83 | hideSuccessMsg: hideSuccessMsg 84 | }).result; 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /management/app/scripts/controllers/error.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp').value('bowser', window.bowser); 4 | 5 | /** 6 | * Error Controller 7 | * 8 | * Assign properties that show if the Transport was loaded successfully and 9 | * if the plugin is up to date to the Angular scope. 10 | * 11 | * @see index.html 12 | * @see error.html 13 | * @see error.install.html 14 | */ 15 | angular.module('webwalletApp').controller('ErrorCtrl', function ( 16 | config, 17 | trezor, 18 | trezorApi, 19 | trezorError, 20 | udevError, 21 | $scope) { 22 | 23 | 'use strict'; 24 | 25 | $scope.udevError = udevError; 26 | 27 | if (trezorError === null) { 28 | $scope.error = false; 29 | } else { 30 | $scope.error = true; 31 | $scope.errorMessage = trezorError.message; 32 | } 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /management/app/scripts/controllers/main.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | /** 4 | * Main Controller 5 | * 6 | * Load deviceList, deviceService, and firmwareService. These modules 7 | * immediately start listening to device events. They are responsible for most 8 | * of the functionality of the app, that is not triggered by the user. 9 | * 10 | * @see main.html 11 | */ 12 | angular.module('webwalletApp').controller('MainCtrl', function ( 13 | $scope, 14 | deviceList, 15 | deviceService, 16 | udevError, 17 | firmwareService) { 18 | 19 | 'use strict'; 20 | 21 | $scope.closeApp = function () { 22 | chrome.app.window.current().close(); 23 | } 24 | 25 | $scope.isConnected = function () { 26 | return deviceList.all().some( 27 | function (dev) { 28 | return dev.isConnected(); 29 | } 30 | ); 31 | } 32 | 33 | $scope.isEmpty = function () { 34 | return deviceList.count() === 0; 35 | } 36 | 37 | if (!$scope.isEmpty() && !$scope.isConnected()) { 38 | var first = (deviceList.all())[0]; 39 | deviceList.navigateTo(first); 40 | } 41 | 42 | $scope.udevError = udevError; 43 | }); 44 | -------------------------------------------------------------------------------- /management/app/scripts/controllers/modal/pin.js: -------------------------------------------------------------------------------- 1 | /*global angular */ 2 | 3 | angular.module('webwalletApp').controller('ModalPinCtrl', function ( 4 | $scope, 5 | deviceService, 6 | deviceList) { 7 | 8 | 'use strict'; 9 | 10 | $scope.ratePin = function (pin) { 11 | var strength = $scope.device.ratePin(pin); 12 | if (strength === 0) return 'long'; 13 | if (strength < 3000) return 'weak'; 14 | if (strength < 60000) return 'fine'; 15 | if (strength < 360000) return 'strong'; 16 | return 'ultimate'; 17 | }; 18 | }); 19 | -------------------------------------------------------------------------------- /management/app/scripts/controllers/navbar.js: -------------------------------------------------------------------------------- 1 | angular.module('webwalletApp').controller('NavbarCtrl', function ( 2 | $scope) { 3 | 4 | 'use strict'; 5 | $scope.closeApp = function() { 6 | chrome.app.window.current().close(); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /management/app/scripts/directives/flashMessages.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * Flash Messages 7 | * 8 | * Usage Examples 9 | * 10 | * 1. Simple success message 11 | * 12 | * flash('My success message'); 13 | * 14 | * 2. Error message 15 | * 16 | * flash('error', 'My error message'); 17 | * 18 | * or 19 | * 20 | * flash.error('My error message'); 21 | * 22 | * 3. Message with a custom template: 23 | * 24 | * flash.warning({ 25 | * template: 'Angular template as string {{foo}} {{bar}}.', 26 | * foo: 'Foooo', 27 | * bar: 'bar baz' 28 | * }); 29 | * 30 | * 4. List of messages 31 | * 32 | * flash( 33 | * [ 34 | * 'My success message', 35 | * { 36 | * level: 'error', 37 | * info: 'My info message' 38 | * } 39 | * ] 40 | * ); 41 | */ 42 | angular.module('webwalletApp') 43 | .factory('flash', function ($rootScope, $timeout, $interpolate) { 44 | 'use strict'; 45 | 46 | var messages = [], 47 | reset; 48 | 49 | function cleanup() { 50 | $timeout.cancel(reset); 51 | reset = $timeout(function () { 52 | messages = []; 53 | }); 54 | } 55 | 56 | function emit() { 57 | $rootScope.$emit('flash:message', messages, cleanup); 58 | } 59 | 60 | $rootScope.$on('$locationChangeSuccess', emit); 61 | 62 | function asMessage(level, text) { 63 | if (!text) { 64 | text = level; 65 | level = 'success'; 66 | } else if (text.template) { 67 | return { 68 | html: $interpolate(text.template)(text), 69 | level: level 70 | }; 71 | } 72 | return { 73 | level: level, 74 | text: text 75 | }; 76 | } 77 | 78 | function asArrayOfMessages(level, text) { 79 | if (level instanceof Array) { 80 | return level.map(function (message) { 81 | if (message.level || message.text) { 82 | return asMessage(message.level, message.text); 83 | } 84 | return asMessage(message); 85 | }); 86 | } 87 | return [asMessage(level, text)]; 88 | } 89 | 90 | var flash = function (level, text) { 91 | messages = asArrayOfMessages(level, text); 92 | emit(messages); 93 | }; 94 | 95 | ['error', 'warning', 'info', 'success'].forEach(function (level) { 96 | flash[level] = function (text) { 97 | flash(level, text); 98 | }; 99 | }); 100 | 101 | flash.clear = function () { 102 | emit(null); 103 | }; 104 | 105 | return flash; 106 | }) 107 | 108 | .directive('flashMessages', function () { 109 | return { 110 | controller: function ($scope, $rootScope) { 111 | $rootScope.$on('flash:message', function (_, messages, done) { 112 | $scope.messages = messages; 113 | done(); 114 | }); 115 | }, 116 | restrict: 'EA', 117 | replace: true, 118 | template: '', 119 | transclude: true 120 | }; 121 | }); 122 | -------------------------------------------------------------------------------- /management/app/scripts/directives/focus.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('webwalletApp') 4 | .directive('focus', function () { 5 | return function (scope, element) { 6 | element[0].focus(); 7 | }; 8 | }); 9 | -------------------------------------------------------------------------------- /management/app/scripts/filters.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp') 4 | .filter('password', function () { 5 | 'use strict'; 6 | return function(s) { 7 | return s.replace(/./, '*'); 8 | }; 9 | }); 10 | -------------------------------------------------------------------------------- /management/app/scripts/filters/filters.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp') 4 | .filter('password', function () { 5 | 'use strict'; 6 | return function (s) { 7 | return s.replace(/./g, '\u2731'); 8 | }; 9 | }); 10 | -------------------------------------------------------------------------------- /management/app/scripts/services/TrezorDevice.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp').factory('TrezorDevice', function ( 4 | _, 5 | $q, 6 | $log, 7 | config, 8 | utils, 9 | $timeout, 10 | $interval) { 11 | 12 | 'use strict'; 13 | 14 | function TrezorDevice(desc) { 15 | this.id = desc.id || null; 16 | this.path = desc.path || null; 17 | this.features = null; 18 | this.error = null; 19 | this.forgetOnDisconnect = null; 20 | 21 | this._passphrase = null; 22 | this._session = null; 23 | this._desc = null; 24 | this._loadingLevel = 0; 25 | } 26 | 27 | TrezorDevice.prototype.DEFAULT_LABEL = 'My TREZOR'; 28 | TrezorDevice.prototype.LABEL_MAX_LENGTH = 16; 29 | 30 | // If device is without messages for this many milliseconds, 31 | // session is cleared 32 | TrezorDevice.prototype.SESSION_TTL = 2 * 60 * 1000; 33 | 34 | TrezorDevice.EVENT_PIN = 'pin'; 35 | TrezorDevice.EVENT_BUTTON = 'button'; 36 | TrezorDevice.EVENT_PASSPHRASE = 'passphrase'; 37 | TrezorDevice.EVENT_WORD = 'word'; 38 | TrezorDevice.EVENT_SEND = 'send'; 39 | TrezorDevice.EVENT_ERROR = 'error'; 40 | TrezorDevice.EVENT_RECEIVE = 'receive'; 41 | 42 | TrezorDevice.EVENT_CONNECT = 'connect'; 43 | TrezorDevice.EVENT_DISCONNECT = 'disconnect'; 44 | 45 | TrezorDevice.EVENT_PREFIX = 'device.'; 46 | TrezorDevice.EVENT_TYPES = [ 47 | TrezorDevice.EVENT_PIN, 48 | TrezorDevice.EVENT_PASSPHRASE, 49 | TrezorDevice.EVENT_BUTTON, 50 | TrezorDevice.EVENT_WORD, 51 | TrezorDevice.EVENT_SEND, 52 | TrezorDevice.EVENT_ERROR, 53 | TrezorDevice.EVENT_RECEIVE 54 | ]; 55 | 56 | TrezorDevice.REQ_BUTTON_FIRMWARE = 'ButtonRequest_FirmwareCheck'; 57 | 58 | TrezorDevice.prototype.STATUS_LOADING = "loading"; 59 | TrezorDevice.prototype.STATUS_CONNECTED = "connected"; 60 | TrezorDevice.prototype.STATUS_DISCONNECTED = "disconnected"; 61 | 62 | /** 63 | * Disconnect the device and unsubscribe from account updates from the 64 | * server backend. 65 | */ 66 | TrezorDevice.prototype.destroy = function () { 67 | this.disconnect(); 68 | }; 69 | 70 | TrezorDevice.deserialize = function (data) { 71 | var dev = new TrezorDevice(data); 72 | 73 | //old bitcoin.js version conversion 74 | if (typeof data.passphrase === "string") { 75 | console.log("[device] converting from old to new passphrase..."); 76 | console.log("[device] old: ", data.passphrase); 77 | 78 | var newPass = utils.hexToBytes(data.passphrase); 79 | newPass = Array.prototype.slice.call(newPass); 80 | console.log("[device] new: ", newPass); 81 | 82 | dev._passphrase = newPass; 83 | } else { 84 | dev._passphrase = data.passphrase; 85 | } 86 | 87 | 88 | dev.features = data.features; 89 | dev.forgetOnDisconnect = data.forgetOnDisconnect; 90 | 91 | return dev; 92 | }; 93 | 94 | TrezorDevice.prototype.serialize = function () { 95 | return { 96 | id: this.id, 97 | path: this.path, 98 | passphrase: this._passphrase, 99 | features: this.features, 100 | forgetOnDisconnect: this.forgetOnDisconnect 101 | }; 102 | }; 103 | 104 | // 105 | // Clearing sessions 106 | // 107 | 108 | TrezorDevice.prototype.clearSessionAndReleaseSync = function () { 109 | this._session.clearSessionSync(); 110 | this._session.releaseSync(); 111 | } 112 | 113 | TrezorDevice.prototype.clearSession = function () { 114 | console.log("[device] clearing session"); 115 | var self = this; 116 | return this._session.clearSession().then(function () { 117 | self.maybeReloadFeatures(); 118 | }); 119 | } 120 | 121 | /** 122 | * Does all the things that are needed when the tab is closed 123 | * - clearing session, calling release 124 | * 125 | * It runs synchronously because onbeforeunload cannot 126 | * have synchronous tab 127 | * 128 | * :: TrezorDevice -> Void 129 | */ 130 | TrezorDevice.prototype.runOnClose = function () { 131 | if (this._session != null) { 132 | // plugin has supportsSync == false 133 | if (this._session.supportsSync) { 134 | // if call in progress, synchronous call hangs 135 | // which could hang the browser => we don't want that 136 | if (!this._callInProgress) { 137 | this.clearSessionAndReleaseSync(); 138 | } 139 | } else { 140 | // let's at least try to clear session asynchronously anyway 141 | // (it will usually work) 142 | this.clearSession(); 143 | } 144 | } 145 | } 146 | 147 | 148 | 149 | /** 150 | * On send and receive, cancel previous clearing timeout and 151 | * set up new clearing timeout 152 | * 153 | * If nothing happens in SESSION_TTL miliseconds, it is not stopped 154 | * and the session is cleared 155 | * 156 | * :: TrezorDevice -> Void 157 | */ 158 | TrezorDevice.prototype.setClearTimeout = function () { 159 | 160 | this.cancelClearTimeout(); 161 | 162 | this._lastActivityStamp = Math.floor(Math.floor(Date.now() / 1000)); 163 | this._checkActivityTimeout = $timeout(function () { 164 | if (this.isConnected()) { 165 | if (!this._callInProgress) { 166 | this.clearSession(); 167 | } 168 | } 169 | }.bind(this), this.SESSION_TTL); 170 | 171 | this._lastActivityInterval = $interval(function () { 172 | var now = Math.floor(Math.floor(Date.now() / 1000)); 173 | this._activityDiff = now - this._lastActivityStamp; 174 | }.bind(this), 1000); 175 | } 176 | 177 | TrezorDevice.prototype.timeToClear = function () { 178 | var activityDiff = this._activityDiff; 179 | if (this._activityDiff == null) { 180 | activityDiff = 0; 181 | } 182 | var reverse = this.SESSION_TTL / 1000 - activityDiff; 183 | if (reverse < 0) { 184 | return "soon"; //while clearing is happenning 185 | } 186 | var minutes = Math.floor(reverse / 60); 187 | var seconds = reverse - minutes * 60; 188 | return "in " + minutes + ":" + seconds; 189 | } 190 | 191 | /** 192 | * Cancels clearTimeout 193 | * 194 | * :: TrezorDevice -> Void 195 | */ 196 | TrezorDevice.prototype.cancelClearTimeout = function () { 197 | if (this._checkActivityTimeout != null) { 198 | $timeout.cancel(this._checkActivityTimeout); 199 | $interval.cancel(this._lastActivityInterval); 200 | } 201 | } 202 | 203 | // 204 | // Status & features 205 | // 206 | 207 | /** 208 | * Reload features without re-initializing 209 | * 210 | * :: TrezorDevice -> Promise 211 | */ 212 | TrezorDevice.prototype.reloadFeatures = function () { 213 | //if features is null, it's probably not loaded yet or it's in some other weird state 214 | if (this.features != null) { 215 | if (this.supports('getFeatures')) { 216 | return this._session.getFeatures().then(function (res) { 217 | this.features = res.message; 218 | }.bind(this)); 219 | } 220 | } 221 | return $q.when(); 222 | } 223 | 224 | 225 | /** 226 | * If Trezor supports getFeatures, reloads features, 227 | * else calls initializeDevice (in case of older Trezors) 228 | */ 229 | TrezorDevice.prototype.maybeReloadFeatures = function () { 230 | //if features is null, it's probably not loaded yet or it's in some other weird state 231 | if (this.features != null) { 232 | if (this.supports('getFeatures')) { 233 | return this.reloadFeatures(); 234 | } else { 235 | return this.initializeDevice(); 236 | } 237 | } 238 | return $q.when(); 239 | } 240 | 241 | /** 242 | * Should I display "lock this device" icon? 243 | * 244 | * :: TrezorDevice -> Bool 245 | */ 246 | TrezorDevice.prototype.displayLock = function () { 247 | return this.lockStatus() === "unlocked"; 248 | } 249 | 250 | /** 251 | * Lock status 252 | * 253 | * Either "NA" (not applicable - old FW, disconnected) 254 | * or "locked" / "unlocked" 255 | * 256 | * :: TrezorDevice -> String 257 | */ 258 | TrezorDevice.prototype.lockStatus = function () { 259 | if (!this._lockMakesSense()) { 260 | return "NA"; 261 | } else { 262 | if (this._isUnlocked()) { 263 | return "unlocked"; 264 | } else { 265 | return "locked"; 266 | } 267 | } 268 | } 269 | 270 | /** 271 | * Is device unlocked? 272 | * 273 | * Shouldn't be used "directly", since it doesn't check for connectedness or 274 | * for firmware version 275 | * 276 | * :: TrezorDevice -> Boolean 277 | */ 278 | TrezorDevice.prototype._isUnlocked = function () { 279 | 280 | // it could be probably done by && and || but this is more readable, I think 281 | 282 | if (this.features.passphrase_protection) { 283 | if (this.features.pin_protection) { 284 | return this.features.pin_cached && this.features.passphrase_cached; 285 | } else { 286 | return this.features.passphrase_cached; 287 | } 288 | } else { 289 | if (this.features.pin_protection) { 290 | return this.features.pin_cached; 291 | } else { 292 | return false; 293 | } 294 | } 295 | } 296 | 297 | /** 298 | * Checks if displaying lock makes sense 299 | * 300 | * loading, disconnected, old FW == doesn't make sense, false 301 | * 302 | * :: TrezorDevice -> Boolean 303 | */ 304 | TrezorDevice.prototype._lockMakesSense = function () { 305 | if (!this.supports('cacheInFeatures')) { 306 | return false; 307 | } 308 | 309 | if (this.status() !== this.STATUS_CONNECTED) { 310 | return false; 311 | } 312 | return (this.features.passphrase_protection || this.features.pin_protection); 313 | } 314 | 315 | 316 | /** 317 | * Is device in a "loading" state? 318 | * Meaning both "device is initializing" and "accounts are loading". 319 | * 320 | * :: TrezorDevice -> Bool 321 | */ 322 | TrezorDevice.prototype.isLoading = function () { 323 | return (!!this._loadingLevel); 324 | }; 325 | 326 | 327 | TrezorDevice.prototype.withLoading = function (fn) { 328 | var self = this; 329 | 330 | self._loadingLevel++; 331 | 332 | var fnRes = fn(); 333 | 334 | return $q.when(fnRes).finally(function () { 335 | self._loadingLevel--; 336 | if (self._loadingLevel === 0) { 337 | return self.maybeReloadFeatures(); 338 | } 339 | return $q.when(); 340 | }).then(function () { 341 | return fnRes; 342 | }); 343 | }; 344 | 345 | 346 | /** 347 | * Returns status of a device, as a string. 348 | * 349 | * :: TrezorDevice -> 350 | * String -- string, that is either "connected", "disconnected" or "loading" 351 | */ 352 | TrezorDevice.prototype.status = function () { 353 | if (this.isLoading()) return this.STATUS_LOADING; 354 | if (this.isConnected()) return this.STATUS_CONNECTED; 355 | return this.STATUS_DISCONNECTED; 356 | }; 357 | 358 | /** 359 | * Is device in a state where it can send transactions? 360 | * Note: this will return TRUE even when it's "locked" by PIN/passphrase 361 | * 362 | * :: TrezorDevice -> Bool 363 | */ 364 | TrezorDevice.prototype.canSendTx = function () { 365 | return (this.isConnected() && !(this.isLoading())); 366 | } 367 | 368 | TrezorDevice.prototype.label = function () { 369 | if (this.features && this.features.label) 370 | return this.features.label; 371 | else 372 | return this.DEFAULT_LABEL; 373 | }; 374 | 375 | 376 | TrezorDevice.prototype.defaultCoin = function () { 377 | return _.find(this.features.coins, { 378 | coin_name: config.coin 379 | }); 380 | }; 381 | 382 | TrezorDevice.prototype.supports = function (key) { 383 | if (this.features && config.features[key]) { 384 | return [ 385 | this.features.major_version, 386 | this.features.minor_version, 387 | this.features.patch_version, 388 | ].join('.') >= config.features[key]; 389 | } else { 390 | return false; 391 | } 392 | }; 393 | 394 | // 395 | // Passphrase 396 | // 397 | 398 | TrezorDevice.prototype.hasSavedPassphrase = function () { 399 | return !!this._passphrase; 400 | }; 401 | 402 | TrezorDevice.prototype.checkPassphraseAndSave = function (passphrase) { 403 | var hash = this._hashPassphrase(passphrase); 404 | 405 | if (this._passphrase) 406 | //deep equality of two arrays 407 | return JSON.stringify(this._passphrase) === JSON.stringify(hash); 408 | else { 409 | this._passphrase = hash; 410 | return true; 411 | } 412 | }; 413 | 414 | TrezorDevice.prototype._hashPassphrase = function (passphrase) { 415 | var secret = 'TREZOR#' + this.id + '#' + passphrase; 416 | var buffer = utils.sha256x2(secret, { 417 | asBytes: true 418 | }); 419 | return Array.prototype.slice.call(buffer); 420 | }; 421 | 422 | // 423 | // HW connections 424 | // 425 | 426 | /** 427 | * :: TrezorDevice -> Bool 428 | */ 429 | TrezorDevice.prototype.isConnected = function () { 430 | return !!this._session; 431 | }; 432 | 433 | TrezorDevice.prototype.connect = function (session) { 434 | this._session = session; 435 | this.on = this._session.on.bind(this._session); 436 | this.once = this._session.once.bind(this._session); 437 | this.removeListener = this._session.removeListener.bind(this._session); 438 | 439 | this._callInProgress = false; 440 | 441 | }; 442 | 443 | TrezorDevice.prototype.addSessionListeners = function () { 444 | 445 | var onSend = function () { 446 | this._callInProgress = true; 447 | }.bind(this) 448 | 449 | var onReceiveOrError = function () { 450 | this.setClearTimeout(); 451 | this._callInProgress = false; 452 | }.bind(this) 453 | 454 | this.on('send', onSend); 455 | this.on('receive', onReceiveOrError); 456 | this.on('error', onReceiveOrError); 457 | 458 | } 459 | 460 | 461 | 462 | TrezorDevice.prototype.disconnect = function () { 463 | 464 | this.cancelClearTimeout(); 465 | 466 | if (this._session) 467 | this._session.release(); 468 | this._session = null; 469 | }; 470 | 471 | // 472 | // HW initialization 473 | // 474 | 475 | TrezorDevice.prototype.isEmpty = function () { 476 | return !this.features || !this.features.initialized; 477 | }; 478 | 479 | TrezorDevice.prototype.initializeDevice = function () { 480 | var self = this, 481 | delay = 3000, // delay between attempts 482 | max = 60; // give up after n attempts 483 | 484 | // keep trying to initialize 485 | return utils.endure(callInitialize, delay, max) 486 | .then( 487 | function (res) { 488 | return (self.features = res.message); 489 | }, 490 | function (err) { 491 | self.features = null; 492 | throw err; 493 | } 494 | ); 495 | 496 | function callInitialize() { 497 | if (!self.isConnected()) // return falsey to cancel endure() 498 | return false; 499 | 500 | return self._session.initialize().then( 501 | function (res) { 502 | var features = res.message; 503 | if (features.bootloader_mode) { 504 | self.id = self.path; 505 | } else { 506 | self.id = features.device_id; 507 | } 508 | self.error = null; 509 | return res; 510 | }, 511 | function (err) { 512 | self.error = err.message || 'Failed to initialize the device.'; 513 | throw err; 514 | } 515 | ); 516 | } 517 | }; 518 | 519 | 520 | 521 | TrezorDevice.prototype.flash = function (firmware) { 522 | var self = this; 523 | 524 | return self._session.eraseFirmware().then(function () { 525 | return self._session.uploadFirmware(firmware); 526 | }); 527 | }; 528 | 529 | TrezorDevice.prototype.wipe = function () { 530 | var self = this; 531 | 532 | return self.withLoading(function () { 533 | return self._session.initialize() 534 | .then(function () { 535 | return self._session.wipeDevice(); 536 | }) 537 | }); 538 | }; 539 | 540 | TrezorDevice.prototype.reset = function (settings) { 541 | var self = this, 542 | sett = angular.copy(settings); 543 | 544 | return self.withLoading(function () { 545 | return self._session.initialize() 546 | .then(function () { 547 | return self._session.resetDevice(sett); 548 | }) 549 | .then(function () { 550 | return self.initializeDevice(); 551 | }) 552 | }); 553 | }; 554 | 555 | TrezorDevice.prototype.load = function (settings) { 556 | var self = this, 557 | sett = angular.copy(settings); 558 | 559 | try { // try to decode as xprv 560 | sett.node = utils.xprv2node(sett.payload); 561 | } catch (e) { // use as mnemonic on fail 562 | sett.mnemonic = sett.payload; 563 | } 564 | delete sett.payload; 565 | 566 | return self.withLoading(function () { 567 | return self._session.initialize() 568 | .then(function () { 569 | return self._session.loadDevice(sett); 570 | }) 571 | .then(function () { 572 | return self.initializeDevice(); 573 | }) 574 | }); 575 | }; 576 | 577 | TrezorDevice.prototype.recover = function (settings) { 578 | var self = this, 579 | sett = angular.copy(settings); 580 | 581 | sett.enforce_wordlist = true; 582 | 583 | return self.withLoading(function () { 584 | return self._session.initialize() 585 | .then(function () { 586 | return self._session.recoverDevice(sett); 587 | }) 588 | .then(function () { 589 | return self.initializeDevice(); 590 | }) 591 | }); 592 | }; 593 | 594 | TrezorDevice.prototype.changeLabel = function (label) { 595 | var self = this; 596 | 597 | if (label.length > this.LABEL_MAX_LENGTH) { 598 | label = label.slice(0, this.LABEL_MAX_LENGTH); 599 | } 600 | 601 | return self.withLoading(function () { 602 | return self.maybeReloadFeatures() 603 | .then(function () { 604 | return self._session.applySettings({ 605 | label: label 606 | }); 607 | }) 608 | .then(function () { 609 | return self.maybeReloadFeatures(); 610 | }); 611 | }); 612 | }; 613 | 614 | //input: boolean - true if I want to enable, false if I want to disable 615 | TrezorDevice.prototype.togglePassphrase = function (enable) { 616 | var self = this; 617 | 618 | return self.withLoading(function () { 619 | return self.maybeReloadFeatures() 620 | .then(function () { 621 | return self._session.applySettings({ 622 | use_passphrase: (!!enable) 623 | }); 624 | }) 625 | .then(function () { 626 | return self.maybeReloadFeatures(); 627 | }); 628 | }); 629 | }; 630 | 631 | TrezorDevice.prototype.changePin = function (remove) { 632 | var self = this; 633 | 634 | return self.withLoading(function () { 635 | return self.maybeReloadFeatures() 636 | .then(function () { 637 | return self._session.changePin(remove); 638 | }) 639 | .then(function () { 640 | return self.maybeReloadFeatures(); 641 | }); 642 | }); 643 | }; 644 | 645 | TrezorDevice.prototype.ratePin = function (pin) { 646 | var digits, strength; 647 | 648 | if (pin.length > 9) 649 | return 0; 650 | 651 | digits = _.uniq(pin.split('')).length; 652 | strength = fac(9) / fac(9 - digits); 653 | 654 | return strength; 655 | 656 | function fac(n) { 657 | var i, nf = 1; 658 | for (i = 2; i <= n; i++) nf *= i; 659 | return nf; 660 | } 661 | }; 662 | 663 | 664 | TrezorDevice.prototype.shouldShow = function () { 665 | if (this.features.bootloader_mode === true) { 666 | return false; 667 | } 668 | if (this.features.vendor !== "bitcointrezor.com") { 669 | return false; 670 | } 671 | return true; 672 | } 673 | 674 | 675 | TrezorDevice.prototype.firmwareString = function () { 676 | return this.features.major_version + "." + this.features.minor_version + "." + this.features.patch_version; 677 | } 678 | 679 | return TrezorDevice; 680 | 681 | }); 682 | -------------------------------------------------------------------------------- /management/app/scripts/services/console.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Console wrappers 3 | * 4 | * Stores all logs in `window.console.logs` Array. 5 | */ 6 | (function (window) { 7 | 'use strict'; 8 | 9 | if (typeof (console) !== "undefined") { 10 | 11 | var orig = { 12 | error: window.console.error, 13 | warn: window.console.warn, 14 | info: window.console.info, 15 | debug: window.console.debug, 16 | log: window.console.log 17 | }, 18 | level; 19 | 20 | window.console.logs = []; 21 | 22 | for (level in orig) { 23 | if (orig.hasOwnProperty(level)) { 24 | window.console[level] = createMethod(orig[level], level); 25 | } 26 | } 27 | } 28 | 29 | function createMethod(method, level) { 30 | return function () { 31 | if (method !== undefined) { 32 | var args = Array.prototype.slice.call(arguments), 33 | time = new Date().toUTCString(); 34 | window.console.logs.push([level, time].concat(args)); 35 | try { 36 | method.apply(window.console, args); 37 | } catch (e) { 38 | Function.prototype.apply.call(method, window.console, args); 39 | } 40 | } 41 | }; 42 | } 43 | 44 | }(this)); 45 | -------------------------------------------------------------------------------- /management/app/scripts/services/deviceService.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | /** 4 | * Device Service 5 | * 6 | * Perform various actions when a device is connected / disconnected. 7 | * 8 | * Device-related actions that should be performed as a result of a direct 9 | * user interaction are (and always should be) handled the Device Controller. 10 | * 11 | * The only way how this Device Service communicates with the Device Controller 12 | * is by broadcasting events to Angular's scope. 13 | * 14 | * On device connect: 15 | * 16 | * - Navigate to the Device URL (if we are on homepage). 17 | * - Initialize accounts. 18 | * - Pause device list watching while we communicate with the device. 19 | * 20 | * On device disconnect: 21 | * 22 | * - Do nothing. 23 | */ 24 | angular.module('webwalletApp') 25 | .service('deviceService', function ( 26 | trezor, trezorApi, TrezorDevice, deviceList, $rootScope, $location) { 27 | 28 | 'use strict'; 29 | 30 | var _forgetRequested = false, 31 | EVENT_ASK_FORGET = 'device.askForget', 32 | EVENT_ASK_DISCONNECT = 'device.askDisconnect', 33 | EVENT_CLOSE_DISCONNECT = 'device.closeDisconnect'; 34 | 35 | this.EVENT_ASK_FORGET = EVENT_ASK_FORGET; 36 | this.EVENT_ASK_DISCONNECT = EVENT_ASK_DISCONNECT; 37 | this.EVENT_CLOSE_DISCONNECT = EVENT_CLOSE_DISCONNECT; 38 | 39 | // Before initialize hooks 40 | deviceList.registerBeforeInitHook(setupWatchPausing); 41 | deviceList.registerBeforeInitHook(setupEventBroadcast); 42 | deviceList.registerBeforeInitHook(deviceList.navigateTo.bind(deviceList), 20); 43 | 44 | // Disconnect hooks 45 | deviceList.registerDisconnectHook(onDisconnect); 46 | 47 | // Forget hooks 48 | deviceList.registerForgetHook(onForget); 49 | deviceList.registerAfterForgetHook(navigateToDefaultDevice); 50 | 51 | // Watch for newly connected and disconnected devices 52 | deviceList.watch(deviceList.POLLING_PERIOD); 53 | 54 | /** 55 | * Pause refreshing of the passed device while a communicate with the 56 | * device is in progress. While the watching is passed, webwallet will 57 | * not add / remove the device from the device when 58 | * it's connected / disconnected, nor will it execute any hooks. 59 | * 60 | * @see DeviceList#_connnect() 61 | * @see DeviceList#_disconnect() 62 | * @see DeviceList#_progressWithConnected() 63 | * 64 | * @param {TrezorDevice} dev Device object 65 | */ 66 | function setupWatchPausing(dev) { 67 | if (trezor instanceof trezorApi.PluginTransport) { 68 | dev.on(TrezorDevice.EVENT_SEND, 69 | deviceList.pauseWatch.bind(deviceList)); 70 | dev.on(TrezorDevice.EVENT_ERROR, 71 | deviceList.resumeWatch.bind(deviceList)); 72 | dev.on(TrezorDevice.EVENT_RECEIVE, 73 | deviceList.resumeWatch.bind(deviceList)); 74 | } 75 | } 76 | 77 | /** 78 | * Broadcast all events on passed device to the Angular scope. 79 | * 80 | * @see _broadcastEvent() 81 | * 82 | * @param {TrezorDevice} dev Device 83 | */ 84 | function setupEventBroadcast(dev) { 85 | TrezorDevice.EVENT_TYPES.forEach(function (type) { 86 | _broadcastEvent($rootScope, dev, type); 87 | }); 88 | } 89 | 90 | /** 91 | * Broadcast an event of passed type on passed device to 92 | * the Angular scope. 93 | * 94 | * The event type is prefixed with `TrezorDevice#EVENT_PREFIX`. 95 | * 96 | * @param {$scope} scope Angular scope 97 | * @param {TrezorDevice} dev Device 98 | * @param {String} type Event type 99 | */ 100 | function _broadcastEvent(scope, dev, type) { 101 | dev.on(type, function () { 102 | var args = [].slice.call(arguments); 103 | args.unshift(dev); 104 | args.unshift(TrezorDevice.EVENT_PREFIX + type); 105 | scope.$broadcast.apply(scope, args); 106 | }); 107 | } 108 | 109 | /** 110 | * If the current URL is the URL of the device being disconnected, 111 | * go to the default device or to homepage if no devices are connected. 112 | */ 113 | function navigateToDefaultDevice() { 114 | var dev = deviceList.getDefault(); 115 | if (dev) { 116 | deviceList.navigateTo(dev); 117 | return; 118 | } 119 | $location.path('/'); 120 | } 121 | 122 | /** 123 | * Forget current device. 124 | * 125 | * If the device is connected, ask the user to disconnect it before. 126 | * 127 | * This is achieved by aborting the forget hooks if the device is 128 | * connected. When the device is disconnected, this method is called 129 | * again, but in that case it passes without aborting anything, because 130 | * the device is no longer connected. 131 | * 132 | * Passed `param` object has these mandatory properties: 133 | * - {TrezorDevice} `dev`: Device instance 134 | * - {Boolean} `requireDisconnect`: Can the user allowed to cancel the 135 | * modal, or does he/she have to disconnect the device? 136 | * 137 | * @see DeviceList#forget() 138 | * @see DeviceCtrl.askToDisconnectOnForget() 139 | * 140 | * @param {Object} param Parameters in format: 141 | * {dev: TrezorDevice, 142 | * requireDisconnect: Boolean} 143 | * @throws Error 144 | */ 145 | function onForget(param) { 146 | // Device after firmware update 147 | if (!param.dev) { 148 | return; 149 | } 150 | // If the device is not connected, forget it immediately. 151 | if (!param.dev.isConnected()) { 152 | $rootScope.$broadcast(EVENT_CLOSE_DISCONNECT, param); 153 | return; 154 | } 155 | // If the device is connected, ask to user to disconnect it. 156 | _forgetRequested = true; 157 | $rootScope.$broadcast(EVENT_ASK_DISCONNECT, param); 158 | deviceList.abortHook(); 159 | } 160 | 161 | /** 162 | * When a device is disconnected, ask the user if he/she wants to 163 | * forget it or remember. 164 | * 165 | * @see DeviceCtrl.forgetOnDisconnect() 166 | * 167 | * @param {TrezorDevice} dev Device 168 | */ 169 | function onDisconnect(dev) { 170 | deviceList.forget(dev); 171 | } 172 | 173 | }); 174 | -------------------------------------------------------------------------------- /management/app/scripts/services/firmwareService.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | /** 4 | * Firmware Service 5 | * 6 | * TODO Document Firmware Service 7 | */ 8 | angular.module('webwalletApp').value('FIRMWARE_LIST_URL', '/data/releases.json'); 9 | 10 | angular.module('webwalletApp').service('firmwareService', function FirmwareService( 11 | FIRMWARE_LIST_URL, 12 | $http, 13 | $rootScope, 14 | deviceList) { 15 | 16 | 'use strict'; 17 | 18 | 19 | function getFirmwaresConcat() { 20 | var devices = deviceList.all(); 21 | if (devices.length === 0) { 22 | return ""; 23 | } 24 | return devices.map(function (device) { 25 | return device.firmwareString(); 26 | }).join(","); 27 | } 28 | 29 | 30 | var _timestamp = new Date().getTime(), 31 | _firmwareList = $http.get(FIRMWARE_LIST_URL), 32 | _modalOpen = false; 33 | 34 | this.EVENT_CONNECT = 'firmware.connect'; 35 | this.EVENT_DISCONNECT = 'firmware.disconnect'; 36 | 37 | this.EVENT_BOOTLOADER = 'firmware.bootloader'; 38 | this.EVENT_NORMAL = 'firmware.normal'; 39 | this.EVENT_CANDIDATE = 'firmware.candidate'; 40 | this.EVENT_OUTDATED = 'firmware.outdated'; 41 | 42 | /* 43 | * Connect and disconnect events for the Controller -- sent even 44 | * if the Firmware modal is open. 45 | */ 46 | deviceList.registerBeforeInitHook(function sendConnectEvent(dev) { 47 | $rootScope.$broadcast(this.EVENT_CONNECT, dev); 48 | }.bind(this), 5); 49 | deviceList.registerDisconnectHook(function sendDisconnectEvent(dev) { 50 | $rootScope.$broadcast(this.EVENT_DISCONNECT, dev); 51 | }.bind(this), 5); 52 | 53 | /** 54 | * After initialize hook 55 | */ 56 | deviceList.registerAfterInitHook(function firmwareMain(dev) { 57 | 58 | // Bootloader mode 59 | if (dev.features.bootloader_mode) { 60 | $rootScope.$broadcast(this.EVENT_BOOTLOADER, dev); 61 | 62 | /* 63 | * Abort the whole after init process, if the firmware update 64 | * modal is opened -- that means no other after init hooks will 65 | * be executed. 66 | */ 67 | if (_modalOpen) { 68 | deviceList.abortHook(); 69 | } 70 | 71 | return latest() 72 | .then(function (firmware) { 73 | _modalOpen = true; 74 | $rootScope.$broadcast( 75 | this.EVENT_CANDIDATE, { 76 | dev: dev, 77 | firmware: firmware, 78 | } 79 | ); 80 | deviceList.abortHook(); 81 | }.bind(this)); 82 | 83 | // Normal mode 84 | } else { 85 | $rootScope.$broadcast(this.EVENT_NORMAL, dev); 86 | 87 | /* 88 | * Abort the whole after init process, if the firmware update 89 | * modal is opened -- that means no other after init hooks will 90 | * be executed. 91 | */ 92 | if (_modalOpen) { 93 | deviceList.abortHook(); 94 | } 95 | 96 | return check(dev.features) 97 | .then(function (firmware) { 98 | if (!firmware) { 99 | return; 100 | } 101 | if (firmware.required) { 102 | _modalOpen = true; 103 | } 104 | $rootScope.$broadcast( 105 | this.EVENT_OUTDATED, { 106 | dev: dev, 107 | firmware: firmware, 108 | version: _getVersion(dev.features) 109 | } 110 | ); 111 | if (firmware.required) { 112 | deviceList.abortHook(); 113 | } 114 | }.bind(this)); 115 | } 116 | }.bind(this), 10); 117 | 118 | /** 119 | * Disconnect hook 120 | */ 121 | deviceList.registerDisconnectHook(function (dev) { 122 | /* 123 | * Abort the whole disconnect process, if the firmware update 124 | * modal is opened -- that means no other disconnect hooks will 125 | * be executed. 126 | */ 127 | if (_modalOpen) { 128 | deviceList.abortHook(); 129 | } 130 | }, 10); 131 | 132 | function _getVersion(features) { 133 | return [+features.major_version, +features.minor_version, +features.patch_version]; 134 | } 135 | 136 | function latest() { 137 | return _firmwareList.then(function (res) { 138 | return res.data[0]; 139 | }); 140 | } 141 | 142 | function check(features) { 143 | return _firmwareList.then(function (res) { 144 | return _pick(features, res.data); 145 | }); 146 | } 147 | 148 | this.check = check; 149 | 150 | this.download = function (firmware) { 151 | return $http.get(firmware.url).then(function (res) { 152 | if (!_validate(res.data)) { 153 | throw new Error('Downloaded firmware is invalid'); 154 | } 155 | return res.data; 156 | }); 157 | }; 158 | 159 | function _validate(firmware) { 160 | var magic = '54525a52'; // 'TRZR' in hex 161 | 162 | return (firmware.substr(0, magic.length) === magic) && 163 | // * 2 because of hex 164 | (firmware.length >= 4096 * 2) && 165 | (firmware.length <= 1024 * (512 - 64) * 2); 166 | } 167 | 168 | function _pick(features, list) { 169 | var firmware = list[0], 170 | version = _getVersion(features), 171 | i; 172 | 173 | // No firmware available. 174 | if (!firmware) { 175 | return; 176 | } 177 | 178 | // Features are up to date. 179 | if (_versionCmp(firmware.version, version) < 1) { 180 | return; 181 | } 182 | 183 | for (i = 0; i < list.length; i = i + 1) { // collect required flags 184 | if (_versionCmp(list[i], features) === 0) { 185 | break; 186 | } 187 | if (list[i].required) { 188 | firmware.required = true; 189 | break; 190 | } 191 | } 192 | 193 | return firmware; 194 | } 195 | 196 | function _versionCmp(a, b) { 197 | if (a[0] - b[0]) { 198 | return a[0] - b[0]; 199 | } 200 | if (a[1] - b[1]) { 201 | return a[1] - b[1]; 202 | } 203 | if (a[2] - b[2]) { 204 | return a[2] - b[2]; 205 | } 206 | return 0; 207 | } 208 | 209 | /** 210 | * Set flag that marks if the Firmware modal dialog is open. 211 | * 212 | * @param {Boolean} modalOpen True if Firmware modal is open 213 | */ 214 | this.setModalOpen = function (modalOpen) { 215 | _modalOpen = modalOpen; 216 | }; 217 | 218 | /** 219 | * Is the Firmware modal dialog open? 220 | * 221 | * @return {Boolean} True if Firmware modal is open 222 | */ 223 | this.isModalOpen = function () { 224 | return _modalOpen; 225 | }; 226 | 227 | }); 228 | -------------------------------------------------------------------------------- /management/app/scripts/services/modals/modalOpener.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp').service('modalOpener', function ( 4 | $rootScope, 5 | $timeout, 6 | selecter, 7 | $document, 8 | $modal) { 9 | 10 | 'use strict'; 11 | 12 | this.currentlyOpenedModal = null; 13 | 14 | this.isModalOpened = function () { 15 | return (this.currentlyOpenedModal != null); 16 | } 17 | 18 | /** 19 | * Opens modal window. 20 | * 21 | * Modal window should have options "yes" and "no" (or similar), where 22 | * "yes" is binded to "close()" and "no" to "dismiss()". 23 | * 24 | * Returns a promise that is resolved if the user chooses "yes" 25 | * and failed if the user chooses "no" 26 | * 27 | * @return {Promise} 28 | */ 29 | this.openModal = function (scope, name, size, extendScope, allowBackspace, emitData) { 30 | var windowClass = name.replace(".", "-", "g") + "modal"; 31 | if (typeof extendScope === "undefined") { 32 | extendScope = {}; 33 | } 34 | if (typeof allowBackspace === "undefined") { 35 | allowBackspace = false; 36 | } 37 | 38 | scope = angular.extend(scope.$new(), extendScope); 39 | var modal = $modal.open({ 40 | templateUrl: 'views/modal/' + name + '.html', 41 | backdrop: 'static', 42 | keyboard: false, 43 | scope: scope, 44 | size: size, 45 | windowClass: windowClass 46 | }); 47 | var self = this; 48 | modal.opened.then(function () { 49 | self.currentlyOpenedModal = modal; 50 | scope.$emit('modal.' + name + '.show', emitData); 51 | 52 | $timeout(function () {}, 1).then(function () { 53 | angular.element("input[autofocus]").trigger('focus'); 54 | }).then(function () { 55 | scope.autoselect = function () { 56 | var selectElem = $document.find('.autoselect'); 57 | if (selectElem.length) { 58 | selecter.selectRange(selectElem[0]); 59 | } 60 | }; 61 | scope.autoselect(); 62 | }); 63 | }); 64 | modal.result.finally(function () { 65 | self.currentlyOpenedModal = null; 66 | scope.$emit('modal.' + name + '.hide', emitData); 67 | }); 68 | 69 | if (!allowBackspace) { 70 | this.stopBackpaceOnModal(modal) 71 | } 72 | 73 | return { 74 | result: modal.result, 75 | modal: modal, 76 | scope: scope 77 | }; 78 | } 79 | 80 | /**** 81 | * Stop backspace on already existing modal window 82 | * 83 | * It is called in openModal, but can be called from anywhere else 84 | * (for example, it's called in firmware.js or setup.js) 85 | */ 86 | this.stopBackpaceOnModal = function (modal) { 87 | modal.opened.then(function () { 88 | stopBackspace(); 89 | }) 90 | modal.result.finally(function () { 91 | resumeBackspace(); 92 | }) 93 | } 94 | 95 | function stopBackspace() { 96 | $(document).unbind('keydown.modalOpener').bind('keydown.modalOpener', function (event) { 97 | var doPrevent = false; 98 | if (event.keyCode === 8) { 99 | var d = event.srcElement || event.target; 100 | if ((d.tagName.toUpperCase() === 'INPUT' && 101 | ( 102 | d.type.toUpperCase() === 'TEXT' || 103 | d.type.toUpperCase() === 'PASSWORD' || 104 | d.type.toUpperCase() === 'FILE' || 105 | d.type.toUpperCase() === 'EMAIL' || 106 | d.type.toUpperCase() === 'SEARCH' || 107 | d.type.toUpperCase() === 'DATE') 108 | ) || 109 | d.tagName.toUpperCase() === 'TEXTAREA') { 110 | doPrevent = d.readOnly || d.disabled; 111 | } else { 112 | doPrevent = true; 113 | } 114 | } 115 | 116 | if (doPrevent) { 117 | event.preventDefault(); 118 | } 119 | }) 120 | } 121 | 122 | function resumeBackspace() { 123 | $(document).unbind('keydown.modalOpener').bind('keydown.modalOpener', function (event) {}) 124 | } 125 | }); 126 | -------------------------------------------------------------------------------- /management/app/scripts/services/modals/passphraseModalService.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp').service('passphraseModalService', function ( 4 | $rootScope, 5 | $document, 6 | modalOpener, 7 | $modal) { 8 | 9 | 'use strict'; 10 | 11 | this.openedModal = null; 12 | 13 | this.closeOpen = function () { 14 | if (this.openedModal != null) { 15 | this.openedModal.modal.close(); 16 | } 17 | this.openedModal = null; 18 | } 19 | 20 | this.showModal = function ($scope, e, dev, callback) { 21 | 22 | if (dev.id !== $scope.device.id) { 23 | return; 24 | } 25 | 26 | var hasSaved = $scope.device.hasSavedPassphrase(); 27 | 28 | if (!hasSaved) { 29 | dev.forgetOnDisconnect = true; 30 | //if we don't forget while displaying passphrase and disconnecting, it gets into weird state with no xpubs 31 | } 32 | 33 | var modal = modalOpener.openModal($scope, 'passphrase', 'sm', { 34 | check: !hasSaved, 35 | checkCorrect: false, 36 | values: { 37 | passphrase: '', 38 | passphraseCheck: '' 39 | }, 40 | installHandler: installSubmitHandlers 41 | }); 42 | 43 | this.openedModal = modal; 44 | 45 | modal.result.then( 46 | function (res) { 47 | if (!$scope.device.checkPassphraseAndSave(res)) { 48 | callback(new Error('Invalid passphrase')); 49 | } else { 50 | callback(null, res); 51 | } 52 | if (!hasSaved) { 53 | dev.forgetOnDisconnect = null; //back into default forget state 54 | } 55 | }, 56 | function (err) { 57 | callback(err); 58 | } 59 | ); 60 | 61 | modal.scope.$watch('values.passphrase', checkPassphrase); 62 | modal.scope.$watch('values.passphraseCheck', checkPassphrase); 63 | 64 | function checkPassphrase() { 65 | var v = modal.scope.values; 66 | if (!modal.scope.check) { 67 | modal.scope.checkCorrect = true; 68 | return; 69 | } 70 | modal.scope.checkCorrect = 71 | (v.passphrase === v.passphraseCheck) && 72 | (v.passphrase.length <= 50); 73 | } 74 | 75 | function installSubmitHandlers() { 76 | var submit = document.getElementById('passphrase-submit'), 77 | form = document.getElementById('passphrase-form'); 78 | 79 | submit.addEventListener('submit', submitModal, false); 80 | submit.addEventListener('click', submitModal, false); 81 | form.addEventListener('submit', submitModal, false); 82 | form.addEventListener('keypress', function (e) { 83 | if (e.keyCode === 13 && modal.scope.checkCorrect) { 84 | submitModal(); 85 | } 86 | }, true); 87 | 88 | function submitModal() { 89 | modal.modal.close(modal.scope.values.passphrase); 90 | return false; 91 | } 92 | } 93 | } 94 | 95 | 96 | }); 97 | -------------------------------------------------------------------------------- /management/app/scripts/services/modals/pinModalService.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp').service('pinModalService', function ( 4 | $rootScope, 5 | $document, 6 | modalOpener, 7 | $modal) { 8 | 9 | 'use strict'; 10 | 11 | this.openedModal = null; 12 | 13 | this.closeOpen = function () { 14 | if (this.openedModal != null) { 15 | this.openedModal.modal.close(); 16 | } 17 | this.openedModal = null; 18 | } 19 | 20 | /** 21 | * Ask the user to set the device PIN. 22 | * 23 | * Bind keypress events that allow the user to control the number 24 | * buttons (dial) using a keyboard. 25 | * 26 | * @param {Event} event Event object 27 | * @param {TrezorDevice} dev Device 28 | * @param {String} type Action type. Possible values: 29 | * - 'PinMatrixRequestType_Current' 30 | * - 'PinMatrixRequestType_NewFirst' 31 | * - 'PinMatrixRequestType_NewSecond' 32 | * @param {Function} callback Called as `callback(err, res)` 33 | */ 34 | this.showModal = function ($scope, e, dev, type, callback) { 35 | 36 | if (dev.id !== $scope.device.id) 37 | return; 38 | 39 | var modal = modalOpener.openModal($scope, 'pin', 'sm', { 40 | pin: '', 41 | type: type 42 | }, true); 43 | 44 | this.openedModal = modal; 45 | 46 | modal.scope.addPin = function (num) { 47 | modal.scope.pin = modal.scope.pin + num.toString(); 48 | /* 49 | * When the user clicks a number button, the button gets focus. 50 | * Then when the user presses Enter it triggers another click on the 51 | * button instead of submiting the whole Pin Modal. Therefore we need 52 | * to focus the document after each click on a number button. 53 | */ 54 | $document.focus(); 55 | }; 56 | 57 | modal.scope.delPin = function () { 58 | modal.scope.pin = modal.scope.pin.slice(0, -1); 59 | }; 60 | 61 | modal.scope.isPinSet = function () { 62 | return modal.scope.pin.length > 0; 63 | }; 64 | 65 | 66 | $document.on('keydown', _pinKeydownHandler); 67 | $document.focus(); 68 | 69 | modal.result.then( 70 | function (res) { 71 | $document.off('keydown', _pinKeydownHandler); 72 | callback(null, res); 73 | }, 74 | function (err) { 75 | $document.off('keydown', _pinKeydownHandler); 76 | callback(err); 77 | } 78 | ); 79 | 80 | function _pinKeydownHandler(e) { 81 | var k = e.which, 82 | num; 83 | if (k === 8) { // Backspace 84 | modal.scope.delPin(); 85 | modal.scope.$digest(); 86 | return false; 87 | } else if (k === 13) { // Enter 88 | modal.modal.close(modal.scope.pin); 89 | return false; 90 | } else if (_isNumericKey(k)) { 91 | num = _getNumberFromKey(k); 92 | modal.scope.addPin(String.fromCharCode(num)); 93 | modal.scope.$digest(); 94 | } 95 | } 96 | 97 | function _isNumericKey(k) { 98 | return (k >= 49 && k <= 57) || (k >= 97 && k <= 105); 99 | } 100 | 101 | function _getNumberFromKey(k) { 102 | return (k >= 97) ? (k - (97 - 49)) : k; 103 | } 104 | } 105 | 106 | 107 | }); 108 | -------------------------------------------------------------------------------- /management/app/scripts/services/modals/setupModalService.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp').service('setupModalService', function ( 4 | $rootScope, 5 | modalOpener, 6 | $modal) { 7 | 8 | 'use strict'; 9 | 10 | this.openedModal = null; 11 | 12 | this.closeOpen = function () { 13 | if (this.openedModal != null) { 14 | this.openedModal.close(); 15 | } 16 | this.openedModal = null; 17 | } 18 | 19 | this.showModal = function ($scope) { 20 | this.openedModal = $modal.open({ 21 | templateUrl: 'views/modal/setup.html', 22 | size: 'lg', 23 | windowClass: 'buttonmodal', 24 | backdrop: 'static', 25 | keyboard: false, 26 | scope: $scope 27 | }); 28 | modalOpener.stopBackpaceOnModal(this.openedModal); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /management/app/scripts/services/slider.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp').directive('slider', function () { 4 | 'use strict'; 5 | 6 | return { 7 | require: 'ngModel', 8 | link: function (scope, element, attr, ngModel) { 9 | element 10 | .on('slide', function (ev) { 11 | ngModel.$setViewValue(ev.value); 12 | }) 13 | .trigger({ 14 | type: 'slide', 15 | value: element.attr('data-slider-value') 16 | }) 17 | .slider(); 18 | } 19 | }; 20 | }); 21 | -------------------------------------------------------------------------------- /management/app/scripts/services/udevError.js: -------------------------------------------------------------------------------- 1 | angular.module('webwalletApp').service('udevError', function ( 2 | trezor, 3 | trezorApi, 4 | flash, 5 | $q 6 | ) { 7 | 'use strict'; 8 | 9 | 10 | this._makesSenseAsking = function () { 11 | if (!(trezor instanceof trezorApi.ChromeExtensionTransport)) { 12 | return false; 13 | } 14 | if (window.navigator.platform.lastIndexOf("Linux") !== 0) { 15 | return false; 16 | } 17 | // we are on "linux" - but it can also be chrome OS! 18 | // We have to detect if it's chromeos 19 | if (window.navigator.userAgent.indexOf("CrOS") === -1) { 20 | return true; 21 | } else { 22 | return false; 23 | } 24 | } 25 | 26 | // this is "cached" error status 27 | this.errorStatus = false; 28 | 29 | // this returns promise with an updated error status, 30 | // also sets up this.errorstatus 31 | this.getCurrentErrorStatus = function () { 32 | var res; 33 | if (!this._makesSenseAsking()) { 34 | res = $q.when(false); 35 | } else { 36 | res = trezor.udevStatus().then(function (udevStatus) { 37 | if (udevStatus === "display") { 38 | return true; 39 | } else { 40 | return false; 41 | } 42 | }) 43 | } 44 | res.then(function (errorStatus) { 45 | this.errorStatus = errorStatus; 46 | }.bind(this)); 47 | return res; 48 | } 49 | 50 | //this gets called on start 51 | this.getCurrentErrorStatus(); 52 | 53 | }) 54 | -------------------------------------------------------------------------------- /management/app/scripts/services/utils.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | 3 | angular.module('webwalletApp') 4 | .value('_', window._) 5 | .value('Buffer', window.vendor.Buffer) 6 | .value('ecurve', window.vendor.ecurve) 7 | .value('bitcoin', window.vendor.bitcoin) 8 | .value('base58check', window.vendor.bs58check); 9 | 10 | angular.module('webwalletApp').factory('selecter', function () { 11 | return { 12 | selectRange: function (elem) { 13 | var selection, range, 14 | document = window.document, 15 | body = document.body; 16 | 17 | if (body.createTextRange) { // ms 18 | range = body.createTextRange(); 19 | range.moveToElementText(elem); 20 | range.select(); 21 | return; 22 | } 23 | 24 | if (window.getSelection) { // moz, opera, webkit 25 | selection = window.getSelection(); 26 | range = document.createRange(); 27 | range.selectNodeContents(elem); 28 | selection.removeAllRanges(); 29 | selection.addRange(range); 30 | return; 31 | } 32 | } 33 | 34 | }; 35 | }); 36 | 37 | angular.module('webwalletApp').directive('debug', function () { 38 | return { 39 | restrict: 'E', 40 | transclude: true, 41 | template: '
', 42 | controller: function (config, $scope) { 43 | $scope.debug = config.debug; 44 | } 45 | }; 46 | }); 47 | 48 | angular.module('webwalletApp').filter('amount', function (utils) { 49 | var MIN_DIGITS = 2; 50 | var PAD_DIGITS = 8; 51 | 52 | return function (amount, sign) { 53 | var str = utils.amount2str(amount); 54 | 55 | // find the fraction dot 56 | var dot = str.indexOf('.'); 57 | if (dot < 0) { 58 | str = str + '.'; 59 | dot = str.length - 1; 60 | } 61 | 62 | // make sure we have at least MIN_DIGITS 63 | var digits = str.length - dot - 1; 64 | if (digits < MIN_DIGITS) { 65 | str = str + new Array(MIN_DIGITS - digits + 1).join('0'); 66 | digits = MIN_DIGITS; 67 | } 68 | 69 | // pad with spaces 70 | var padding = PAD_DIGITS - digits; 71 | if (padding > 0) { 72 | str = str + new Array(padding + 1).join(' '); 73 | } 74 | 75 | // prepend the sign 76 | if (sign && str >= 0) { 77 | str = '+' + str; 78 | } 79 | 80 | return str; 81 | }; 82 | }); 83 | 84 | angular.module('webwalletApp').filter('ordinal', function () { 85 | return function (val) { 86 | var i = 'th'; 87 | 88 | switch (+val % 10) { 89 | case 1: 90 | i = 'st'; 91 | break; 92 | case 2: 93 | i = 'nd'; 94 | break; 95 | case 3: 96 | i = 'rd'; 97 | break; 98 | } 99 | switch (+val % 100) { 100 | case 11: 101 | case 12: 102 | case 13: 103 | i = 'th'; 104 | break; 105 | } 106 | 107 | return '' + val + i; 108 | }; 109 | }); 110 | 111 | angular.module('webwalletApp').filter('bip32Path', function () { 112 | return function (val) { 113 | return 'm/' + val.map(function (x) { 114 | return (x & 0x80000000) ? (x & 0x7FFFFFFF) + "'" : x; 115 | }).join('/'); 116 | }; 117 | }); 118 | 119 | angular.module('webwalletApp').service('utils', function Utils( 120 | config, 121 | trezor, 122 | trezorApi, 123 | ecurve, 124 | bitcoin, 125 | base58check, 126 | Buffer, 127 | _, 128 | $q, 129 | $log, 130 | $http, 131 | $interval, 132 | $timeout, 133 | $location, 134 | $rootScope) { 135 | 136 | // 137 | // codecs 138 | // 139 | 140 | function stringToBytes(str) { 141 | return new Buffer(str, 'binary'); 142 | } 143 | 144 | function bytesToString(bytes) { 145 | return new Buffer(bytes).toString('binary'); 146 | } 147 | 148 | function base64ToBytes(str) { 149 | return new Buffer(str, 'base64'); 150 | } 151 | 152 | function bytesToBase64(bytes) { 153 | return new Buffer(bytes).toString('base64'); 154 | } 155 | 156 | function hexToBytes(str) { 157 | return new Buffer(str, 'hex'); 158 | } 159 | 160 | function bytesToHex(bytes) { 161 | return new Buffer(bytes).toString('hex'); 162 | } 163 | 164 | function utf8ToHex(utf8) { 165 | var str = unescape(encodeURIComponent(utf8)); 166 | return bytesToHex(stringToBytes(str)); 167 | } 168 | 169 | function hexToUtf8(hex) { 170 | var str = bytesToString(hexToBytes(hex)); 171 | return decodeURIComponent(escape(str)); 172 | } 173 | 174 | this.stringToBytes = stringToBytes; 175 | this.bytesToString = bytesToString; 176 | 177 | this.base64ToBytes = base64ToBytes; 178 | this.bytesToBase64 = bytesToBase64; 179 | 180 | this.hexToBytes = hexToBytes; 181 | this.bytesToHex = bytesToHex; 182 | 183 | this.utf8ToHex = utf8ToHex; 184 | this.hexToUtf8 = hexToUtf8; 185 | 186 | // 187 | // numeric amounts 188 | // 189 | 190 | function amount2str(n) { 191 | return (n / 100000000).toString(); 192 | } 193 | 194 | function str2amount(s) { 195 | // check the decimal places 196 | var parts = s.split('.'); 197 | if (parts.length === 2) { 198 | var decpart = parts[1]; 199 | if (decpart.length > 8) { 200 | throw new TypeError("Amount has too many decimals"); 201 | } 202 | } 203 | 204 | // convert to number 205 | var n = +s; 206 | var res = Math.round(n * 1e8); 207 | if (isNaN(res)) { 208 | throw new TypeError("Amount is not a number"); 209 | } 210 | 211 | return res; 212 | } 213 | 214 | this.amount2str = amount2str; 215 | this.str2amount = str2amount; 216 | 217 | // 218 | // crypto 219 | // 220 | 221 | function sha256x2(value) { 222 | return bitcoin.crypto.hash256(value); 223 | } 224 | 225 | this.sha256x2 = sha256x2; 226 | 227 | // 228 | // http 229 | // 230 | 231 | function httpPoll(config, throttle) { 232 | var deferred = $q.defer(), 233 | promise = deferred.promise, 234 | cancelled = false, 235 | request; 236 | 237 | promise.cancel = function () { 238 | cancelled = true; 239 | }; 240 | 241 | request = _.throttle(function () { 242 | $http(config) 243 | .then(function (res) { 244 | if (!cancelled) { 245 | deferred.notify(res); 246 | request(); 247 | } 248 | }) 249 | .catch(deferred.reject); 250 | }, throttle); 251 | 252 | request(); 253 | 254 | return promise; 255 | } 256 | 257 | this.httpPoll = httpPoll; 258 | 259 | // 260 | // hdnode 261 | // 262 | 263 | // decode private key from xprv base58 string to hdnode structure 264 | function xprv2node(xprv) { 265 | var bytes = base58check.decode(xprv), 266 | hex = bytesToHex(bytes), 267 | node = {}; 268 | 269 | if (hex.substring(90, 92) !== '00') 270 | throw new Error('Contains invalid private key'); 271 | 272 | node.depth = parseInt(hex.substring(8, 10), 16); 273 | node.fingerprint = parseInt(hex.substring(10, 18), 16); 274 | node.child_num = parseInt(hex.substring(18, 26), 16); 275 | node.chain_code = hex.substring(26, 90); 276 | node.private_key = hex.substring(92, 156); // skip 0x00 indicating privkey 277 | 278 | return node; 279 | } 280 | 281 | // decode public key from xpub base58 string to hdnode structure 282 | function xpub2node(xpub) { 283 | var bytes = base58check.decode(xpub), 284 | hex = bytesToHex(bytes), 285 | node = {}; 286 | 287 | node.depth = parseInt(hex.substring(8, 10), 16); 288 | node.fingerprint = parseInt(hex.substring(10, 18), 16); 289 | node.child_num = parseInt(hex.substring(18, 26), 16); 290 | node.chain_code = hex.substring(26, 90); 291 | node.public_key = hex.substring(90, 156); 292 | 293 | return node; 294 | } 295 | 296 | // encode public key hdnode to xpub base58 string 297 | function node2xpub(node, version) { 298 | var hex, bytes, xpub; 299 | 300 | hex = hexpad(version, 8) + hexpad(node.depth, 2) + hexpad(node.fingerprint, 8) + hexpad(node.child_num, 8) + node.chain_code + node.public_key; 301 | 302 | bytes = hexToBytes(hex); 303 | xpub = base58check.encode(bytes); 304 | return xpub; 305 | 306 | function hexpad(n, l) { 307 | var s = parseInt(n).toString(16); 308 | while (s.length < l) s = '0' + s; 309 | return s; 310 | } 311 | } 312 | 313 | function node2address(node, type) { 314 | var pubkey = node.public_key, 315 | bytes = hexToBytes(pubkey), 316 | hash = bitcoin.crypto.hash160(bytes); 317 | 318 | return address2str(hash, type); 319 | } 320 | 321 | function address2str(hash, version) { 322 | var csum, 323 | bytes, 324 | hashWithVersion, 325 | versionHash = new Buffer(1); 326 | 327 | versionHash[0] = +version; 328 | bytes = Buffer.concat([versionHash, hash]); 329 | 330 | return base58check.encode(bytes); 331 | } 332 | 333 | function decodeAddress(address) { 334 | var bytes, hash, csum; 335 | 336 | bytes = base58check.decode(address); 337 | hash = bytes.slice(0, 21); 338 | 339 | return { 340 | version: bytes[0], 341 | hash: bytes.slice(1) 342 | }; 343 | } 344 | 345 | function deriveChildNode(node, index) { 346 | var key, 347 | child, 348 | child2 349 | 350 | child = _deriveChildNode(node, index); 351 | _normalizeNode(child); 352 | 353 | if (trezor instanceof trezorApi.PluginTransport) { 354 | child2 = trezor.deriveChildNode(node, index); 355 | _normalizeNode(child2); 356 | 357 | if (!(_.isEqual(child, child2))) { 358 | $log.error('CKD check failed', { 359 | parent: node, 360 | jsChild: child, 361 | pluginChild: child2 362 | }) 363 | throw new Error('Child node derivation failed'); 364 | } 365 | } 366 | 367 | return child; 368 | } 369 | 370 | function _normalizeNode(node) { 371 | node.public_key = node.public_key.toUpperCase(); 372 | node.chain_code = node.chain_code.toUpperCase(); 373 | node.fingerprint = node.fingerprint.toString(); 374 | node.child_num = node.child_num.toString(); 375 | node.depth = node.depth.toString(); 376 | } 377 | 378 | function _deriveChildNode(node, index) { 379 | var parent = _node2bjsNode(node), 380 | child = _bjsNode2Node(parent.derive(index)); 381 | child.path = node.path.concat([index]); 382 | return child; 383 | } 384 | 385 | function _node2bjsNode(node) { 386 | var chainCode = new Buffer(node.chain_code, 'hex'); 387 | var publicKey = new Buffer(node.public_key, 'hex'); 388 | 389 | var Q = ecurve.Point.decodeFrom(bitcoin.ECPubKey.curve, publicKey); 390 | var bjsNode = new bitcoin.HDNode(Q, chainCode); 391 | 392 | bjsNode.path = node.path; 393 | bjsNode.depth = +node.depth; 394 | bjsNode.index = node.child_num; 395 | bjsNode.parentFingerprint = node.fingerprint; 396 | 397 | return bjsNode; 398 | } 399 | 400 | function _bjsNode2Node(bjsNode) { 401 | return { 402 | path: bjsNode.path, 403 | depth: bjsNode.depth, 404 | child_num: bjsNode.index, 405 | fingerprint: bjsNode.parentFingerprint, 406 | public_key: bjsNode.pubKey.toHex(), 407 | chain_code: bjsNode.chainCode.toString('hex') 408 | }; 409 | } 410 | 411 | this.xprv2node = xprv2node; 412 | this.xpub2node = xpub2node; 413 | this.node2xpub = node2xpub; 414 | this.node2address = node2address; 415 | this.address2str = address2str; 416 | this.decodeAddress = decodeAddress; 417 | this.deriveChildNode = deriveChildNode; 418 | 419 | // 420 | // promise utils 421 | // 422 | 423 | // returns a promise that gets notified every n msec 424 | function tick(n) { 425 | return $interval(null, n); 426 | } 427 | 428 | // keeps calling fn while the returned promise is being rejected 429 | // fn can cancel by returning falsey 430 | // if given delay, waits for delay msec before calling again 431 | // if given max, gives up after max attempts and rejects with 432 | // the latest error 433 | function endure(fn, delay, max) { 434 | var pr = fn(); 435 | 436 | if (!pr) 437 | return $q.reject('Cancelled'); 438 | 439 | return pr.then(null, function (err) { 440 | 441 | if (max !== undefined && max < 1) // we have no attempt left 442 | throw err; 443 | 444 | var retry = function () { 445 | return endure(fn, delay, max ? max - 1 : max); 446 | }; 447 | 448 | return $timeout(retry, delay); // retry after delay 449 | }); 450 | } 451 | 452 | this.tick = tick; 453 | this.endure = endure; 454 | 455 | /** 456 | * Redirect to passed path. 457 | * 458 | * @param {String} path Path to redirect to 459 | * @return {Promise} Promise that is resolved after the redirection is 460 | * complete. 461 | */ 462 | function redirect(path) { 463 | var deferred = $q.defer(), 464 | off; 465 | if ($location.path() !== path) { 466 | $location.path(path); 467 | off = $rootScope.$on('$locationChangeSuccess', function () { 468 | deferred.resolve(); 469 | off(); 470 | }); 471 | } else { 472 | deferred.resolve(); 473 | } 474 | return deferred.promise; 475 | } 476 | 477 | this.redirect = redirect; 478 | 479 | }); 480 | 481 | /* 482 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round 483 | */ 484 | 485 | // Closure 486 | (function () { 487 | 488 | /** 489 | * Decimal adjustment of a number. 490 | * 491 | * @param {String} type The type of adjustment. 492 | * @param {Number} value The number. 493 | * @param {Integer} exp The exponent (the 10 logarithm of the adjustment base). 494 | * @returns {Number} The adjusted value. 495 | */ 496 | function decimalAdjust(type, value, exp) { 497 | // If the exp is undefined or zero... 498 | if (typeof exp === 'undefined' || +exp === 0) { 499 | return Math[type](value); 500 | } 501 | value = +value; 502 | exp = +exp; 503 | // If the value is not a number or the exp is not an integer... 504 | if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) { 505 | return NaN; 506 | } 507 | // Shift 508 | value = value.toString().split('e'); 509 | value = Math[type](+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp))); 510 | // Shift back 511 | value = value.toString().split('e'); 512 | return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp)); 513 | } 514 | 515 | // Decimal round 516 | if (!Math.round10) { 517 | Math.round10 = function (value, exp) { 518 | return decimalAdjust('round', value, exp); 519 | }; 520 | } 521 | // Decimal floor 522 | if (!Math.floor10) { 523 | Math.floor10 = function (value, exp) { 524 | return decimalAdjust('floor', value, exp); 525 | }; 526 | } 527 | // Decimal ceil 528 | if (!Math.ceil10) { 529 | Math.ceil10 = function (value, exp) { 530 | return decimalAdjust('ceil', value, exp); 531 | }; 532 | } 533 | 534 | })(); 535 | -------------------------------------------------------------------------------- /management/app/scripts/templates.error.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trezor-graveyard/trezor-chrome-extension/dd7c8859af6ccd7beeed32ada3c5f169cfa12194/management/app/scripts/templates.error.js -------------------------------------------------------------------------------- /management/app/scripts/templates.webwallet.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trezor-graveyard/trezor-chrome-extension/dd7c8859af6ccd7beeed32ada3c5f169cfa12194/management/app/scripts/templates.webwallet.js -------------------------------------------------------------------------------- /management/app/styles/angular-csp.css: -------------------------------------------------------------------------------- 1 | /* Include this file in your html if you are using the CSP mode. */ 2 | 3 | @charset "UTF-8"; 4 | 5 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], 6 | .ng-cloak, .x-ng-cloak, 7 | .ng-hide:not(.ng-hide-animate) { 8 | display: none !important; 9 | } 10 | 11 | ng\:form { 12 | display: block; 13 | } 14 | 15 | .ng-animate-shim { 16 | visibility:hidden; 17 | } 18 | 19 | .ng-anchor { 20 | position:absolute; 21 | } 22 | -------------------------------------------------------------------------------- /management/app/styles/main.css: -------------------------------------------------------------------------------- 1 | /* Layout, header, footer */ 2 | 3 | html { 4 | position: relative; 5 | overflow-y: scroll; 6 | min-height: 100%; 7 | } 8 | 9 | body { 10 | margin: 0 0 120px; /* bottom = footer height */ 11 | } 12 | 13 | [ng-cloak] { 14 | display: none !important; 15 | } 16 | 17 | .topbar { 18 | margin-bottom:0px; 19 | } 20 | 21 | 22 | .page-header .navbar-brand { 23 | padding: 0; 24 | padding-left: 30px; 25 | margin-top: 4px; 26 | } 27 | 28 | .page-container { 29 | padding-top: 5px; 30 | } 31 | 32 | .page-footer { 33 | position: absolute; 34 | left: 0; 35 | right: 0; 36 | bottom: 0; 37 | height: 4em; 38 | padding-left: 30px; 39 | color: #999; 40 | font-size: smaller; 41 | } 42 | 43 | .page-footer a { 44 | color: #999; 45 | } 46 | 47 | /* Text utils */ 48 | 49 | .text-capitals { 50 | text-transform: capitalize; 51 | } 52 | 53 | .text-break { 54 | word-break: break-all; 55 | } 56 | 57 | .text-monospace { 58 | font-family: monospace; 59 | } 60 | 61 | /* Override default elements */ 62 | 63 | .help-block { 64 | color: #999; 65 | } 66 | 67 | .form-inline .help-block { 68 | display: inline-block; 69 | margin-left: 20px; 70 | } 71 | 72 | .lead { 73 | font-size: 16px; 74 | } 75 | 76 | .jumbotron, 77 | .container .jumbotron { 78 | padding: 32px; 79 | margin-top: 32px; 80 | } 81 | 82 | .tooltip-trigger { 83 | display: inline-block; 84 | } 85 | 86 | .tooltip.tooltip-danger.right .tooltip-arrow { 87 | border-right-color: #A94442; 88 | } 89 | 90 | .tooltip.tooltip-danger .tooltip-inner { 91 | background-color: #A94442; 92 | border-color: #A94442; 93 | color: #FFFFFF; 94 | } 95 | 96 | .jumbotron-white { 97 | background: white; 98 | color: rgb(92, 92, 92); 99 | } 100 | 101 | .jumbotron-nomargin { 102 | margin-top: 0px !important; 103 | } 104 | 105 | 106 | /* Override rounded corners */ 107 | 108 | .form-control, 109 | .input-lg, 110 | .btn, 111 | .alert, 112 | .container .jumbotron, 113 | .dropdown-menu, 114 | .modal-content, 115 | .nav-tabs > li > a, 116 | .btn-group-lg > .btn, 117 | .list-group-item:first-child, 118 | .list-group-item:last-child { 119 | border-radius: 0; 120 | } 121 | 122 | /* Icons */ 123 | 124 | @font-face { 125 | font-family: 'trezorfont'; 126 | src: url('../fonts/trezorfont.eot') format('embedded-opentype'), 127 | url('../fonts/trezorfont.woff') format('woff'), 128 | url('../fonts/trezorfont.ttf') format('truetype'); 129 | } 130 | 131 | .glyphicon-trezor, 132 | .glyphicon-gears, 133 | .glyphicon-connect-trezor, 134 | .glyphicon-lock, 135 | .glyphicon-unlock { 136 | position: relative; 137 | top: 1px; 138 | display: inline-block; 139 | font-family: "trezorfont"; 140 | font-style: normal; 141 | font-weight: normal; 142 | line-height: 1; 143 | } 144 | 145 | .glyphicon-trezor:before { 146 | content: "\5f"; 147 | } 148 | 149 | .glyphicon-gears:before { 150 | content: "\a0"; 151 | } 152 | 153 | .glyphicon-connect-trezor:before { 154 | content: "\2009"; 155 | } 156 | 157 | .glyphicon-lock:before { 158 | content: "\2003"; 159 | font-size:125%; 160 | } 161 | 162 | .dev-lock:hover .glyphicon-lock:before { 163 | content: "\2002"; 164 | font-size:125%; 165 | } 166 | 167 | /* Inputs */ 168 | 169 | .input-with-icon { 170 | display: inline-block; 171 | position: relative; 172 | } 173 | 174 | .input-icon { 175 | position: absolute; 176 | font-size: 18px; 177 | right: 7px; 178 | top: 9px; 179 | } 180 | 181 | .form-horizontal .input-with-icon { 182 | display: block; 183 | } 184 | 185 | /* Alerts */ 186 | 187 | .alert { 188 | border: 0; 189 | padding: 15px 20px; 190 | } 191 | 192 | .alert > h3:last-child, 193 | .alert > h4:last-child, 194 | .alert > h5:last-child { 195 | margin-top: 10px; 196 | } 197 | 198 | /* Alert icon */ 199 | 200 | .alert-withicon { 201 | position: relative; 202 | padding-left: 4em; 203 | } 204 | 205 | .alert-icon { 206 | position: absolute; 207 | top: 50%; 208 | left: 0.5em; 209 | margin-top: -0.5em; 210 | font-size: 200%; 211 | opacity: 0.3; 212 | } 213 | 214 | .alert-withicon.alert-lg { 215 | padding-left: 8em; 216 | } 217 | 218 | .alert-withicon.alert-lg .alert-icon { 219 | font-size: 400%; 220 | } 221 | 222 | /* Log alert */ 223 | 224 | .alert .text-monospace { 225 | display: inline-block; 226 | word-break: break-all; 227 | } 228 | 229 | .alert textarea { 230 | width: 100%; 231 | height: 4em; 232 | } 233 | 234 | .alert .btn-link { 235 | color: #000; 236 | opacity: 0.2; 237 | } 238 | 239 | /* Alert buttons */ 240 | 241 | .alert-buttons { 242 | padding-top: 0.6em; 243 | } 244 | 245 | .alert-buttons .btn-link { 246 | opacity: 0.5; 247 | } 248 | 249 | /* Modals */ 250 | 251 | .modal-dialog { 252 | width: 400px; 253 | } 254 | 255 | .modal-sm { 256 | width: 300px; 257 | } 258 | 259 | .modal-lg { 260 | width: 600px; 261 | } 262 | 263 | .modal-footer { 264 | margin-top: 0; 265 | } 266 | 267 | .modal-body > .alert:last-child { 268 | margin-bottom: 0; 269 | } 270 | 271 | .modal-upper-header { 272 | background-color: rgb(66, 139, 202); 273 | color: white; 274 | padding-top: 8px; 275 | padding-left: 16px; 276 | padding-bottom: 8px; 277 | } 278 | 279 | .modal-upper-header p { 280 | margin: 0px; 281 | } 282 | 283 | .modal-naked .modal-content { 284 | border: 0; 285 | box-shadow: none; 286 | -webkit-box-shadow: none; 287 | } 288 | 289 | /* Installer dialog */ 290 | 291 | .installers { 292 | background: none; 293 | } 294 | 295 | .installers h3 { 296 | margin-bottom: 20px 297 | } 298 | 299 | .installers-chrome-primary-form { 300 | margin-top: 10px 301 | } 302 | 303 | .installers hr { 304 | margin-top: 50px 305 | } 306 | 307 | .installers-info { 308 | margin-bottom: 40px !important 309 | } 310 | 311 | .installers-muted { 312 | margin-top: 40px; 313 | font-size: 18px !important 314 | } 315 | .installers-footer { 316 | margin-top: 50px; 317 | font-size: 12px !important; 318 | padding: 0 100px 319 | } 320 | 321 | /* Udev dialog */ 322 | 323 | .udev-number { 324 | display: inline; 325 | font-size: 27px !important; 326 | margin-right: 20px; 327 | } 328 | 329 | .udev-number-connect { 330 | display: inline; 331 | font-size: 27px !important; 332 | margin-right: 50px; 333 | } 334 | 335 | .udev-glyphicon { 336 | font-size:63px; 337 | top:20px; 338 | width:70px 339 | } 340 | 341 | .udev-text-connect { 342 | display:inline; 343 | font-size:20px 344 | } 345 | 346 | /* Animations */ 347 | 348 | .modal-backdrop.am-fade { 349 | opacity: .5; 350 | transition: opacity .15s linear; 351 | } 352 | 353 | .modal-backdrop.am-fade.ng-enter { 354 | opacity: 0; 355 | } 356 | 357 | .modal-backdrop.am-fade.ng-enter-active { 358 | opacity: .5; 359 | } 360 | 361 | .modal-backdrop.am-fade.ng-leave { 362 | opacity: .5; 363 | } 364 | 365 | .modal-backdrop.am-fade.ng-leave-active { 366 | opacity: 0; 367 | } 368 | 369 | /* Tab navigation */ 370 | 371 | .nav-tabs .nav-tab-small { 372 | float: right; 373 | } 374 | 375 | /* Line divider component */ 376 | 377 | .line-divider { 378 | margin: 20px 0; 379 | position: relative; 380 | } 381 | 382 | .line-divider > hr { 383 | position: absolute; 384 | top: 50%; 385 | width: 100%; 386 | margin: 0; 387 | } 388 | 389 | .line-divider > .line-divider-content { 390 | position: relative; 391 | } 392 | 393 | .slider-horizontal { 394 | width: 100% !important; 395 | } 396 | 397 | /* Device screen component */ 398 | 399 | .samp-screen { 400 | display: block; 401 | padding: 9.5px; 402 | margin: 10px 0 0; 403 | font-size: 13px; 404 | line-height: 1.42857143; 405 | background: #333333; 406 | color: #ffffff; 407 | } 408 | 409 | /* Error app, installer modal */ 410 | 411 | .btn-installer-download .glyphicon { 412 | top: 2px; 413 | } 414 | 415 | .btn-installer-platform { 416 | opacity: 0.7; 417 | } 418 | 419 | /* Device nav */ 420 | 421 | 422 | /* Application logs */ 423 | 424 | .alert-logs textarea { 425 | font-size: 80%; 426 | } 427 | 428 | .alert-device-details { 429 | width: 500px; 430 | } 431 | 432 | .wipe-warning { 433 | color: #D8100A; 434 | } 435 | 436 | /* Alert bar */ 437 | 438 | .alertbar { 439 | margin: 0; 440 | padding: 12px; 441 | } 442 | 443 | .alertbar .btn { 444 | margin-left: 10px; 445 | } 446 | 447 | /* Overlays */ 448 | 449 | .overlay-container { 450 | position: relative; 451 | } 452 | 453 | .overlay { 454 | position: absolute; 455 | top: 0; 456 | left: 0; 457 | right: 0; 458 | bottom: 0; 459 | text-align: center; 460 | } 461 | 462 | .overlay-default { 463 | background: rgba(255, 255, 255, 0.8); 464 | } 465 | 466 | .overlay-content { 467 | position: absolute; 468 | top: 40%; 469 | left: 0; 470 | right: 0; 471 | } 472 | 473 | .overlay-max-wrapper { 474 | position: absolute; 475 | height: 500px; 476 | width: 100%; 477 | max-height: 100%; 478 | } 479 | 480 | 481 | /* Device setup & restore */ 482 | 483 | .devsetup { 484 | padding-right: 30px; 485 | border-right: 1px solid #EEE; 486 | } 487 | 488 | .devsetup-strength { 489 | margin-top: 10px; 490 | } 491 | 492 | .devsetup-strength-label { 493 | display: block; 494 | text-transform: uppercase; 495 | font-size: 10px; 496 | } 497 | 498 | .devrestore { 499 | padding: 20px; 500 | } 501 | 502 | /* PIN modal */ 503 | 504 | .pinmodal .modal-body { 505 | padding-bottom: 0; 506 | } 507 | 508 | .pinmodal .modal-footer { 509 | margin-top: 0; 510 | } 511 | 512 | .pinmodal .form-group { 513 | margin: 0; 514 | } 515 | 516 | .pinmodal .col-sm-4 .btn { 517 | width: 83px; 518 | height: 83px; 519 | } 520 | 521 | .pinmodal .col-sm-4 .btn:focus { 522 | outline: 0; 523 | } 524 | 525 | .pinmodal-dial .form-group { 526 | margin-left: -5px; 527 | margin-right: -5px; 528 | margin-bottom: 10px; 529 | } 530 | 531 | .pinmodal-dial .col-sm-4 { 532 | padding-left: 5px; 533 | padding-right: 5px; 534 | } 535 | 536 | .pinmodal-pin { 537 | position: relative; 538 | margin: 0.5em 0; 539 | padding-right: 50px; 540 | } 541 | 542 | .pinmodal-pin-value { 543 | height: 1.2em; 544 | padding-top: 0.05em; 545 | font-size: 200%; 546 | line-height: 1; 547 | overflow: hidden; 548 | text-overflow: ellipsis; 549 | } 550 | 551 | .pinmodal-pin .btn-back { 552 | position: absolute; 553 | right: 0; 554 | top: 0; 555 | } 556 | 557 | .pinmodal-rating { 558 | padding-bottom: 0.5em; 559 | } 560 | 561 | /* Backspace button used in PIN modal */ 562 | 563 | .btn-back { 564 | position: relative; 565 | height: 28px; 566 | color: #333; 567 | background: #e1e1e1; 568 | padding: 0 0.5em; 569 | line-height: 28px; 570 | } 571 | 572 | .btn-back:hover, 573 | .btn-back:focus, 574 | .btn-back:active { 575 | color: #000; 576 | background: #d9d9d9; 577 | } 578 | 579 | .btn-back .arrow { 580 | width: 0; 581 | height: 0; 582 | position: absolute; 583 | left: -15px; 584 | top: -1px; 585 | border-top: 14px solid transparent; 586 | border-bottom: 14px solid transparent; 587 | border-right: 14px solid #e1e1e1; 588 | } 589 | 590 | .btn-back:hover .arrow, 591 | .btn-back:focus .arrow, 592 | .btn-back:active .arrow { 593 | border-right-color: #d9d9d9; 594 | } 595 | 596 | 597 | /* Firmware */ 598 | 599 | .firmware-info { 600 | display: block; 601 | padding: 0.5em 1em; 602 | margin: 0px 0px 10px; 603 | color: #333; 604 | background-color: #F5F5F5; 605 | border: 1px solid #CCC; 606 | border-radius: 4px; 607 | } 608 | 609 | .firmware-info h5 { 610 | font-weight: bold; 611 | } 612 | 613 | .firmware-info dt { 614 | font-weight: normal; 615 | float: left; 616 | clear: both; 617 | } 618 | 619 | .firmware-changelog { 620 | white-space: pre; 621 | word-break: break-all; 622 | word-wrap: break-word; 623 | } 624 | 625 | 626 | /* Device connect page */ 627 | 628 | .main-icon { 629 | font-size: 600%; 630 | } 631 | 632 | .main-connect-icon { 633 | font-size: 1100%; 634 | } 635 | 636 | /* Device settings */ 637 | 638 | .tab-content { 639 | padding-top: 30px; 640 | } 641 | 642 | .tab-content .row { 643 | padding-bottom: 16px; 644 | } 645 | 646 | .settings-left { 647 | text-align: right; 648 | font-weight: bold; 649 | padding-top: 4px; 650 | } 651 | 652 | .settings-right { 653 | padding-top: 4px; 654 | } 655 | 656 | .settings-btn button { 657 | width: 100%; 658 | text-align: left; 659 | } 660 | 661 | .wipe-text { 662 | padding-top: 4px; 663 | } 664 | 665 | .btn-padding { 666 | padding-top: 4px; 667 | } 668 | 669 | @media (max-width: 1199px) and (min-width: 992px) { 670 | .small-responsive-btn { 671 | font-size: 80%; 672 | padding-left: 5px; 673 | } 674 | 675 | .small-responsive-span { 676 | font-size: 83% 677 | } 678 | } 679 | 680 | .loading-sign-info { 681 | position: absolute; 682 | right: 10px; 683 | bottom: 8px; 684 | opacity: 0.5; 685 | color: #428BCA; 686 | } 687 | 688 | .loading-sign-info-text { 689 | display:inline-block; 690 | position: relative; 691 | bottom: 1px; 692 | } 693 | 694 | 695 | .seed-words { 696 | min-height: 350px; 697 | } 698 | -------------------------------------------------------------------------------- /management/app/views/debug.button.html: -------------------------------------------------------------------------------- 1 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /management/app/views/debug.link.html: -------------------------------------------------------------------------------- 1 | 3 | Show log 4 | 5 | -------------------------------------------------------------------------------- /management/app/views/debug.log.html: -------------------------------------------------------------------------------- 1 |
3 |
5 | 10 |

Log

11 | 12 |

Warning: This log can contain your XPUB. When you give it 13 | to a third party, you allow it to see your whole transaction 14 | history.

15 |
16 |
17 | -------------------------------------------------------------------------------- /management/app/views/device/advanced.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |
7 | 8 |
9 |
10 | Firmware 11 |
12 |
13 |

{{device.firmwareString()}}

14 |

15 | Your device has the latest firmware. If you still want to load the latest firmware to your device again, 16 | disconnect your device and then plug it 17 | in while holding both device buttons pressed. 18 |

19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 | Passphrase 27 |
28 | 29 | 30 |
33 |

34 | Passphrase encryption allows you to access new wallets, 35 | each hidden behind a particular passphrase. Your old 36 | accounts will be accessible behind an empty passphrase. 37 |

38 |

39 | If you forget your passphrase, your wallet is lost for 40 | good. There is no way to recover your funds. 41 |

42 |

43 | Read more in user manual 44 |

45 |

46 | 51 |

52 | 59 | 65 | 66 |
67 | 68 | 69 |
72 |

73 | If you disable the passphrase encryption, your current 74 | funds will not appear. You will have to enable the 75 | passphrase encryption again to see your current wallet. 76 |

77 |

78 | Read more in user manual 79 |

80 |

81 | 86 |

87 | 94 | 100 | 101 |
102 |
103 | 104 |
105 | 106 | 109 | 110 |
111 |
112 | Disable PIN 113 |
114 | 115 |
123 | 129 |
130 |
131 |
135 |
136 |

137 | Using PIN protection is highly recommended. PIN prevents 138 | unauthorized persons from stealing your bitcoins even when 139 | they get physical access to your device. 140 |

141 |
142 | 143 |
144 | 145 | 146 | 149 | 150 | 151 |
152 |
153 | Wipe device 154 |
155 | 156 |
157 |

Wiping the device removes all its information.

158 |

Use this feature only if you have your Recovery Seed or 159 | you don't have any coins on your device.

160 | 161 |
162 | 176 |
177 |
178 |
179 | 180 |
181 | 182 |
183 | 184 |
185 | -------------------------------------------------------------------------------- /management/app/views/device/index.basic.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Label 5 |
6 |
7 | {{device.label()}} 8 |
9 |
15 | 21 |
22 |
23 | 24 |
25 |
26 | PIN protection 27 |
28 |
29 | Enabled 30 | Disabled 31 |
32 |
43 | 52 |
53 |
54 | 55 |
61 |
62 |

63 | Using PIN protection is highly recommended. PIN prevents unauthorized persons from stealing 64 | your bitcoins even when they get physical access to your device. 65 |

66 |
67 | 68 | 69 | 70 |
71 | -------------------------------------------------------------------------------- /management/app/views/device/index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 9 |
10 |

Want to restore your wallet from recovery seed?

11 | TREZOR Recovery 14 |
15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 | 33 | 34 | 35 |
36 | 37 | -------------------------------------------------------------------------------- /management/app/views/device/index.setup.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Welcome to TREZOR Setup!

4 |

5 | Please take your time to read all of the instructions. Setting up your TREZOR 6 | will take only a few minutes. When you're done, you can start using your TREZOR. 7 |

8 | 9 |
10 | 11 | 14 |

15 | This label will be shown on the display when you plug your TREZOR in. 16 | This is useful if you have more than one device. 17 |

18 |
19 | 20 | 21 |
22 | 24 |
25 | 26 |
27 | -------------------------------------------------------------------------------- /management/app/views/device/nav.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /management/app/views/device/recovery.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | Please connect your device to access this section. 5 |
6 | 7 |
9 | 10 |
13 |

Seed recovery

14 | 15 |
16 | Please follow the instructions on your device. 17 | Words are going to be entered in shuffled order.
18 | Also you'll be asked to retype some words that 19 | are not part of your recovery seed. 20 |
21 | 22 |
23 |
24 | 25 | 30 | 31 | 40 | 41 | Confirm choice by pressing enter. 43 | 44 |
45 |
46 | 47 |
Recovered seed words:
48 |
    49 |
  • {{word}}
  • 51 |
52 |
53 | 54 |
57 |
58 | 59 |
60 | 61 | 64 |
65 | 66 |
67 | 69 | 78 |
79 |
80 | 12 words 81 |
82 |
83 | 18 words 84 |
85 |
86 | 24 words 87 |
88 |
89 |
90 | 91 |
92 | 96 |
97 | 98 |
99 | 103 |
104 | 105 |
106 | 109 | Cancel 111 |
112 | 113 |
114 |
115 | 116 |
117 | 118 |
119 | -------------------------------------------------------------------------------- /management/app/views/device/wipe.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 | Please connect your device to access this section. 7 |
8 | 9 |
10 | 11 |
12 |
13 | 14 |

Do you really want to wipe the device?

15 |

All data from the device will be lost! You can recover your funds 16 | using recovery seed.

17 |

18 | 20 | Cancel 21 |

22 |
23 |
24 | 25 |
26 | 27 |
28 | -------------------------------------------------------------------------------- /management/app/views/deviceDetails.link.html: -------------------------------------------------------------------------------- 1 | 3 | Device details | 4 | 5 | -------------------------------------------------------------------------------- /management/app/views/error.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Transport loading failed

5 | {{errorMessage}} 6 | 7 |
8 | 9 |
10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /management/app/views/error.udev.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

One last step

Linux users need to do one last step to get started. You can:

4 |
    7 | 8 |
  • Go to TREZOR Wallet and follow instructions there
  • 9 |
  • Alternatively, you can do the following: 10 |
      11 |
    1. Disconnect your TREZOR device.
    2. 12 |
    3. Create new file /etc/udev/rules.d/51-trezor.rules in your favourite text editor (root privileges are necessary).
    4. 13 |
    5. Copy the following two lines, paste them into the file and save it.
    6. 14 | 15 |
    16 |
    17 |
    SUBSYSTEM=="usb", ATTR{idVendor}=="534c", ATTR{idProduct}=="0001", MODE="0666", GROUP="dialout", SYMLINK+="trezor%n"
    18 | KERNEL=="hidraw*", ATTRS{idVendor}=="534c", ATTRS{idProduct}=="0001",  MODE="0666", GROUP="dialout"
    19 |
    20 |
      21 |
    1. Reconnect your device.
    2. 22 |
    23 |
  • 24 |
25 |
26 | -------------------------------------------------------------------------------- /management/app/views/main.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |
6 |

This app is intended for a simple device management.

7 | 8 | 9 | 10 |

Connect your TREZOR to start.

11 |
12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /management/app/views/modal/button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 74 | -------------------------------------------------------------------------------- /management/app/views/modal/disconnect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 11 | 14 | 19 | -------------------------------------------------------------------------------- /management/app/views/modal/disconnect.wipe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 10 | -------------------------------------------------------------------------------- /management/app/views/modal/firmware.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 12 | 50 | 55 |
56 | 57 |
58 | 59 | 64 | 73 |
74 | 75 |
76 | 77 | 83 | 121 | 130 |
131 | 132 |
133 | 134 | 139 | 146 | 151 |
152 | 153 |
154 | 159 |
160 | 161 |
162 | 169 |
170 | 171 |
172 | 189 |
190 | 191 |
192 | 198 |
199 | 200 |
201 | 207 |
208 | 209 |
210 | -------------------------------------------------------------------------------- /management/app/views/modal/forget.disconnected.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 9 | 13 | -------------------------------------------------------------------------------- /management/app/views/modal/forget.requested.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 13 | 17 | -------------------------------------------------------------------------------- /management/app/views/modal/forget.wipe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 10 | 13 | -------------------------------------------------------------------------------- /management/app/views/modal/label.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 21 | -------------------------------------------------------------------------------- /management/app/views/modal/passphrase.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 59 | 64 | -------------------------------------------------------------------------------- /management/app/views/modal/pin.html: -------------------------------------------------------------------------------- 1 | 2 |
5 | 6 | 25 | 97 | 103 | 104 | 105 |
106 | -------------------------------------------------------------------------------- /management/app/views/modal/setup.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 42 | -------------------------------------------------------------------------------- /management/app/views/modal/upperheader.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /management/app/views/topBars.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
5 |
6 | New TREZOR firmware is available. 7 | Upgrade for the newest features and bug fixes. 8 | 10 |
11 |
12 |
13 | 14 | -------------------------------------------------------------------------------- /management/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webwallet", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "lodash": "~2.4.1", 6 | "jquery": "~2.1.0", 7 | "angular": "~1.3.0", 8 | "angular-route": "~1.3.0", 9 | "angular-sanitize": "~1.3.0", 10 | "angular-bootstrap": "~0.12.0", 11 | "angular-qr": "~0.2.0", 12 | "bootstrap": "~3.2.0", 13 | "bootstrap-slider": "seiyria/bootstrap-slider#~1.9.0", 14 | "trezor.js": "trezor/trezor.js#4.1.3" 15 | }, 16 | "devDependencies": {}, 17 | "resolutions": { 18 | "angular": "~1.3.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /management/firmware_copy.py: -------------------------------------------------------------------------------- 1 | import json 2 | import shutil 3 | import os 4 | 5 | if not os.path.exists('../extension/data'): 6 | os.makedirs('../extension/data') 7 | 8 | with open('data/firmware/releases.json') as data_file: 9 | data = json.load(data_file) 10 | 11 | top = data[0] 12 | 13 | 14 | url = top["url"] 15 | filename = url[url.rfind("/")+1 : ] 16 | 17 | shutil.copy('data/firmware/' + filename, '../extension/data/firmware.bin.hex') 18 | 19 | shutil.copy('data/config_signed.bin', '../extension/data/config_signed.bin'); 20 | 21 | top["url"] = '/data/firmware.bin.hex'; 22 | 23 | with open('../extension/data/releases.json', 'w') as outfile: 24 | json.dump([ top ], outfile, indent=4) 25 | 26 | 27 | -------------------------------------------------------------------------------- /management/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webwallet", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "grunt": "~0.4.5", 6 | "grunt-angular-templates": "~0.5.7", 7 | "grunt-autoprefixer": "~1.0.1", 8 | "grunt-concurrent": "^1.0.0", 9 | "grunt-contrib-clean": "~0.6.0", 10 | "grunt-contrib-compass": "~1.0.1", 11 | "grunt-contrib-concat": "~0.5.0", 12 | "grunt-contrib-connect": "~0.8.0", 13 | "grunt-contrib-copy": "^0.7.0", 14 | "grunt-contrib-cssmin": "~0.10.0", 15 | "grunt-contrib-htmlmin": "~0.3.0", 16 | "grunt-contrib-jshint": "~0.10.0", 17 | "grunt-contrib-uglify": "~0.6.0", 18 | "grunt-contrib-watch": "~0.6.1", 19 | "grunt-filerev": "~2.1.2", 20 | "grunt-google-cdn": "~0.4.3", 21 | "grunt-ng-annotate": "^0.10.0", 22 | "grunt-rev": "~0.1.0", 23 | "grunt-svgmin": "~1.0.0", 24 | "grunt-usemin": "~2.4.0", 25 | "jshint-stylish": "~1.0.0", 26 | "load-grunt-tasks": "~0.6.0", 27 | "time-grunt": "~1.0.0", 28 | "grunt-cli": "~0.1.13", 29 | "bower": "~1.4.1" 30 | }, 31 | "engines": { 32 | "node": ">=0.8.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /manifest_no_matches.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TREZOR Chrome Extension", 3 | "description": "Chrome extension for TREZOR", 4 | "version": "1.2.7", 5 | "author": "SatoshiLabs", 6 | "short_name": "TREZOR Extension", 7 | 8 | "manifest_version": 2, 9 | "minimum_chrome_version": "38.0.2125.7", 10 | "icons": { 11 | "16": "icon16.png", 12 | "48": "icon48.png", 13 | "128": "icon128.png" 14 | }, 15 | 16 | "sockets": { 17 | "udp": { 18 | "bind": [ 19 | "127.0.0.1:21327", "127.0.0.1:21328","127.0.0.1:21329", 20 | "127.0.0.1:21330", "127.0.0.1:21331","127.0.0.1:21332" 21 | ], 22 | "send": ["127.0.0.1:21324", "127.0.0.1:21325","127.0.0.1:21326"] 23 | } 24 | }, 25 | "app": { 26 | "background": { 27 | "scripts": ["index.js"] 28 | } 29 | }, 30 | "externally_connectable": "WILL BE CHANGED BY SCRIPT", 31 | "permissions": [ 32 | "hid", { 33 | "usbDevices": [{ 34 | "vendorId": 21324, 35 | "productId": 1 36 | }, 37 | { 38 | "vendorId": 4617, 39 | "productId": 21441 40 | }, 41 | { 42 | "vendorId": 4617, 43 | "productId": 21440 44 | } 45 | ] 46 | }, 47 | "storage" 48 | ], 49 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlBLI7C8hybngCmlKgfLytKQiqrTQYHQpz6E0DcWYeqC047J5hFtxbODYsPIn9sCJePv/LCW9jOc3G3B/aUGK1Lks+DrbnNlDgI/Ja4MzYEEaYyLFquHJlQe7B2doMTLS9JQfkWsXe/IQW8vfYq5Jo4JmiucXUbr37Xlsl0U/ScwSz0hDnzHQfpCpuLuUQP6kQrqbJ8Rd3NDlh5qAB0BfewDoDmlgGBlV+za80I8KYRSwgAvfEndevcsXQzV6myKptdqpffEbGhjwZ63ehofbnlr4c5i25OkMirI5bfcmP+6g/wYt4zOBnCep8MK8eG92NvocSBsu530XDywuCiY+2QIDAQAB" 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trezor-chrome-extension", 3 | "version": "1.2.4", 4 | "description": "Chrome extension for communication with Trezor", 5 | "main": "index.js", 6 | "keywords": [ 7 | "chrome", 8 | "trezor", 9 | "bitcoin" 10 | ], 11 | "contributors": [ 12 | { 13 | "name": "Mike Tsao", 14 | "mail": "mike@sowbug.com" 15 | }, 16 | { 17 | "name": "Liz Fong-Jones", 18 | "mail": "lizf@google.com" 19 | }, 20 | { 21 | "name": "William Wolf", 22 | "mail": "throughnothing@gmail.com" 23 | }, 24 | { 25 | "name": "Karel Bílek", 26 | "mail": "kb@karelbilek.com" 27 | } 28 | ], 29 | "license": "GPL-3.0", 30 | "homepage": "https://github.com/trezor/trezor-chrome-extension", 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/trezor/trezor-chrome-extension.git" 34 | }, 35 | "dependencies": { 36 | "trezor-link-browser-extension": "0.2.85" 37 | }, 38 | "devDependencies": { 39 | "babel-plugin-add-module-exports": "^0.1.2", 40 | "babel-plugin-transform-class-properties": "^6.6.0", 41 | "babel-plugin-transform-flow-strip-types": "^6.7.0", 42 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 43 | "babel-preset-chrome-49": "^1.0.0", 44 | "babelify": "^7.2.0", 45 | "browserify": "^13.1.0", 46 | "envify": "^3.4.0", 47 | "fast-async": "^6.0.31" 48 | }, 49 | "browserify": { 50 | "transform": [ 51 | [ 52 | "babelify", 53 | { 54 | "presets": [ 55 | "chrome-49" 56 | ], 57 | "plugins": [ 58 | "transform-flow-strip-types", 59 | "transform-class-properties", 60 | "transform-object-rest-spread", 61 | "add-module-exports", 62 | "fast-async" 63 | ] 64 | } 65 | ], 66 | [ 67 | "envify" 68 | ] 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/.eslintignore: -------------------------------------------------------------------------------- 1 | config_proto_compiled.js 2 | -------------------------------------------------------------------------------- /src/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/protobufjs/src/.* 3 | .*/node_modules/bitcoinjs-lib-zcash/.* 4 | .*/node_modules/bigi/.* 5 | 6 | [include] 7 | ../node_modules/trezor-link-browser-extension/ 8 | ../node_modules/json-stable-stringify/* 9 | ../node_modules/protobufjs/* 10 | ../node_modules/object.values/* 11 | ../node_modules/semver-compare/* 12 | 13 | [libs] 14 | ../flowtype_definitions/chrome.js 15 | ../node_modules/trezor-link-browser-extension/flowtype/bitcoinjs-lib.js 16 | ../node_modules/trezor-link-browser-extension/flowtype/chrome.js 17 | ../node_modules/trezor-link-browser-extension/flowtype/webusb.js 18 | ../node_modules/trezor-link-browser-extension/flowtype/node-hid.js 19 | 20 | [options] 21 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue 22 | esproposal.class_instance_fields=enable 23 | esproposal.decorators=ignore 24 | -------------------------------------------------------------------------------- /src/chrome/platformInfo.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /** 3 | * This file is part of the TREZOR project. 4 | * 5 | * Copyright (C) 2015 SatoshiLabs 6 | * 7 | * This library is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Lesser General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This library is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Lesser General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Lesser General Public License 18 | * along with this library. If not, see . 19 | */ 20 | 21 | "use strict"; 22 | 23 | export function manifest(): Promise { 24 | return new Promise((resolve, reject) => { 25 | try { 26 | resolve(chrome.runtime.getManifest()); 27 | } catch (e) { 28 | reject(e); 29 | } 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/chrome/storage.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /** 3 | * This file is part of the TREZOR project. 4 | * 5 | * Copyright (C) 2015 SatoshiLabs 6 | * 7 | * This library is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Lesser General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This library is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Lesser General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Lesser General Public License 18 | * along with this library. If not, see . 19 | */ 20 | 21 | // Chrome's extension storage is actually more complicated 22 | // than LocalStorage, because it uses callbacks => 23 | // I am converting them to (IMO) more manageable Promises 24 | 25 | "use strict"; 26 | 27 | // Get from storage 28 | export function get(key: string): Promise { 29 | return new Promise((resolve, reject) => { 30 | try { 31 | chrome.storage.local.get(key, (items) => { 32 | if (chrome.runtime.lastError) { 33 | reject(chrome.runtime.lastError); 34 | } else { 35 | if (items[key] === null || items[key] === undefined) { 36 | resolve(null); 37 | } else { 38 | resolve(items[key]); 39 | } 40 | resolve(items); 41 | } 42 | }); 43 | } catch (e) { 44 | reject(e); 45 | } 46 | }); 47 | } 48 | 49 | // Set to storage 50 | export function set(key:string, value:any): Promise { 51 | return new Promise((resolve, reject) => { 52 | try { 53 | const obj: {} = {}; 54 | obj[key] = value; 55 | chrome.storage.local.set(obj, () => { 56 | if (chrome.runtime.lastError) { 57 | reject(chrome.runtime.lastError); 58 | } else { 59 | resolve(undefined); 60 | } 61 | }); 62 | } catch (e) { 63 | reject(e); 64 | } 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | "use strict"; 4 | 5 | import type {AcquireInput, TrezorDeviceInfoWithSession as LinkDevice} from "trezor-link-browser-extension"; 6 | 7 | import Link from "trezor-link-browser-extension"; 8 | import * as storage from "./chrome/storage"; 9 | import {manifest} from "./chrome/platformInfo"; 10 | 11 | type MessageFromTrezor = {type: string, message: Object}; 12 | type StatusInfo = {version: string, configured: boolean} 13 | 14 | type TrezorDeviceInfo = { 15 | path: string; 16 | vendor: number; 17 | product: number; 18 | serialNumber: number; // always 0 19 | session: ?string; // might be null/undefined 20 | } 21 | 22 | const TREZOR_VENDOR_ID: number = 0x534c; 23 | const TREZOR_PRODUCT_ID: number = 0x0001; 24 | 25 | const ParallelTransport = Link.Parallel; 26 | const LowlevelTransport = Link.Lowlevel; 27 | const ChromeHidPlugin = Link.ChromeHid; 28 | const ChromeUdpPlugin = Link.ChromeUdp; 29 | // const FallbackTransport = Link.Fallback; 30 | // const BridgeTransport = Link.Bridge; 31 | /* const link = new FallbackTransport( 32 | [ 33 | new BridgeTransport(), 34 | new LowlevelTransport(new ChromeHidPlugin()), 35 | ] 36 | );*/ 37 | 38 | const udpPlugin = new ChromeUdpPlugin(); 39 | const link = new ParallelTransport( 40 | { 41 | "udp": new LowlevelTransport(udpPlugin), 42 | "hid": new LowlevelTransport(new ChromeHidPlugin()), 43 | } 44 | ); 45 | 46 | link.init(process.env.NODE_ENV === "debug").catch((err) => console.error("Cannot init", err)); 47 | 48 | // when we try to read messages and it's null, we look into storage 49 | // if it's not saved. If it is saved, we try to configure again 50 | async function messagesReload(): Promise { 51 | const hasMessages = link.configured; 52 | if (hasMessages) { 53 | return; 54 | } 55 | let savedConfigure: string = ""; 56 | try { 57 | savedConfigure = await storage.get("savedConfigure"); 58 | } catch (e) { 59 | throw new Error("No protocol definition, call configure."); 60 | } 61 | await configure(savedConfigure); 62 | } 63 | 64 | async function ping() { 65 | return "pong"; 66 | } 67 | 68 | function convertDevices(devices: Array): Array { 69 | return devices.map(device => { 70 | return { 71 | ...device, 72 | vendor: TREZOR_VENDOR_ID, 73 | product: TREZOR_PRODUCT_ID, 74 | serialNumber: 0, 75 | }; 76 | }); 77 | } 78 | 79 | async function enumerate(): Promise> { 80 | return convertDevices(await link.enumerate()); 81 | } 82 | 83 | async function listen(previous: mixed): Promise> { 84 | let convertedPrevious: ?Array = null; 85 | if (previous != null) { 86 | if (typeof previous === "object") { 87 | if (previous instanceof Array) { 88 | convertedPrevious = previous.map((d: mixed): LinkDevice => { 89 | if (typeof d !== "object" || d == null) { 90 | throw new Error("Device is not an object"); 91 | } 92 | if (typeof d.path !== "string" && typeof d.path !== "number") { 93 | throw new Error("Device path is strange"); 94 | } 95 | const path: string = d.path.toString(); 96 | let session: ?string = null; 97 | if (d.session != null) { 98 | if (typeof d.session !== "string" && typeof d.session !== "number") { 99 | throw new Error("Device session is strange"); 100 | } 101 | session = d.session.toString(); 102 | } 103 | const r: LinkDevice = {path, session}; 104 | return r; 105 | }); 106 | } 107 | } 108 | } 109 | return convertDevices(await link.listen(convertedPrevious)); 110 | } 111 | 112 | function parseAcquireInput(input: mixed): AcquireInput { 113 | if (typeof input === "string") { 114 | return {path: input, previous: null, checkPrevious: false}; 115 | } else if (typeof input === "object" && input != null) { 116 | if (typeof input.path !== "string" && typeof input.path !== "number") { 117 | throw new Error("Device path is strange."); 118 | } 119 | const path: string = input.path.toString(); 120 | let previous: ?string = null; 121 | if (input.previous != null) { 122 | if (typeof input.previous !== "string" && typeof input.previous !== "number") { 123 | throw new Error("Device session is strange."); 124 | } 125 | previous = input.previous.toString(); 126 | } 127 | 128 | return {path, previous, checkPrevious: true}; 129 | } 130 | throw new Error("Acquire input is strange."); 131 | } 132 | 133 | async function acquire(input: mixed): Promise<{session: string}> { 134 | const session = await link.acquire(parseAcquireInput(input)); 135 | return {session}; 136 | } 137 | 138 | async function release(input: mixed): Promise { 139 | if (typeof input !== "string" && typeof input !== "number") { 140 | throw new Error("Device session is strange."); 141 | } 142 | await link.release(input.toString()); 143 | return "Success"; 144 | } 145 | 146 | async function udevStatus(): Promise { 147 | // const hasError: boolean = await hidTransport.showUdevError(); 148 | // return hasError ? "display" : "hide"; 149 | return "hide"; 150 | } 151 | 152 | async function call(input: mixed): Promise { 153 | if (typeof input !== "object" || input == null) { 154 | throw new Error("Input is not an object"); 155 | } 156 | if (typeof input.id !== "string" && typeof input.id !== "number") { 157 | throw new Error("Session is strange."); 158 | } 159 | if (typeof input.type !== "string") { 160 | throw new Error("Type is not a string."); 161 | } 162 | const type: string = input.type; 163 | if (typeof input.message !== "object" || input.message == null) { 164 | throw new Error("Message is not an object."); 165 | } 166 | const message: Object = input.message; 167 | const id: string = input.id.toString(); 168 | await messagesReload(); 169 | return await link.call(id, type, message); 170 | } 171 | 172 | async function configure(input: mixed): Promise { 173 | if (typeof input !== "string") { 174 | throw new Error("Configure input is strange."); 175 | } 176 | const body: string = input; 177 | await storage.set(body); 178 | await link.configure(body); 179 | return "Success"; 180 | } 181 | 182 | async function _version(): Promise { 183 | const version = (await manifest()).version; 184 | if (version == null) { 185 | throw new Error("Manifest doesn't have a version!"); 186 | } 187 | return version; 188 | } 189 | 190 | async function _configured(): Promise { 191 | try { 192 | await messagesReload(); 193 | return true; 194 | } catch (e) { 195 | return false; 196 | } 197 | } 198 | 199 | async function info(): Promise { 200 | return { 201 | version: await _version(), 202 | configured: await _configured(), 203 | }; 204 | } 205 | 206 | const responseFunctions = { 207 | ping, 208 | enumerate, 209 | listen, 210 | acquire, 211 | release, 212 | udevStatus, 213 | call, 214 | configure, 215 | info, 216 | }; 217 | 218 | const setUdp = function (ports: Array) { 219 | storage.set("udp", JSON.stringify(ports)); 220 | udpPlugin.setPorts(ports); 221 | console.log("Ports added", ports); 222 | }; 223 | 224 | window.setUdp = setUdp; 225 | 226 | const startingPromise = storage.get("udp").then((udpSerialized) => { 227 | const udpStorage = JSON.parse(udpSerialized); 228 | if (udpStorage instanceof Array) { 229 | setUdp(udpStorage); 230 | } 231 | return; 232 | }); 233 | 234 | function handleMessage(request: Object, sender: ChromeMessageSender, sendResponse: (response: Object) => void): boolean { 235 | if (process.env.NODE_ENV === "debug") { 236 | console.log("Message arrived: ", request); 237 | } 238 | 239 | const responseFunction = (responseFunctions[request.type]) 240 | ? responseFunctions[request.type] 241 | : () => { 242 | throw new Error("No function defined for " + request.type); 243 | }; 244 | 245 | const nonThrowingResponse = (body) => { 246 | try { 247 | return startingPromise.then(() => responseFunction(body)); 248 | } catch (e) { 249 | return Promise.reject(e); 250 | } 251 | }; 252 | 253 | nonThrowingResponse(request.body).then((responseBody) => { 254 | if (process.env.NODE_ENV === "debug") { 255 | console.log("Response sent: ", JSON.parse(JSON.stringify(responseBody)), JSON.parse(JSON.stringify(request))); 256 | } 257 | 258 | sendResponse({ 259 | type: "response", 260 | body: responseBody, 261 | }); 262 | }).catch((error) => { 263 | if (process.env.NODE_ENV === "debug") { 264 | console.log("Error sent: ", error, JSON.parse(JSON.stringify(request))); 265 | } 266 | 267 | sendResponse({ 268 | type: "error", 269 | message: error.message || error, 270 | }); 271 | }); 272 | 273 | // "return true" is necessary for asynchronous message passing, 274 | // don't remove it! 275 | return true; 276 | } 277 | 278 | chrome.runtime.onMessage.addListener(handleMessage); 279 | chrome.runtime.onMessageExternal.addListener(handleMessage); 280 | let windowOpen: boolean = false; 281 | 282 | chrome.app.runtime.onLaunched.addListener(() => { 283 | if (!windowOpen) { 284 | chrome.app.window.create("management/index.html", { 285 | "innerBounds": { 286 | "width": 774, 287 | "height": 774, 288 | }, 289 | }, (newWindow) => { 290 | windowOpen = true; 291 | newWindow.onClosed.addListener(() => { 292 | windowOpen = false; 293 | }); 294 | }); 295 | } 296 | }); 297 | 298 | storage.get("udp").then((udpSerialized) => { 299 | const udpStorage = JSON.parse(udpSerialized); 300 | if (udpStorage instanceof Array) { 301 | udpPlugin.setPorts(udpStorage); 302 | } 303 | }); 304 | --------------------------------------------------------------------------------