Skip to content

Commit cb679cd

Browse files
author
8go
committed
2 bug fixes, new CLI option, more inline documentation
1 parent bcc2591 commit cb679cd

File tree

8 files changed

+359
-104
lines changed

8 files changed

+359
-104
lines changed

README.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ![Trezor icon](https://github.com/8go/TrezorSymmetricFileEncryption/blob/master/icons/TrezorSymmetricFileEncryption.png)
1+
# ![Trezor icon](icons/TrezorSymmetricFileEncryption.png)
22

33
# Trezor Symmetric File Encryption
44

@@ -74,7 +74,7 @@ Run:
7474
Run-time command line options are
7575

7676
```
77-
TrezorSymmetricFileEncryption.py [-v] [-h] [-l <level>] [-t] [-2] [-s] [-w] [-o | -e | -d | -m | -n] [-p <passphrase>] <files>
77+
TrezorSymmetricFileEncryption.py [-v] [-h] [-l <level>] [-t] [-e | -o | -d | -m | -n] [-2] [-s] [-w] [-p <passphrase>] [-r] [-R] <files>
7878
-v, --verion
7979
print the version number
8080
-h, --help
@@ -84,6 +84,13 @@ TrezorSymmetricFileEncryption.py [-v] [-h] [-l <level>] [-t] [-2] [-s] [-w] [-o
8484
-t, --terminal
8585
run in the terminal, except for a possible PIN query
8686
and a Passphrase query this avoids the GUI
87+
-e, --encrypt
88+
encrypt file and keep output filename as plaintext
89+
(appends .tsfe suffix to input file)
90+
-o, --obfuscatedencrypt
91+
encrypt file and obfuscate output file name
92+
-d, --decrypt
93+
decrypt file
8794
-m, --encnameonly
8895
just encrypt the plaintext filename, show what the obfuscated
8996
filename would be; does not encrypt the file itself;
@@ -92,13 +99,6 @@ TrezorSymmetricFileEncryption.py [-v] [-h] [-l <level>] [-t] [-2] [-s] [-w] [-o
9299
just decrypt the obfuscated filename;
93100
does not decrypt the file itself;
94101
incompaible with `-o`, `-e`, and `-m`
95-
-d, --decrypt
96-
decrypt file
97-
-e, --encrypt
98-
encrypt file and keep output filename as plaintext
99-
(appends .tsfe suffix to input file)
100-
-o, --obfuscatedencrypt
101-
encrypt file and obfuscate output file name
102102
-2, --twice
103103
paranoid mode; encrypt file a second time on the Trezor chip itself;
104104
only relevant for `-e` and `-o`; ignored in all other cases.
@@ -107,6 +107,14 @@ TrezorSymmetricFileEncryption.py [-v] [-h] [-l <level>] [-t] [-2] [-s] [-w] [-o
107107
master passphrase used for Trezor.
108108
It is recommended that you do not use this command line option
109109
but rather give the passphrase through a small window interaction.
110+
-r, --readpinfromstdin
111+
read the PIN, if needed, from the standard input, i.e. terminal,
112+
when in terminal mode `-t`. By default, even with `-t` set
113+
it is read via a GUI window.
114+
-R, --readpassphrasefromstdin
115+
read the passphrase, when needed, from the standard input,
116+
when in terminal mode `-t`. By default, even with `-t` set
117+
it is read via a GUI window.
110118
-s, --safety
111119
doublechecks the encryption process by decrypting the just
112120
encrypted file immediately and comparing it to original file;

TrezorSymmetricFileEncryption.py

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
'''
44
Use TREZOR as a hardware device for symmetric file encryption
55
6-
Usage: python TrezorSymmetricFileEncryption.py [-v] [-h] [-o | -e | -d] <files>
6+
Usage: python TrezorSymmetricFileEncryption.py
7+
Usage: python TrezorSymmetricFileEncryption.py --help
78
89
Source and readme is on www.github.com, search for TrezorSymmetricFileEncryption
910
1011
'''
1112

1213
import sys
1314
import logging
15+
import getpass
1416

1517
from PyQt4 import QtGui, QtCore
1618
from PyQt4.QtCore import QTimer
@@ -33,42 +35,90 @@
3335

3436
class QtTrezorMixin(object):
3537
"""
36-
Mixin for input of passhprases.
38+
Mixin for input of Trezor PIN and passhprases.
39+
Works via both, terminal as well as PyQt GUI
3740
"""
3841

3942
def __init__(self, *args, **kwargs):
4043
super(QtTrezorMixin, self).__init__(*args, **kwargs)
4144
self.passphrase = None
45+
self.readpinfromstdin = None
46+
self.readpassphrasefromstdin = None
4247

4348
def callback_ButtonRequest(self, msg):
4449
return proto.ButtonAck()
4550

4651
def callback_PassphraseRequest(self, msg):
4752
if self.passphrase is not None:
48-
return proto.PassphraseAck(passphrase=self.passphrase)
53+
return proto.PassphraseAck(passphrase=unicode(self.passphrase))
4954

50-
dialog = TrezorPassphraseDialog()
51-
if not dialog.exec_():
52-
sys.exit(3)
55+
if self.readpassphrasefromstdin:
56+
# read passphrase from stdin
57+
try:
58+
passphrase = getpass.getpass("Please enter passphrase: ")
59+
passphrase = unicode(passphrase)
60+
except KeyboardInterrupt:
61+
sys.stderr.write("\nKeyboard interrupt: passphrase not read. Aborting.\n")
62+
sys.exit(3)
63+
except Exception, e:
64+
sys.stderr.write("Critical error: Passphrase not read. Aborting. (%s)" % e)
65+
sys.exit(3)
5366
else:
54-
passphrase = dialog.passphraseEdit.text()
55-
passphrase = unicode(passphrase)
67+
dialog = TrezorPassphraseDialog()
68+
if not dialog.exec_():
69+
sys.exit(3)
70+
else:
71+
passphrase = dialog.passphraseEdit.text()
72+
passphrase = unicode(passphrase)
5673

5774
return proto.PassphraseAck(passphrase=passphrase)
5875

5976
def callback_PinMatrixRequest(self, msg):
60-
dialog = EnterPinDialog()
61-
if not dialog.exec_():
62-
sys.exit(7)
77+
if self.readpinfromstdin:
78+
# read PIN from stdin
79+
print " 7 8 9"
80+
print " 4 5 6"
81+
print " 1 2 3"
82+
try:
83+
pin = getpass.getpass("Please enter PIN: ")
84+
except KeyboardInterrupt:
85+
sys.stderr.write("\nKeyboard interrupt: PIN not read. Aborting.\n")
86+
sys.exit(7)
87+
except Exception, e:
88+
sys.stderr.write("Critical error: PIN not read. Aborting. (%s)" % e)
89+
sys.exit(7)
90+
else:
91+
dialog = EnterPinDialog()
92+
if not dialog.exec_():
93+
sys.exit(7)
94+
pin = q2s(dialog.pin())
6395

64-
pin = q2s(dialog.pin())
6596
return proto.PinMatrixAck(pin=pin)
6697

6798
def prefillPassphrase(self, passphrase):
6899
"""
69100
Instead of asking for passphrase, use this one
70101
"""
71-
self.passphrase = passphrase.decode("utf-8")
102+
if passphrase is not None:
103+
self.passphrase = passphrase.decode("utf-8")
104+
else:
105+
self.passphrase = None
106+
107+
def prefillReadpinfromstdin(self, readpinfromstdin=False):
108+
"""
109+
Specify if PIN should be read from stdin instead of from GUI
110+
@param readpinfromstdin: True to force it to read from stdin, False otherwise
111+
@type readpinfromstdin: C{bool}
112+
"""
113+
self.readpinfromstdin = readpinfromstdin
114+
115+
def prefillReadpassphrasefromstdin(self, readpassphrasefromstdin=False):
116+
"""
117+
Specify if passphrase should be read from stdin instead of from GUI
118+
@param readpassphrasefromstdin: True to force it to read from stdin, False otherwise
119+
@type readpassphrasefromstdin: C{bool}
120+
"""
121+
self.readpassphrasefromstdin = readpassphrasefromstdin
72122

73123
class QtTrezorClient(ProtocolMixin, QtTrezorMixin, BaseClient):
74124
"""
@@ -196,6 +246,9 @@ def showGui(trezor, settings, fileMap, logger):
196246
sys.exit(1)
197247

198248
trezor.clear_session()
249+
trezor.prefillReadpinfromstdin(settings.RArg)
250+
trezor.prefillReadpassphrasefromstdin(settings.AArg)
251+
trezor.prefillPassphrase(settings.PArg)
199252

200253
fileMap = file_map.FileMap(trezor,logger)
201254

@@ -216,8 +269,6 @@ def showGui(trezor, settings, fileMap, logger):
216269
"the encrypted files will be shredded after decryption. "
217270
"Abort if you are uncertain or don't understand.", logging.WARNING,
218271
"Dangerous arguments", settings, logger, None)
219-
if settings.PArg is not None:
220-
trezor.prefillPassphrase(settings.PArg)
221272

222273
processing.processAll(trezor, settings, fileMap, logger, dialog=None)
223274
sys.exit(0)

basics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
TSFEFILEFORMATVERSION = 1
88

99
# Name of software version, must be less than 16 long
10-
TSFEVERSION = "v0.4"
10+
TSFEVERSION = "v0.4.1"
1111

1212
# Date of software version, only used in GUI
1313
TSFEVERSIONTEXT = "May 2017"

dialogs.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from ui_trezor_chooser_dialog import Ui_TrezorChooserDialog
1414

1515
from encoding import q2s, s2q
16-
from processing import processAllFromApply, reportLogging
16+
from processing import processAll, reportLogging
1717

1818
import basics
1919

@@ -398,7 +398,7 @@ def validate(self):
398398
# def handleButtonClick(self, button):
399399
# sb = self.buttonBox.standardButton(button)
400400
# if sb == QtGui.QDialogButtonBox.Apply:
401-
# processAllFromApply(self, self.trezor, self.settings, self.fileMap, self.logger) #
401+
# processAll(self.trezor, self.settings, self.fileMap, self.logger, self) #
402402
# # elif sb == QtGui.QDialogButtonBox.Reset:
403403
# # reportLogging("Reset Clicked, quitting now...", logging.DEBUG,
404404
# # "UI", self.settings, self.logger, self)
@@ -410,12 +410,7 @@ def accept(self):
410410
if self.validate():
411411
reportLogging("Apply was called by user request. Start processing now.",
412412
logging.DEBUG, "GUI IO", self.settings, self.logger, self)
413-
processAllFromApply(self, self.trezor, self.settings, self.fileMap, self.logger) #
414-
# move the cursor to the end of the text, scroll to the bottom
415-
cursor = self.textBrowser.textCursor()
416-
cursor.setPosition(len(self.textBrowser.toPlainText()))
417-
self.textBrowser.ensureCursorVisible()
418-
self.textBrowser.setTextCursor(cursor)
413+
processAll(self.trezor, self.settings, self.fileMap, self.logger, self) #
419414
else:
420415
reportLogging("Apply was called by user request. Apply is denied. "
421416
"User input is not valid for processing. Did you select a file?",

file_map.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class FileMap(object):
4444

4545
def __init__(self, trezor, logger):
4646
assert trezor is not None
47-
self.blob = {}
47+
self.blob = None
4848
self.trezor = trezor
4949
self.outerKey = None # outer AES-CBC key, 1st-level encryption
5050
self.outerIv = None # IV for data blob encrypted with outerKey
@@ -76,32 +76,36 @@ def createDecFile(self, fname):
7676
#os.chmod(fname, stat.S_IRUSR | stat.S_IWUSR )
7777
self.logger.error("File %s cannot be written. "
7878
"No write permissions. Skipping it.", fname)
79-
return
79+
raise IOError("File IO error: File %s cannot be written. "
80+
"No write permissions. Skipping it. "
81+
"Change file permissions and try again." % (fname))
8082

8183
self.loadBlobFromEncFile(originalFilename)
8284
with open(fname, 'w+b') as f:
8385
s = len(self.blob)
8486
f.write(self.blob)
8587
if f.tell() != s:
86-
raise IOError("File IO problem - not enough data written")
88+
self.logger.error("File IO problem: not enough data written "
89+
"(file=%s, target=%d, done=%d)" % (fname, s, f.tell()))
90+
raise IOError("File IO problem - not enough data written "
91+
"(file=%s, target=%d, done=%d)" % (fname, s, f.tell()))
8792
self.logger.debug("Decryption wrote %d bytes to file %s.",s,fname)
88-
f.flush()
89-
f.close()
9093
# overwrite with nonsense to shred memory
9194
rng = Random.new()
9295
self.outerKey = rng.read(KEYSIZE)
9396
return fname # output file name
9497

9598
def loadBlobFromEncFile(self, fname):
9699
"""
97-
Load/read data from encrypted file, decrypt data amd store data in blob
100+
Load/read data from encrypted file,
101+
decrypt data amd store data in blob
98102
Requires Trezor connected.
99103
100104
@param fname: name of the encrypted file to decrypt
101105
102106
@throws IOError: if reading file failed
103107
"""
104-
with file(fname) as f:
108+
with open(fname, 'rb') as f:
105109
header = f.read(len(Magic.headerStr))
106110
if header != Magic.headerStr:
107111
raise IOError("Bad header in storage file")
@@ -161,7 +165,8 @@ def loadBlobFromEncFile(self, fname):
161165
for (ch1, ch2) in zip(hmacDigest, newHmacDigest):
162166
hmacCompare |= int(ch1 != ch2)
163167
if hmacCompare != 0:
164-
raise IOError("Corrupted disk format - HMAC does not match or bad passphrase")
168+
raise IOError("Corrupted disk format - HMAC does not match "
169+
"or bad passphrase. Try again with the correct passphrase.")
165170

166171
if self.noOfEncryptions == 2:
167172
encrypted = self.decryptOnTrezorDevice(encrypted, Magic.levelTwoKey)
@@ -177,11 +182,12 @@ def createEncFile(self, fname, obfuscate, twice):
177182

178183
with open(fname, 'rb') as f:
179184
# Size 0 will read the ENTIRE file into memory!
180-
m = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ) #File is open read-only
185+
# File is open read-only
186+
# mmap does not implement __exit__ so we cannot use "with mmap... as m:"
187+
m = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ)
181188
s = m.size()
182189
self.blob = m.read(s)
183-
m.close()
184-
f.close()
190+
del m
185191
if len(self.blob) != s:
186192
raise IOError("File IO problem - not enough data read")
187193
self.logger.debug("Read %d bytes from file %s.",s,fname)
@@ -228,7 +234,7 @@ def saveBlobToEncFile(self, fname, obfuscate, twice):
228234
#raise IOError("File " + fname + " cannot be written. "
229235
# "No write permissions. Skipping it.")
230236

231-
with file(fname, "wb") as f:
237+
with open(fname, "wb") as f:
232238
version = basics.TSFEFILEFORMATVERSION
233239
futureUse = ""
234240
f.write(Magic.headerStr)
@@ -256,8 +262,6 @@ def saveBlobToEncFile(self, fname, obfuscate, twice):
256262
f.write(encrypted)
257263
f.write(hmacDigest)
258264
ww = f.tell()
259-
f.flush()
260-
f.close()
261265
self.logger.debug("Wrote %d bytes to file %s.", ww, fname)
262266
return fname
263267

@@ -267,8 +271,8 @@ def obfuscateFilename(self, plaintextFileName):
267271
--> homegrown padding == obfuscated filename
268272
"""
269273
pad16 = Padding(BLOCKSIZE).pad(plaintextFileName)
270-
self.logger.debug("Press confirm on Trezor device to encrypt file "
271-
"name %s (if necessary).", plaintextFileName)
274+
# self.logger.debug("Press confirm on Trezor device to encrypt file "
275+
# "name %s (if necessary).", plaintextFileName)
272276
# we do not use an IV here so that we can quickly deobfuscate
273277
# filenames without having to read the file
274278
encFn = self.trezor.encrypt_keyvalue(Magic.fileNameNode,
@@ -321,6 +325,7 @@ def encrypt(self, plaintext, iv, key):
321325
"""
322326
Pad plaintext with PKCS#5 and encrypt it.
323327
"""
328+
self.logger.debug("AES CBC encryption with key of size %d bits." % (len(key)*8))
324329
cipher = AES.new(key, AES.MODE_CBC, iv)
325330
padded = Padding(BLOCKSIZE).pad(plaintext)
326331
return cipher.encrypt(padded)
@@ -335,6 +340,7 @@ def decrypt(self, ciphertext, iv, key):
335340
"""
336341
Decrypt ciphertext, unpad it and return
337342
"""
343+
self.logger.debug("AES CBC decryption with key of size %d bits." % (len(key)*8))
338344
cipher = AES.new(key, AES.MODE_CBC, iv)
339345
plaintext = cipher.decrypt(ciphertext)
340346
unpadded = Padding(BLOCKSIZE).unpad(plaintext)

0 commit comments

Comments
 (0)