├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── TrezorSymmetricFileEncryption.py ├── basics.py ├── comments.md ├── dialog.ui ├── dialogs.py ├── encoding.py ├── icons ├── TrezorSymmetricFileEncryption.176x60.png ├── TrezorSymmetricFileEncryption.216x100.svg ├── TrezorSymmetricFileEncryption.ico ├── TrezorSymmetricFileEncryption.icon.ico ├── TrezorSymmetricFileEncryption.icon.png ├── TrezorSymmetricFileEncryption.icon.svg ├── TrezorSymmetricFileEncryption.png ├── TrezorSymmetricFileEncryption.svg ├── file.svg ├── trezor.bg.png ├── trezor.bg.svg └── trezor.svg ├── processing.py ├── screenshots ├── screenshot_TrezorSymmetricFileEncryption_aboutWindow.version04b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow1.version01a.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow1.version02b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow1.version03b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow1.version04b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow2.version01a.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow2.version02b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow2.version03b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow2.version04b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow3.version03b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow3.version04b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow4.version03b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow4.version04b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow5.version03b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow5.version04b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow6.version03b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow6.version04b.png ├── screenshot_TrezorSymmetricFileEncryption_mainWindow7.version03b.png ├── screenshot_TrezorSymmetricFileEncryption_passphraseEntryWindow.png └── screenshot_TrezorSymmetricFileEncryption_pinEntryWindow.png ├── settings.py ├── singleFileExecutableLinuxCreate.sh ├── singleFileExecutableLinuxReadme.txt ├── testTrezorSymmetricFileEncryption.sh ├── trezor_app_generic.py ├── trezor_app_specific.py ├── trezor_chooser_dialog.ui ├── trezor_gui.py ├── trezor_passphrase_dialog.ui ├── trezor_pin_dialog.ui └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # UI auto-generated files 92 | ui_*.py 93 | 94 | # Misc 95 | *.org 96 | *.bak 97 | 98 | # Left over test files 99 | __*img* 100 | *.tsfe 101 | __*.test.txt 102 | __*.random.bin 103 | TQFYqK1nha1IfLy_qBxdGwlGRytelGRJ 104 | __time_measurements__.txt 105 | 106 | test.log 107 | 108 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | UI_GENERATED := \ 2 | ui_trezor_chooser_dialog.py \ 3 | ui_trezor_pin_dialog.py \ 4 | ui_trezor_passphrase_dialog.py \ 5 | ui_dialog.py \ 6 | #end of UI_GENERATED 7 | 8 | all: $(UI_GENERATED) 9 | 10 | ui_%.py: %.ui 11 | pyuic5 -o $@ $< 12 | 13 | clean: 14 | rm -f $(UI_GENERATED) 15 | rm -rf __pycache__ 16 | rm -f *.pyc 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Trezor icon](icons/TrezorSymmetricFileEncryption.png) 2 | 3 | # Trezor Symmetric File Encryption 4 | 5 | **:star: :star: :star: Use your [Trezor](http://www.trezor.io/) device to symmetrically encrypt and decrypt files :star: :star: :star:** 6 | 7 | **:lock: :unlock: :key: Hardware-backed file encryption with Trezor :key: :unlock: :lock:** 8 | 9 | `TrezorSymmetricFileEncryption` is a small, simple tool that 10 | allows you to symmetrically encrypt and decrypt files. 11 | 12 | # Features 13 | 14 | * Trezor convenience 15 | * Trezor security 16 | * One Trezor for all your needs: [gpg](https://github.com/romanz/trezor-agent), [ssh](https://github.com/romanz/trezor-agent), **symmetric encryption**, etc. 17 | * Encrypt your files for your use, guarantee your privacy 18 | * Requires confirmation button click on Trezor device to perform decrypt operation. 19 | * For the paranoid there is now an option to encrypt the file(s) twice. 20 | In any mode, the file is first AES encrypted on the PC with a key generated 21 | and en/decrypted by the Trezor device requiring a click on the `Confirm` 22 | button of the Trezor. In the paranoid mode, the file is then encrypted 23 | a second time. This second encryption is done within the Trezor device 24 | and not on the PC, with no key ever touching the memory of the PC. 25 | The PC just feeds the file 26 | to the Trezor and receives the results, but the PC is not doing any actual 27 | encryption. The actual en/decryption takes place on the Trezor chip. 28 | This paranoid mode is significantly slower than the regular mode. 29 | * It supports both GUI mode and Terminal mode. 30 | * Since it is a program that has a full CLI (command line interface) 31 | it is easy to create scripts or to automate workflows. Keep in mind though 32 | that you will have to confirm on the Trezor by clicking its `Confirm` button. 33 | * Optionally obfuscates/encrypts filenames on encryption to hide meta-data 34 | (i.e. the file names) 35 | * Use it before and after you store sensitive information on 36 | DropBox, Google Drive or similar. 37 | 38 | # Screenshot 39 | 40 | Below a sample screenshot. More screenshots [here](screenshots). 41 | 42 | ![screenshot](screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow6.version04b.png) 43 | 44 | # Runtime requirements 45 | 46 | * Use of passphrases must have been already enabled on your [Trezor](https://www.trezor.io) device. 47 | * [Trezor](https://www.trezor.io) device 48 | * [Python](https://www.python.org/) v2.7 or 3.4+ 49 | * [PyCrypto](https://pypi.python.org/pypi/pycrypto) 50 | * [PyQt5](https://pypi.python.org/pypi/PyQt5) 51 | * [Qt5](https://doc.qt.io/qt-5/) 52 | * [trezorlib from python-trezor](https://github.com/trezor/python-trezor) 53 | * [Versions 0.5.0 and older used PyQy4 instead of PyQy5. Read the README.md 54 | file of v0.5.0 for build requirements, dependencies, etc. Basically anything 55 | relating to PyQt5 has to be replaced with the corresponding component in PyQt4. 56 | `pyuic5` becomes `pyuic4`. `pyqt5-dev-tools` becomes `pyqt4-dev-tools` 57 | and so forth.] 58 | 59 | # Building 60 | 61 | Even though the whole code is in Python, there are few Qt5 `.ui` form files that 62 | need to be transformed into Python files. There is `Makefile`, you just need to run 63 | 64 | make 65 | 66 | ## Build requirements 67 | 68 | * PyQt5 development tools are necessary, namely `pyuic5` (look for a package named 69 | `pyqt5-dev-tools`, `PyQt5-devel` or similar). Required to run `make`. 70 | * Depending on one's set-up one might need: `qttools5-dev-tools` 71 | (also sets up some of the Qt5 environment variables) 72 | * Depending on one's set-up one might need: `python-pyqt5` (Qt5 bindings for Python 2) 73 | * Depending on one's set-up one might need: `python3-pyqt5` (Qt5 bindings for Python 3) 74 | * Depending on one's set-up one might need: `python-pyqt5.qtsvg` (to display SVG logos in Python 2) 75 | * Depending on one's set-up one might need: `python3-pyqt5.qtsvg` (to display SVG logos in Python 3) 76 | 77 | # Running 78 | 79 | Run: 80 | 81 | python TrezorSymmetricFileEncryption.py 82 | or 83 | 84 | python3 TrezorSymmetricFileEncryption.py 85 | 86 | Run-time command line options are 87 | 88 | ``` 89 | TrezorSymmetricFileEncryption.py [-v] [-h] [-l ] [-t] 90 | [-e | -o | -d | -m | -n] 91 | [-2] [-s] [-w] [-p ] [-r] [-R] [q] 92 | -v, --version 93 | print the version number 94 | -h, --help 95 | print short help text 96 | -l, --logging 97 | set logging level, integer from 1 to 5, 1=full logging, 5=no logging 98 | -t, --terminal 99 | run in the terminal, except for a possible PIN query 100 | and a Passphrase query this avoids the GUI 101 | -e, --encrypt 102 | encrypt file and keep output filename as plaintext 103 | (appends .tsfe suffix to input file) 104 | -o, --obfuscatedencrypt 105 | encrypt file and obfuscate output file name 106 | -d, --decrypt 107 | decrypt file 108 | -m, --encnameonly 109 | just encrypt the plaintext filename, show what the obfuscated 110 | filename would be; does not encrypt the file itself; 111 | incompaible with `-d` and `-n` 112 | -n, --decnameonly 113 | just decrypt the obfuscated filename; 114 | does not decrypt the file itself; 115 | incompaible with `-o`, `-e`, and `-m` 116 | -2, --twice 117 | paranoid mode; encrypt file a second time on the Trezor chip itself; 118 | only relevant for `-e` and `-o`; ignored in all other cases. 119 | Consider filesize: The Trezor chip is slow. 1M takes roughly 75 seconds. 120 | -p, --passphrase 121 | master passphrase used for Trezor. 122 | It is recommended that you do not use this command line option 123 | but rather give the passphrase through a small window interaction. 124 | -r, --readpinfromstdin 125 | read the PIN, if needed, from the standard input, i.e. terminal, 126 | when in terminal mode `-t`. By default, even with `-t` set 127 | it is read via a GUI window. 128 | -R, --readpassphrasefromstdin 129 | read the passphrase, when needed, from the standard input, 130 | when in terminal mode `-t`. By default, even with `-t` set 131 | it is read via a GUI window. 132 | -s, --safety 133 | doublechecks the encryption process by decrypting the just 134 | encrypted file immediately and comparing it to original file; 135 | doublechecks the decryption process by encrypting the just 136 | decrypted file immediately and comparing it to original file; 137 | Ignored for `-m` and `-n`. 138 | Primarily useful for testing. 139 | -w, --wipe 140 | shred the inputfile after creating the output file 141 | i.e. shred the plaintext file after encryption or 142 | shred the encrypted file after decryption; 143 | only relevant for `-d`, `-e` and `-o`; ignored in all other cases. 144 | Use with extreme caution. May be used together with `-s`. 145 | -q, --noconfirm 146 | Eliminates the `Confirm` click on the Trezor button. 147 | This was only added to facilitate batch testing. 148 | It should be used EXCLUSIVELY for testing purposes. 149 | Do NOT use this option with real files! 150 | Furthermore, files encryped with `-n` cannot be decrypted 151 | without `-n`. 152 | 153 | 154 | one or multiple files to be encrypted or decrypted 155 | 156 | All arguments are optional. 157 | 158 | All output files are always placed in the same directory as the input files. 159 | 160 | By default the GUI will be used. 161 | 162 | You can avoid the GUI by using `-t`, forcing the Terminal mode. 163 | If you specify filename, possibly some `-o`, `-e`, or `-d` option, then 164 | only PIN and Passphrase will be collected through windows. 165 | 166 | Most of the time TrezorSymmetricFileEncryption can detect automatically if 167 | it needs to decrypt or encrypt by analyzing the given input file name. 168 | So, in most of the cases you do not need to specify any 169 | de/encryption option. 170 | TrezorSymmetricFileEncryption will simply do the right thing. 171 | In the very rare case that TrezorSymmetricFileEncryption determines 172 | the wrong encrypt/decrypt operation you can force it to use the right one 173 | by using either `-e` or `-d` or selecting the appropriate option in the GUI. 174 | 175 | If TrezorSymmetricFileEncryption automatically determines 176 | that it has to encrypt of file, it will chose by default the 177 | `-e` option, and create a plaintext encrypted files with an `.tsfe` suffix. 178 | 179 | If you want the output file name to be obfuscated you 180 | must use the `-o` (obfuscate) flag or select that option in the GUI. 181 | 182 | Be aware of computation time and file sizes when you use `-2` option. 183 | Encrypting on the Trezor takes time: 1M roughtly 75sec. 50M about 1h. 184 | Without `-2` it is very fast, a 1G file taking roughly 15 seconds. 185 | 186 | For safety the file permission of encrypted files is set to read-only. 187 | 188 | Examples: 189 | # specify everything in the GUI 190 | TrezorSymmetricFileEncryption.py 191 | 192 | # specify everything in the GUI, set logging to verbose Debug level 193 | TrezorSymmetricFileEncryption.py -l 1 194 | 195 | # encrypt contract producing contract.doc.tsfe 196 | TrezorSymmetricFileEncryption.py contract.doc 197 | 198 | # encrypt contract and obfuscate output producing e.g. TQFYqK1nha1IfLy_qBxdGwlGRytelGRJ 199 | TrezorSymmetricFileEncryption.py -o contract.doc 200 | 201 | # encrypt contract and obfuscate output producing e.g. TQFYqK1nha1IfLy_qBxdGwlGRytelGRJ 202 | # performs safety check and then shreds contract.doc 203 | TrezorSymmetricFileEncryption.py -e -o -s -w contract.doc 204 | 205 | # decrypt contract producing contract.doc 206 | TrezorSymmetricFileEncryption.py contract.doc.tsfe 207 | 208 | # decrypt obfuscated contract producing contract.doc 209 | TrezorSymmetricFileEncryption.py TQFYqK1nha1IfLy_qBxdGwlGRytelGRJ 210 | 211 | # shows plaintext name of encrypted file, e.g. contract.doc 212 | TrezorSymmetricFileEncryption.py -n TQFYqK1nha1IfLy_qBxdGwlGRytelGRJ 213 | 214 | Keyboard shortcuts of GUI: 215 | Apply, Save: Control-A, Control-S 216 | Cancel, Quit: Esc, Control-Q 217 | Copy to clipboard: Control-C 218 | Version, About: Control-V 219 | Set encrypt operation: Control-E 220 | Set decrypt operation: Control-D 221 | Set obfuscate option: Control-O 222 | Set twice option: Control-2 223 | Set safety option: Control-T 224 | Set wipe option: Control-W 225 | ``` 226 | 227 | # Testing 228 | 229 | Run the `Bash` script 230 | 231 | ./testTrezorSymmetricFileEncryption.sh 1K 232 | 233 | or for a full lengthy test 234 | 235 | ./testTrezorSymmetricFileEncryption.sh 236 | 237 | # FAQ - Frequently Asked Questions 238 | 239 | **Question:** Shouldn't there be two executables? One for encrypting 240 | and another one for decrypting? 241 | 242 | **Answer:** No. There is only one Python file which does both encryption and decryption. 243 | - - - 244 | **Question:** What are the command line options? 245 | 246 | **Answer:** See description above. But in the vast majority of cases you 247 | do not need to set or use any command line options. 248 | TrezorSymmetricFileEncryption will in most cases automatically detect 249 | if it needs to encrypt or decrypt. 250 | - - - 251 | **Question:** Are there any RSA keys involved somewhere? 252 | 253 | **Answer:** No. There are no RSA keys, there is no asymmetric encryption. 254 | - - - 255 | **Question:** Can I send encrypted files to my friends and have them decrypt them? 256 | 257 | **Answer:** No. Only you have the Trezor that can decrypt the files. 258 | **You** encrypt the files, and **you** decrypt them later. 259 | TrezorSymmetricFileEncryption is not built for sharing. 260 | For sharing encrypted files use asymmetric encryption 261 | like [gpg](https://gnupg.org/). 262 | By the way, Trezor supports gpg encryption/decryption. 263 | In short, only the holder of the Trezor who also knows the PIN and the 264 | TrezorSymmetricFileEncryption master password (= Trezor passphrase) can 265 | decrypt the file(s). 266 | - - - 267 | **Question:** What crypto technology is used? 268 | 269 | **Answer:** At the heart of it all is the 270 | python-trezor/trezorlib/client.py/encrypt_keyvalue() 271 | function of the Python client library of [Trezor](https://www.trezor.io) 272 | and AES-CBC encryption. 273 | - - - 274 | **Question:** Is there a config file or a settings file? 275 | 276 | **Answer:** No, there are no config and no settings files. 277 | - - - 278 | **Question:** Does TrezorSymmetricFileEncryption require online connectivity, 279 | Internet access? 280 | 281 | **Answer:** No. 282 | - - - 283 | **Question:** How many files are there? 284 | 285 | **Answer:** If you have Python installed, then there are just a 286 | handful of Python files. Alternatively, if you don't want to 287 | install Python one can create a single-file-executable 288 | with tools like [pyinstaller](www.pyinstaller.org). In that case you just have a 289 | single-file-executablefile. 290 | - - - 291 | **Question:** In which language is TrezorSymmetricFileEncryption written? 292 | 293 | **Answer:** [Python](https://www.python.org/). It runs on Python 2.7 and 3.4+. 294 | - - - 295 | **Question:** Do I need to have a [Trezor](https://www.trezor.io/) in 296 | order to use TrezorSymmetricFileEncryption? 297 | 298 | **Answer:** Yes, a Trezor is required. 299 | - - - 300 | **Question:** Is there any limit on the file size for encryption or decryption? 301 | 302 | **Answer:** Yes. Currently it is 2G minus a few bytes. On old computers 303 | with very little memory, it might be less than 2G due to memory limitations. 304 | - - - 305 | **Question:** Can I see the source code? 306 | 307 | **Answer:** Yes, this is an open source software project. 308 | You can find and download all source code from 309 | [Github](https://github.com/8go/TrezorSymmetricFileEncryption) or 310 | any of its forks. 311 | - - - 312 | **Question:** Does the TrezorSymmetricFileEncryption contain ads? 313 | 314 | **Answer:** No. 315 | - - - 316 | **Question:** Does TrezorSymmetricFileEncryption cost money? 317 | 318 | **Answer:** No. It is free, libre, and open source. 319 | - - - 320 | **Question:** Does TrezorSymmetricFileEncryption call home? 321 | Send any information anywhere? 322 | 323 | **Answer:** No. Never. You can also use it on an air-gapped computer if you 324 | want to. It does not contain any networking code at all. It does not update 325 | itself automatically. It cannot send anything anywhere. 326 | - - - 327 | **Question:** Does TrezorSymmetricFileEncryption have a backdoor? 328 | 329 | **Answer:** No. Read the source code to convince yourself. 330 | - - - 331 | **Question:** How can I know that TrezorSymmetricFileEncryption does not contain a virus? 332 | 333 | **Answer:** Download the source from 334 | [Github](https://github.com/8go/TrezorSymmetricFileEncryption) 335 | and inspect the source code for viruses. Don't download it from unreliable sources. 336 | - - - 337 | **Question:** Can someone steal or duplicate the key used for encryption or decryption? 338 | 339 | **Answer:** No, the key never leaves the Trezor. 340 | - - - 341 | **Question:** Can a keyboard logger steal a key? 342 | 343 | **Answer:** No, it never leaves the Trezor. 344 | - - - 345 | **Question:** Can a screen grabber or a person looking over my shoulder steal a key? 346 | 347 | **Answer:** No, it never leaves the Trezor. 348 | - - - 349 | **Question:** What can be stolen? How can it be stolen? 350 | 351 | **Answer:** A virus or malware could steal your plain text file before you 352 | encrypt it or after you decrypt it. Once you have a safe encrypted copy 353 | you can consider shredding the plain text copy of the file(s). For extremely 354 | sensitive information consider using an air-gapped computer or 355 | a [LiveDvd OS](https://en.wikipedia.org/wiki/Live_DVD) if you have one available. 356 | - - - 357 | **Question:** Is TrezorSymmetricFileEncryption portable? 358 | 359 | **Answer:** Yes. It is just a handful of Python files 360 | or a single-file-executable. 361 | You can move it around via an USB stick, SD card, email or cloud service. 362 | - - - 363 | **Question:** Can I contribute to the project? 364 | 365 | **Answer:** Yes. It is open source. 366 | Go to [Github](https://github.com/8go/TrezorSymmetricFileEncryption). 367 | You can also help by getting the word out. 368 | If you like it or like the idea please spread the word on Twitter, Reddit, 369 | Facebook, etc. It will be appreciated. 370 | - - - 371 | **Question:** What if I lose my Trezor and my 24 Trezor seed words or 372 | my TrezorSymmetricFileEncryption master password (= Trezor passphrase)? 373 | 374 | **Answer:** Then you will not be able to decrypt your previously encrypted 375 | file. For practical purposes you have lost those files. Brute-forcing is 376 | not a viable work-around. 377 | - - - 378 | **Question:** What if I lose my Trezor or someone steals my Trezor? 379 | 380 | **Answer:** As long as the thief cannot guess your TrezorSymmetricFileEncryption master 381 | password (= Trezor passphrase) the thief cannot use it to decrypt your files. 382 | A good PIN helps too. If the thief can guess your PIN and thereafter is able 383 | to brute-force your TrezorSymmetricFileEncryption master password 384 | (= Trezor passphrase) then he can decrypt your files. So, use a good PIN and 385 | a good passphrase and you will be safe. After losing your Trezor you will need 386 | to get a new Trezor to decrypt your files. Decryption without a Trezor device 387 | could be done in pure software 388 | knowing the 24 seed words and the passphrase, but that software has not been 389 | written yet. 390 | - - - 391 | **Question:** On which platforms, operating systems is 392 | TrezorSymmetricFileEncryption available? 393 | 394 | **Answer:** On all platforms, operating systems where 395 | [Python](https://www.python.org/) and PyQt5 is available: Windows, Linux, Unix, 396 | Mac OS X. Internet searches show Python and PyQt5 for Android and iOS, 397 | but it has not been investigated, built, or tested on Android or iOS. 398 | Testing has only been done on Linux. 399 | - - - 400 | **Question:** Is it fast? 401 | 402 | **Answer:** Regular mode (encrypting once) is fast; like any AES implementation. 403 | Encrypting or decrypting a 1G file takes about 15 seconds, but 404 | your mileage may vary as speed depends on CPU and disk speed. If you 405 | encrypt a second time on the Trezor device itself, it is slow as the CPU 406 | performance on the Trezor device is limited. Encrypting a second time 407 | takes about 75 seconds per Megabyte. 408 | - - - 409 | **Question:** Are there any warranties or guarantees? 410 | 411 | **Answer:** No, there are no warranties or guarantees whatsoever. 412 | - - - 413 | **Question:** More questions? 414 | 415 | **Answer:** Let us know. 416 | - - - 417 | 418 | on :octocat: with :heart: 419 | -------------------------------------------------------------------------------- /TrezorSymmetricFileEncryption.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Use TREZOR as a hardware device for symmetric file encryption 5 | 6 | Usage: python TrezorSymmetricFileEncryption.py 7 | Usage: python TrezorSymmetricFileEncryption.py --help 8 | 9 | Source and readme is on www.github.com, search for TrezorSymmetricFileEncryption 10 | 11 | ''' 12 | 13 | from __future__ import absolute_import 14 | from __future__ import division 15 | from __future__ import print_function 16 | 17 | import sys 18 | import logging 19 | import codecs 20 | 21 | from PyQt5.QtWidgets import QApplication # for the clipboard and window 22 | 23 | from dialogs import Dialog 24 | 25 | import basics 26 | import settings 27 | import processing 28 | from trezor_app_specific import FileMap 29 | import trezor_app_generic 30 | 31 | """ 32 | The file with the main function. 33 | 34 | Code should work on both Python 2.7 as well as 3.4. 35 | Requires PyQt5. 36 | (Old version supported PyQt4.) 37 | """ 38 | 39 | 40 | def showGui(trezor, dialog, settings): 41 | """ 42 | Initialize, ask for encrypt/decrypt options, 43 | ask for files to be decrypted/encrypted, 44 | ask for master passphrase = trezor passphrase. 45 | 46 | Makes sure a session is created on Trezor so that the passphrase 47 | will be cached until disconnect. 48 | 49 | @param trezor: Trezor client 50 | @param settings: Settings object to store command line arguments or 51 | items selected in GUI 52 | """ 53 | settings.settings2Gui(dialog) 54 | if not dialog.exec_(): 55 | # Esc or exception or Quit/Close/Done 56 | settings.mlogger.log("Shutting down due to user request " 57 | "(Done/Quit was called).", logging.DEBUG, "GUI IO") 58 | # sys.exit(4) 59 | settings.gui2Settings(dialog) 60 | 61 | 62 | def useTerminal(fileMap, settings): 63 | if settings.WArg: 64 | settings.mlogger.log("The option `--wipe` is set. In case of " 65 | "encryption, the original plaintext files will " 66 | "be shredded after encryption. In case of decryption, " 67 | "the encrypted files will be shredded after decryption. " 68 | "Abort if you are uncertain or don't understand.", logging.WARNING, 69 | "Dangerous arguments") 70 | processing.processAll(fileMap, settings, dialog=None) 71 | 72 | 73 | def main(): 74 | if sys.version_info[0] < 3: # Py2-vs-Py3: 75 | # redirecting output to a file can cause unicode problems 76 | # read: https://stackoverflow.com/questions/5530708/ 77 | # To fix it either run the scripts as: PYTHONIOENCODING=utf-8 python TrezorSymmetricFileEncryption.py 78 | # or add the following line of code. 79 | # Only shows up in python2 TrezorSymmetricFileEncryption.py >> log scenarios 80 | # Exception: 'ascii' codec can't encode characters in position 10-13: ordinal not in range(128) 81 | sys.stdout = codecs.getwriter('utf-8')(sys.stdout) 82 | 83 | app = QApplication(sys.argv) 84 | if app is None: # just to get rid f the linter warning on above line 85 | print("Critical error: Qt cannot be initialized.") 86 | sets = settings.Settings() # initialize settings 87 | # parse command line 88 | args = settings.Args(sets) 89 | args.parseArgs(sys.argv[1:]) 90 | 91 | trezor = trezor_app_generic.setupTrezor(sets.TArg, sets.mlogger) 92 | # trezor.clear_session() ## not needed 93 | trezor.prefillReadpinfromstdin(sets.RArg) 94 | trezor.prefillReadpassphrasefromstdin(sets.AArg) 95 | if sets.PArg is None: 96 | trezor.prefillPassphrase(u'') 97 | else: 98 | trezor.prefillPassphrase(sets.PArg) 99 | 100 | # if everything is specified in the command line then do not call the GUI 101 | if ((sets.PArg is None) or (len(sets.inputFiles) <= 0)) and (not sets.TArg): 102 | dialog = Dialog(trezor, sets) 103 | sets.mlogger.setQtextbrowser(dialog.textBrowser) 104 | sets.mlogger.setQtextheader(dialog.descrHeader()) 105 | sets.mlogger.setQtextcontent(dialog.descrContent()) 106 | sets.mlogger.setQtexttrailer(dialog.descrTrailer()) 107 | else: 108 | sets.mlogger.log("Everything was specified or --terminal was set, " 109 | "hence the GUI will not be called.", logging.INFO, u"Arguments") 110 | 111 | sets.mlogger.log("Trezor label: %s" % trezor.features.label, 112 | logging.INFO, "Trezor IO") 113 | sets.mlogger.log("For each operation click 'Confirm' on Trezor " 114 | "to give permission.", logging.INFO, "Trezor IO") 115 | 116 | fileMap = FileMap(trezor, sets) 117 | 118 | if ((sets.PArg is None) or (len(sets.inputFiles) <= 0)) and (not sets.TArg): 119 | # something was not specified, so we call the GUI 120 | # or user wants GUI, so we call the GUI 121 | dialog.setFileMap(fileMap) 122 | dialog.setVersion(basics.VERSION_STR) 123 | showGui(trezor, dialog, sets) 124 | else: 125 | useTerminal(fileMap, sets) 126 | # cleanup 127 | sets.mlogger.log("Cleaning up before shutting down.", logging.DEBUG, "Info") 128 | trezor.close() 129 | 130 | 131 | if __name__ == '__main__': 132 | main() 133 | -------------------------------------------------------------------------------- /basics.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | import logging 6 | from encoding import unpack 7 | 8 | """ 9 | This file contains some constant variables like version numbers, 10 | default values, etc. 11 | """ 12 | 13 | # Name of application 14 | NAME = u'TrezorSymmetricFileEncryption' 15 | 16 | # Name of software version, must be less than 16 bytes long 17 | VERSION_STR = u'v0.6.2' 18 | 19 | # Date of software version, only used in GUI 20 | VERSION_DATE_STR = u'June 2017' 21 | 22 | # default log level 23 | DEFAULT_LOG_LEVEL = logging.INFO # CRITICAL, ERROR, WARNING, INFO, DEBUG 24 | 25 | # short acronym used for name of logger 26 | LOGGER_ACRONYM = u'tsfe' 27 | 28 | # location of logo image 29 | LOGO_IMAGE = u'icons/TrezorSymmetricFileEncryption.216x100.svg' 30 | 31 | # file extension for encrypted files with plaintext filename 32 | FILEEXT = u'.tsfe' 33 | 34 | # Data storage version, format of TSFE file 35 | FILEFORMAT_VERSION = 1 36 | 37 | 38 | class Magic(object): 39 | """ 40 | Few magic constant definitions so that we know which nodes to search 41 | for keys. 42 | """ 43 | 44 | headerStr = b'TSFE' 45 | hdr = unpack("!I", headerStr) 46 | 47 | # first level encryption 48 | # unlock key for first level AES encryption, key from Trezor, en/decryption on PC 49 | levelOneNode = [hdr, unpack("!I", b'DEC1')] 50 | levelOneKey = "Decrypt file for first time?" # string to derive wrapping key from 51 | 52 | # second level encryption 53 | # second level AES encryption, de/encryption on trezor device 54 | levelTwoNode = [hdr, unpack("!I", b'DEC2')] 55 | levelTwoKey = "Decrypt file for second time?" 56 | 57 | # only used for filename encryption (no confirm button click desired) 58 | fileNameNode = [hdr, unpack("!I", b'FLNM')] # filename encryption for filename obfuscation 59 | fileNameKey = "Decrypt filename only?" 60 | -------------------------------------------------------------------------------- /comments.md: -------------------------------------------------------------------------------- 1 | # Comments 2 | 3 | These are just internal comments taken during development. 4 | 5 | # Trezor limits 6 | 7 | In the function `trezor/python-trezor/trezorlib/client.py/encrypt_keyvalue(self, n, key, value, ask_on_encrypt=True, ask_on_decrypt=True, iv=b'')` 8 | the primary input is `value`. The length of `value` must be a multiple of 16 9 | (AES blocksize of 128 bits). It must be buffered to multiple-of-16-bytes if not. 10 | The same number of bytes that go in, come out, as usual for AES. 11 | E.g. The return of 144-bytes in, is 144-bytes out. 12 | Performance for encrypt and decrypt are the same, as usual for AES. 13 | 14 | # Crypto/Cipher/blockalgo.py limit 15 | 16 | The function `Crypto/Cipher/blockalgo.py/encrypt()` the input is limited to 2G (2**31). 17 | 18 | If a file larger than 2G is encrypted this exception is thrown 19 | ``` 20 | ./TrezorSymmetricFileEncryption.py -t 4.4G.img # 4G input file 21 | Traceback (most recent call last): 22 | File "./TrezorSymmetricFileEncryption.py", line 1093, in 23 | doWork(trezor, settings, fileMap) 24 | File "./TrezorSymmetricFileEncryption.py", line 1051, in doWork 25 | convertFile(inputFile, fileMap) 26 | File "./TrezorSymmetricFileEncryption.py", line 1038, in convertFile 27 | encryptFile(inputFile, fileMap, False) 28 | File "./TrezorSymmetricFileEncryption.py", line 1012, in encryptFile 29 | fileMap.save(inputFile, obfuscate) 30 | File "/home/manfred/briefcase/workspace/src/github.com/8go/TrezorSymmetricFileEncryption/file_map.py", line 123, in save 31 | encrypted = self.encryptOuter(serialized, self.outerIv) 32 | File "/home/manfred/briefcase/workspace/src/github.com/8go/TrezorSymmetricFileEncryption/file_map.py", line 138, in encryptOuter 33 | return self.encrypt(plaintext, iv, self.outerKey) 34 | File "/home/manfred/briefcase/workspace/src/github.com/8go/TrezorSymmetricFileEncryption/file_map.py", line 146, in encrypt 35 | return cipher.encrypt(padded) 36 | File "/usr/lib/python2.7/dist-packages/Crypto/Cipher/blockalgo.py", line 244, in encrypt 37 | return self._cipher.encrypt(plaintext) 38 | OverflowError: size does not fit in an int 39 | ``` 40 | 41 | In order to handle files larger than 2G, one would have to junk and reassemble to 2G junks before/after the `cipher.encrypt(padded)` call. 42 | 43 | # Size limits 44 | 45 | Currently files are limited to 2G minus a few bytes. 46 | There is also a limit in the fileformat used by TrezorSymmetricFileEncryption. 47 | It stores the data length as 4-bytes. So, if one would want to go beyond 4G one 48 | would have to change the TrezorSymmetricFileEncryption storage file format 49 | to store the data size as 8 bytes. 50 | 51 | # Performance 52 | 53 | ## 1-level En/Decryption 54 | 55 | AES is very fast. 56 | Most files take less than a seconds but depends on disk speed, CPU, etc. 57 | Encrypting/decrypting a 2G file with 1-level en/decryption took about 15 sec on a computer with a very slow disk but fast CPU. 58 | Encryption time and decryption time are usually the same. 59 | 60 | ## 2-level En/decryption 61 | 62 | The Trezor chip is slow. It takes the Trezor (model 1) device about 75 seconds to en/decrypt 1M. In other words, it can do 0.8MB/min. E.g. for a 20MB file that are 25 minutes. 63 | 50MB in about 1 hour. 64 | 65 | # To-do list 66 | 67 | - [x] file obfuscation 68 | - [x] inner, 2-nd round encryption, new GUI button for it 69 | - [x] add icon to PIN and passphrase GUIs 70 | - [x] add screenshots to README.md 71 | - [x] screenshots of v0.2alpha 72 | - [x] make the image smaller on main window 73 | - [x] more Testing 74 | - [ ] get help with getting the word out, anyone wants to spread the word on Twitter, Reddit, with Trezor, Facebook, etc.? 75 | 76 | # Migrating to Python3 77 | 78 | Doing only Python 2.7 or only 3.4 is okay, but making both work on the same code base is cumbersome. 79 | The combination would be Py2.7 | Py3.4 + PyQt4.11. 80 | 81 | * Basic description of the problem is [here](https://docs.python.org/3/howto/pyporting.html) with some pointers as how to start. 82 | * [2to3](https://docs.python.org/3/library/2to3.html) has been done. It was trivial. Only a few lines of code changed. 83 | * [modernize](https://python-modernize.readthedocs.io/en/latest/) has been done. Again, it was just suggesting a few new lines related to the `range` operator. 84 | * [futurize](http://python-future.org/automatic_conversion.html) was also done. It suggested only a few `import` lines. The 3 lines were added to all .py files. 85 | ``` 86 | from __future__ import absolute_import 87 | from __future__ import division 88 | from __future__ import print_function 89 | ``` 90 | * Changes related to GUI are: 91 | PyQt4.11 for Py3.4 does not have class QString. It expects unicode objects. Simple hacks like 92 | the folowing are not likely to work. 93 | ``` 94 | try: 95 | from PyQt4.QtCore import QString 96 | except ImportError: 97 | # we are using Python3 so QString is not defined 98 | QString = type("") 99 | ``` 100 | * Since Py2.7 does not have bytes and handles everything as strings. A common layer would have to be introduced 101 | that simulates bytes on Py2.7. Some good code starting points can be found at 102 | [python3porting.com](http://python3porting.com/problems.html#bytes-strings-and-unicode). 103 | * In Debian 9 Py2 will remain the default Py version, so Py2.7 does not seem to be going away. 104 | * According to Python.org Python 2.7 will be maintained till 2020. 105 | 106 | In short, for the time being it does not seem worth it to add code to make it run on both 2.7 and 3.4. 107 | It seems one can wait until 2.7 becomes outdated and then port to 3.5, breaking and leaving 2.7 behind. 108 | -------------------------------------------------------------------------------- /dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 840 10 | 820 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 800 22 | 700 23 | 24 | 25 | 26 | 27 | 16777215 28 | 16777215 29 | 30 | 31 | 32 | Trezor Symmetric File Encryption 33 | 34 | 35 | 36 | icons/trezor.bg.svgicons/trezor.bg.svg 37 | 38 | 39 | 40 | 41 | 42 | 43 | 0 44 | 0 45 | 46 | 47 | 48 | 49 | 50 | 51 | icons/TrezorSymmetricFileEncryption.216x100.svg 52 | 53 | 54 | false 55 | 56 | 57 | Qt::AlignCenter 58 | 59 | 60 | -10 61 | 62 | 63 | Qt::NoTextInteraction 64 | 65 | 66 | 67 | 68 | 69 | 70 | Choose one operation and multiple options (if empty all will be chosen automatically) 71 | 72 | 73 | 74 | 75 | 76 | 77 | Qt::Horizontal 78 | 79 | 80 | 81 | 82 | 83 | 84 | QLayout::SetDefaultConstraint 85 | 86 | 87 | 88 | 89 | 6 90 | 91 | 92 | QLayout::SetDefaultConstraint 93 | 94 | 95 | 96 | 97 | 98 | 0 99 | 0 100 | 101 | 102 | 103 | 104 | 0 105 | 0 106 | 107 | 108 | 109 | 110 | 75 111 | true 112 | 113 | 114 | 115 | Chose Encrypt Operation 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 0 124 | 0 125 | 126 | 127 | 128 | 129 | 0 130 | 0 131 | 132 | 133 | 134 | 135 | 0 136 | 25 137 | 138 | 139 | 140 | Encrypt file 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 0 149 | 0 150 | 151 | 152 | 153 | 154 | 0 155 | 0 156 | 157 | 158 | 159 | 160 | 0 161 | 25 162 | 163 | 164 | 165 | Show only obfuscated filename (without encrypting file) 166 | 167 | 168 | 169 | 170 | 171 | 172 | Qt::Vertical 173 | 174 | 175 | QSizePolicy::Minimum 176 | 177 | 178 | 179 | 20 180 | 65 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 6 191 | 192 | 193 | QLayout::SetDefaultConstraint 194 | 195 | 196 | 197 | 198 | 199 | 0 200 | 0 201 | 202 | 203 | 204 | 205 | 0 206 | 0 207 | 208 | 209 | 210 | 211 | 75 212 | true 213 | 214 | 215 | 216 | Chose Encrypt Options 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 0 225 | 0 226 | 227 | 228 | 229 | 230 | 0 231 | 0 232 | 233 | 234 | 235 | 236 | 0 237 | 25 238 | 239 | 240 | 241 | Obfuscate filename 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 0 250 | 0 251 | 252 | 253 | 254 | 255 | 0 256 | 0 257 | 258 | 259 | 260 | 261 | 0 262 | 25 263 | 264 | 265 | 266 | Encrypt twice (very slow on large files) 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 0 275 | 0 276 | 277 | 278 | 279 | 280 | 0 281 | 0 282 | 283 | 284 | 285 | 286 | 0 287 | 25 288 | 289 | 290 | 291 | Perform safety check on encrypted file 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 0 300 | 0 301 | 302 | 303 | 304 | 305 | 0 306 | 0 307 | 308 | 309 | 310 | 311 | 0 312 | 25 313 | 314 | 315 | 316 | Shred plaintext file after encryption 317 | 318 | 319 | 320 | 321 | 322 | 323 | Qt::Vertical 324 | 325 | 326 | QSizePolicy::Minimum 327 | 328 | 329 | 330 | 20 331 | 0 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | Qt::Horizontal 344 | 345 | 346 | 347 | 348 | 349 | 350 | QLayout::SetMinimumSize 351 | 352 | 353 | 354 | 355 | 6 356 | 357 | 358 | QLayout::SetFixedSize 359 | 360 | 361 | 362 | 363 | 364 | 0 365 | 0 366 | 367 | 368 | 369 | 370 | 0 371 | 0 372 | 373 | 374 | 375 | 376 | 75 377 | true 378 | 379 | 380 | 381 | Chose Decrypt Operation 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 0 390 | 0 391 | 392 | 393 | 394 | 395 | 0 396 | 0 397 | 398 | 399 | 400 | Decrypt file 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 0 409 | 0 410 | 411 | 412 | 413 | 414 | 0 415 | 0 416 | 417 | 418 | 419 | Decrypt only obfuscated filename (but not the file) 420 | 421 | 422 | 423 | 424 | 425 | 426 | Qt::Vertical 427 | 428 | 429 | QSizePolicy::Minimum 430 | 431 | 432 | 433 | 20 434 | 0 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 6 445 | 446 | 447 | QLayout::SetFixedSize 448 | 449 | 450 | 451 | 452 | 453 | 0 454 | 0 455 | 456 | 457 | 458 | 459 | 0 460 | 0 461 | 462 | 463 | 464 | 465 | 75 466 | true 467 | 468 | 469 | 470 | Chose Decrypt Options 471 | 472 | 473 | 474 | 475 | 476 | 477 | Perform safety check on plaintext file 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 0 486 | 0 487 | 488 | 489 | 490 | 491 | 0 492 | 0 493 | 494 | 495 | 496 | 497 | 0 498 | 0 499 | 500 | 501 | 502 | Shred encrypted file after decryption 503 | 504 | 505 | 506 | 507 | 508 | 509 | Qt::Vertical 510 | 511 | 512 | QSizePolicy::Minimum 513 | 514 | 515 | 516 | 20 517 | 0 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | Qt::Horizontal 530 | 531 | 532 | 533 | 534 | 535 | 536 | Select one or multiple files 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | Select... 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | Enter master passphrase for Trezor 558 | 559 | 560 | 561 | 562 | 563 | 564 | QLineEdit::Password 565 | 566 | 567 | 568 | 569 | 570 | 571 | Repeat master passphrase for Trezor 572 | 573 | 574 | 575 | 576 | 577 | 578 | QLineEdit::Password 579 | 580 | 581 | 582 | 583 | 584 | 585 | Status 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 50 594 | 50 595 | 596 | 597 | 598 | 599 | 16777215 600 | 16777215 601 | 602 | 603 | 604 | 605 | 0 606 | 150 607 | 608 | 609 | 610 | Qt::NoFocus 611 | 612 | 613 | false 614 | 615 | 616 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 617 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 618 | p, li { white-space: pre-wrap; } 619 | </style></head><body style=" font-family:'Cantarell'; font-size:11pt; font-weight:400; font-style:normal;"> 620 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'DejaVu Sans'; font-size:12pt; font-weight:600;">Welcome to TrezorSymmetricFileEncryption. </span></p> 621 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'DejaVu Sans'; font-size:12pt;">This is version 0.5.0.</span></p> 622 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'DejaVu Sans'; font-size:12pt;"><br /></p> 623 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'DejaVu Sans'; font-size:12pt;">You need to choose a master passphrase that will be used as a Trezor passphrase to encrypt and decrypt files. If forgotten, there's only bruteforcing left.</span></p></body></html> 624 | 625 | 626 | Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse 627 | 628 | 629 | 630 | 631 | 632 | 633 | Qt::Horizontal 634 | 635 | 636 | QDialogButtonBox::Apply|QDialogButtonBox::Close 637 | 638 | 639 | 640 | 641 | 642 | 643 | Apply 644 | 645 | 646 | Ctrl+A 647 | 648 | 649 | 650 | 651 | Done 652 | 653 | 654 | Ctrl+D 655 | 656 | 657 | 658 | 659 | 660 | 661 | buttonBox 662 | accepted() 663 | Dialog 664 | accept() 665 | 666 | 667 | 266 668 | 760 669 | 670 | 671 | 157 672 | 274 673 | 674 | 675 | 676 | 677 | buttonBox 678 | rejected() 679 | Dialog 680 | reject() 681 | 682 | 683 | 334 684 | 760 685 | 686 | 687 | 286 688 | 274 689 | 690 | 691 | 692 | 693 | 694 | -------------------------------------------------------------------------------- /dialogs.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | import os 6 | import os.path 7 | import logging 8 | import sys 9 | 10 | from PyQt5.QtWidgets import QApplication, QDialog, QDialogButtonBox, QShortcut 11 | from PyQt5.QtWidgets import QMessageBox, QFileDialog 12 | from PyQt5.QtGui import QPixmap, QKeySequence 13 | from PyQt5.QtCore import QT_VERSION_STR, QDir 14 | from PyQt5.Qt import PYQT_VERSION_STR 15 | 16 | from ui_dialog import Ui_Dialog 17 | 18 | import basics 19 | import encoding 20 | from processing import processAll 21 | 22 | """ 23 | This code should cover the GUI of the business logic of the application. 24 | 25 | Code should work on both Python 2.7 as well as 3.4. 26 | Requires PyQt5. 27 | (Old version supported PyQt4.) 28 | """ 29 | 30 | 31 | class Dialog(QDialog, Ui_Dialog): 32 | 33 | DESCRHEADER = """ 34 | 35 | Welcome to """ + basics.NAME + """, version """ + basics.VERSION_STR + """ from 36 | """ + basics.VERSION_DATE_STR + """
37 | En/decrypting once is fast. 38 | En/decrypting twice is slow on large files. 39 |
If you lose your master passphrase you will not be able to 40 | decrypt your file(s). You may leave the master passphrase empty.
41 | """ 42 | DESCRTRAILER = "

" 43 | 44 | def __init__(self, trezor, settings): 45 | super(Dialog, self).__init__() 46 | 47 | # Set up the user interface from Designer. 48 | self.setupUi(self) 49 | 50 | self.trezor = trezor 51 | self.settings = settings 52 | self.fileMap = None 53 | 54 | self.radioButtonEncFile.toggled.connect(self.validateEncFile) 55 | self.radioButtonEncFilename.toggled.connect(self.validateEncFilename) 56 | self.radioButtonDecFile.toggled.connect(self.validateDecFile) 57 | self.radioButtonDecFilename.toggled.connect(self.validateDecFilename) 58 | self.checkBoxEncO.toggled.connect(self.validateEncO) 59 | self.checkBoxEnc2.toggled.connect(self.validateEnc2) 60 | self.checkBoxEncS.toggled.connect(self.validateEncS) 61 | self.checkBoxEncW.toggled.connect(self.validateEncW) 62 | self.checkBoxDecS.toggled.connect(self.validateDecS) 63 | self.checkBoxDecW.toggled.connect(self.validateDecW) 64 | 65 | self.checkBoxEncO.setEnabled(False) 66 | self.checkBoxEnc2.setEnabled(False) 67 | self.checkBoxEncS.setEnabled(False) 68 | self.checkBoxEncW.setEnabled(False) 69 | self.checkBoxDecW.setEnabled(False) 70 | self.checkBoxDecS.setEnabled(False) 71 | 72 | self.masterEdit1.textChanged.connect(self.validate) 73 | self.masterEdit2.textChanged.connect(self.validate) 74 | self.selectedFileEdit.textChanged.connect(self.validate) 75 | self.selectedFileButton.clicked.connect(self.selectFile) 76 | self.validate() 77 | self.version = u'' 78 | self.fileNames = [] 79 | self.description1 = self.DESCRHEADER 80 | self.description2 = u'' 81 | self.description3 = self.DESCRTRAILER 82 | 83 | # Apply is not automatically set up, only OK is automatically set up 84 | button = self.buttonBox.button(QDialogButtonBox.Apply) # QtGui.QDialogButtonBox.Ok 85 | button.clicked.connect(self.accept) 86 | # Abort is automatically set up as Reject, like Cancel 87 | 88 | # self.buttonBox.clicked.connect(self.handleButtonClick) # connects ALL buttons 89 | # Created the action in GUI with designer-qt4 90 | self.actionApply.triggered.connect(self.accept) # Save 91 | self.actionDone.triggered.connect(self.reject) # Quit 92 | QShortcut(QKeySequence(u"Ctrl+Q"), self, self.reject) # Quit 93 | QShortcut(QKeySequence(u"Ctrl+S"), self, self.accept) # Save 94 | QShortcut(QKeySequence(u"Ctrl+A"), self, self.accept) # Apply 95 | QShortcut(QKeySequence(u"Ctrl+C"), self, self.copy2Clipboard) 96 | QShortcut(QKeySequence(u"Ctrl+V"), self, self.printAbout) # Version/About 97 | QShortcut(QKeySequence(u"Ctrl+E"), self, self.setEnc) # Enc 98 | QShortcut(QKeySequence(u"Ctrl+D"), self, self.setDec) # 99 | QShortcut(QKeySequence(u"Ctrl+O"), self, self.setEncObf) # 100 | QShortcut(QKeySequence(u"Ctrl+2"), self, self.setEncTwice) # 101 | QShortcut(QKeySequence(u"Ctrl+T"), self, self.setEncDecSafe) # 102 | QShortcut(QKeySequence(u"Ctrl+W"), self, self.setEncDecWipe) # 103 | QShortcut(QKeySequence(u"Ctrl+M"), self, self.setEncFn) # 104 | QShortcut(QKeySequence(u"Ctrl+N"), self, self.setDecFn) # 105 | 106 | self.clipboard = QApplication.clipboard() 107 | self.textBrowser.selectionChanged.connect(self.selectionChanged) 108 | 109 | def descrHeader(self): 110 | return self.DESCRHEADER 111 | 112 | def descrContent(self): 113 | return self.description2 114 | 115 | def descrTrailer(self): 116 | return self.DESCRTRAILER 117 | 118 | def printAbout(self): 119 | """ 120 | Show window with about and version information. 121 | """ 122 | msgBox = QMessageBox(QMessageBox.Information, "About", 123 | "About " + basics.NAME + ":

" + basics.NAME + " " + 124 | "is a file encryption and decryption tool using a Trezor hardware " 125 | "device for safety and security. Symmetric AES cryptography is used " 126 | "at its core.

" + 127 | "" + basics.NAME + " Version: " + basics.VERSION_STR + 128 | " from " + basics.VERSION_DATE_STR + 129 | "

Python Version: " + sys.version.replace(" \n", "; ") + 130 | "

Qt Version: " + QT_VERSION_STR + 131 | "

PyQt Version: " + PYQT_VERSION_STR) 132 | msgBox.setIconPixmap(QPixmap(basics.LOGO_IMAGE)) 133 | msgBox.exec_() 134 | 135 | def validateDecFile(self): 136 | if self.checkBoxEncO.isChecked(): 137 | self.checkBoxEncO.setChecked(False) 138 | if self.checkBoxEnc2.isChecked(): 139 | self.checkBoxEnc2.setChecked(False) 140 | if self.checkBoxEncS.isChecked(): 141 | self.checkBoxEncS.setChecked(False) 142 | if self.checkBoxEncW.isChecked(): 143 | self.checkBoxEncW.setChecked(False) 144 | self.checkBoxEncO.setEnabled(False) 145 | self.checkBoxEnc2.setEnabled(False) 146 | self.checkBoxEncS.setEnabled(False) 147 | self.checkBoxEncW.setEnabled(False) 148 | self.checkBoxDecW.setEnabled(True) 149 | self.checkBoxDecS.setEnabled(True) 150 | self.validate() 151 | 152 | def validateDecFilename(self): 153 | if self.checkBoxEncO.isChecked(): 154 | self.checkBoxEncO.setChecked(False) 155 | if self.checkBoxEnc2.isChecked(): 156 | self.checkBoxEnc2.setChecked(False) 157 | if self.checkBoxEncS.isChecked(): 158 | self.checkBoxEncS.setChecked(False) 159 | if self.checkBoxEncW.isChecked(): 160 | self.checkBoxEncW.setChecked(False) 161 | if self.checkBoxDecS.isChecked(): 162 | self.checkBoxDecS.setChecked(False) 163 | if self.checkBoxDecW.isChecked(): 164 | self.checkBoxDecW.setChecked(False) 165 | self.checkBoxEncO.setEnabled(False) 166 | self.checkBoxEnc2.setEnabled(False) 167 | self.checkBoxEncS.setEnabled(False) 168 | self.checkBoxEncW.setEnabled(False) 169 | self.checkBoxDecS.setEnabled(False) 170 | self.checkBoxDecW.setEnabled(False) 171 | self.validate() 172 | 173 | def validateEncFile(self): 174 | if self.checkBoxDecS.isChecked(): 175 | self.checkBoxDecS.setChecked(False) 176 | if self.checkBoxDecW.isChecked(): 177 | self.checkBoxDecW.setChecked(False) 178 | self.checkBoxEncO.setEnabled(True) 179 | self.checkBoxEnc2.setEnabled(True) 180 | self.checkBoxEncS.setEnabled(True) 181 | self.checkBoxEncW.setEnabled(True) 182 | self.checkBoxDecS.setEnabled(False) 183 | self.checkBoxDecW.setEnabled(False) 184 | self.validate() 185 | 186 | def validateEncFilename(self): 187 | if self.checkBoxEncO.isChecked(): 188 | self.checkBoxEncO.setChecked(False) 189 | if self.checkBoxEnc2.isChecked(): 190 | self.checkBoxEnc2.setChecked(False) 191 | if self.checkBoxEncS.isChecked(): 192 | self.checkBoxEncS.setChecked(False) 193 | if self.checkBoxEncW.isChecked(): 194 | self.checkBoxEncW.setChecked(False) 195 | if self.checkBoxDecS.isChecked(): 196 | self.checkBoxDecS.setChecked(False) 197 | if self.checkBoxDecW.isChecked(): 198 | self.checkBoxDecW.setChecked(False) 199 | self.checkBoxEncO.setEnabled(False) 200 | self.checkBoxEnc2.setEnabled(False) 201 | self.checkBoxEncS.setEnabled(False) 202 | self.checkBoxEncW.setEnabled(False) 203 | self.checkBoxDecS.setEnabled(False) 204 | self.checkBoxDecW.setEnabled(False) 205 | self.validate() 206 | 207 | def validateEncO(self): 208 | if self.checkBoxEncO.isChecked(): 209 | self.settings.mlogger.log("You have selected the option `--obfuscate`. " 210 | "After encrypting the file(s) the encrypted file(s) will be " 211 | "renamed to encrypted, strange looking names. This hides the meta-data," 212 | "i.e. the filename.", logging.INFO, "Arguments") 213 | 214 | def validateEnc2(self): 215 | if self.checkBoxEnc2.isChecked(): 216 | self.settings.mlogger.log("You have selected the option `--twice`. " 217 | "Files will be encrypted not on your computer but on the Trezor " 218 | "device itself. This is slow. It takes 75 seconds for 1M. " 219 | "In other words, 0.8M/min. " 220 | "Remove this option if the file is too big or " 221 | "you do not want to wait.", logging.INFO, 222 | "Dangerous arguments") 223 | 224 | def validateEncS(self): 225 | if self.checkBoxEncS.isChecked(): 226 | self.settings.mlogger.log("You have selected the option `--safety`. " 227 | "After encrypting the file(s) the file(s) will immediately " 228 | "be decrypted and the output compared to the original file(s). " 229 | "This safety check definitely guarantees that all is well.", logging.INFO, 230 | "Arguments") 231 | 232 | def validateEncW(self): 233 | if self.checkBoxEncW.isChecked(): 234 | self.settings.mlogger.log("You have selected the option `--wipe`. " 235 | "The original plaintext files will " 236 | "be shredded and permanently deleted after encryption. " 237 | "Remove this option if you are uncertain or don't understand.", logging.WARN, 238 | "Dangerous arguments") 239 | 240 | def validateDecS(self): 241 | if self.checkBoxEncS.isChecked(): 242 | self.settings.mlogger.log("You have selected the option `--safety`. " 243 | "After decrypting the file(s) the file(s) will immediately " 244 | "be encrypted and the output compared to the original file(s). " 245 | "This safety check definitely guarantees that all is well.", logging.INFO, 246 | "Arguments") 247 | 248 | def validateDecW(self): 249 | if self.checkBoxDecW.isChecked(): 250 | self.settings.mlogger.log("You have selected the option `--wipe`. " 251 | "The encrypted files will be shredded and permanently deleted after decryption. " 252 | "Remove this option if you are uncertain or don't understand.", logging.WARN, 253 | "Dangerous arguments") 254 | 255 | def selectionChanged(self): 256 | """ 257 | called whenever selected text in textarea is changed 258 | """ 259 | # self.textBrowser.copy() # copy selected to clipboard 260 | # self.settings.mlogger.log("Copied text to clipboard: %s" % self.clipboard.text(), 261 | # logging.DEBUG, "Clipboard") 262 | pass 263 | 264 | def copy2Clipboard(self): 265 | self.textBrowser.copy() # copy selected to clipboard 266 | # This is content from the Status textarea, so no secrets here, we can log it 267 | self.settings.mlogger.log("Copied text to clipboard: %s" % self.clipboard.text(), 268 | logging.DEBUG, "Clipboard") 269 | 270 | def setFileMap(self, fileMap): 271 | self.fileMap = fileMap 272 | 273 | def setVersion(self, version): 274 | self.version = version 275 | 276 | def setDescription(self, extradescription): 277 | self.textBrowser.setHtml(self.description1 + extradescription + self.description3) 278 | 279 | def appendDescription(self, extradescription): 280 | """ 281 | @param extradescription: text in HTML format, use
for linebreaks 282 | """ 283 | self.description2 += extradescription 284 | self.textBrowser.setHtml(self.description1 + self.description2 + self.description3) 285 | 286 | def encObf(self): 287 | """ 288 | Returns True if radio button for option "encrypt and obfuscate filename" 289 | is selected 290 | """ 291 | return self.checkBoxEncO.isChecked() 292 | 293 | def setEncObf(self, arg=True): 294 | self.checkBoxEncO.setChecked(arg) 295 | 296 | def encTwice(self): 297 | return self.checkBoxEnc2.isChecked() 298 | 299 | def setEncTwice(self, arg=True): 300 | self.checkBoxEnc2.setChecked(arg) 301 | 302 | def encSafe(self): 303 | return self.checkBoxEncS.isChecked() 304 | 305 | def setEncSafe(self, arg=True): 306 | self.checkBoxEncS.setChecked(arg) 307 | 308 | def decSafe(self): 309 | return self.checkBoxDecS.isChecked() 310 | 311 | def setDecSafe(self, arg=True): 312 | self.checkBoxDecS.setChecked(arg) 313 | 314 | def setEncDecSafe(self, arg=True): 315 | if self.enc(): 316 | self.setEncSafe(arg) 317 | if self.dec(): 318 | self.setDecSafe(arg) 319 | 320 | def encWipe(self): 321 | return self.checkBoxEncW.isChecked() 322 | 323 | def setEncWipe(self, arg=True): 324 | self.checkBoxEncW.setChecked(arg) 325 | 326 | def decWipe(self): 327 | return self.checkBoxDecW.isChecked() 328 | 329 | def setDecWipe(self, arg=True): 330 | self.checkBoxDecW.setChecked(arg) 331 | 332 | def setEncDecWipe(self, arg=True): 333 | if self.enc(): 334 | self.setEncWipe(arg) 335 | if self.dec(): 336 | self.setDecWipe(arg) 337 | 338 | def enc(self): 339 | """ 340 | Returns True if radio button for option "encrypt with plaintext filename" 341 | is selected 342 | """ 343 | return self.radioButtonEncFile.isChecked() 344 | 345 | def setEnc(self, arg=True): 346 | self.radioButtonEncFile.setChecked(arg) 347 | 348 | def encFn(self): 349 | """ 350 | Returns True if radio button for option "encrypt with plaintext filename" 351 | is selected 352 | """ 353 | return self.radioButtonEncFilename.isChecked() 354 | 355 | def setEncFn(self, arg=True): 356 | self.radioButtonEncFilename.setChecked(arg) 357 | 358 | def dec(self): 359 | """ 360 | Returns True if radio button for option "decrypt" is selected 361 | """ 362 | return self.radioButtonDecFile.isChecked() 363 | 364 | def setDec(self, arg=True): 365 | self.radioButtonDecFile.setChecked(arg) 366 | 367 | def decFn(self): 368 | """ 369 | Returns True if radio button for option "decrypt only filename" is 370 | selected 371 | """ 372 | return self.radioButtonDecFilename.isChecked() 373 | 374 | def setDecFn(self, arg=True): 375 | self.radioButtonDecFilename.setChecked(arg) 376 | 377 | def pw1(self): 378 | return encoding.normalize_nfc(self.masterEdit1.text()) 379 | 380 | def setPw1(self, arg): 381 | self.masterEdit1.setText(encoding.normalize_nfc(arg)) 382 | 383 | def pw2(self): 384 | return encoding.normalize_nfc(self.masterEdit2.text()) 385 | 386 | def setPw2(self, arg): 387 | self.masterEdit2.setText(encoding.normalize_nfc(arg)) 388 | 389 | def setSelectedFile(self, fileNames): 390 | """ 391 | Takes a py list as input and concatenates it and 392 | then places it into the single-line text field 393 | """ 394 | filenamesconcat = "" 395 | for file in fileNames: 396 | head, tail = os.path.split(file) 397 | filenamesconcat += '"' + tail + '"' + ' ' 398 | self.selectedFileEdit.setText(encoding.normalize_nfc(filenamesconcat)) 399 | 400 | def setSelectedFiles(self, fileNames): 401 | """ 402 | Takes a py list as input 403 | """ 404 | self.fileNames = fileNames 405 | 406 | def selectedFile(self): 407 | """ 408 | This returns a concatenated string of basenames. 409 | This is most likely not what you want. 410 | Most likely you want a nice list of full filenames with paths, 411 | so use selectedFiles() 412 | """ 413 | return self.selectedFileEdit.text() 414 | 415 | def selectedFiles(self): 416 | """ 417 | Returns a list of full filenames with paths 418 | """ 419 | return self.fileNames 420 | 421 | def selectFile(self): 422 | """ 423 | Show file dialog and return file(s) user has chosen. 424 | """ 425 | path = QDir.currentPath() 426 | dialog = QFileDialog(self, "Select file(s)", 427 | path, "(*)") 428 | dialog.setFileMode(QFileDialog.ExistingFiles) 429 | dialog.setAcceptMode(QFileDialog.AcceptOpen) 430 | 431 | res = dialog.exec_() 432 | if not res: 433 | return 434 | 435 | # in Qt4 this was QStringList, in Qt5 this is a regular list of unicode strings 436 | self.fileNames = dialog.selectedFiles() 437 | self.settings.mlogger.log("Selected files are: %s" % self.fileNames, 438 | logging.DEBUG, "GUI IO") 439 | self.setSelectedFile(self.fileNames) 440 | 441 | def validate(self): 442 | """ 443 | Enable OK/Apply buttons only if both master and backup are repeated 444 | without typo and some file is selected and 445 | exactly a single decrypt/encrypt option from the radio buttons is set. 446 | And only when Encrypt is selected. 447 | On decrypt passphrase does not need to be specified twice. 448 | """ 449 | if self.dec() or self.decFn(): 450 | self.labelPw2.setText(u"Neither needed nor used to decrypt") 451 | else: 452 | self.labelPw2.setText(u"Repeat master passphrase for Trezor") 453 | 454 | same = self.pw1() == self.pw2() 455 | fileSelected = (self.selectedFileEdit.text() != u'') 456 | 457 | # QDialogButtonBox.Ok 458 | button = self.buttonBox.button(QDialogButtonBox.Apply) 459 | button.setEnabled(fileSelected and (same or self.dec() or self.decFn())) 460 | return fileSelected and (same or self.dec() or self.decFn()) 461 | 462 | # def handleButtonClick(self, button): 463 | # sb = self.buttonBox.standardButton(button) 464 | # if sb == QDialogButtonBox.Apply: 465 | # processAll(self.fileMap, self.settings, self) 466 | # # elif sb == QDialogButtonBox.Reset: 467 | # # self.settings.mlogger.log("Reset Clicked, quitting now...", logging.DEBUG, 468 | # # "UI") 469 | 470 | def accept(self): 471 | """ 472 | Apply button was pressed 473 | """ 474 | if self.validate(): 475 | self.settings.mlogger.log("Apply was called by user request. Start processing now.", 476 | logging.DEBUG, "GUI IO") 477 | processAll(self.fileMap, self.settings, self) # 478 | else: 479 | self.settings.mlogger.log("Apply was called by user request. Apply is denied. " 480 | "User input is not valid for processing. Did you select a file?", 481 | logging.DEBUG, "GUI IO") 482 | 483 | # Don't set up a reject() method, it is automatically created. 484 | # If created here again it would overwrite the default one 485 | # def reject(self): 486 | # self.close() 487 | -------------------------------------------------------------------------------- /encoding.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | import sys 6 | import os 7 | import os.path 8 | import random 9 | import struct 10 | import unicodedata 11 | 12 | """ 13 | This is generic code that should work untouched accross all applications. 14 | This code implements generic encoding functions. 15 | 16 | Code should work on both Python 2.7 as well as 3.4. 17 | Requires PyQt5. 18 | (Old version supported PyQt4.) 19 | """ 20 | 21 | 22 | def unpack(fmt, s): 23 | # u = lambda fmt, s: struct.unpack(fmt, s)[0] 24 | return(struct.unpack(fmt, s)[0]) 25 | 26 | 27 | def pack(fmt, s): 28 | # p = lambda fmt, s: struct.pack(fmt, s)[0] 29 | return(struct.pack(fmt, s)) 30 | 31 | 32 | def normalize_nfc(txt): 33 | """ 34 | Utility function to bridge Py2 and Py3 incompatibilities. 35 | Convert to NFC unicode. 36 | Takes string-equivalent or bytes-equivalent and 37 | returns str-equivalent in NFC unicode format. 38 | Py2: str (aslias bytes), unicode 39 | Py3: bytes, str (in unicode format) 40 | Py2-vs-Py3: 41 | """ 42 | if sys.version_info[0] < 3: # Py2-vs-Py3: 43 | if isinstance(txt, unicode): 44 | return unicodedata.normalize('NFC', txt) 45 | if isinstance(txt, str): 46 | return unicodedata.normalize('NFC', txt.decode('utf-8')) 47 | else: 48 | if isinstance(txt, bytes): 49 | return unicodedata.normalize('NFC', txt.decode('utf-8')) 50 | if isinstance(txt, str): 51 | return unicodedata.normalize('NFC', txt) 52 | 53 | 54 | def tobytes(txt): 55 | """ 56 | Utility function to bridge Py2 and Py3 incompatibilities. 57 | Convert to bytes. 58 | Takes string-equivalent or bytes-equivalent and returns bytesequivalent. 59 | Py2: str (aslias bytes), unicode 60 | Py3: bytes, str (in unicode format) 61 | Py2-vs-Py3: 62 | """ 63 | if sys.version_info[0] < 3: # Py2-vs-Py3: 64 | if isinstance(txt, unicode): 65 | return txt.encode('utf-8') 66 | if isinstance(txt, str): # == bytes 67 | return txt 68 | else: 69 | if isinstance(txt, bytes): 70 | return txt 71 | if isinstance(txt, str): 72 | return txt.encode('utf-8') 73 | 74 | 75 | class Padding(object): 76 | """ 77 | PKCS#7 Padding for block cipher having 16-byte blocks 78 | """ 79 | 80 | def __init__(self, blocksize): 81 | self.blocksize = blocksize 82 | 83 | def pad(self, s): 84 | """ 85 | In Python 2 input s is a string, a char list. 86 | Python 2 returns a string. 87 | In Python 3 input s is bytes. 88 | Python 3 returns bytes. 89 | """ 90 | BS = self.blocksize 91 | if sys.version_info[0] > 2: # Py2-vs-Py3: 92 | return s + (BS - len(s) % BS) * bytes([BS - len(s) % BS]) 93 | else: 94 | return s + (BS - len(s) % BS) * chr(BS - len(s) % BS) 95 | 96 | def unpad(self, s): 97 | if sys.version_info[0] > 2: # Py2-vs-Py3: 98 | return s[0:-s[-1]] 99 | else: 100 | return s[0:-ord(s[-1])] 101 | 102 | 103 | class PaddingHomegrown(object): 104 | """ 105 | Pad filenames that are already base64 encoded. Must have length of multiple of 4. 106 | Base64 always have a length of mod 4, padded with = 107 | Examples: YQ==, YWI=, YWJj, YWJjZA==, YWJjZGU=, YWJjZGVm, ... 108 | On homegrown padding we remove the = pad, then we pad to a mod 16 length. 109 | If length is already mod 16, it will be padded with 16 chars. So in all cases we pad. 110 | The last letter always represents how many chars have been padded (A=1, ..., P=16). 111 | The last letter is in the alphabet A..P. 112 | The padded letters before the last letter are pseudo-random in the a..zA..Z alphabet. 113 | """ 114 | 115 | def __init__(self): 116 | self.homegrownblocksize = 16 117 | self.base64blocksize = 4 118 | 119 | def pad(self, s): 120 | """ 121 | Input must be a string in valid base64 format. 122 | Returns a string. 123 | """ 124 | # the randomness can be poor, it does not matter, 125 | # it is just used for buffer letters in the file name 126 | urandom_entropy = os.urandom(64) 127 | random.seed(urandom_entropy) 128 | # remove the base64 buffer char = 129 | t = s.replace(u'=', u'') 130 | initlen = len(t) 131 | BS = self.homegrownblocksize 132 | bufLen = BS - len(t) % BS 133 | r = initlen*ord(t[-1])*ord(t[:1]) 134 | for x in range(0, bufLen-1): 135 | # Old version: 136 | # this was not convenient, 137 | # on various encryptions of the same file, multiple encrypted files 138 | # with different names would be created, requiring cleanup by 139 | # the user as the mapping from plaintext filename to obfuscated 140 | # filename was not deterministic 141 | # r = random.randint(0, 51) # old version 142 | # New version 143 | # deterministic mapping of plaintext filename to obfuscated file name 144 | r = (((r+17)*15485863) % 52) 145 | if r < 26: 146 | c = chr(r+ord('a')) 147 | else: 148 | c = chr(r+ord('A')-26) 149 | t += c 150 | t += chr(BS - initlen % BS + ord('A') - 1) 151 | return t 152 | 153 | def unpad(self, s): 154 | """ 155 | Input must be a string in valid base64 format. 156 | Returns a string. 157 | """ 158 | t = s[0:-(ord(s[-1])-ord('A')+1)] 159 | BS = self.base64blocksize 160 | return t + "=" * ((BS - len(t) % BS) % BS) 161 | 162 | 163 | def escape(str): 164 | """ 165 | Escape the letter \ as \\ in a string. 166 | """ 167 | if str is None: 168 | return u'' 169 | return str.replace('\\', '\\\\') 170 | -------------------------------------------------------------------------------- /icons/TrezorSymmetricFileEncryption.176x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/icons/TrezorSymmetricFileEncryption.176x60.png -------------------------------------------------------------------------------- /icons/TrezorSymmetricFileEncryption.216x100.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /icons/TrezorSymmetricFileEncryption.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/icons/TrezorSymmetricFileEncryption.ico -------------------------------------------------------------------------------- /icons/TrezorSymmetricFileEncryption.icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/icons/TrezorSymmetricFileEncryption.icon.ico -------------------------------------------------------------------------------- /icons/TrezorSymmetricFileEncryption.icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/icons/TrezorSymmetricFileEncryption.icon.png -------------------------------------------------------------------------------- /icons/TrezorSymmetricFileEncryption.icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /icons/TrezorSymmetricFileEncryption.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/icons/TrezorSymmetricFileEncryption.png -------------------------------------------------------------------------------- /icons/TrezorSymmetricFileEncryption.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /icons/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icons/trezor.bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/icons/trezor.bg.png -------------------------------------------------------------------------------- /icons/trezor.bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /icons/trezor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /processing.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | import sys 6 | import logging 7 | import re 8 | import datetime 9 | import traceback 10 | import os 11 | import os.path 12 | import stat 13 | import base64 14 | import hashlib 15 | import filecmp 16 | 17 | from Crypto import Random 18 | 19 | from trezorlib.client import CallException, PinException 20 | 21 | import basics 22 | import encoding 23 | 24 | 25 | def shred(filename, passes, settings=None, logger=None): 26 | """ 27 | Shred the file named `filename` `passes` times. 28 | There is no guarantee that the file will actually be shredded. 29 | The OS or the smart disk might buffer it in a cache and 30 | data might remain on the physical disk. 31 | This is a best effort. 32 | 33 | @param filename: the name of the file to shred 34 | @type filename: C{string} 35 | @param passes: how often the file should be overwritten 36 | @type passes: C{int} 37 | @param settings: holds settings for how to log info/warnings/errors 38 | @type settings: L{Settings} 39 | @param logger: holds logger for where to log info/warnings/errors 40 | @type logger: L{logging.Logger} 41 | @return: True if successful, otherwise False 42 | @rtype: C{bool} 43 | """ 44 | try: 45 | if not os.path.isfile(filename): 46 | raise IOError("Cannot shred, \"%s\" is not a file." % filename) 47 | 48 | ld = os.path.getsize(filename) 49 | with open(filename, "wb") as fh: 50 | for _ in range(int(passes)): 51 | data = b'\x00' * ld 52 | fh.write(data) 53 | fh.seek(0, 0) 54 | with open(filename, "wb") as fh: 55 | fh.truncate(0) 56 | urandom_entropy = os.urandom(64) 57 | randomBytes1 = hashlib.sha256(urandom_entropy).digest() 58 | urandom_entropy = os.urandom(64) 59 | randomBytes2 = hashlib.sha256(urandom_entropy).digest() 60 | randomB64bytes = base64.urlsafe_b64encode(randomBytes1+randomBytes2) 61 | randomB64str = encoding.normalize_nfc(randomB64bytes) 62 | randomB64str = randomB64str.replace(u'=', u'-') 63 | os.rename(filename, randomB64str) 64 | os.remove(randomB64str) 65 | except IOError as e: 66 | if settings is not None: 67 | settings.mlogger.log("Skipping shredding of file \"%s\" (IO error: %s)" % 68 | (filename, e), logging.WARN, "IO Error") 69 | elif logger is not None: 70 | logger.warning("Skipping shredding of file \"%s\" (IO error: %s)" % 71 | (filename, e)) 72 | else: 73 | print("Skipping shredding of file \"%s\" (IO error: %s)" % 74 | (filename, e)) 75 | return False 76 | if settings is not None: 77 | settings.mlogger.log("File \"%s\" has been shredded and deleted." % filename, 78 | logging.INFO, "File IO") 79 | elif logger is not None: 80 | logger.info("Info: File \"%s\" has been shredded and deleted." % filename) 81 | else: 82 | print("Info: File \"%s\" has been shredded and deleted." % filename) 83 | return True 84 | 85 | 86 | def analyzeFilename(inputFile, settings): 87 | """ 88 | Determine from the input filename if we have to Encrypt or decrypt the file. 89 | 90 | Returns "d" or "e" for decrypt or encrypt 91 | EncryptObfuscate will not be returned by this function, 92 | for EncryptObfuscate the user must use -o option. 93 | The default if nothing is specified is the normal plaintext encrypt. 94 | 95 | If it ends in .tsfe then return "d" (only encrypted files should end in .tsfe) 96 | If it does not end in .tsfe and has a . 97 | then return "e" (obfuscated filenames cannot contain .) 98 | If no . in filename && ( length % 16 != 16 || filename contains letters 99 | like &, @, ^, %, $, etc. || last letter is not in A..Q ) 100 | then return "e" 101 | (encrypted obfuscated files have filename length mod 16, do not 102 | contain special chars except - and _, end in A..Q) 103 | Else return "d" (no ., length is mod 16, no special chars, end in A..P) 104 | 105 | @param inputFile: filename to analyze 106 | @type inputFile: C{string} 107 | @param settings: holds settings for how to log info/warnings/errors 108 | @type settings: L{Settings} 109 | """ 110 | settings.mlogger.log("Analyzing filename %s" % inputFile, logging.DEBUG, "Debug") 111 | head, tail = os.path.split(inputFile) 112 | 113 | if '.' in tail: 114 | if tail.endswith(basics.FILEEXT): 115 | return("d") 116 | else: 117 | return("e") 118 | else: 119 | if ((len(tail) % 16) == 0) and (re.search(r'[^\-_a-zA-Z0-9]', tail) is None) and (re.search(r'[^\A-P]', tail[-1:]) is None): 120 | return("d") 121 | else: 122 | return("e") 123 | 124 | 125 | def decryptFileNameOnly(inputFile, settings, fileMap): 126 | """ 127 | Decrypt a filename. 128 | If it ends with .tsfe then the filename is plain text 129 | Otherwise the filename is obfuscated. 130 | 131 | @param inputFile: filename to decrypt 132 | @type inputFile: C{string} 133 | @param settings: holds settings for how to log info/warnings/errors 134 | @type settings: L{Settings} 135 | """ 136 | settings.mlogger.log("Decrypting filename %s" % inputFile, logging.DEBUG, "Debug") 137 | head, tail = os.path.split(inputFile) 138 | 139 | if tail.endswith(basics.FILEEXT) or (not ((len(tail) % 16 == 0) and 140 | (re.search(r'[^\-_a-zA-Z0-9]', tail) is None) and 141 | (re.search(r'[^\A-P]', tail[-1:]) is None))): 142 | isObfuscated = False 143 | if tail.endswith(basics.FILEEXT): 144 | plaintextfname = inputFile[:-len(basics.FILEEXT)] 145 | else: 146 | plaintextfname = inputFile 147 | else: 148 | isObfuscated = True 149 | plaintextfname = os.path.join(head, fileMap.deobfuscateFilename(tail)) 150 | 151 | phead, ptail = os.path.split(plaintextfname) 152 | if not isObfuscated: 153 | settings.mlogger.log("Plaintext filename/path: \"%s\"" % plaintextfname, 154 | logging.DEBUG, "Filename deobfuscation") 155 | settings.mlogger.log("Filename/path \"%s\" is already in plaintext." % tail, 156 | logging.INFO, "Filename deobfuscation") 157 | else: 158 | settings.mlogger.log("Encrypted filename/path: \"%s\"" % inputFile, 159 | logging.DEBUG, "Filename deobfuscation") 160 | settings.mlogger.log("Plaintext filename/path: \"%s\"" % plaintextfname, 161 | logging.DEBUG, "Filename deobfuscation") 162 | settings.mlogger.log("Plaintext filename of \"%s\" is \"%s\"." % 163 | (tail, ptail), logging.NOTSET, 164 | "Filename deobfuscation") 165 | return plaintextfname 166 | 167 | 168 | def decryptFile(inputFile, settings, fileMap): 169 | """ 170 | Decrypt a file. 171 | If it ends with .tsfe then the filename is plain text 172 | Otherwise the filename is obfuscated. 173 | 174 | @param inputFile: name of file to decrypt 175 | @type inputFile: C{string} 176 | @param settings: holds settings for how to log info/warnings/errors 177 | @type settings: L{Settings} 178 | """ 179 | settings.mlogger.log("Decrypting file %s" % inputFile, logging.DEBUG, 180 | "Debug") 181 | head, tail = os.path.split(inputFile) 182 | 183 | if not os.path.isfile(inputFile): 184 | settings.mlogger.log("File \"%s\" does not exist, is not a proper file, " 185 | "or is a directory. Skipping it." % inputFile, logging.ERROR, 186 | "File IO Error") 187 | return 188 | else: 189 | if not os.access(inputFile, os.R_OK): 190 | settings.mlogger.log("File \"%s\" cannot be read. No read permissions. " 191 | "Skipping it." % inputFile, logging.ERROR, "File IO Error") 192 | return 193 | 194 | if tail.endswith(basics.FILEEXT): 195 | isEncrypted = True 196 | elif ((len(tail) % 16 == 0) and 197 | (re.search(r'[^\-_a-zA-Z0-9]', tail) is None) and 198 | (re.search(r'[^\A-P]', tail[-1:]) is None)): 199 | isEncrypted = True 200 | else: 201 | isEncrypted = False 202 | 203 | if not isEncrypted: 204 | settings.mlogger.log("File/path seems plaintext: \"%s\"" % inputFile, 205 | logging.DEBUG, "File decryption") 206 | settings.mlogger.log("File \"%s\" seems to be already in plaintext. " 207 | "Decrypting a plaintext file will fail. Skipping file." % tail, 208 | logging.WARNING, "File decryption") 209 | return None 210 | else: 211 | outputfname, isobfuscated, istwice, outerKey, outerIv, innerIv = fileMap.createDecFile(inputFile) 212 | # for safety make decrypted file rw to user only 213 | os.chmod(outputfname, stat.S_IRUSR | stat.S_IWUSR) 214 | ohead, otail = os.path.split(outputfname) 215 | settings.mlogger.log("Encrypted file/path: \"%s\"" % inputFile, 216 | logging.DEBUG, "File decryption") 217 | settings.mlogger.log("Decrypted file/path: \"%s\"" % outputfname, 218 | logging.DEBUG, "File decryption") 219 | settings.mlogger.log("File \"%s\" has been decrypted successfully. " 220 | "Decrypted file \"%s\" was produced." % (tail, otail), 221 | logging.NOTSET, "File decryption") 222 | safe = True 223 | if settings.SArg and settings.DArg: 224 | safe = safetyCheckDecrypt(inputFile, outputfname, fileMap, isobfuscated, istwice, outerKey, outerIv, innerIv, settings) 225 | if safe and os.path.isfile(inputFile) and settings.WArg and settings.DArg: 226 | # encrypted files are usually read-only, make rw before shred 227 | os.chmod(inputFile, stat.S_IRUSR | stat.S_IWUSR) 228 | shred(inputFile, 3, settings) 229 | rng = Random.new() 230 | # overwrite with nonsense to shred memory 231 | outerKey = rng.read(len(outerKey)) # file_map.KEYSIZE 232 | outerIv = rng.read(len(outerIv)) 233 | if innerIv is not None: 234 | innerIv = rng.read(len(innerIv)) 235 | del outerKey 236 | del outerIv 237 | del innerIv 238 | return outputfname 239 | 240 | 241 | def encryptFileNameOnly(inputFile, settings, fileMap): 242 | """ 243 | Encrypt a filename. 244 | Show only what the obfuscated filename would be, without encrypting the file 245 | 246 | @param inputFile: filename to encrypt 247 | @type inputFile: C{string} 248 | @param settings: holds settings for how to log info/warnings/errors 249 | @type settings: L{Settings} 250 | """ 251 | settings.mlogger.log("Encrypting filename %s" % inputFile, logging.DEBUG, 252 | "Debug") 253 | head, tail = os.path.split(inputFile) 254 | 255 | if analyzeFilename(inputFile, settings) == "d": 256 | settings.mlogger.log("Filename/path seems decrypted: \"%s\"" % inputFile, 257 | logging.DEBUG, "File decryption") 258 | settings.mlogger.log("Filename/path \"%s\" looks like an encrypted file. " 259 | "Why would you encrypt its filename? This looks strange." % tail, 260 | logging.WARNING, "Filename obfuscation") 261 | 262 | obfFileName = os.path.join(head, fileMap.obfuscateFilename(tail)) 263 | 264 | ohead, otail = os.path.split(obfFileName) 265 | settings.mlogger.log("Plaintext filename/path: \"%s\"" % inputFile, 266 | logging.DEBUG, "Filename obfuscation") 267 | settings.mlogger.log("Obfuscated filename/path: \"%s\"" % obfFileName, 268 | logging.DEBUG, "Filename obfuscation") 269 | # Do not modify or remove the next line. 270 | # The test harness, the test shell script requires it. 271 | settings.mlogger.log("Obfuscated filename/path of \"%s\" is \"%s\"." % (tail, otail), 272 | logging.NOTSET, "Filename obfuscation") 273 | return obfFileName 274 | 275 | 276 | def encryptFile(inputFile, settings, fileMap, obfuscate, twice, outerKey, outerIv, innerIv): 277 | """ 278 | Encrypt a file. 279 | if obfuscate == false then keep the output filename in plain text and add .tsfe 280 | 281 | @param inputFile: name of file to encrypt 282 | @type inputFile: C{string} 283 | @param settings: holds settings for how to log info/warnings/errors 284 | @type settings: L{Settings} 285 | @param fileMap: object to use to handle file format of encrypted file 286 | @type fileMap: L{file_map.FileMap} 287 | @param obfuscate: bool to indicate if an obfuscated filename (True) is 288 | desired or a plaintext filename (False) 289 | @type obfuscate: C{bool} 290 | @param twice: bool to indicate if file should be encrypted twice 291 | @type twice: C{bool} 292 | @param outerKey: usually None, 293 | if the same file is encrypted twice 294 | it is different be default, by design, because the outerKey and outerIv are random. 295 | If one wants to produce 296 | an identical encrypted file multiple time (e.g. for a safetyCheckDec()) 297 | then one needs to fix the outerKey and outerIv. 298 | If you want to give it a fixed value, pass it to the function, 299 | otherwise set it to None. 300 | @param outerIv: see outerKey 301 | @param outerKey: 32 bytes 302 | @param outerIv: 16 bytes 303 | """ 304 | settings.mlogger.log("Encrypting file %s" % inputFile, logging.DEBUG, 305 | "Debug") 306 | head, tail = os.path.split(inputFile) 307 | 308 | if not os.path.isfile(inputFile): 309 | settings.mlogger.log("File \"%s\" does not exist, is not a proper file, " 310 | "or is a directory. Skipping it." % inputFile, logging.ERROR, 311 | "File IO Error") 312 | return 313 | else: 314 | if not os.access(inputFile, os.R_OK): 315 | settings.mlogger.log("File \"%s\" cannot be read. No read permissions. " 316 | "Skipping it." % inputFile, logging.ERROR, "File IO Error") 317 | if (os.path.getsize(inputFile) > 8388608) and twice: # 8M+ and -2 option 318 | settings.mlogger.log("This will take more than 10 minutes. Are you sure " 319 | "you want to wait? En/decrypting each Megabyte on the Trezor " 320 | "(model 1) takes about 75 seconds, or 0.8MB/min. The file \"%s\" " 321 | "would take about %d minutes. If you want to en/decrypt fast " 322 | "remove the `-2` or `--twice` option." % 323 | (tail, os.path.getsize(inputFile) // 819200), 324 | logging.WARNING, "Filename obfuscation") # 800K/min 325 | 326 | if tail.endswith(basics.FILEEXT): 327 | isEncrypted = True 328 | elif ((len(tail) % 16 == 0) and 329 | (re.search(r'[^\-_a-zA-Z0-9]', tail) is None) and 330 | (re.search(r'[^\A-P]', tail[-1:]) is None)): 331 | isEncrypted = True 332 | else: 333 | isEncrypted = False 334 | 335 | if isEncrypted: 336 | settings.mlogger.log("File/path seems encrypted: \"%s\"" % inputFile, 337 | logging.DEBUG, "File encryption") 338 | settings.mlogger.log("File \"%s\" seems to be encrypted already. " 339 | "Are you sure you want to (possibly) encrypt it again?" % tail, 340 | logging.WARNING, "File enncryption") 341 | 342 | outputfname = fileMap.createEncFile(inputFile, obfuscate, twice, outerKey, outerIv, innerIv) 343 | # for safety make encrypted file read-only 344 | os.chmod(outputfname, stat.S_IRUSR) 345 | ohead, otail = os.path.split(outputfname) 346 | settings.mlogger.log("Plaintext file/path: \"%s\"" % inputFile, 347 | logging.DEBUG, "File encryption") 348 | settings.mlogger.log("Encrypted file/path: \"%s\"" % outputfname, 349 | logging.DEBUG, "File encryption") 350 | if twice: 351 | twicetext = " twice" 352 | else: 353 | twicetext = "" 354 | settings.mlogger.log("File \"%s\" has been encrypted successfully%s. Encrypted " 355 | "file \"%s\" was produced." % (tail, twicetext, otail), logging.NOTSET, 356 | "File encryption") 357 | safe = True 358 | if settings.SArg and settings.EArg: 359 | safe = safetyCheckEncrypt(inputFile, outputfname, fileMap, settings) 360 | if safe and settings.WArg and settings.EArg: 361 | shred(inputFile, 3, settings) 362 | return outputfname 363 | 364 | 365 | def safetyCheckEncrypt(plaintextFname, encryptedFname, fileMap, settings): 366 | """ 367 | check if previous encryption worked by 368 | renaming plaintextFname file to plaintextFname..org 369 | decrypting file named encryptedFname producing new file decryptedFname 370 | comparing/diffing decryptedFname to original file now named plaintextFname..org 371 | removing decrypted file decryptedFname 372 | renaming original file plaintextFname..org back to input plaintextFname 373 | 374 | @param plaintextFname: name of existing plaintext file whose previous encryption needs to be double-checked 375 | @type plaintextFname: C{string} 376 | @param encryptedFname: name of existing encrypted file (i.e. the plaintext file encrypted) 377 | @type encryptedFname: C{string} 378 | @param settings: holds settings for how to log info/warnings/errors 379 | @type settings: L{Settings} 380 | @param fileMap: object to use to handle file format of encrypted file 381 | @type fileMap: L{file_map.FileMap} 382 | @returns: True if safety check passes successfuly, False otherwise 383 | @rtype: C{bool} 384 | """ 385 | urandom_entropy = os.urandom(64) 386 | randomBytes = hashlib.sha256(urandom_entropy).digest() 387 | # randomBytes is bytes in Py3, str in Py2 388 | # base85 encoding not yet implemented in Python 2.7, (requires Python 3+) 389 | # so we use base64 encoding 390 | # replace the base64 buffer char = 391 | randomB64bytes = base64.urlsafe_b64encode(randomBytes) 392 | # randomB64bytes is bytes in Py3, str in Py2 393 | randomB64str = encoding.normalize_nfc(randomB64bytes) 394 | randomB64str = randomB64str.replace(u'=', u'-') 395 | originalFname = plaintextFname + u"." + randomB64str + u".orignal" 396 | os.rename(plaintextFname, originalFname) 397 | decryptedFname = decryptFile(encryptedFname, settings, fileMap) 398 | aresame = filecmp.cmp(decryptedFname, originalFname, shallow=False) 399 | ihead, itail = os.path.split(plaintextFname) 400 | ohead, otail = os.path.split(encryptedFname) 401 | if aresame: 402 | settings.mlogger.log("Safety check of file \"%s\" (\"%s\") was successful." % 403 | (otail, itail), 404 | logging.INFO, "File encryption") 405 | else: 406 | settings.mlogger.log("Fatal error: Safety check of file \"%s\" (\"%s\") failed! " 407 | "You must inestigate. Encryption was flawed!" % 408 | (otail, itail), 409 | logging.CRITICAL, "File encryption") 410 | os.remove(decryptedFname) 411 | os.rename(originalFname, plaintextFname) 412 | return aresame 413 | 414 | 415 | def safetyCheckDecrypt(encryptedFname, plaintextFname, fileMap, isobfuscated, 416 | istwice, outerKey, outerIv, innerIv, settings): 417 | """ 418 | check if previous decryption worked by 419 | renaming encryptedFname file to encryptedFname..org 420 | encrypting file named plaintextFname producing new file newencryptedFname 421 | comparing/diffing newencryptedFname to original file now named encryptedFname..org 422 | removing decrypted file newencryptedFname 423 | renaming original file encryptedFname..org back to input encryptedFname 424 | 425 | @param encryptedFname: name of existing encrypted file whose previous decryption needs to be double-checked 426 | @type encryptedFname: C{string} 427 | @param plaintextFname: name of existing plaintext file (i.e. the encrypted file decrypted) 428 | @type plaintextFname: C{string} 429 | @param fileMap: object to use to handle file format of encrypted file 430 | @type fileMap: L{file_map.FileMap} 431 | @param obfuscate: bool to indicate if encryptedFname was obfuscated (True) or not (False) before 432 | @type obfuscate: C{bool} 433 | @param twice: bool to indicate if encryptedFname was encrypted twice before 434 | @type twice: C{bool} 435 | @param outerKey: usually None, 436 | if the same file is encrypted twice 437 | it is different be default, by design, because the outerKey and outerIv are random. 438 | If one wants to produce 439 | an identical encrypted file multiple time (e.g. for a safetyCheckDec()) 440 | then one needs to fix the outerKey and outerIv. 441 | If you want to give it a fixed value, pass it to the function, 442 | otherwise set it to None. 443 | @param outerIv: see outerKey 444 | @param outerKey: 32 bytes 445 | @param outerIv: 16 bytes 446 | @param settings: holds settings for how to log info/warnings/errors 447 | @type settings: L{Settings} 448 | @returns: True if safety check passes successfuly, False otherwise 449 | @rtype: C{bool} 450 | """ 451 | settings.mlogger.log("Safety check on decrypted file \"%s\" with " 452 | "obfuscation = %s and double-encryption = %s." % 453 | (encryptedFname, isobfuscated, istwice), 454 | logging.DEBUG, "File decryption") 455 | urandom_entropy = os.urandom(64) 456 | randomBytes = hashlib.sha256(urandom_entropy).digest() 457 | # base85 encoding not yet implemented in Python 2.7, (requires Python 3+) 458 | # so we use base64 encoding 459 | # replace the base64 buffer char = 460 | randomB64bytes = base64.urlsafe_b64encode(randomBytes) 461 | randomB64str = encoding.normalize_nfc(randomB64bytes) 462 | randomB64str = randomB64str.replace(u'=', u'-') 463 | originalFname = encryptedFname + u"." + randomB64str + u".orignal" 464 | os.rename(encryptedFname, originalFname) 465 | newencryptedFname = encryptFile(plaintextFname, settings, fileMap, 466 | isobfuscated, istwice, outerKey, outerIv, innerIv) 467 | aresame = filecmp.cmp(newencryptedFname, originalFname, shallow=False) 468 | ihead, itail = os.path.split(encryptedFname) 469 | ohead, otail = os.path.split(plaintextFname) 470 | if aresame: 471 | settings.mlogger.log("Safety check of file \"%s\" (\"%s\") was successful." % 472 | (otail, itail), 473 | logging.INFO, "File decryption") 474 | else: 475 | settings.mlogger.log("Fatal error: Safety check of file \"%s\" (\"%s\") failed! " 476 | "You must inestigate. Decryption was flawed!" % 477 | (otail, itail), 478 | logging.CRITICAL, "File decryption") 479 | os.remove(newencryptedFname) 480 | os.rename(originalFname, encryptedFname) 481 | return aresame 482 | 483 | 484 | def convertFile(inputFile, fileMap, settings): 485 | """ 486 | Encrypt or decrypt one file. 487 | Which operation will be performed is derived from `settings` 488 | or from analyzing the filename `inputFile` 489 | 490 | @param inputFile: name of the file to be either encrypted or decrypted 491 | @type inputFile: C{string} 492 | @param settings: holds settings for how to log info/warnings/errors 493 | @type settings: L{Settings} 494 | @param fileMap: object to use to handle file format of encrypted file 495 | @type fileMap: L{file_map.FileMap} 496 | """ 497 | if settings.DArg: 498 | # decrypt by choice 499 | decryptFile(inputFile, settings, fileMap) 500 | elif settings.MArg: 501 | # encrypt (name only) by choice 502 | encryptFileNameOnly(inputFile, settings, fileMap) 503 | elif settings.NArg: 504 | # decrypt (name only) by choice 505 | decryptFileNameOnly(inputFile, settings, fileMap) 506 | elif settings.EArg and settings.OArg: 507 | # encrypt and obfuscate by choice 508 | encryptFile(inputFile, settings, fileMap, True, settings.XArg, None, None, None) 509 | elif settings.EArg and not settings.OArg: 510 | # encrypt by choice 511 | encryptFile(inputFile, settings, fileMap, False, settings.XArg, None, None, None) 512 | else: 513 | hint = analyzeFilename(inputFile, settings) 514 | if hint == "d": 515 | # decrypt by default 516 | settings.DArg = True 517 | decryptFile(inputFile, settings, fileMap) 518 | settings.DArg = False 519 | else: 520 | # encrypt by default 521 | settings.EArg = True 522 | encryptFile(inputFile, settings, fileMap, False, settings.XArg, None, None, None) 523 | settings.EArg = False 524 | 525 | 526 | def doWork(fileMap, settings, dialog=None): 527 | """ 528 | Do the real work, perform the main business logic. 529 | Input comes from settings. 530 | Loop through the list of filenames in `settings` 531 | and process each one. 532 | This function should be shared by GUI mode and Terminal mode. 533 | 534 | @param fileMap: object to use to handle file format of encrypted file 535 | @type fileMap: L{file_map.FileMap} 536 | @param settings: holds settings for how to log info/warnings/errors, 537 | also holds the mlogger 538 | @type settings: L{Settings} 539 | @param dialog: holds GUI window for where to log info/warnings/errors 540 | @type dialog: L{dialogs.Dialog} 541 | """ 542 | 543 | settings.mlogger.log("Time entering doWork(): %s" % datetime.datetime.now(), 544 | logging.DEBUG, "Debug") 545 | 546 | for inputFile in settings.inputFiles: 547 | try: 548 | settings.mlogger.log("Working on file: %s" % inputFile, 549 | logging.DEBUG, "Debug") 550 | convertFile(inputFile, fileMap, settings) 551 | except PinException: 552 | settings.mlogger.log("Trezor reports invalid PIN. Aborting.", 553 | logging.CRITICAL, "Trezor IO") 554 | sys.exit(8) 555 | except CallException: 556 | # button cancel on Trezor, so exit 557 | settings.mlogger.log("Trezor reports that user clicked 'Cancel' " 558 | "on Trezor device. Aborting.", logging.CRITICAL, "Trezor IO") 559 | sys.exit(6) 560 | except IOError as e: 561 | settings.mlogger.log("IO error: %s" % e, logging.CRITICAL, 562 | "Critical Exception") 563 | if settings.logger.getEffectiveLevel() == logging.DEBUG: 564 | traceback.print_exc() # prints to stderr 565 | except Exception as e: 566 | settings.mlogger.log("Critical error: %s" % e, logging.CRITICAL, 567 | "Critical Exception") 568 | if settings.logger.getEffectiveLevel() == logging.DEBUG: 569 | traceback.print_exc() # prints to stderr 570 | settings.mlogger.log("Time leaving doWork(): %s" % datetime.datetime.now(), 571 | logging.DEBUG, "Debug") 572 | 573 | 574 | def processAll(fileMap, settings, dialog=None): 575 | """ 576 | Do the real work, perform the main business logic. 577 | Input comes from settings (Terminal mode) or dialog (GUI mode). 578 | Output goes to settings (Terminal mode) or dialog (GUI mode). 579 | This function should be shared by GUI mode and Terminal mode. 580 | 581 | If dialog is None then processAll() has been called 582 | from Terminal and there is no GUI. 583 | If dialog is not None processAll() has been called 584 | from GUI and there is a window. 585 | 586 | Input is in settings.input, 587 | 588 | @param fileMap: object to use to handle file format of encrypted file 589 | @type fileMap: L{file_map.FileMap} 590 | @param settings: holds settings for how to log info/warnings/errors 591 | used to hold inputs and outputs 592 | @type settings: L{Settings} 593 | @param dialog: holds GUI window for access to GUI input, output 594 | @type dialog: L{dialogs.Dialog} 595 | """ 596 | if dialog is not None: 597 | settings.mlogger.log("Apply button was clicked", 598 | logging.DEBUG, "Debug") 599 | settings.gui2Settings(dialog) # move input from GUI to settings 600 | doWork(fileMap, settings, dialog) 601 | if dialog is not None: 602 | settings.settings2Gui(dialog) # move output from settings to GUI 603 | settings.mlogger.log("Apply button was processed, returning to GUI", 604 | logging.DEBUG, "Debug") 605 | -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_aboutWindow.version04b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_aboutWindow.version04b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow1.version01a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow1.version01a.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow1.version02b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow1.version02b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow1.version03b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow1.version03b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow1.version04b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow1.version04b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow2.version01a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow2.version01a.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow2.version02b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow2.version02b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow2.version03b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow2.version03b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow2.version04b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow2.version04b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow3.version03b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow3.version03b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow3.version04b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow3.version04b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow4.version03b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow4.version03b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow4.version04b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow4.version04b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow5.version03b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow5.version03b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow5.version04b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow5.version04b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow6.version03b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow6.version03b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow6.version04b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow6.version04b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow7.version03b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_mainWindow7.version03b.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_passphraseEntryWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_passphraseEntryWindow.png -------------------------------------------------------------------------------- /screenshots/screenshot_TrezorSymmetricFileEncryption_pinEntryWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8go/TrezorSymmetricFileEncryption/1d9542252b8cdea191fd9375a0e6b3eaa50f1d4a/screenshots/screenshot_TrezorSymmetricFileEncryption_pinEntryWindow.png -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | import sys 6 | import logging 7 | import getopt 8 | 9 | from PyQt5.QtWidgets import QMessageBox 10 | from PyQt5.QtCore import QT_VERSION_STR 11 | from PyQt5.Qt import PYQT_VERSION_STR 12 | 13 | import basics 14 | import encoding 15 | from utils import BaseSettings, BaseArgs 16 | 17 | """ 18 | This is code that should be adapted to your applications. 19 | This code implements Settings and Argument parsing. 20 | 21 | Classes BaseSettings and BaseArgs from utils.py 22 | should be subclassed her as Settings and Args. 23 | 24 | Code should work on both Python 2.7 as well as 3.4. 25 | Requires PyQt5. 26 | (Old version supported PyQt4.) 27 | """ 28 | 29 | 30 | class Settings(BaseSettings): 31 | """ 32 | Placeholder for settings 33 | Settings such as command line options, GUI selected values, 34 | user input, etc. 35 | """ 36 | 37 | def __init__(self, logger=None, mlogger=None): 38 | """ 39 | @param logger: holds logger for where to log info/warnings/errors 40 | If None, a default logger will be created. 41 | @type logger: L{logging.Logger} 42 | @param mlogger: holds mlogger for where to log info/warnings/errors 43 | If None, a default mlogger will be created. 44 | @type mlogger: L{utils.MLogger} 45 | """ 46 | super(Settings, self).__init__(logger, mlogger) 47 | self.TArg = False 48 | self.EArg = False 49 | self.OArg = False 50 | self.DArg = False 51 | self.MArg = False 52 | self.NArg = False 53 | self.XArg = False # -2, --twice 54 | self.PArg = None 55 | self.RArg = None # -r read PIN 56 | self.AArg = None # -R read passphrase 57 | self.SArg = False # Safety check 58 | self.WArg = False # Wipe plaintxt after encryption 59 | self.QArg = False # noconfirm 60 | self.inputFiles = [] # list of input filenames 61 | 62 | def logSettings(self): 63 | self.logger.debug(self.__str__()) 64 | 65 | def gui2Settings(self, dialog): 66 | """ 67 | This method should be implemented in the subclass. 68 | Copy the settings info from the dialog GUI to the Settings instance. 69 | """ 70 | self.DArg = dialog.dec() 71 | self.NArg = dialog.decFn() 72 | self.EArg = dialog.enc() 73 | self.MArg = dialog.encFn() 74 | self.OArg = dialog.encObf() 75 | self.XArg = dialog.encTwice() 76 | self.SArg = dialog.encSafe() or dialog.decSafe() 77 | self.WArg = dialog.encWipe() or dialog.decWipe() 78 | self.PArg = dialog.pw1() 79 | self.RArg = False 80 | self.AArg = False 81 | if self.PArg is None: 82 | self.PArg = "" 83 | # if passphrase has changed we must clear the session, 84 | # otherwise Trezor will used cached passphrase, i.e. 85 | # Trezor will not issue callback to ask for passphrase 86 | if (dialog.trezor.passphrase is None) or (dialog.trezor.passphrase != self.PArg.decode("utf-8")): 87 | self.mlogger.log("Passphrase has changed. If PIN is set it will " 88 | "have to be entered again.", logging.INFO, 89 | "Trezor IO") 90 | dialog.trezor.clear_session() 91 | dialog.trezor.prefillPassphrase(self.PArg) 92 | dialog.trezor.prefillReadpinfromstdin(False) 93 | dialog.trezor.prefillReadpassphrasefromstdin(False) 94 | self.inputFiles = dialog.selectedFiles() 95 | self.mlogger.log(self, logging.DEBUG, "Settings") 96 | 97 | def settings2Gui(self, dialog): 98 | """ 99 | This method should be implemented in the subclass. 100 | Copy the settings info from the Settings instance to the dialog GUI. 101 | """ 102 | dialog.setVersion(basics.VERSION_STR) 103 | dialog.setDescription("") 104 | dialog.setSelectedFile(self.inputFiles) 105 | dialog.setSelectedFiles(self.inputFiles) 106 | dialog.setDec(self.DArg) 107 | dialog.setDecFn(self.NArg) 108 | dialog.setDecWipe(self.WArg and self.DArg) 109 | dialog.setEnc(self.EArg) 110 | dialog.setEncFn(self.MArg) 111 | dialog.setEncObf(self.OArg and self.EArg) 112 | dialog.setEncTwice(self.XArg and self.EArg) 113 | dialog.setEncSafe(self.SArg and self.EArg) 114 | dialog.setDecSafe(self.SArg and self.DArg) 115 | dialog.setEncWipe(self.WArg and self.EArg) 116 | dialog.setPw1(self.PArg) 117 | dialog.setPw2(self.PArg) 118 | if self.RArg: 119 | self.RArg = False 120 | self.mlogger.log("In GUI mode `-r` option will be ignored.", 121 | logging.INFO, "Arguments") 122 | if self.AArg: 123 | self.AArg = False 124 | self.mlogger.log("In GUI mode `-R` option will be ignored.", 125 | logging.INFO, "Arguments") 126 | dialog.trezor.prefillReadpinfromstdin(False) 127 | dialog.trezor.prefillReadpassphrasefromstdin(False) 128 | self.mlogger.log(self, logging.DEBUG, "Settings") 129 | 130 | def __str__(self): 131 | return(super(Settings, self).__str__() + "\n" + 132 | "settings.TArg = %s\n" % self.TArg + 133 | "settings.EArg = %s\n" % self.EArg + 134 | "settings.OArg = %s\n" % self.OArg + 135 | "settings.DArg = %s\n" % self.DArg + 136 | "settings.MArg = %s\n" % self.MArg + 137 | "settings.NArg = %s\n" % self.NArg + 138 | "settings.XArg = %s\n" % self.XArg + 139 | "settings.PArg = %s\n" % u"***" + # do not log passphrase 140 | "settings.RArg = %s\n" % self.RArg + 141 | "settings.AArg = %s\n" % self.AArg + 142 | "settings.SArg = %s\n" % self.SArg + 143 | "settings.WArg = %s\n" % self.WArg + 144 | "settings.QArg = %s\n" % self.QArg + 145 | "settings.inputFiles = %s" % self.inputFiles) 146 | 147 | 148 | class Args(BaseArgs): 149 | """ 150 | CLI Argument handling 151 | """ 152 | 153 | def __init__(self, settings, logger=None): 154 | """ 155 | Get all necessary parameters upfront, so the user 156 | does not have to provide them later on each call. 157 | 158 | @param settings: place to store settings 159 | @type settings: L{Settings} 160 | @param logger: holds logger for where to log info/warnings/errors 161 | if no logger is given it uses the default logger of settings. 162 | So, usually this would be None. 163 | @type logger: L{logging.Logger} 164 | """ 165 | super(Args, self).__init__(settings, logger) 166 | 167 | def printVersion(self): 168 | super(Args, self).printVersion() 169 | 170 | def printUsage(self): 171 | print("""TrezorSymmetricFileEncryption.py [-v] [-h] [-l ] [-t] 172 | [-e | -o | -d | -m | -n] 173 | [-2] [-s] [-w] [-p ] [-r] [-R] [q] 174 | -v, --version 175 | print the version number 176 | -h, --help 177 | print short help text 178 | -l, --logging 179 | set logging level, integer from 1 to 5, 1=full logging, 5=no logging 180 | -t, --terminal 181 | run in the terminal, except for a possible PIN query 182 | and a Passphrase query this avoids the GUI 183 | -e, --encrypt 184 | encrypt file and keep output filename as plaintext 185 | (appends .tsfe suffix to input file) 186 | -o, --obfuscatedencrypt 187 | encrypt file and obfuscate output file name 188 | -d, --decrypt 189 | decrypt file 190 | -m, --encnameonly 191 | just encrypt the plaintext filename, show what the obfuscated 192 | filename would be; does not encrypt the file itself; 193 | incompaible with `-d` and `-n` 194 | -n, --decnameonly 195 | just decrypt the obfuscated filename; 196 | does not decrypt the file itself; 197 | incompaible with `-o`, `-e`, and `-m` 198 | -2, --twice 199 | paranoid mode; encrypt file a second time on the Trezor chip itself; 200 | only relevant for `-e` and `-o`; ignored in all other cases. 201 | Consider filesize: The Trezor chip is slow. 1M takes roughly 75 seconds. 202 | -p, --passphrase 203 | master passphrase used for Trezor. 204 | It is recommended that you do not use this command line option 205 | but rather give the passphrase through a small window interaction. 206 | -r, --readpinfromstdin 207 | read the PIN, if needed, from the standard input, i.e. terminal, 208 | when in terminal mode `-t`. By default, even with `-t` set 209 | it is read via a GUI window. 210 | -R, --readpassphrasefromstdin 211 | read the passphrase, when needed, from the standard input, 212 | when in terminal mode `-t`. By default, even with `-t` set 213 | it is read via a GUI window. 214 | -s, --safety 215 | doublechecks the encryption process by decrypting the just 216 | encrypted file immediately and comparing it to original file; 217 | doublechecks the decryption process by encrypting the just 218 | decrypted file immediately and comparing it to original file; 219 | Ignored for `-m` and `-n`. 220 | Primarily useful for testing. 221 | -w, --wipe 222 | shred the inputfile after creating the output file 223 | i.e. shred the plaintext file after encryption or 224 | shred the encrypted file after decryption; 225 | only relevant for `-d`, `-e` and `-o`; ignored in all other cases. 226 | Use with extreme caution. May be used together with `-s`. 227 | -q, --noconfirm 228 | Eliminates the `Confirm` click on the Trezor button. 229 | This was only added to facilitate batch testing. 230 | It should be used EXCLUSIVELY for testing purposes. 231 | Do NOT use this option with real files! 232 | Furthermore, files encryped with `-n` cannot be decrypted 233 | without `-n`. 234 | 235 | 236 | one or multiple files to be encrypted or decrypted 237 | 238 | All arguments are optional. 239 | 240 | All output files are always placed in the same directory as the input files. 241 | 242 | By default the GUI will be used. 243 | 244 | You can avoid the GUI by using `-t`, forcing the Terminal mode. 245 | If you specify filename, possibly some `-o`, `-e`, or `-d` option, then 246 | only PIN and Passphrase will be collected through windows. 247 | 248 | Most of the time TrezorSymmetricFileEncryption can detect automatically if 249 | it needs to decrypt or encrypt by analyzing the given input file name. 250 | So, in most of the cases you do not need to specify any 251 | de/encryption option. 252 | TrezorSymmetricFileEncryption will simply do the right thing. 253 | In the very rare case that TrezorSymmetricFileEncryption determines 254 | the wrong encrypt/decrypt operation you can force it to use the right one 255 | by using either `-e` or `-d` or selecting the appropriate option in the GUI. 256 | 257 | If TrezorSymmetricFileEncryption automatically determines 258 | that it has to encrypt of file, it will chose by default the 259 | `-e` option, and create a plaintext encrypted files with an `.tsfe` suffix. 260 | 261 | If you want the output file name to be obfuscated you 262 | must use the `-o` (obfuscate) flag or select that option in the GUI. 263 | 264 | Be aware of computation time and file sizes when you use `-2` option. 265 | Encrypting on the Trezor takes time: 1M roughtly 75sec. 50M about 1h. 266 | Without `-2` it is very fast, a 1G file taking roughly 15 seconds. 267 | 268 | For safety the file permission of encrypted files is set to read-only. 269 | 270 | Examples: 271 | # specify everything in the GUI 272 | TrezorSymmetricFileEncryption.py 273 | 274 | # specify everything in the GUI, set logging to verbose Debug level 275 | TrezorSymmetricFileEncryption.py -l 1 276 | 277 | # encrypt contract producing contract.doc.tsfe 278 | TrezorSymmetricFileEncryption.py contract.doc 279 | 280 | # encrypt contract and obfuscate output producing e.g. TQFYqK1nha1IfLy_qBxdGwlGRytelGRJ 281 | TrezorSymmetricFileEncryption.py -o contract.doc 282 | 283 | # encrypt contract and obfuscate output producing e.g. TQFYqK1nha1IfLy_qBxdGwlGRytelGRJ 284 | # performs safety check and then shreds contract.doc 285 | TrezorSymmetricFileEncryption.py -e -o -s -w contract.doc 286 | 287 | # decrypt contract producing contract.doc 288 | TrezorSymmetricFileEncryption.py contract.doc.tsfe 289 | 290 | # decrypt obfuscated contract producing contract.doc 291 | TrezorSymmetricFileEncryption.py TQFYqK1nha1IfLy_qBxdGwlGRytelGRJ 292 | 293 | # shows plaintext name of encrypted file, e.g. contract.doc 294 | TrezorSymmetricFileEncryption.py -n TQFYqK1nha1IfLy_qBxdGwlGRytelGRJ 295 | 296 | Keyboard shortcuts of GUI: 297 | Apply, Save: Control-A, Control-S 298 | Cancel, Quit: Esc, Control-Q 299 | Copy to clipboard: Control-C 300 | Version, About: Control-V 301 | Set encrypt operation: Control-E 302 | Set decrypt operation: Control-D 303 | Set obfuscate option: Control-O 304 | Set twice option: Control-2 305 | Set safety option: Control-T 306 | Set wipe option: Control-W 307 | """) 308 | 309 | def parseArgs(self, argv, settings=None, logger=None): 310 | """ 311 | Parse the command line arguments and store the results in `settings`. 312 | Report errors to `logger`. 313 | 314 | @param settings: place to store settings; 315 | if None the default settings from the Args class will be used. 316 | So, usually this argument would be None. 317 | @type settings: L{Settings} 318 | @param logger: holds logger for where to log info/warnings/errors 319 | if None the default logger from the Args class will be used. 320 | So, usually this argument would be None. 321 | @type logger: L{logging.Logger} 322 | """ 323 | # do not call super class parseArgs as partial parsing does not work 324 | # superclass parseArgs is only useful if there are just -v -h -l [level] 325 | # get defaults 326 | if logger is None: 327 | logger = self.logger 328 | if settings is None: 329 | settings = self.settings 330 | try: 331 | opts, args = getopt.getopt(argv, "vhl:tmn2swdeop:rRq", 332 | ["version", "help", "logging=", "terminal", "encnameonly", "decnameonly", 333 | "twice", "safety", "decrypt", "encrypt", "obfuscatedencrypt", 334 | "passphrase=", "readpinfromstdin", "readpassphrasefromstdin", 335 | "noconfirm"]) 336 | except getopt.GetoptError as e: 337 | logger.critical(u'Wrong arguments. Error: %s.', e) 338 | try: 339 | msgBox = QMessageBox(QMessageBox.Critical, u"Wrong arguments", 340 | "Error: %s" % e) 341 | msgBox.exec_() 342 | except Exception: 343 | pass 344 | sys.exit(2) 345 | loglevelused = False 346 | for opt, arg in opts: 347 | arg = encoding.normalize_nfc(arg) 348 | if opt in ("-h", "--help"): 349 | self.printUsage() 350 | sys.exit() 351 | elif opt in ("-v", "--version"): 352 | self.printVersion() 353 | sys.exit() 354 | elif opt in ("-l", "--logging"): 355 | loglevelarg = arg 356 | loglevelused = True 357 | elif opt in ("-t", "--terminal"): 358 | settings.TArg = True 359 | elif opt in ("-m", "--encnameonly"): 360 | settings.MArg = True 361 | elif opt in ("-n", "--decnameonly"): 362 | settings.NArg = True 363 | elif opt in ("-d", "--decrypt"): 364 | settings.DArg = True 365 | elif opt in ("-e", "--encrypt"): 366 | settings.EArg = True 367 | elif opt in ("-o", "--obfuscatedencrypt"): 368 | settings.OArg = True 369 | elif opt in ("-2", "--twice"): 370 | settings.XArg = True 371 | elif opt in ("-s", "--safety"): 372 | settings.SArg = True 373 | elif opt in ("-w", "--wipe"): 374 | settings.WArg = True 375 | elif opt in ("-p", "--passphrase"): 376 | settings.PArg = arg 377 | elif opt in ("-r", "--readpinfromstdin"): 378 | settings.RArg = True 379 | elif opt in ("-R", "--readpassphrasefromstdin"): 380 | settings.AArg = True 381 | elif opt in ("-q", "--noconfirm"): 382 | settings.QArg = True 383 | 384 | if loglevelused: 385 | try: 386 | loglevel = int(loglevelarg) 387 | except Exception as e: 388 | self.settings.mlogger.log(u"Logging level not specified correctly. " 389 | "Must be integer between 1 and 5. (%s)" % loglevelarg, logging.CRITICAL, 390 | "Wrong arguments", settings.TArg, logger) 391 | sys.exit(18) 392 | if loglevel > 5 or loglevel < 1: 393 | self.settings.mlogger.log(u"Logging level not specified correctly. " 394 | "Must be integer between 1 and 5. (%s)" % loglevelarg, logging.CRITICAL, 395 | "Wrong arguments", settings.TArg, logger) 396 | sys.exit(19) 397 | settings.LArg = loglevel * 10 # https://docs.python.org/2/library/logging.html#levels 398 | logger.setLevel(settings.LArg) 399 | 400 | for arg in args: 401 | # convert all input as possible to unicode UTF-8 NFC 402 | settings.inputFiles.append(encoding.normalize_nfc(arg)) 403 | if (settings.DArg and settings.EArg) or (settings.DArg and settings.OArg): 404 | self.settings.mlogger.log("You cannot specify both decrypt and encrypt. " 405 | "It is one or the other. Either -d or -e or -o.", logging.CRITICAL, 406 | "Wrong arguments", True, logger) 407 | sys.exit(2) 408 | if (settings.MArg and settings.DArg) or (settings.MArg and settings.NArg): 409 | self.settings.mlogger.log("You cannot specify both \"encrypt filename\" and " 410 | "\"decrypt file(name)\". It is one or the other. " 411 | "Don't use -m when using -d or -n (and vice versa).", logging.CRITICAL, 412 | "Wrong arguments", True, logger) 413 | sys.exit(2) 414 | if (settings.NArg and settings.EArg) or (settings.NArg and settings.OArg) or (settings.NArg and settings.MArg): 415 | self.settings.mlogger.log("You cannot specify both \"decrypt filename\" and " 416 | "\"encrypt file(name)\". It is one or the other. Don't use " 417 | "-n when using -e, -o, or -m (and vice versa).", logging.CRITICAL, 418 | "Wrong arguments", True, logger) 419 | sys.exit(2) 420 | if settings.OArg: 421 | settings.EArg = True # treat O like an extra flag, used in addition 422 | if settings.MArg: 423 | settings.EArg = False 424 | settings.OArg = False 425 | if settings.NArg: 426 | settings.DArg = False 427 | if (settings.MArg and settings.DArg): 428 | self.settings.mlogger.log("You cannot specify -d and -m at the same time.", logging.CRITICAL, 429 | "Wrong arguments", True, logger) 430 | sys.exit(2) 431 | if (settings.NArg and settings.EArg) or (settings.NArg and settings.OArg): 432 | self.settings.mlogger.log("You cannot specify -e or -o at the same time as -n.", logging.CRITICAL, 433 | "Wrong arguments", True, logger) 434 | sys.exit(2) 435 | if (settings.MArg and settings.OArg) or (settings.MArg and settings.XArg) or \ 436 | (settings.MArg and settings.SArg) or (settings.MArg and settings.WArg): 437 | self.settings.mlogger.log("You cannot specify -o, -2, -s or -w with -m", logging.CRITICAL, 438 | "Wrong arguments", True, logger) 439 | sys.exit(2) 440 | if (settings.NArg and settings.OArg) or (settings.NArg and settings.XArg) or \ 441 | (settings.NArg and settings.SArg) or (settings.NArg and settings.WArg): 442 | self.settings.mlogger.log("You cannot specify -o, -2, -s or -w with -n", logging.CRITICAL, 443 | "Wrong arguments", True, logger) 444 | sys.exit(2) 445 | if (settings.DArg and settings.OArg) or (settings.DArg and settings.XArg): 446 | self.settings.mlogger.log("You cannot specify -o or -2 with -d", logging.CRITICAL, 447 | "Wrong arguments", True, logger) 448 | sys.exit(2) 449 | 450 | settings.mlogger.setTerminalMode(settings.TArg) 451 | self.settings.mlogger.log(u"%s Version: %s (%s)" % 452 | (basics.NAME, basics.VERSION_STR, basics.VERSION_DATE_STR), 453 | logging.INFO, "Version", True, logger) 454 | self.settings.mlogger.log(u"Python: %s" % sys.version.replace(" \n", "; "), 455 | logging.INFO, "Version", True, logger) 456 | self.settings.mlogger.log(u"Qt Version: %s" % QT_VERSION_STR, 457 | logging.INFO, "Version", True, logger) 458 | self.settings.mlogger.log(u"PyQt Version: %s" % PYQT_VERSION_STR, 459 | logging.INFO, "Version", True, logger) 460 | self.settings.mlogger.log(u'Logging level set to %s (%d).' % 461 | (logging.getLevelName(settings.LArg), settings.LArg), 462 | logging.INFO, "Logging", True, logger) 463 | self.settings.mlogger.log(self.settings, 464 | logging.DEBUG, "Settings", True, logger) 465 | if settings.QArg: 466 | self.settings.mlogger.log(u'Warning: the `--noconfirm` option is set. ' 467 | 'This should only be set for batch testing. Do not use this ' 468 | 'mode with real files.', 469 | logging.WARNING, "Settings", True, logger) 470 | -------------------------------------------------------------------------------- /singleFileExecutableLinuxCreate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Requires Python and pip to be installed" 4 | echo "Read: http://www.pyinstaller.org/" 5 | echo "Something similar should work on Windows as well" 6 | echo "Read: https://mborgerson.com/creating-an-executable-from-a-python-script" 7 | 8 | su -c "pip install pyinstaller" root 9 | pyinstaller --hidden-import pkgutil --windowed --icon=icons/TrezorSymmetricFileEncryption.icon.ico --onefile TrezorSymmetricFileEncryption.py 10 | echo "Single file executable is: ./dist/TrezorSymmetricFileEncryption" 11 | ls -lh ./dist/TrezorSymmetricFileEncryption 12 | -------------------------------------------------------------------------------- /singleFileExecutableLinuxReadme.txt: -------------------------------------------------------------------------------- 1 | This single file executable for Linux was created in May 2017 2 | on Debian GNU/Linux 8 (jessie) 64-bit. 3 | To reproduce it: 4 | - install Python 5 | - install pip 6 | - download TrezorSymmetricFileEncryption from https://github.com 7 | - run "make" 8 | - run script singleFileExecutableOnLinuxCreate.sh 9 | The output is a 25M file TrezorSymmetricFileEncryption in dist/TrezorSymmetricFileEncryption 10 | -------------------------------------------------------------------------------- /testTrezorSymmetricFileEncryption.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # directory of the test script, i.e. the test directory 4 | DIR=$(dirname "$(readlink -f "$0")") 5 | APP="TrezorSymmetricFileEncryption.py" 6 | PASSPHRASE="test" 7 | OPT=" -t -l 1 -q -p $PASSPHRASE " # base options 8 | LOG=test.log 9 | 10 | green=$(tput setaf 2) # green color 11 | red=$(tput setaf 1) # red color 12 | reset=$(tput sgr0) # revert to normal/default 13 | 14 | # outputs to stdout the --help usage message. 15 | usage () { 16 | echo "${0##*/}: Usage: ${0##*/} [--help] ..." 17 | echo "${0##*/}: e.g. ${0##*/} 1K" 18 | echo "${0##*/}: e.g. ${0##*/} 10K" 19 | echo "${0##*/}: e.g. ${0##*/} 1M" 20 | echo "${0##*/}: e.g. ${0##*/} 1K 2K 3K" 21 | echo "${0##*/}: Tests larger than 1M will take minutes (0.8M/min)." 22 | } 23 | 24 | if [ $# -eq 1 ]; then 25 | case "${1,,}" in 26 | --help | --hel | --he | --h | -help | -h | -v | --v | --version) 27 | usage; exit 0 ;; 28 | esac 29 | fi 30 | 31 | # main 32 | if [ $# -ge 1 ]; then 33 | pushd $DIR > /dev/null 34 | rm -f $LOG 35 | for py in $(which python2) $(which python3); do 36 | echo "" 37 | echo "Note : Now performing tests with version $py" 38 | set -- "$@" 39 | plaintextfilearray=() 40 | encryptedfilearray=() 41 | rm -f __time_measurements__.txt 42 | echo "Note : Watch your Trezor device, click \"Confirm\" button on Trezor when required." 43 | echo "Step 1: Preparing all test files" 44 | for size in "$@"; do 45 | rm __${size}.img* &> /dev/null 46 | fallocate -l ${size} __${size}.x.img 47 | dd if=/dev/random of=__${size}.random.bin bs=32b count=1 &> /dev/null 48 | echo "This is a test." > __${size}.test.txt 49 | cat __${size}.test.txt __${size}.random.bin __${size}.x.img __${size}.random.bin __${size}.test.txt > __${size}.img 50 | rm __${size}.test.txt __${size}.random.bin __${size}.x.img 51 | plaintextfilearray+=("__${size}.img") 52 | encryptedfilearray+=("__${size}.img.tsfe") 53 | done 54 | echo "Step 2: Encrypting with: " $py $APP $OPT -e "${plaintextfilearray[@]}" 55 | $py $APP $OPT -e "${plaintextfilearray[@]}" &>> $LOG 56 | echo "Step 3: Encrypting filenames with: " $py $APP $OPT -m "${plaintextfilearray[@]}" 57 | # prints lines like his: Obfuscated filename/path of "LICENSE" is "TQFYqK1nha1IfLy_qBxdGwlGRytelGRJ". 58 | $py $APP $OPT -m "${plaintextfilearray[@]}" 2>> $LOG | sed -n 's/.*".*".*"\(.*\)".*/\1/p' > __obfFileNames__.txt 59 | readarray -t obfuscatedfilearray < __obfFileNames__.txt 60 | rm __obfFileNames__.txt 61 | echo "Step 4: Encrypting and obfuscating files with: " $py $APP $OPT -o "${plaintextfilearray[@]}" 62 | /usr/bin/time -o __time_measurements__.txt -f "%E" -a $py $APP $OPT -o "${plaintextfilearray[@]}" &>> $LOG 63 | for size in "$@"; do 64 | mv __${size}.img __${size}.img.org 65 | done 66 | echo "Step 5: Decrypting files with: " $py $APP $OPT -d "${encryptedfilearray[@]}" 67 | $py $APP $OPT -d "${encryptedfilearray[@]}" &>> $LOG 68 | echo "Step 6: Comparing original files with en+decrypted files with plaintext filenames" 69 | for size in "$@"; do 70 | diff __${size}.img __${size}.img.org 71 | rm __${size}.img 72 | done 73 | echo "Step 7: Decrypting and deobfuscating files with: " $py $APP $OPT -d "${obfuscatedfilearray[@]}" 74 | /usr/bin/time -o __time_measurements__.txt -f "%E" -a $py $APP $OPT -d "${obfuscatedfilearray[@]}" &>> $LOG 75 | echo "Step 8: Comparing original files with en+decrypted files with obfuscated filenames" 76 | for size in "$@"; do 77 | diff __${size}.img __${size}.img.org 78 | rm __${size}.img.org 79 | done 80 | for obffile in "${obfuscatedfilearray[@]}"; do 81 | rm -f "$obffile" 82 | done 83 | echo "Step 9: Encrypting and obfuscating files with 2-level-encryption: " $py $APP $OPT -o -2 "${plaintextfilearray[@]}" 84 | /usr/bin/time -o __time_measurements__.txt -f "%E" -a $py $APP $OPT -o -2 "${plaintextfilearray[@]}" &>> $LOG 85 | for size in "$@"; do 86 | mv __${size}.img __${size}.img.org 87 | done 88 | echo "Step 10: Decrypting and deobfuscating files with 2-level-encryption: " $py $APP $OPT -d "${obfuscatedfilearray[@]}" 89 | /usr/bin/time -o __time_measurements__.txt -f "%E" -a $py $APP $OPT -d "${obfuscatedfilearray[@]}" &>> $LOG 90 | echo "Step 11: Comparing original files with en+decrypted files with obfuscated filenames" 91 | for size in "$@"; do 92 | diff __${size}.img __${size}.img.org 93 | done 94 | echo "Step 12: Encrypting with safety check and wipe: " $py $APP $OPT -e -s -w "${plaintextfilearray[@]}" 95 | $py $APP $OPT -e -s -w "${plaintextfilearray[@]}" &>> $LOG 96 | for size in "$@"; do 97 | ls __${size}.img 2> /dev/null # file should not exist 98 | done 99 | echo "Step 13: Decrypting with safety check and wipe: " $py $APP $OPT -d -s -w "${encryptedfilearray[@]}" 100 | $py $APP $OPT -d -s -w "${encryptedfilearray[@]}" &>> $LOG 101 | for size in "$@"; do 102 | ls __${size}.img.tsfe 2> /dev/null # file should not exist 103 | diff __${size}.img __${size}.img.org 104 | done 105 | echo "Step 14: Encrypting with obfuscation, safety check and wipe: " $py $APP $OPT -e -o -s -w "${plaintextfilearray[@]}" 106 | $py $APP $OPT -e -o -s -w "${plaintextfilearray[@]}" &>> $LOG 107 | for size in "$@"; do 108 | ls __${size}.img 2> /dev/null # file should not exist 109 | done 110 | echo "Step 15: Decrypting with safety check and wipe: " $py $APP $OPT -d -w "${obfuscatedfilearray[@]}" 111 | $py $APP $OPT -d -s -w "${obfuscatedfilearray[@]}" &>> $LOG 112 | for size in "$@"; do 113 | diff __${size}.img __${size}.img.org 114 | done 115 | for obffile in "${obfuscatedfilearray[@]}"; do 116 | ls "$obffile" 2> /dev/null # file should not exist 117 | done 118 | for size in "$@"; do 119 | rm -f __${size}.img 120 | rm -f __${size}.img.org 121 | rm -f __${size}.img.tsfe 122 | done 123 | for obffile in "${obfuscatedfilearray[@]}"; do 124 | rm -f "$obffile" 125 | done 126 | echo "End : If no warnings or errors were echoed, then there were no errors, all tests terminated successfully." 127 | done 128 | echo 129 | count=$(grep -i error *$LOG | wc -l) 130 | sum=$((count)) 131 | echo "Log files contain " $count " errors." 132 | count=$(grep -i critical *$LOG | wc -l) 133 | sum=$((sum + count)) 134 | echo "Log files contain " $count " critical issues." 135 | count=$(grep -i warning *$LOG | grep -v noconfirm | grep -v "The option \`--wipe\` is set" | grep -v "exists and encryption will overwrite it" | wc -l) 136 | sum=$((sum + count)) 137 | echo "Log files contain " $count " warnings." 138 | count=$(grep -i ascii *$LOG | wc -l) 139 | sum=$((sum + count)) 140 | echo "Log files contain " $count " ascii-vs-unicode issues." 141 | count=$(grep -i unicode *$LOG | wc -l) 142 | sum=$((sum + count)) 143 | echo "Log files contain " $count " unicode issues." 144 | count=$(grep -i latin *$LOG | wc -l) 145 | sum=$((sum + count)) 146 | echo "Log files contain " $count " latin-vs-unicode issues." 147 | count=$(grep -i byte *$LOG | grep -v " from file " | grep -v " to file " | wc -l) 148 | sum=$((sum + count)) 149 | echo "Log files contain " $count " byte-vs-unicode issues." 150 | if [ $sum -eq 0 ]; then 151 | rm -f $LOG 152 | fi 153 | popd > /dev/null 154 | else 155 | # zero arguments, we run preset default test cases 156 | echo "Note : This default test will take about 3-10 minutes." 157 | echo "Note : If you have a PIN set, you will have to possibly enter it several times. Consider disabling it." 158 | #echo "Note : Be aware that you will have to press the 'Confirm' button on the Trezor many times." 159 | ${0} 1K 10K 100K 1M 160 | fi 161 | exit 0 162 | -------------------------------------------------------------------------------- /trezor_app_generic.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | import sys 6 | import getpass 7 | import logging 8 | 9 | from trezorlib.client import ProtocolMixin 10 | from trezorlib.transport_hid import HidTransport 11 | from trezorlib.client import BaseClient # CallException, PinException 12 | from trezorlib import messages_pb2 as proto 13 | from trezorlib.transport import ConnectionError 14 | 15 | from trezor_gui import TrezorPassphraseDialog, TrezorPinDialog, TrezorChooserDialog 16 | 17 | import encoding 18 | 19 | """ 20 | This is generic code that should work untouched accross all applications. 21 | 22 | This code is written specifically such that both Terminal-only mode as well as 23 | GUI mode are supported for all 3 operations: Trezor choser, PIN entry, 24 | Passphrase entry. 25 | Each of the windows can be turned on or off individually with the 3 flags: 26 | readpinfromstdin, readpassphrasefromstdin, and readdevicestringfromstdin. 27 | 28 | Code should work on both Python 2.7 as well as 3.4. 29 | Requires PyQt5. 30 | (Old version supported PyQt4.) 31 | """ 32 | 33 | 34 | """ 35 | Utility function to bridge Py2 and Py3 incompatibilities. 36 | Maps Py2 raw_input() to input() for Py2. 37 | Py2: raw_input() 38 | Py3: input() 39 | sys.version_info[0] 40 | Py2-vs-Py3: 41 | """ 42 | try: # Py2-vs-Py3: 43 | input = raw_input 44 | except NameError: 45 | pass 46 | 47 | 48 | class QtTrezorMixin(object): 49 | """ 50 | Mixin for input of Trezor PIN and passhprases. 51 | Works via both, terminal as well as PyQt GUI 52 | """ 53 | 54 | def __init__(self, *args, **kwargs): 55 | super(QtTrezorMixin, self).__init__(*args, **kwargs) 56 | self.passphrase = None 57 | self.readpinfromstdin = None 58 | self.readpassphrasefromstdin = None 59 | 60 | def callback_ButtonRequest(self, msg): 61 | return proto.ButtonAck() 62 | 63 | def callback_PassphraseRequest(self, msg): 64 | if self.passphrase is not None: 65 | return proto.PassphraseAck(passphrase=str(self.passphrase)) 66 | 67 | if self.readpassphrasefromstdin: 68 | # read passphrase from stdin 69 | try: 70 | passphrase = getpass.getpass(u"Please enter passphrase: ") 71 | passphrase = encoding.normalize_nfc(passphrase) 72 | except KeyboardInterrupt: 73 | sys.stderr.write(u"\nKeyboard interrupt: passphrase not read. Aborting.\n") 74 | sys.exit(3) 75 | except Exception as e: 76 | sys.stderr.write(u"Critical error: Passphrase not read. Aborting. (%s)" % e) 77 | sys.exit(3) 78 | else: 79 | dialog = TrezorPassphraseDialog() 80 | if not dialog.exec_(): 81 | sys.exit(3) 82 | else: 83 | passphrase = dialog.passphrase() 84 | 85 | return proto.PassphraseAck(passphrase=passphrase) 86 | 87 | def callback_PinMatrixRequest(self, msg): 88 | if self.readpinfromstdin: 89 | # read PIN from stdin 90 | print(u" 7 8 9") 91 | print(u" 4 5 6") 92 | print(u" 1 2 3") 93 | try: 94 | pin = getpass.getpass(u"Please enter PIN: ") 95 | except KeyboardInterrupt: 96 | sys.stderr.write(u"\nKeyboard interrupt: PIN not read. Aborting.\n") 97 | sys.exit(7) 98 | except Exception as e: 99 | sys.stderr.write(u"Critical error: PIN not read. Aborting. (%s)" % e) 100 | sys.exit(7) 101 | else: 102 | dialog = TrezorPinDialog() 103 | if not dialog.exec_(): 104 | sys.exit(7) 105 | pin = dialog.pin() 106 | 107 | return proto.PinMatrixAck(pin=pin) 108 | 109 | def prefillPassphrase(self, passphrase): 110 | """ 111 | Instead of asking for passphrase, use this one 112 | """ 113 | if passphrase is not None: 114 | self.passphrase = encoding.normalize_nfc(passphrase) 115 | else: 116 | self.passphrase = None 117 | 118 | def prefillReadpinfromstdin(self, readpinfromstdin=False): 119 | """ 120 | Specify if PIN should be read from stdin instead of from GUI 121 | @param readpinfromstdin: True to force it to read from stdin, False otherwise 122 | @type readpinfromstdin: C{bool} 123 | """ 124 | self.readpinfromstdin = readpinfromstdin 125 | 126 | def prefillReadpassphrasefromstdin(self, readpassphrasefromstdin=False): 127 | """ 128 | Specify if passphrase should be read from stdin instead of from GUI 129 | @param readpassphrasefromstdin: True to force it to read from stdin, False otherwise 130 | @type readpassphrasefromstdin: C{bool} 131 | """ 132 | self.readpassphrasefromstdin = readpassphrasefromstdin 133 | 134 | 135 | class QtTrezorClient(ProtocolMixin, QtTrezorMixin, BaseClient): 136 | """ 137 | Trezor client with Qt input methods 138 | """ 139 | pass 140 | 141 | 142 | class TrezorChooser(object): 143 | """Class for working with Trezor device via HID""" 144 | 145 | def __init__(self, readdevicestringfromstdin=False): 146 | self.readdevicestringfromstdin = readdevicestringfromstdin 147 | 148 | def getDevice(self): 149 | """ 150 | Get one from available devices. Widget will be shown if more 151 | devices are available. 152 | """ 153 | devices = self.enumerateHIDDevices() 154 | if not devices: 155 | return None 156 | 157 | transport = self.chooseDevice(devices) 158 | client = QtTrezorClient(transport) 159 | return client 160 | 161 | def enumerateHIDDevices(self): 162 | """Returns Trezor HID devices""" 163 | devices = HidTransport.enumerate() 164 | 165 | return devices 166 | 167 | def chooseDevice(self, devices): 168 | """ 169 | Choose device from enumerated list. If there's only one Trezor, 170 | that will be chosen. 171 | 172 | If there are multiple Trezors, diplays a widget with list 173 | of Trezor devices to choose from. 174 | 175 | devices is a list of device 176 | A device is something like: 177 | in Py2: ['0001:0008:00', '0001:0008:01'] 178 | In Py3: [b'0001:0008:00', b'0001:0008:01'] 179 | 180 | @returns HidTransport object of selected device 181 | """ 182 | if not len(devices): 183 | raise RuntimeError(u"No Trezor connected!") 184 | 185 | if len(devices) == 1: 186 | try: 187 | return HidTransport(devices[0]) 188 | except IOError: 189 | raise RuntimeError(u"Trezor is currently in use") 190 | 191 | # maps deviceId string to device label 192 | deviceMap = {} 193 | for device in devices: 194 | try: 195 | transport = HidTransport(device) 196 | client = QtTrezorClient(transport) 197 | label = client.features.label and client.features.label or "" 198 | client.close() 199 | 200 | deviceMap[device[0]] = label 201 | except IOError: 202 | # device in use, do not offer as choice 203 | continue 204 | 205 | if not deviceMap: 206 | raise RuntimeError(u"All connected Trezors are in use!") 207 | 208 | if self.readdevicestringfromstdin: 209 | print(u'Chose your Trezor device please. ' 210 | 'Devices currently in use are not listed:') 211 | ii = 0 212 | for device in deviceMap: 213 | print('%d %s' % (ii, deviceMap[device])) 214 | ii += 1 215 | ii -= 1 216 | while True: 217 | inputstr = input(u"Please provide the number of the device " 218 | "chosen: (%d-%d, Carriage return to quit) " % (0, ii)) 219 | 220 | if inputstr == '': 221 | raise RuntimeError(u"No Trezors device chosen! Quitting.") 222 | try: 223 | inputint = int(inputstr) 224 | except Exception: 225 | print(u'Wrong input. You must enter a number ' 226 | 'between %d and %d. Try again.' % (0, ii)) 227 | continue 228 | if inputint < 0 or inputint > ii: 229 | print(u'Wrong input. You must enter a number ' 230 | 'between %d and %d. Try again.' % (0, ii)) 231 | continue 232 | break 233 | # Py2-vs-Py3: dictionaries are different in Py2 and Py3 234 | if sys.version_info[0] > 2: # Py2-vs-Py3: 235 | deviceStr = list(deviceMap.keys())[ii] 236 | else: 237 | deviceStr = deviceMap.keys()[ii] 238 | else: 239 | dialog = TrezorChooserDialog(deviceMap) 240 | if not dialog.exec_(): 241 | raise RuntimeError(u"No Trezors device chosen! Quitting.") 242 | deviceStr = dialog.chosenDeviceStr() 243 | return HidTransport([deviceStr, None]) 244 | 245 | 246 | def setupTrezor(readdevicestringfromstdin=False, mlogger=None): 247 | """ 248 | setup Trezor, 249 | on error exit program 250 | """ 251 | try: 252 | if mlogger is not None: 253 | mlogger.log(u"Starting Trezor initialization", logging.DEBUG, u"Trezor Info") 254 | trezorChooser = TrezorChooser(readdevicestringfromstdin) 255 | trezor = trezorChooser.getDevice() 256 | except (ConnectionError, RuntimeError) as e: 257 | if mlogger is not None: 258 | mlogger.log(u"Connection to Trezor failed: %s" % e, 259 | logging.CRITICAL, u"Trezor Error") 260 | sys.exit(1) 261 | 262 | if trezor is None: 263 | if mlogger is not None: 264 | mlogger.log(u"No available Trezor found, quitting.", 265 | logging.CRITICAL, u"Trezor Error") 266 | sys.exit(1) 267 | return trezor 268 | -------------------------------------------------------------------------------- /trezor_chooser_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | TrezorChooserDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | 15 | 200 16 | 200 17 | 18 | 19 | 20 | 21 | 800 22 | 600 23 | 24 | 25 | 26 | Choose Trezor to use 27 | 28 | 29 | 30 | icons/trezor.bg.svgicons/trezor.bg.svg 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | icons/trezor.38x55.svg 40 | 41 | 42 | Qt::AlignCenter 43 | 44 | 45 | 46 | 47 | 48 | 49 | Choose Trezor to use (only unused devices are listed) 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Qt::Horizontal 60 | 61 | 62 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | buttonBox 72 | accepted() 73 | TrezorChooserDialog 74 | accept() 75 | 76 | 77 | 248 78 | 254 79 | 80 | 81 | 157 82 | 274 83 | 84 | 85 | 86 | 87 | buttonBox 88 | rejected() 89 | TrezorChooserDialog 90 | reject() 91 | 92 | 93 | 316 94 | 260 95 | 96 | 97 | 286 98 | 274 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /trezor_gui.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | from PyQt5.QtWidgets import QDialog, QListWidgetItem 6 | from PyQt5.QtCore import Qt, QVariant 7 | 8 | from ui_trezor_chooser_dialog import Ui_TrezorChooserDialog 9 | from ui_trezor_pin_dialog import Ui_TrezorPinDialog 10 | from ui_trezor_passphrase_dialog import Ui_TrezorPassphraseDialog 11 | 12 | import encoding 13 | 14 | """ 15 | This is generic code that should work untouched accross all applications. 16 | This code implements the Trezor IO in GUI mode. 17 | It covers all 3 operations: Trezor choser, PIN entry, 18 | Passphrase entry. 19 | 20 | Code should work on both Python 2.7 as well as 3.4. 21 | Requires PyQt5. 22 | (Old version supported PyQt4.) 23 | """ 24 | 25 | 26 | class TrezorChooserDialog(QDialog, Ui_TrezorChooserDialog): 27 | 28 | def __init__(self, deviceMap): 29 | """ 30 | Create dialog and fill it with labels from deviceMap 31 | 32 | @param deviceMap: dict device string -> device label 33 | """ 34 | QDialog.__init__(self) 35 | self.setupUi(self) 36 | 37 | for deviceStr, label in deviceMap.items(): 38 | item = QListWidgetItem(label) 39 | item.setData(Qt.UserRole, QVariant(deviceStr)) 40 | self.trezorList.addItem(item) 41 | self.trezorList.setCurrentRow(0) 42 | 43 | def chosenDeviceStr(self): 44 | """ 45 | Returns device string of chosen Trezor 46 | in Py3: must return str, i.e. bytes; not unicode! 47 | """ 48 | itemData = self.trezorList.currentItem().data(Qt.UserRole) 49 | deviceStr = encoding.tobytes(itemData) 50 | return deviceStr 51 | 52 | 53 | class TrezorPassphraseDialog(QDialog, Ui_TrezorPassphraseDialog): 54 | 55 | def __init__(self): 56 | QDialog.__init__(self) 57 | self.setupUi(self) 58 | 59 | def passphrase(self): 60 | return encoding.normalize_nfc(self.passphraseEdit.text()) 61 | 62 | 63 | class TrezorPinDialog(QDialog, Ui_TrezorPinDialog): 64 | 65 | def __init__(self): 66 | QDialog.__init__(self) 67 | self.setupUi(self) 68 | 69 | self.pb1.clicked.connect(self.pinpadPressed) 70 | self.pb2.clicked.connect(self.pinpadPressed) 71 | self.pb3.clicked.connect(self.pinpadPressed) 72 | self.pb4.clicked.connect(self.pinpadPressed) 73 | self.pb5.clicked.connect(self.pinpadPressed) 74 | self.pb6.clicked.connect(self.pinpadPressed) 75 | self.pb7.clicked.connect(self.pinpadPressed) 76 | self.pb8.clicked.connect(self.pinpadPressed) 77 | self.pb9.clicked.connect(self.pinpadPressed) 78 | 79 | def pin(self): 80 | return encoding.normalize_nfc(self.pinEdit.text()) 81 | 82 | def pinpadPressed(self): 83 | sender = self.sender() 84 | objName = sender.objectName() 85 | digit = objName[-1] 86 | self.pinEdit.setText(self.pinEdit.text() + digit) 87 | -------------------------------------------------------------------------------- /trezor_passphrase_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | TrezorPassphraseDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 180 11 | 12 | 13 | 14 | 15 | 200 16 | 180 17 | 18 | 19 | 20 | 21 | 400 22 | 180 23 | 24 | 25 | 26 | Dialog 27 | 28 | 29 | 30 | icons/trezor.bg.svgicons/trezor.bg.svg 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | icons/trezor.38x55.svg 40 | 41 | 42 | false 43 | 44 | 45 | Qt::AlignCenter 46 | 47 | 48 | 49 | 50 | 51 | 52 | Enter passphrase for Trezor: 53 | 54 | 55 | 56 | 57 | 58 | 59 | Qt::ImhHiddenText|Qt::ImhNoAutoUppercase|Qt::ImhNoPredictiveText|Qt::ImhSensitiveData 60 | 61 | 62 | 1024 63 | 64 | 65 | QLineEdit::Password 66 | 67 | 68 | 69 | 70 | 71 | 72 | Qt::Horizontal 73 | 74 | 75 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | buttonBox 85 | accepted() 86 | TrezorPassphraseDialog 87 | accept() 88 | 89 | 90 | 248 91 | 254 92 | 93 | 94 | 157 95 | 274 96 | 97 | 98 | 99 | 100 | buttonBox 101 | rejected() 102 | TrezorPassphraseDialog 103 | reject() 104 | 105 | 106 | 316 107 | 260 108 | 109 | 110 | 286 111 | 274 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /trezor_pin_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | TrezorPinDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 340 11 | 12 | 13 | 14 | 15 | 180 16 | 300 17 | 18 | 19 | 20 | 21 | 400 22 | 360 23 | 24 | 25 | 26 | Enter PIN 27 | 28 | 29 | 30 | icons/trezor.bg.svgicons/trezor.bg.svg 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | icons/trezor.38x55.svg 40 | 41 | 42 | false 43 | 44 | 45 | Qt::AlignCenter 46 | 47 | 48 | 49 | 50 | 51 | 52 | Enter PIN 53 | 54 | 55 | 56 | 57 | 58 | 59 | 20 60 | 61 | 62 | 20 63 | 64 | 65 | 40 66 | 67 | 68 | 20 69 | 70 | 71 | 72 | 73 | ? 74 | 75 | 76 | 77 | 78 | 79 | 80 | ? 81 | 82 | 83 | 84 | 85 | 86 | 87 | ? 88 | 89 | 90 | 91 | 92 | 93 | 94 | Qt::ImhDigitsOnly|Qt::ImhHiddenText|Qt::ImhNoAutoUppercase|Qt::ImhNoPredictiveText|Qt::ImhSensitiveData 95 | 96 | 97 | 9 98 | 99 | 100 | QLineEdit::Password 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 | Qt::Horizontal 152 | 153 | 154 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 155 | 156 | 157 | 158 | 159 | 160 | 161 | pinEdit 162 | pb8 163 | pb9 164 | buttonBox 165 | pb7 166 | pb4 167 | pb5 168 | pb6 169 | pb1 170 | pb2 171 | pb3 172 | 173 | 174 | 175 | 176 | buttonBox 177 | accepted() 178 | TrezorPinDialog 179 | accept() 180 | 181 | 182 | 248 183 | 254 184 | 185 | 186 | 157 187 | 274 188 | 189 | 190 | 191 | 192 | buttonBox 193 | rejected() 194 | TrezorPinDialog 195 | reject() 196 | 197 | 198 | 316 199 | 260 200 | 201 | 202 | 286 203 | 274 204 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | 6 | import sys 7 | import logging 8 | import getopt 9 | 10 | from PyQt5.QtWidgets import QMessageBox 11 | from PyQt5.QtCore import QT_VERSION_STR 12 | from PyQt5.Qt import PYQT_VERSION_STR 13 | 14 | import basics 15 | from encoding import normalize_nfc 16 | 17 | """ 18 | This is generic code that should work untouched accross all applications. 19 | This code implements Logging for both Terminal and GUI mode. 20 | It implements Settings and Argument parsing. 21 | 22 | Code should work on both Python 2.7 as well as 3.4. 23 | Requires PyQt5. 24 | (Old version supported PyQt4.) 25 | """ 26 | 27 | 28 | def input23(prompt=u''): # Py2-vs-Py3: 29 | """ 30 | Utility function to bridge Py2 and Py3 incompatibilities. 31 | Maps Py2 raw_input() to input() for Py2. 32 | Py2: raw_input() 33 | Py3: input() 34 | """ 35 | if sys.version_info[0] < 3: # Py2-vs-Py3: 36 | return normalize_nfc(raw_input(prompt)) 37 | else: 38 | return normalize_nfc(input(prompt)) 39 | 40 | 41 | class MLogger(object): 42 | """ 43 | class for logging that covers, print, logger, and writing to GUI QTextBrowser widget. 44 | Its is called *M*Logger because it can log to *M*ultiple streams 45 | such as stdout, QTextBrowser, msgBox, ... 46 | Alternatively, he MLogger could have been implemented according to this 47 | strategy: https://stackoverflow.com/questions/24469662/how-to-redirect-logger-output-into-pyqt-text-widget 48 | """ 49 | 50 | def __init__(self, terminalMode=None, logger=None, qtextbrowser=None): 51 | """ 52 | Get as many necessary parameters upfront as possible, so the user 53 | does not have to provide them later on each call. 54 | 55 | @param terminalMode: log only to terminal? 56 | @type terminalMode: C{bool} 57 | @param logger: holds logger for where to log info/warnings/errors 58 | @type logger: L{logging.Logger} 59 | @param qtextbrowser: holds GUI widget for where to log info/warnings/errors 60 | @type qtextbrowser: L{PyQt5.QtWidgets.QTextBrowser} 61 | """ 62 | self.terminalMode = terminalMode 63 | self.logger = logger 64 | self.qtextbrowser = qtextbrowser 65 | # qtextbrowser text will be created by assembling: 66 | # qtextheader + qtextContent + qtextTrailer 67 | self.qtextheader = u'' 68 | self.qtextcontent = u'' 69 | self.qtexttrailer = u'' 70 | 71 | def setTerminalMode(self, terminalMode): 72 | self.terminalMode = terminalMode 73 | 74 | def setLogger(self, logger): 75 | self.logger = logger 76 | 77 | def setQtextbrowser(self, qtextbrowser): 78 | """ 79 | @param qtextbrowser: holds GUI widget for where to log info/warnings/errors 80 | @type qtextbrowser: L{PyQt5.QtWidgets.QTextBrowser} 81 | """ 82 | self.qtextbrowser = qtextbrowser 83 | 84 | def setQtextheader(self, str): 85 | """ 86 | @param str: string to report/log 87 | @type str: C{string} 88 | """ 89 | self.qtextheader = str 90 | 91 | def setQtextcontent(self, str): 92 | """ 93 | @param str: string to report/log 94 | @type str: C{string} 95 | """ 96 | self.qtextcontent = str 97 | 98 | def appendQtextcontent(self, str): 99 | """ 100 | @param str: string to report/log 101 | @type str: C{string} 102 | """ 103 | self.qtextcontent += str 104 | 105 | def setQtexttrailer(self, str): 106 | """ 107 | @param str: string to report/log 108 | @type str: C{string} 109 | """ 110 | self.qtexttrailer = str 111 | 112 | def qtext(self): 113 | return self.qtextheader + self.qtextcontent + self.qtexttrailer 114 | 115 | def moveCursorToBottomQtext(self): 116 | # move the cursor to the end of the text, scroll to the bottom 117 | cursor = self.qtextbrowser.textCursor() 118 | cursor.setPosition(len(self.qtextbrowser.toPlainText())) 119 | self.qtextbrowser.ensureCursorVisible() 120 | self.qtextbrowser.setTextCursor(cursor) 121 | 122 | def publishQtext(self): 123 | self.qtextbrowser.setHtml(self.qtext()) 124 | self.moveCursorToBottomQtext() 125 | 126 | def log(self, str, level, title, terminalMode=None, logger=None, qtextbrowser=None): 127 | """ 128 | Displays string `str` depending on scenario: 129 | a) in terminal mode: thru logger (except if loglevel == NOTSET) 130 | b) in GUI mode and GUI window open: (qtextbrowser!=None) in qtextbrowser of GUI window 131 | c) in GUI mode but window still/already closed: (qtextbrowser==None) thru QMessageBox() 132 | 133 | If terminalMode=None, logger=None, qtextbrowser=None, then the 134 | corresponding value from self is used. So, None means this 135 | value should default to the preset value of the class Log. 136 | 137 | @param str: string to report/log 138 | @type str: C{string} 139 | @param level: log level from DEBUG to CRITICAL from L{logging} 140 | @type level: C{int} 141 | @param title: window title text (only used if there is a window) 142 | @type title: C{string} 143 | 144 | @param terminalMode: log only to terminal? 145 | @type terminalMode: C{bool} 146 | @param logger: holds logger for where to log info/warnings/errors 147 | @type logger: L{logging.Logger} 148 | @param qtextbrowser: holds GUI widget for where to log info/warnings/errors 149 | @type qtextbrowser: L{PyQt5.QtWidgets.QTextBrowser} 150 | """ 151 | # get defaults 152 | if terminalMode is None: 153 | terminalMode = self.terminalMode 154 | if logger is None: 155 | logger = self.logger 156 | if qtextbrowser is None: 157 | qtextbrowser = self.qtextbrowser 158 | # initialize 159 | if logger is None: 160 | logging.basicConfig(stream=sys.stderr, level=basics.DEFAULT_LOG_LEVEL) 161 | logger = logging.getLogger(basics.LOGGER_ACRONYM) 162 | if qtextbrowser is None: 163 | guiExists = False 164 | else: 165 | guiExists = True 166 | if guiExists: 167 | if terminalMode is None: 168 | terminalMode = False 169 | else: 170 | if terminalMode is None: 171 | terminalMode = True 172 | if level == logging.NOTSET: 173 | if terminalMode: 174 | print(str) # stdout 175 | elif guiExists: 176 | print(str) # stdout 177 | self.appendQtextcontent(u"
%s" % (str)) 178 | else: 179 | print(str) # stdout 180 | try: 181 | msgBox = QMessageBox(QMessageBox.Information, 182 | title, u"%s" % (str)) 183 | msgBox.exec_() 184 | except Exception: 185 | pass 186 | elif level == logging.DEBUG: 187 | if terminalMode: 188 | logger.debug(str) 189 | elif guiExists: 190 | logger.debug(str) 191 | if logger.getEffectiveLevel() <= level: 192 | self.appendQtextcontent(u"
Debug: %s" % str) 193 | else: 194 | # don't spam the user with too many pop-ups 195 | # For debug, instead of a pop-up we write to stdout 196 | logger.debug(str) 197 | elif level == logging.INFO: 198 | if terminalMode: 199 | logger.info(str) 200 | elif guiExists: 201 | logger.info(str) 202 | if logger.getEffectiveLevel() <= level: 203 | self.appendQtextcontent(u"
Info: %s" % (str)) 204 | else: 205 | logger.info(str) 206 | if logger.getEffectiveLevel() <= level: 207 | try: 208 | msgBox = QMessageBox(QMessageBox.Information, 209 | title, u"Info: %s" % (str)) 210 | msgBox.exec_() 211 | except Exception: 212 | pass 213 | elif level == logging.WARNING: 214 | if terminalMode: 215 | logger.warning(str) 216 | elif guiExists: 217 | logger.warning(str) 218 | if logger.getEffectiveLevel() <= level: 219 | self.appendQtextcontent(u"
Warning: %s" % (str)) 220 | else: 221 | logger.warning(str) 222 | if logger.getEffectiveLevel() <= level: 223 | try: 224 | msgBox = QMessageBox(QMessageBox.Warning, 225 | title, u"Warning: %s" % (str)) 226 | msgBox.exec_() 227 | except Exception: 228 | pass 229 | elif level == logging.ERROR: 230 | if terminalMode: 231 | logger.error(str) 232 | elif guiExists: 233 | logger.error(str) 234 | if logger.getEffectiveLevel() <= level: 235 | self.appendQtextcontent(u"
Error: %s" % (str)) 236 | else: 237 | logger.error(str) 238 | if logger.getEffectiveLevel() <= level: 239 | try: 240 | msgBox = QMessageBox(QMessageBox.Critical, 241 | title, u"Error: %s" % (str)) 242 | msgBox.exec_() 243 | except Exception: 244 | pass 245 | elif level == logging.CRITICAL: 246 | if terminalMode: 247 | logger.critical(str) 248 | elif guiExists: 249 | logger.critical(str) 250 | if logger.getEffectiveLevel() <= level: 251 | self.appendQtextcontent(u"
Critical: %s" % (str)) 252 | else: 253 | logger.critical(str) 254 | if logger.getEffectiveLevel() <= level: 255 | try: 256 | msgBox = QMessageBox(QMessageBox.Critical, 257 | title, u"Critical: %s" % (str)) 258 | msgBox.exec_() 259 | except Exception: 260 | pass 261 | if qtextbrowser is not None: 262 | # flush changes to GUI 263 | self.publishQtext() 264 | 265 | 266 | class BaseSettings(object): 267 | """ 268 | Placeholder for settings 269 | Settings such as command line options, GUI selected values, 270 | user input, etc. 271 | This class is supposed to be subclassed, e.g. as Settings 272 | to adapt to the specifics of the application. 273 | """ 274 | 275 | def __init__(self, logger=None, mlogger=None): 276 | """ 277 | @param logger: holds logger for where to log info/warnings/errors 278 | If None, a default logger will be created. 279 | @type logger: L{logging.Logger} 280 | @param mlogger: holds mlogger for where to log info/warnings/errors 281 | If None, a default mlogger will be created. 282 | @type mlogger: L{utils.MLogger} 283 | """ 284 | self.VArg = False 285 | self.HArg = False 286 | self.LArg = basics.DEFAULT_LOG_LEVEL 287 | 288 | if logger is None: 289 | """ 290 | If "import sys" is not repeated here then 291 | logger will fail with error 292 | UnicodeEncodeError: 'latin-1' codec can't encode character '\u1ebd' in position 1: ordinal not in range(256) 293 | when foreign strings like "ñẽë儿ë" are used in the command line. 294 | """ 295 | import sys 296 | logging.basicConfig(stream=sys.stderr, level=basics.DEFAULT_LOG_LEVEL) 297 | self.logger = logging.getLogger(basics.LOGGER_ACRONYM) 298 | else: 299 | self.logger = logger 300 | 301 | if mlogger is None: 302 | self.mlogger = MLogger(terminalMode=None, logger=self.logger, qtextbrowser=None) 303 | else: 304 | self.mlogger = mlogger 305 | 306 | def logSettings(self): 307 | self.logger.debug(self.__str__()) 308 | 309 | def gui2Settings(self, dialog): 310 | """ 311 | This method should be implemented in the subclass. 312 | Copy the settings info from the dialog GUI to the Settings instance. 313 | """ 314 | pass 315 | 316 | def settings2Gui(self, dialog): 317 | """ 318 | This method should be implemented in the subclass. 319 | Copy the settings info from the Settings instance to the dialog GUI. 320 | """ 321 | pass 322 | 323 | def __str__(self): 324 | return("settings.VArg = %s\n" % self.VArg + 325 | "settings.HArg = %s\n" % self.HArg + 326 | "settings.LArg = %s" % self.LArg) 327 | 328 | 329 | class BaseArgs(object): 330 | """ 331 | CLI Argument handling 332 | This class is supposed to be subclassed, e.g. as Args 333 | to adapt to the specifics of the application. 334 | """ 335 | 336 | def __init__(self, settings, logger=None): 337 | """ 338 | Get all necessary parameters upfront, so the user 339 | does not have to provide them later on each call. 340 | 341 | @param settings: place to store settings 342 | @type settings: L{Settings} 343 | @param logger: holds logger for where to log info/warnings/errors 344 | if no logger is given it uses the default logger of settings. 345 | So, usually this would be None. 346 | @type logger: L{logging.Logger} 347 | """ 348 | self.settings = settings 349 | if logger is None: 350 | self.logger = settings.logger 351 | else: 352 | self.logger = logger 353 | 354 | def printVersion(self): 355 | print(u"%s Version: %s (%s)" % (basics.NAME, basics.VERSION_STR, basics.VERSION_DATE_STR)) 356 | print(u"Python: %s" % sys.version.replace(" \n", "; ")) 357 | print(u"Qt Version: %s" % QT_VERSION_STR) 358 | print(u"PyQt Version: %s" % PYQT_VERSION_STR) 359 | 360 | def printUsage(self): 361 | """ 362 | This method should be implemented in the subclass. 363 | """ 364 | print(basics.NAME + '''.py [-h] [-v] [-l ] 365 | -v, --version 366 | Print the version number 367 | -h, --help 368 | Print help text 369 | -l, --logging 370 | Set logging level, integer from 1 to 5, 1=full logging, 5=no logging 371 | ''') 372 | 373 | def parseArgs(self, argv, settings=None, logger=None): 374 | """ 375 | Parse the command line arguments and store the results in `settings`. 376 | Report errors to `logger`. 377 | 378 | This method should be implemented in the subclass. 379 | Calling this method is only useful when the application has exactly 380 | the 3 arguments -h -v -l [level] 381 | 382 | @param settings: place to store settings; 383 | if None the default settings from the Args class will be used. 384 | So, usually this argument would be None. 385 | @type settings: L{Settings} 386 | @param logger: holds logger for where to log info/warnings/errors 387 | if None the default logger from the Args class will be used. 388 | So, usually this argument would be None. 389 | @type logger: L{logging.Logger} 390 | """ 391 | # get defaults 392 | if logger is None: 393 | logger = self.logger 394 | if settings is None: 395 | settings = self.settings 396 | try: 397 | opts, args = getopt.getopt(argv, "vhl:", 398 | ["version", "help", "logging="]) 399 | except getopt.GetoptError as e: 400 | msgBox = QMessageBox(QMessageBox.Critical, u"Wrong arguments", 401 | u"Error: %s" % e) 402 | msgBox.exec_() 403 | logger.critical(u'Wrong arguments. Error: %s.', e) 404 | sys.exit(2) 405 | loglevelused = False 406 | for opt, arg in opts: 407 | arg = normalize_nfc(arg) 408 | if opt in ("-h", "--help"): 409 | self.printUsage() 410 | sys.exit() 411 | elif opt in ("-v", "--version"): 412 | self.printVersion() 413 | sys.exit() 414 | elif opt in ("-l", "--logging"): 415 | loglevelarg = arg 416 | loglevelused = True 417 | 418 | if loglevelused: 419 | try: 420 | loglevel = int(loglevelarg) 421 | except Exception: 422 | self.settings.mlogger.log(u"Logging level not specified correctly. " 423 | "Must be integer between 1 and 5. (%s)" % loglevelarg, logging.CRITICAL, 424 | "Wrong arguments", True, logger) 425 | sys.exit(18) 426 | if loglevel > 5 or loglevel < 1: 427 | self.settings.mlogger.log(u"Logging level not specified correctly. " 428 | "Must be integer between 1 and 5. (%s)" % loglevelarg, logging.CRITICAL, 429 | "Wrong arguments", True, logger) 430 | sys.exit(19) 431 | settings.LArg = loglevel * 10 # https://docs.python.org/2/library/logging.html#levels 432 | logger.setLevel(settings.LArg) 433 | 434 | self.settings.mlogger.log(u"%s Version: %s (%s)" % 435 | (basics.NAME, basics.VERSION_STR, basics.VERSION_DATE_STR), 436 | logging.INFO, "Version", True, logger) 437 | self.settings.mlogger.log(u"Python: %s" % sys.version.replace(" \n", "; "), 438 | logging.INFO, "Version", True, logger) 439 | self.settings.mlogger.log(u"Qt Version: %s" % QT_VERSION_STR, 440 | logging.INFO, "Version", True, logger) 441 | self.settings.mlogger.log(u"PyQt Version: %s" % PYQT_VERSION_STR, 442 | logging.INFO, "Version", True, logger) 443 | self.settings.mlogger.log(u'Logging level set to %s (%d).' % 444 | (logging.getLevelName(settings.LArg), settings.LArg), 445 | logging.INFO, "Logging", True, logger) 446 | self.settings.mlogger.log(settings, 447 | logging.DEBUG, "Settings", True, logger) 448 | --------------------------------------------------------------------------------