This repository was archived by the owner on Jul 27, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathpassword_map.py
243 lines (201 loc) · 7.25 KB
/
password_map.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
import struct
import cPickle
import hmac
import hashlib
from Crypto.Cipher import AES
from Crypto import Random
from backup import Backup
from encoding import Magic, Padding
## On-disk format
# 4 bytes header "TZPW"
# 4 bytes data storage version, network order uint32_t
# 32 bytes AES-CBC-encrypted wrappedOuterKey
# 16 bytes IV
# 2 bytes backup private key size (B)
# B bytes encrypted backup key
# 4 bytes size of data following (N)
# N bytes AES-CBC encrypted blob containing pickled structure for password map
# 32 bytes HMAC-SHA256 over data with same key as AES-CBC data struct above
BLOCKSIZE = 16
MACSIZE = 32
KEYSIZE = 32
class PasswordGroup(object):
"""
Holds data for one password group.
Each entry has three values:
- key
- symetrically AES-CBC encrypted password unlockable only by Trezor
- RSA-encrypted password for creating backup of all password groups
"""
def __init__(self):
self.entries = []
def addEntry(self, key, encryptedValue, backupValue):
"""Add key-value-backud entry"""
self.entries.append((key, encryptedValue, backupValue))
def removeEntry(self, idx):
"""Remove entry at given index"""
self.entries.pop(idx)
def updateEntry(self, idx, key, encryptedValue, backupValue):
"""
Update pair at index idx with given key, value and
backup-encrypted password.
"""
self.entries[idx] = (key, encryptedValue, backupValue)
def entry(self, idx):
"""Return entry with given index"""
return self.entries[idx]
class PasswordMap(object):
"""Storage of groups of passwords in memory"""
def __init__(self, trezor):
assert trezor is not None
self.groups = {}
self.trezor = trezor
self.outerKey = None # outer AES-CBC key
self.outerIv = None # IV for data blob encrypted with outerKey
self.backupKey = None
def addGroup(self, groupName):
"""
Add group by name as utf-8 encoded string
"""
groupName = groupName
if groupName in self.groups:
raise KeyError("Password group already exists")
self.groups[groupName] = PasswordGroup()
def load(self, fname):
"""
Load encrypted passwords from disk file, decrypt outer
layer containing key names. Requires Trezor connected.
@throws IOError: if reading file failed
"""
with file(fname) as f:
header = f.read(len(Magic.headerStr))
if header != Magic.headerStr:
raise IOError("Bad header in storage file")
version = f.read(4)
if len(version) != 4 or struct.unpack("!I", version)[0] != 1:
raise IOError("Unknown version of storage file")
wrappedKey = f.read(KEYSIZE)
if len(wrappedKey) != KEYSIZE:
raise IOError("Corrupted disk format - bad wrapped key length")
self.outerKey = self.unwrapKey(wrappedKey)
self.outerIv = f.read(BLOCKSIZE)
if len(self.outerIv) != BLOCKSIZE:
raise IOError("Corrupted disk format - bad IV length")
lb = f.read(2)
if len(lb) != 2:
raise IOError("Corrupted disk format - bad backup key length")
lb = struct.unpack("!H", lb)[0]
self.backupKey = Backup(self.trezor)
serializedBackup = f.read(lb)
if len(serializedBackup) != lb:
raise IOError("Corrupted disk format - not enough encrypted backup key bytes")
self.backupKey.deserialize(serializedBackup)
ls = f.read(4)
if len(ls) != 4:
raise IOError("Corrupted disk format - bad data length")
l = struct.unpack("!I", ls)[0]
encrypted = f.read(l)
if len(encrypted) != l:
raise IOError("Corrupted disk format - not enough data bytes")
hmacDigest = f.read(MACSIZE)
if len(hmacDigest) != MACSIZE:
raise IOError("Corrupted disk format - HMAC not complete")
#time-invariant HMAC comparison that also works with python 2.6
newHmacDigest = hmac.new(self.outerKey, encrypted, hashlib.sha256).digest()
hmacCompare = 0
for (ch1, ch2) in zip(hmacDigest, newHmacDigest):
hmacCompare |= int(ch1 != ch2)
if hmacCompare != 0:
raise IOError("Corrupted disk format - HMAC does not match or bad passphrase")
serialized = self.decryptOuter(encrypted, self.outerIv)
self.groups = cPickle.loads(serialized)
def save(self, fname):
"""
Write password database to disk, encrypt it. Requires Trezor
connected.
@throws IOError: if writing file failed
"""
assert len(self.outerKey) == KEYSIZE
rnd = Random.new()
self.outerIv = rnd.read(BLOCKSIZE)
wrappedKey = self.wrapKey(self.outerKey)
with file(fname, "wb") as f:
version = 1
f.write(Magic.headerStr)
f.write(struct.pack("!I", version))
f.write(wrappedKey)
f.write(self.outerIv)
serialized = cPickle.dumps(self.groups, cPickle.HIGHEST_PROTOCOL)
encrypted = self.encryptOuter(serialized, self.outerIv)
hmacDigest = hmac.new(self.outerKey, encrypted, hashlib.sha256).digest()
serializedBackup = self.backupKey.serialize()
lb = struct.pack("!H", len(serializedBackup))
f.write(lb)
f.write(serializedBackup)
l = struct.pack("!I", len(encrypted))
f.write(l)
f.write(encrypted)
f.write(hmacDigest)
f.flush()
f.close()
def encryptOuter(self, plaintext, iv):
"""
Pad and encrypt with self.outerKey
"""
return self.encrypt(plaintext, iv, self.outerKey)
def encrypt(self, plaintext, iv, key):
"""
Pad plaintext with PKCS#5 and encrypt it.
"""
cipher = AES.new(key, AES.MODE_CBC, iv)
padded = Padding(BLOCKSIZE).pad(plaintext)
return cipher.encrypt(padded)
def decryptOuter(self, ciphertext, iv):
"""
Decrypt with self.outerKey and unpad
"""
return self.decrypt(ciphertext, iv, self.outerKey)
def decrypt(self, ciphertext, iv, key):
"""
Decrypt ciphertext, unpad it and return
"""
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)
unpadded = Padding(BLOCKSIZE).unpad(plaintext)
return unpadded
def unwrapKey(self, wrappedOuterKey):
"""
Decrypt wrapped outer key using Trezor.
"""
ret = self.trezor.decrypt_keyvalue(Magic.unlockNode, Magic.unlockKey, wrappedOuterKey, ask_on_encrypt=False, ask_on_decrypt=True)
return ret
def wrapKey(self, keyToWrap):
"""
Encrypt/wrap a key. Its size must be multiple of 16.
"""
ret = self.trezor.encrypt_keyvalue(Magic.unlockNode, Magic.unlockKey, keyToWrap, ask_on_encrypt=False, ask_on_decrypt=True)
return ret
def encryptPassword(self, password, groupName):
"""
Encrypt a password. Does PKCS#5 padding before encryption.
Store IV as first block.
@param groupName key that will be shown to user on Trezor and
used to encrypt the password. A string in utf-8
"""
rnd = Random.new()
rndBlock = rnd.read(BLOCKSIZE)
padded = Padding(BLOCKSIZE).pad(password)
ugroup = groupName.decode("utf-8")
ret = rndBlock + self.trezor.encrypt_keyvalue(Magic.groupNode, ugroup, padded, ask_on_encrypt=False, ask_on_decrypt=True, iv=rndBlock)
return ret
def decryptPassword(self, encryptedPassword, groupName):
"""
Decrypt a password. First block is IV. After decryption strips PKCS#5 padding.
@param groupName key that will be shown to user on Trezor and
was used to encrypt the password. A string in utf-8.
"""
ugroup = groupName.decode("utf-8")
iv, encryptedPassword = encryptedPassword[:BLOCKSIZE], encryptedPassword[BLOCKSIZE:]
plain = self.trezor.decrypt_keyvalue(Magic.groupNode, ugroup, encryptedPassword, ask_on_encrypt=False, ask_on_decrypt=True, iv=iv)
password = Padding(BLOCKSIZE).unpad(plain)
return password