Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/werkzeug/security.py: 22%

68 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-09 06:08 +0000

1from __future__ import annotations 

2 

3import hashlib 

4import hmac 

5import os 

6import posixpath 

7import secrets 

8import warnings 

9 

10SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 

11DEFAULT_PBKDF2_ITERATIONS = 600000 

12 

13_os_alt_seps: list[str] = list( 

14 sep for sep in [os.sep, os.path.altsep] if sep is not None and sep != "/" 

15) 

16 

17 

18def gen_salt(length: int) -> str: 

19 """Generate a random string of SALT_CHARS with specified ``length``.""" 

20 if length <= 0: 

21 raise ValueError("Salt length must be at least 1.") 

22 

23 return "".join(secrets.choice(SALT_CHARS) for _ in range(length)) 

24 

25 

26def _hash_internal(method: str, salt: str, password: str) -> tuple[str, str]: 

27 if method == "plain": 

28 warnings.warn( 

29 "The 'plain' password method is deprecated and will be removed in" 

30 " Werkzeug 3.0. Migrate to the 'scrypt' method.", 

31 stacklevel=3, 

32 ) 

33 return password, method 

34 

35 method, *args = method.split(":") 

36 salt = salt.encode("utf-8") 

37 password = password.encode("utf-8") 

38 

39 if method == "scrypt": 

40 if not args: 

41 n = 2**15 

42 r = 8 

43 p = 1 

44 else: 

45 try: 

46 n, r, p = map(int, args) 

47 except ValueError: 

48 raise ValueError("'scrypt' takes 3 arguments.") from None 

49 

50 maxmem = 132 * n * r * p # ideally 128, but some extra seems needed 

51 return ( 

52 hashlib.scrypt(password, salt=salt, n=n, r=r, p=p, maxmem=maxmem).hex(), 

53 f"scrypt:{n}:{r}:{p}", 

54 ) 

55 elif method == "pbkdf2": 

56 len_args = len(args) 

57 

58 if len_args == 0: 

59 hash_name = "sha256" 

60 iterations = DEFAULT_PBKDF2_ITERATIONS 

61 elif len_args == 1: 

62 hash_name = args[0] 

63 iterations = DEFAULT_PBKDF2_ITERATIONS 

64 elif len_args == 2: 

65 hash_name = args[0] 

66 iterations = int(args[1]) 

67 else: 

68 raise ValueError("'pbkdf2' takes 2 arguments.") 

69 

70 return ( 

71 hashlib.pbkdf2_hmac(hash_name, password, salt, iterations).hex(), 

72 f"pbkdf2:{hash_name}:{iterations}", 

73 ) 

74 else: 

75 warnings.warn( 

76 f"The '{method}' password method is deprecated and will be removed in" 

77 " Werkzeug 3.0. Migrate to the 'scrypt' method.", 

78 stacklevel=3, 

79 ) 

80 return hmac.new(salt, password, method).hexdigest(), method 

81 

82 

83def generate_password_hash( 

84 password: str, method: str = "pbkdf2", salt_length: int = 16 

85) -> str: 

86 """Securely hash a password for storage. A password can be compared to a stored hash 

87 using :func:`check_password_hash`. 

88 

89 The following methods are supported: 

90 

91 - ``scrypt``, more secure but not available on PyPy. The parameters are ``n``, 

92 ``r``, and ``p``, the default is ``scrypt:32768:8:1``. See 

93 :func:`hashlib.scrypt`. 

94 - ``pbkdf2``, the default. The parameters are ``hash_method`` and ``iterations``, 

95 the default is ``pbkdf2:sha256:600000``. See :func:`hashlib.pbkdf2_hmac`. 

96 

97 Default parameters may be updated to reflect current guidelines, and methods may be 

98 deprecated and removed if they are no longer considered secure. To migrate old 

99 hashes, you may generate a new hash when checking an old hash, or you may contact 

100 users with a link to reset their password. 

101 

102 :param password: The plaintext password. 

103 :param method: The key derivation function and parameters. 

104 :param salt_length: The number of characters to generate for the salt. 

105 

106 .. versionchanged:: 2.3 

107 Scrypt support was added. 

108 

109 .. versionchanged:: 2.3 

110 The default iterations for pbkdf2 was increased to 600,000. 

111 

112 .. versionchanged:: 2.3 

113 All plain hashes are deprecated and will not be supported in Werkzeug 3.0. 

114 """ 

115 salt = gen_salt(salt_length) 

116 h, actual_method = _hash_internal(method, salt, password) 

117 return f"{actual_method}${salt}${h}" 

118 

119 

120def check_password_hash(pwhash: str, password: str) -> bool: 

121 """Securely check that the given stored password hash, previously generated using 

122 :func:`generate_password_hash`, matches the given password. 

123 

124 Methods may be deprecated and removed if they are no longer considered secure. To 

125 migrate old hashes, you may generate a new hash when checking an old hash, or you 

126 may contact users with a link to reset their password. 

127 

128 :param pwhash: The hashed password. 

129 :param password: The plaintext password. 

130 

131 .. versionchanged:: 2.3 

132 All plain hashes are deprecated and will not be supported in Werkzeug 3.0. 

133 """ 

134 try: 

135 method, salt, hashval = pwhash.split("$", 2) 

136 except ValueError: 

137 return False 

138 

139 return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval) 

140 

141 

142def safe_join(directory: str, *pathnames: str) -> str | None: 

143 """Safely join zero or more untrusted path components to a base 

144 directory to avoid escaping the base directory. 

145 

146 :param directory: The trusted base directory. 

147 :param pathnames: The untrusted path components relative to the 

148 base directory. 

149 :return: A safe path, otherwise ``None``. 

150 """ 

151 if not directory: 

152 # Ensure we end up with ./path if directory="" is given, 

153 # otherwise the first untrusted part could become trusted. 

154 directory = "." 

155 

156 parts = [directory] 

157 

158 for filename in pathnames: 

159 if filename != "": 

160 filename = posixpath.normpath(filename) 

161 

162 if ( 

163 any(sep in filename for sep in _os_alt_seps) 

164 or os.path.isabs(filename) 

165 or filename == ".." 

166 or filename.startswith("../") 

167 ): 

168 return None 

169 

170 parts.append(filename) 

171 

172 return posixpath.join(*parts)