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