src/eric7/Utilities/PasswordChecker.py

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

eric ide

mercurial