eric6/Utilities/PasswordChecker.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2011 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a checker for password strength.
8 """
9
10 from __future__ import unicode_literals
11
12 import re
13
14
15 class PasswordChecker(object):
16 """
17 Class implementing a checker for password strength.
18 """
19 Complexity_VeryWeak = 0
20 Complexity_Weak = 1
21 Complexity_Good = 2
22 Complexity_Strong = 3
23 Complexity_VeryStrong = 4
24
25 Status_Failed = 0
26 Status_Passed = 1
27 Status_Exceeded = 2
28
29 def __init__(self):
30 """
31 Constructor
32 """
33 self.score = {
34 "count": 0,
35 "adjusted": 0,
36 "beforeRedundancy": 0
37 }
38
39 # complexity index
40 self.complexity = {
41 "limits": [20, 50, 60, 80, 100],
42 "value": self.Complexity_VeryWeak
43 }
44
45 # check categories follow
46
47 # length of the password
48 self.passwordLength = {
49 "count": 0,
50 "minimum": 6,
51 "status": self.Status_Failed,
52 "rating": 0,
53 "factor": 0.5, # per character bonus
54 "bonus": 10, # minimum reached? Get a bonus.
55 "penalty": -20, # if we stay under minimum, we get punished
56 }
57
58 # recommended password length
59 self.recommendedPasswordLength = {
60 "count": 0,
61 "minimum": 8,
62 "status": self.Status_Failed,
63 "rating": 0,
64 "factor": 1.2,
65 "bonus": 10,
66 "penalty": -10,
67 }
68
69 # Basic requirements are:
70 # 1) Password Length
71 # 2) Uppercase letter use
72 # 3) Lowercase letter use
73 # 4) Numeric character use
74 # 5) Symbol use
75 self.basicRequirements = {
76 "count": 0,
77 "minimum": 3, # have to be matched to get the bonus
78 "status": self.Status_Failed,
79 "rating": 0,
80 "factor": 1.0,
81 "bonus": 10,
82 "penalty": -10,
83 }
84
85 # how much redundancy is permitted, if the password is
86 # long enough. we will skip the redudancy penalty if this
87 # number is not exceeded (meaning redundancy < this number)
88 self.redundancy = {
89 "value": 1, # 1 means, not double characters,
90 # default to start
91 "permitted": 2.0, # 2 means, in average every character
92 # can occur twice
93 "status": self.Status_Failed,
94 "rating": 0,
95 }
96
97 # number of uppercase letters, such as A-Z
98 self.uppercaseLetters = {
99 "count": 0,
100 "minimum": 1,
101 "status": self.Status_Failed,
102 "rating": 0,
103 "factor": 0.0,
104 "bonus": 10,
105 "penalty": -10,
106 }
107
108 # number of lowercase letters, such as a-z
109 self.lowercaseLetters = {
110 "count": 0,
111 "minimum": 1,
112 "status": self.Status_Failed,
113 "rating": 0,
114 "factor": 0.0,
115 "bonus": 10,
116 "penalty": -10,
117 }
118
119 # number of numeric characters
120 self.numerics = {
121 "count": 0,
122 "minimum": 1,
123 "status": self.Status_Failed,
124 "rating": 0,
125 "factor": 0.0,
126 "bonus": 10,
127 "penalty": -10,
128 }
129
130 # number of symbol characters
131 self.symbols = {
132 "count": 0,
133 "minimum": 1,
134 "status": self.Status_Failed,
135 "rating": 0,
136 "factor": 0.0,
137 "bonus": 10,
138 "penalty": -10,
139 }
140
141 # number of dedicated symbols in the middle
142 self.middleSymbols = {
143 "count": 0,
144 "minimum": 1,
145 "status": self.Status_Failed,
146 "rating": 0,
147 "factor": 0.0,
148 "bonus": 10,
149 "penalty": -10,
150 }
151
152 # number of dedicated numbers in the middle
153 self.middleNumerics = {
154 "count": 0,
155 "minimum": 1,
156 "status": self.Status_Failed,
157 "rating": 0,
158 "factor": 0.0,
159 "bonus": 10,
160 "penalty": -10,
161 }
162
163 # how many sequential characters should be checked
164 # such as "abc" or "MNO" to be not part of the password
165 self.sequentialLetters = {
166 "data": "abcdefghijklmnopqrstuvwxyz",
167 "length": 3,
168
169 "count": 0,
170 "status": self.Status_Failed,
171 "rating": 0,
172 "factor": -1.0,
173 "bonus": 0,
174 "penalty": -10,
175 }
176
177 # how many sequential characters should be checked
178 # such as "123" to be not part of the password
179 self.sequentialNumerics = {
180 "data": "0123456789",
181 "length": 3,
182
183 "count": 0,
184 "status": self.Status_Failed,
185 "rating": 0,
186 "factor": -1.0,
187 "bonus": 0,
188 "penalty": -10,
189 }
190
191 # keyboard patterns to check, typical sequences from your
192 # keyboard
193 self.keyboardPatterns = {
194 # German and English keyboard text
195 "data": [
196 "qwertzuiop", "asdfghjkl", "yxcvbnm", "!\"§$%&/()=", # de
197 "1234567890", # de numbers
198 "qaywsxedcrfvtgbzhnujmik,ol.pö-üä+#", # de up-down
199
200 "qwertyuiop", "asdfghjkl", "zyxcvbnm", "!@#$%^&*()_", # en
201 "1234567890", # en numbers
202 "qazwsxedcrfvtgbyhnujmik,ol.p;/[']\\", # en up-down
203 ],
204 "length": 4, # how long is the pattern to check and blame for?
205
206 "count": 0, # how many of these pattern can be found
207 "status": self.Status_Failed,
208 "rating": 0,
209 "factor": -1.0, # each occurrence is punished with that factor
210 "bonus": 0,
211 "penalty": -10,
212 }
213
214 # check for repeated sequences, like in catcat
215 self.repeatedSequences = {
216 "length": 3,
217
218 "count": 0,
219 "status": self.Status_Failed,
220 "rating": 0,
221 "factor": 0.0,
222 "bonus": 0,
223 "penalty": -10,
224 }
225
226 # check for repeated mirrored sequences, like in tactac
227 self.mirroredSequences = {
228 "length": 3,
229
230 "count": 0,
231 "status": self.Status_Failed,
232 "rating": 0,
233 "factor": 0.0,
234 "bonus": 0,
235 "penalty": -10,
236 }
237
238 self.uppercaseRe = re.compile("[A-Z]")
239 self.lowercaseRe = re.compile("[a-z]")
240 self.numberRe = re.compile("[0-9]")
241 self.symbolRe = re.compile("[^a-zA-Z0-9]")
242
243 def __strReverse(self, string):
244 """
245 Private method to reverse a string.
246
247 @param string string to be reversed (string)
248 @return reversed string (string)
249 """
250 return "".join(reversed(string))
251
252 def __determineStatus(self, value):
253 """
254 Private method to determine the status.
255
256 @param value value to check (integer)
257 @return status (Status_Failed, Status_Passed, Status_Exceeded)
258 """
259 if value == 0:
260 return self.Status_Passed
261 elif value > 0:
262 return self.Status_Exceeded
263 else:
264 return self.Status_Failed
265
266 def __determineBinaryStatus(self, value):
267 """
268 Private method to determine a binary status.
269
270 @param value value to check (integer)
271 @return status (Status_Failed, Status_Passed)
272 """
273 if value == 0:
274 return self.Status_Passed
275 else:
276 return self.Status_Failed
277
278 def checkPassword(self, password):
279 """
280 Public method to check a given password.
281
282 @param password password to be checked (string)
283 @return indication for the password strength (Complexity_VeryWeak,
284 Complexity_Weak, Complexity_Good, Complexity_Strong,
285 Complexity_VeryStrong)
286 """
287 # how long is the password?
288 self.passwordLength["count"] = len(password)
289 self.recommendedPasswordLength["count"] = len(password)
290
291 # Loop through password to check for Symbol, Numeric, Lowercase
292 # and Uppercase pattern matches
293 for index in range(len(password)):
294 if self.uppercaseRe.match(password[index]):
295 self.uppercaseLetters["count"] += 1
296 elif self.lowercaseRe.match(password[index]):
297 self.lowercaseLetters["count"] += 1
298 elif self.numberRe.match(password[index]):
299 if index > 0 and index < len(password) - 1:
300 self.middleNumerics["count"] += 1
301 self.numerics["count"] += 1
302 elif self.symbolRe.match(password[index]):
303 if index > 0 and index < len(password) - 1:
304 self.middleSymbols["count"] += 1
305 self.symbols["count"] += 1
306
307 # check the variance of symbols or better the redundancy
308 # makes only sense for at least two characters
309 if len(password) > 1:
310 uniqueCharacters = []
311 for index1 in range(len(password)):
312 found = False
313 for index2 in range(index1 + 1, len(password)):
314 if password[index1] == password[index2]:
315 found = True
316 break
317 if not found:
318 uniqueCharacters.append(password[index1])
319
320 # calculate a redundancy number
321 self.redundancy["value"] = len(password) / len(uniqueCharacters)
322
323 # Check for sequential alpha string patterns (forward and reverse)
324 # but only, if the string has already a length to check for, does
325 # not make sense to check the password "ab" for the sequential data
326 # "abc"
327 lowercasedPassword = password.lower()
328
329 if self.passwordLength["count"] >= self.sequentialLetters["length"]:
330 for index in range(len(self.sequentialLetters["data"]) -
331 self.sequentialLetters["length"] + 1):
332 fwd = self.sequentialLetters["data"][
333 index:index + self.sequentialLetters["length"]]
334 rev = self.__strReverse(fwd)
335 if lowercasedPassword.find(fwd) != -1:
336 self.sequentialLetters["count"] += 1
337 if lowercasedPassword.find(rev) != -1:
338 self.sequentialLetters["count"] += 1
339
340 # Check for sequential numeric string patterns (forward and reverse)
341 if self.passwordLength["count"] >= self.sequentialNumerics["length"]:
342 for index in range(len(self.sequentialNumerics["data"]) -
343 self.sequentialNumerics["length"] + 1):
344 fwd = self.sequentialNumerics["data"][
345 index:index + self.sequentialNumerics["length"]]
346 rev = self.__strReverse(fwd)
347 if lowercasedPassword.find(fwd) != -1:
348 self.sequentialNumerics["count"] += 1
349 if lowercasedPassword.find(rev) != -1:
350 self.sequentialNumerics["count"] += 1
351
352 # Check common keyboard patterns
353 patternsMatched = []
354 if self.passwordLength["count"] >= self.keyboardPatterns["length"]:
355 for pattern in self.keyboardPatterns["data"]:
356 for index in range(
357 len(pattern) - self.keyboardPatterns["length"] + 1):
358 fwd = pattern[index:index +
359 self.keyboardPatterns["length"]]
360 rev = self.__strReverse(fwd)
361 if lowercasedPassword.find(fwd) != -1:
362 if fwd not in patternsMatched:
363 self.keyboardPatterns["count"] += 1
364 patternsMatched.append(fwd)
365 if lowercasedPassword.find(rev) != -1:
366 if fwd not in patternsMatched:
367 self.keyboardPatterns["count"] += 1
368 patternsMatched.append(rev)
369
370 # Try to find repeated sequences of characters.
371 if self.passwordLength["count"] >= self.repeatedSequences["length"]:
372 for index in range(len(lowercasedPassword) -
373 self.repeatedSequences["length"] + 1):
374 fwd = lowercasedPassword[
375 index:index + self.repeatedSequences["length"]]
376 if lowercasedPassword.find(
377 fwd, index + self.repeatedSequences["length"]) != -1:
378 self.repeatedSequences["count"] += 1
379
380 # Try to find mirrored sequences of characters.
381 if self.passwordLength["count"] >= self.mirroredSequences["length"]:
382 for index in range(len(lowercasedPassword) -
383 self.mirroredSequences["length"] + 1):
384 fwd = lowercasedPassword[
385 index:index + self.mirroredSequences["length"]]
386 rev = self.__strReverse(fwd)
387 if lowercasedPassword.find(
388 fwd, index + self.mirroredSequences["length"]) != -1:
389 self.mirroredSequences["count"] += 1
390
391 # Initial score based on length
392 self.score["count"] = self.passwordLength["count"] * \
393 self.passwordLength["factor"]
394
395 # passwordLength
396 # credit additional length or punish "under" length
397 if self.passwordLength["count"] >= self.passwordLength["minimum"]:
398 # credit additional characters over minimum
399 self.passwordLength["rating"] = self.passwordLength["bonus"] + \
400 (self.passwordLength["count"] -
401 self.passwordLength["minimum"]) * \
402 self.passwordLength["factor"]
403 else:
404 self.passwordLength["rating"] = self.passwordLength["penalty"]
405 self.score["count"] += self.passwordLength["rating"]
406
407 # recommendedPasswordLength
408 # Credit reaching the recommended password length or put a
409 # penalty on it
410 if self.passwordLength["count"] >= \
411 self.recommendedPasswordLength["minimum"]:
412 self.recommendedPasswordLength["rating"] = \
413 self.recommendedPasswordLength["bonus"] + \
414 (self.passwordLength["count"] -
415 self.recommendedPasswordLength["minimum"]) * \
416 self.recommendedPasswordLength["factor"]
417 else:
418 self.recommendedPasswordLength["rating"] = \
419 self.recommendedPasswordLength["penalty"]
420 self.score["count"] += self.recommendedPasswordLength["rating"]
421
422 # lowercaseLetters
423 # Honor or punish the lowercase letter use
424 if self.lowercaseLetters["count"] > 0:
425 self.lowercaseLetters["rating"] = \
426 self.lowercaseLetters["bonus"] + \
427 self.lowercaseLetters["count"] * \
428 self.lowercaseLetters["factor"]
429 else:
430 self.lowercaseLetters["rating"] = self.lowercaseLetters["penalty"]
431 self.score["count"] += self.lowercaseLetters["rating"]
432
433 # uppercaseLetters
434 # Honor or punish the lowercase letter use
435 if self.uppercaseLetters["count"] > 0:
436 self.uppercaseLetters["rating"] = \
437 self.uppercaseLetters["bonus"] + \
438 self.uppercaseLetters["count"] * \
439 self.uppercaseLetters["factor"]
440 else:
441 self.uppercaseLetters["rating"] = self.uppercaseLetters["penalty"]
442 self.score["count"] += self.uppercaseLetters["rating"]
443
444 # numerics
445 # Honor or punish the numerics use
446 if self.numerics["count"] > 0:
447 self.numerics["rating"] = self.numerics["bonus"] + \
448 self.numerics["count"] * self.numerics["factor"]
449 else:
450 self.numerics["rating"] = self.numerics["penalty"]
451 self.score["count"] += self.numerics["rating"]
452
453 # symbols
454 # Honor or punish the symbols use
455 if self.symbols["count"] > 0:
456 self.symbols["rating"] = self.symbols["bonus"] + \
457 self.symbols["count"] * self.symbols["factor"]
458 else:
459 self.symbols["rating"] = self.symbols["penalty"]
460 self.score["count"] += self.symbols["rating"]
461
462 # middleSymbols
463 # Honor or punish the middle symbols use
464 if self.middleSymbols["count"] > 0:
465 self.middleSymbols["rating"] = self.middleSymbols["bonus"] + \
466 self.middleSymbols["count"] * self.middleSymbols["factor"]
467 else:
468 self.middleSymbols["rating"] = self.middleSymbols["penalty"]
469 self.score["count"] += self.middleSymbols["rating"]
470
471 # middleNumerics
472 # Honor or punish the middle numerics use
473 if self.middleNumerics["count"] > 0:
474 self.middleNumerics["rating"] = self.middleNumerics["bonus"] + \
475 self.middleNumerics["count"] * self.middleNumerics["factor"]
476 else:
477 self.middleNumerics["rating"] = self.middleNumerics["penalty"]
478 self.score["count"] += self.middleNumerics["rating"]
479
480 # sequentialLetters
481 # Honor or punish the sequential letter use
482 if self.sequentialLetters["count"] == 0:
483 self.sequentialLetters["rating"] = \
484 self.sequentialLetters["bonus"] + \
485 self.sequentialLetters["count"] * \
486 self.sequentialLetters["factor"]
487 else:
488 self.sequentialLetters["rating"] = \
489 self.sequentialLetters["penalty"]
490 self.score["count"] += self.sequentialLetters["rating"]
491
492 # sequentialNumerics
493 # Honor or punish the sequential numerics use
494 if self.sequentialNumerics["count"] == 0:
495 self.sequentialNumerics["rating"] = \
496 self.sequentialNumerics["bonus"] + \
497 self.sequentialNumerics["count"] * \
498 self.sequentialNumerics["factor"]
499 else:
500 self.sequentialNumerics["rating"] = \
501 self.sequentialNumerics["penalty"]
502 self.score["count"] += self.sequentialNumerics["rating"]
503
504 # keyboardPatterns
505 # Honor or punish the keyboard patterns use
506 if self.keyboardPatterns["count"] == 0:
507 self.keyboardPatterns["rating"] = \
508 self.keyboardPatterns["bonus"] + \
509 self.keyboardPatterns["count"] * \
510 self.keyboardPatterns["factor"]
511 else:
512 self.keyboardPatterns["rating"] = self.keyboardPatterns["penalty"]
513 self.score["count"] += self.keyboardPatterns["rating"]
514
515 # Count our basicRequirements and set the status
516 self.basicRequirements["count"] = 0
517
518 # password length
519 self.passwordLength["status"] = self.__determineStatus(
520 self.passwordLength["count"] - self.passwordLength["minimum"])
521 if self.passwordLength["status"] != self.Status_Failed:
522 # requirement met
523 self.basicRequirements["count"] += 1
524
525 # uppercase letters
526 self.uppercaseLetters["status"] = self.__determineStatus(
527 self.uppercaseLetters["count"] - self.uppercaseLetters["minimum"])
528 if self.uppercaseLetters["status"] != self.Status_Failed:
529 # requirement met
530 self.basicRequirements["count"] += 1
531
532 # lowercase letters
533 self.lowercaseLetters["status"] = self.__determineStatus(
534 self.lowercaseLetters["count"] - self.lowercaseLetters["minimum"])
535 if self.lowercaseLetters["status"] != self.Status_Failed:
536 # requirement met
537 self.basicRequirements["count"] += 1
538
539 # numerics
540 self.numerics["status"] = self.__determineStatus(
541 self.numerics["count"] - self.numerics["minimum"])
542 if self.numerics["status"] != self.Status_Failed:
543 # requirement met
544 self.basicRequirements["count"] += 1
545
546 # symbols
547 self.symbols["status"] = self.__determineStatus(
548 self.symbols["count"] - self.symbols["minimum"])
549 if self.symbols["status"] != self.Status_Failed:
550 # requirement met
551 self.basicRequirements["count"] += 1
552
553 # judge the requirement status
554 self.basicRequirements["status"] = self.__determineStatus(
555 self.basicRequirements["count"] -
556 self.basicRequirements["minimum"])
557 if self.basicRequirements["status"] != self.Status_Failed:
558 self.basicRequirements["rating"] = \
559 self.basicRequirements["bonus"] + \
560 self.basicRequirements["factor"] * \
561 self.basicRequirements["count"]
562 else:
563 self.basicRequirements["rating"] = \
564 self.basicRequirements["penalty"]
565 self.score["count"] += self.basicRequirements["rating"]
566
567 # beyond basic requirements
568 self.recommendedPasswordLength["status"] = self.__determineStatus(
569 self.recommendedPasswordLength["count"] -
570 self.recommendedPasswordLength["minimum"])
571 self.middleNumerics["status"] = self.__determineStatus(
572 self.middleNumerics["count"] -
573 self.middleNumerics["minimum"])
574 self.middleSymbols["status"] = self.__determineStatus(
575 self.middleSymbols["count"] -
576 self.middleSymbols["minimum"])
577 self.sequentialLetters["status"] = self.__determineBinaryStatus(
578 self.sequentialLetters["count"])
579 self.sequentialNumerics["status"] = self.__determineBinaryStatus(
580 self.sequentialNumerics["count"])
581 self.keyboardPatterns["status"] = self.__determineBinaryStatus(
582 self.keyboardPatterns["count"])
583 self.repeatedSequences["status"] = self.__determineBinaryStatus(
584 self.repeatedSequences["count"])
585 self.mirroredSequences["status"] = self.__determineBinaryStatus(
586 self.mirroredSequences["count"])
587
588 # we apply them only, if the length is not awesome
589 if self.recommendedPasswordLength["status"] != self.Status_Exceeded:
590 # repeatedSequences
591 # Honor or punish the use of repeated sequences
592 if self.repeatedSequences["count"] == 0:
593 self.repeatedSequences["rating"] = \
594 self.repeatedSequences["bonus"]
595 else:
596 self.repeatedSequences["rating"] = \
597 self.repeatedSequences["penalty"] + \
598 self.repeatedSequences["count"] * \
599 self.repeatedSequences["factor"]
600
601 # mirroredSequences
602 # Honor or punish the use of mirrored sequences
603 if self.mirroredSequences["count"] == 0:
604 self.mirroredSequences["rating"] = \
605 self.mirroredSequences["bonus"]
606 else:
607 self.mirroredSequences["rating"] = \
608 self.mirroredSequences["penalty"] + \
609 self.mirroredSequences["count"] * \
610 self.mirroredSequences["factor"]
611
612 # save value before redundancy
613 self.score["beforeRedundancy"] = self.score["count"]
614
615 # apply the redundancy
616 # is the password length requirement fulfilled?
617 if self.recommendedPasswordLength["status"] != self.Status_Exceeded:
618 # full penalty, because password is not long enough, only for
619 # a positive score
620 if self.score["count"] > 0:
621 self.score["count"] *= 1.0 / self.redundancy["value"]
622
623 # level it out
624 if self.score["count"] > 100:
625 self.score["adjusted"] = 100
626 elif self.score["count"] < 0:
627 self.score["adjusted"] = 0
628 else:
629 self.score["adjusted"] = self.score["count"]
630
631 # judge it
632 for index in range(len(self.complexity["limits"])):
633 if self.score["adjusted"] <= self.complexity["limits"][index]:
634 self.complexity["value"] = index
635 break
636
637 return self.complexity["value"]

eric ide

mercurial