|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2011 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Package implementing cryptography related functionality. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals |
|
11 |
|
12 import random |
|
13 import base64 |
|
14 |
|
15 from PyQt5.QtCore import QCoreApplication |
|
16 from PyQt5.QtWidgets import QLineEdit, QInputDialog |
|
17 |
|
18 from E5Gui import E5MessageBox |
|
19 |
|
20 import Preferences |
|
21 |
|
22 ############################################################################### |
|
23 ## password handling functions below |
|
24 ############################################################################### |
|
25 |
|
26 |
|
27 EncodeMarker = "CE4" |
|
28 CryptoMarker = "CR5" |
|
29 |
|
30 Delimiter = "$" |
|
31 |
|
32 MasterPassword = None |
|
33 |
|
34 |
|
35 def pwEncode(pw): |
|
36 """ |
|
37 Module function to encode a password. |
|
38 |
|
39 @param pw password to encode (string) |
|
40 @return encoded password (string) |
|
41 """ |
|
42 pop = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" \ |
|
43 ".,;:-_!$?*+#" |
|
44 rpw = "".join(random.sample(pop, 32)) + pw + \ |
|
45 "".join(random.sample(pop, 32)) |
|
46 return EncodeMarker + base64.b64encode(rpw.encode("utf-8")).decode("ascii") |
|
47 |
|
48 |
|
49 def pwDecode(epw): |
|
50 """ |
|
51 Module function to decode a password. |
|
52 |
|
53 @param epw encoded password to decode (string) |
|
54 @return decoded password (string) |
|
55 """ |
|
56 if not epw.startswith(EncodeMarker): |
|
57 return epw # it was not encoded using pwEncode |
|
58 |
|
59 return base64.b64decode(epw[3:].encode("ascii"))[32:-32].decode("utf-8") |
|
60 |
|
61 |
|
62 def __getMasterPassword(): |
|
63 """ |
|
64 Private module function to get the password from the user. |
|
65 """ |
|
66 global MasterPassword |
|
67 |
|
68 pw, ok = QInputDialog.getText( |
|
69 None, |
|
70 QCoreApplication.translate("Crypto", "Master Password"), |
|
71 QCoreApplication.translate("Crypto", "Enter the master password:"), |
|
72 QLineEdit.Password) |
|
73 if ok: |
|
74 from .py3PBKDF2 import verifyPassword |
|
75 masterPassword = Preferences.getUser("MasterPassword") |
|
76 try: |
|
77 if masterPassword: |
|
78 if verifyPassword(pw, masterPassword): |
|
79 MasterPassword = pwEncode(pw) |
|
80 else: |
|
81 E5MessageBox.warning( |
|
82 None, |
|
83 QCoreApplication.translate( |
|
84 "Crypto", "Master Password"), |
|
85 QCoreApplication.translate( |
|
86 "Crypto", |
|
87 """The given password is incorrect.""")) |
|
88 else: |
|
89 E5MessageBox.critical( |
|
90 None, |
|
91 QCoreApplication.translate("Crypto", "Master Password"), |
|
92 QCoreApplication.translate( |
|
93 "Crypto", |
|
94 """There is no master password registered.""")) |
|
95 except ValueError as why: |
|
96 E5MessageBox.warning( |
|
97 None, |
|
98 QCoreApplication.translate("Crypto", "Master Password"), |
|
99 QCoreApplication.translate( |
|
100 "Crypto", |
|
101 """<p>The given password cannot be verified.</p>""" |
|
102 """<p>Reason: {0}""".format(str(why)))) |
|
103 |
|
104 |
|
105 def pwEncrypt(pw, masterPW=None): |
|
106 """ |
|
107 Module function to encrypt a password. |
|
108 |
|
109 @param pw password to encrypt (string) |
|
110 @param masterPW password to be used for encryption (string) |
|
111 @return encrypted password (string) and flag indicating |
|
112 success (boolean) |
|
113 """ |
|
114 if masterPW is None: |
|
115 if MasterPassword is None: |
|
116 __getMasterPassword() |
|
117 if MasterPassword is None: |
|
118 return "", False |
|
119 |
|
120 masterPW = pwDecode(MasterPassword) |
|
121 |
|
122 from .py3PBKDF2 import hashPasswordTuple |
|
123 digestname, iterations, salt, pwHash = hashPasswordTuple(masterPW) |
|
124 key = pwHash[:32] |
|
125 from .py3AES import encryptData |
|
126 try: |
|
127 cipher = encryptData(key, pw.encode("utf-8")) |
|
128 except ValueError: |
|
129 return "", False |
|
130 return CryptoMarker + Delimiter.join([ |
|
131 digestname, |
|
132 str(iterations), |
|
133 base64.b64encode(salt).decode("ascii"), |
|
134 base64.b64encode(cipher).decode("ascii") |
|
135 ]), True |
|
136 |
|
137 |
|
138 def pwDecrypt(epw, masterPW=None): |
|
139 """ |
|
140 Module function to decrypt a password. |
|
141 |
|
142 @param epw hashed password to decrypt (string) |
|
143 @param masterPW password to be used for decryption (string) |
|
144 @return decrypted password (string) and flag indicating |
|
145 success (boolean) |
|
146 """ |
|
147 if not epw.startswith(CryptoMarker): |
|
148 return epw, False # it was not encoded using pwEncrypt |
|
149 |
|
150 if masterPW is None: |
|
151 if MasterPassword is None: |
|
152 __getMasterPassword() |
|
153 if MasterPassword is None: |
|
154 return "", False |
|
155 |
|
156 masterPW = pwDecode(MasterPassword) |
|
157 |
|
158 from .py3AES import decryptData |
|
159 from .py3PBKDF2 import rehashPassword |
|
160 |
|
161 hashParameters, epw = epw[3:].rsplit(Delimiter, 1) |
|
162 try: |
|
163 # recreate the key used to encrypt |
|
164 key = rehashPassword(masterPW, hashParameters)[:32] |
|
165 plaintext = decryptData(key, base64.b64decode(epw.encode("ascii"))) |
|
166 except ValueError: |
|
167 return "", False |
|
168 return plaintext.decode("utf-8"), True |
|
169 |
|
170 |
|
171 def pwReencrypt(epw, oldPassword, newPassword): |
|
172 """ |
|
173 Module function to re-encrypt a password. |
|
174 |
|
175 @param epw hashed password to re-encrypt (string) |
|
176 @param oldPassword password used to encrypt (string) |
|
177 @param newPassword new password to be used (string) |
|
178 @return encrypted password (string) and flag indicating |
|
179 success (boolean) |
|
180 """ |
|
181 plaintext, ok = pwDecrypt(epw, oldPassword) |
|
182 if ok: |
|
183 return pwEncrypt(plaintext, newPassword) |
|
184 else: |
|
185 return "", False |
|
186 |
|
187 |
|
188 def pwRecode(epw, oldPassword, newPassword): |
|
189 """ |
|
190 Module function to re-encode a password. |
|
191 |
|
192 In case of an error the encoded password is returned unchanged. |
|
193 |
|
194 @param epw encoded password to re-encode (string) |
|
195 @param oldPassword password used to encode (string) |
|
196 @param newPassword new password to be used (string) |
|
197 @return encoded password (string) |
|
198 """ |
|
199 if epw == "": |
|
200 return epw |
|
201 |
|
202 if newPassword == "": |
|
203 plaintext, ok = pwDecrypt(epw) |
|
204 return (pwEncode(plaintext) if ok else epw) |
|
205 else: |
|
206 if oldPassword == "": |
|
207 plaintext = pwDecode(epw) |
|
208 cipher, ok = pwEncrypt(plaintext, newPassword) |
|
209 return (cipher if ok else epw) |
|
210 else: |
|
211 npw, ok = pwReencrypt(epw, oldPassword, newPassword) |
|
212 return (npw if ok else epw) |
|
213 |
|
214 |
|
215 def pwConvert(pw, encode=True): |
|
216 """ |
|
217 Module function to convert a plaintext password to the encoded form or |
|
218 vice versa. |
|
219 |
|
220 If there is an error, an empty code is returned for the encode function |
|
221 or the given encoded password for the decode function. |
|
222 |
|
223 @param pw password to encode (string) |
|
224 @param encode flag indicating an encode or decode function (boolean) |
|
225 @return encoded or decoded password (string) |
|
226 """ |
|
227 if pw == "": |
|
228 return pw |
|
229 |
|
230 if encode: |
|
231 # plain text -> encoded |
|
232 if Preferences.getUser("UseMasterPassword"): |
|
233 epw = pwEncrypt(pw)[0] |
|
234 else: |
|
235 epw = pwEncode(pw) |
|
236 return epw |
|
237 else: |
|
238 # encoded -> plain text |
|
239 if Preferences.getUser("UseMasterPassword"): |
|
240 plain, ok = pwDecrypt(pw) |
|
241 else: |
|
242 plain, ok = pwDecode(pw), True |
|
243 return (plain if ok else pw) |
|
244 |
|
245 |
|
246 def changeRememberedMaster(newPassword): |
|
247 """ |
|
248 Module function to change the remembered master password. |
|
249 |
|
250 @param newPassword new password to be used (string) |
|
251 """ |
|
252 global MasterPassword |
|
253 |
|
254 if newPassword == "": |
|
255 MasterPassword = None |
|
256 else: |
|
257 MasterPassword = pwEncode(newPassword) |
|
258 |
|
259 |
|
260 def dataEncrypt(data, password, keyLength=32, hashIterations=10000): |
|
261 """ |
|
262 Module function to encrypt a password. |
|
263 |
|
264 @param data data to encrypt (bytes) |
|
265 @param password password to be used for encryption (string) |
|
266 @keyparam keyLength length of the key to be generated for encryption |
|
267 (16, 24 or 32) |
|
268 @keyparam hashIterations number of hashes to be applied to the password for |
|
269 generating the encryption key (integer) |
|
270 @return encrypted data (bytes) and flag indicating |
|
271 success (boolean) |
|
272 """ |
|
273 from .py3AES import encryptData |
|
274 from .py3PBKDF2 import hashPasswordTuple |
|
275 |
|
276 digestname, iterations, salt, pwHash = \ |
|
277 hashPasswordTuple(password, iterations=hashIterations) |
|
278 key = pwHash[:keyLength] |
|
279 try: |
|
280 cipher = encryptData(key, data) |
|
281 except ValueError: |
|
282 return b"", False |
|
283 return CryptoMarker.encode("utf-8") + Delimiter.encode("utf-8").join([ |
|
284 digestname.encode("utf-8"), |
|
285 str(iterations).encode("utf-8"), |
|
286 base64.b64encode(salt), |
|
287 base64.b64encode(cipher) |
|
288 ]), True |
|
289 |
|
290 |
|
291 def dataDecrypt(edata, password, keyLength=32): |
|
292 """ |
|
293 Module function to decrypt a password. |
|
294 |
|
295 @param edata hashed data to decrypt (string) |
|
296 @param password password to be used for decryption (string) |
|
297 @keyparam keyLength length of the key to be generated for decryption |
|
298 (16, 24 or 32) |
|
299 @return decrypted data (bytes) and flag indicating |
|
300 success (boolean) |
|
301 """ |
|
302 if not edata.startswith(CryptoMarker.encode("utf-8")): |
|
303 return edata, False # it was not encoded using dataEncrypt |
|
304 |
|
305 from .py3AES import decryptData |
|
306 from .py3PBKDF2 import rehashPassword |
|
307 |
|
308 hashParametersBytes, edata = edata[3:].rsplit(Delimiter.encode("utf-8"), 1) |
|
309 hashParameters = hashParametersBytes.decode() |
|
310 try: |
|
311 # recreate the key used to encrypt |
|
312 key = rehashPassword(password, hashParameters)[:keyLength] |
|
313 plaintext = decryptData(key, base64.b64decode(edata)) |
|
314 except ValueError: |
|
315 return "", False |
|
316 return plaintext, True |