src/eric7/Utilities/PasswordChecker.py

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

eric ide

mercurial