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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

63 statements  

1from __future__ import annotations 

2 

3import hashlib 

4import hmac 

5import os 

6import posixpath 

7import secrets 

8 

9SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 

10DEFAULT_PBKDF2_ITERATIONS = 1_000_000 

11 

12_os_alt_seps: list[str] = list( 

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

14) 

15 

16 

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

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

19 if length <= 0: 

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

21 

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

23 

24 

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

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

27 salt_bytes = salt.encode() 

28 password_bytes = password.encode() 

29 

30 if method == "scrypt": 

31 if not args: 

32 n = 2**15 

33 r = 8 

34 p = 1 

35 else: 

36 try: 

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

38 except ValueError: 

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

40 

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

42 return ( 

43 hashlib.scrypt( 

44 password_bytes, salt=salt_bytes, n=n, r=r, p=p, maxmem=maxmem 

45 ).hex(), 

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

47 ) 

48 elif method == "pbkdf2": 

49 len_args = len(args) 

50 

51 if len_args == 0: 

52 hash_name = "sha256" 

53 iterations = DEFAULT_PBKDF2_ITERATIONS 

54 elif len_args == 1: 

55 hash_name = args[0] 

56 iterations = DEFAULT_PBKDF2_ITERATIONS 

57 elif len_args == 2: 

58 hash_name = args[0] 

59 iterations = int(args[1]) 

60 else: 

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

62 

63 return ( 

64 hashlib.pbkdf2_hmac( 

65 hash_name, password_bytes, salt_bytes, iterations 

66 ).hex(), 

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

68 ) 

69 else: 

70 raise ValueError(f"Invalid hash method '{method}'.") 

71 

72 

73def generate_password_hash( 

74 password: str, method: str = "scrypt", salt_length: int = 16 

75) -> str: 

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

77 using :func:`check_password_hash`. 

78 

79 The following methods are supported: 

80 

81 - ``scrypt``, the default. The parameters are ``n``, ``r``, and ``p``, the default 

82 is ``scrypt:32768:8:1``. See :func:`hashlib.scrypt`. 

83 - ``pbkdf2``, less secure. The parameters are ``hash_method`` and ``iterations``, 

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

85 

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

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

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

89 users with a link to reset their password. 

90 

91 :param password: The plaintext password. 

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

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

94 

95 .. versionchanged:: 3.1 

96 The default iterations for pbkdf2 was increased to 1,000,000. 

97 

98 .. versionchanged:: 2.3 

99 Scrypt support was added. 

100 

101 .. versionchanged:: 2.3 

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

103 

104 .. versionchanged:: 2.3 

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

106 """ 

107 salt = gen_salt(salt_length) 

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

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

110 

111 

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

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

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

115 

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

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

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

119 

120 :param pwhash: The hashed password. 

121 :param password: The plaintext password. 

122 

123 .. versionchanged:: 2.3 

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

125 """ 

126 try: 

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

128 except ValueError: 

129 return False 

130 

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

132 

133 

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

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

136 directory to avoid escaping the base directory. 

137 

138 :param directory: The trusted base directory. 

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

140 base directory. 

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

142 """ 

143 if not directory: 

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

145 # otherwise the first untrusted part could become trusted. 

146 directory = "." 

147 

148 parts = [directory] 

149 

150 for filename in pathnames: 

151 if filename != "": 

152 filename = posixpath.normpath(filename) 

153 

154 if ( 

155 any(sep in filename for sep in _os_alt_seps) 

156 or os.path.isabs(filename) 

157 # ntpath.isabs doesn't catch this on Python < 3.11 

158 or filename.startswith("/") 

159 or filename == ".." 

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

161 ): 

162 return None 

163 

164 parts.append(filename) 

165 

166 return posixpath.join(*parts)