Coverage for gws-app/gws/lib/password/__init__.py: 0%

55 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-17 01:37 +0200

1"""Password tools.""" 

2 

3import base64 

4import hashlib 

5import hmac 

6import random 

7import string 

8 

9 

10def compare(a: str, b: str) -> bool: 

11 """Compares two Strings in a safe way to prevent timing attacks. 

12 

13 Args: 

14 a: 1st String. 

15 b: 2nd String. 

16 

17 Returns: 

18 ``True`` if a equals b, ``False`` otherwise. 

19 """ 

20 

21 return hmac.compare_digest(a.encode('utf8'), b.encode('utf8')) 

22 

23 

24def encode(password: str, algo: str = 'sha512') -> str: 

25 """Encode a password into a hash. 

26 

27 Args: 

28 password: String password. 

29 algo: Hashing algorithm. Default is SHA512. 

30 

31 Returns: 

32 Respective hash value in the format ``$algorithm$salt$hash``. 

33 """ 

34 

35 salt = _random_string(8) 

36 h = _pbkdf2(password, salt, algo) 

37 return '$'.join(['', algo, salt, base64.urlsafe_b64encode(h).decode('utf8')]) 

38 

39 

40def check(password: str, encoded: str) -> bool: 

41 """Check if a password matches a hash. 

42 

43 Args: 

44 password: Password as a string. 

45 encoded: Hash of the input password as a string. 

46 

47 Returns: 

48 ``True`` if password matches the hash, else ``False``. 

49 """ 

50 

51 try: 

52 _, algo, salt, hs = str(encoded).split('$') 

53 h1 = base64.urlsafe_b64decode(hs) 

54 h2 = _pbkdf2(password, salt, algo) 

55 except (TypeError, ValueError): 

56 return False 

57 

58 return hmac.compare_digest(h1, h2) 

59 

60 

61class SymbolGroup: 

62 def __init__(self, s, min_len, max_len): 

63 self.chars = s 

64 self.max = max_len 

65 self.min = min_len 

66 self.count = 0 

67 

68 

69def generate( 

70 min_len: int = 16, 

71 max_len: int = 16, 

72 min_lower: int = 0, 

73 max_lower: int = 255, 

74 min_upper: int = 0, 

75 max_upper: int = 255, 

76 min_digit: int = 0, 

77 max_digit: int = 255, 

78 min_punct: int = 0, 

79 max_punct: int = 255, 

80) -> str: 

81 """Generate a random password.""" 

82 

83 groups = [ 

84 SymbolGroup(string.ascii_lowercase, min_lower, max_lower), 

85 SymbolGroup(string.ascii_uppercase, min_upper, max_upper), 

86 SymbolGroup(string.digits, min_digit, max_digit), 

87 SymbolGroup(string.punctuation, min_punct, max_punct), 

88 ] 

89 return generate_with_groups(groups, min_len, max_len) 

90 

91 

92def generate_with_groups( 

93 groups: list[SymbolGroup], 

94 min_len: int = 16, 

95 max_len: int = 16, 

96) -> str: 

97 """Generate a random password from a list of `SymbolGroup` objects.""" 

98 

99 r = random.SystemRandom() 

100 p = [] 

101 

102 for g in groups: 

103 p.extend(r.choices(g.chars, k=g.min)) 

104 g.count = g.min 

105 

106 if len(p) > max_len: 

107 raise ValueError('invalid parameters') 

108 

109 size = r.randint(max(min_len, len(p)), max_len) 

110 

111 while len(p) < size: 

112 sel = ''.join(g.chars for g in groups if g.count < g.max) 

113 if not sel: 

114 raise ValueError('invalid parameters') 

115 c = r.choice(sel) 

116 for g in groups: 

117 if c in g.chars: 

118 g.count += 1 

119 break 

120 p.append(c) 

121 

122 r.shuffle(p) 

123 

124 return ''.join(p) 

125 

126 

127## 

128 

129 

130def _pbkdf2(password, salt, algo): 

131 return hashlib.pbkdf2_hmac(algo, password.encode('utf8'), salt.encode('utf8'), 100000) 

132 

133 

134def _random_string(length): 

135 a = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' 

136 r = random.SystemRandom() 

137 return ''.join(r.choice(a) for _ in range(length))