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

63 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-09 07:17 +0000

1from __future__ import annotations 

2 

3import hashlib 

4import hmac 

5import os 

6import posixpath 

7import secrets 

8 

9SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 

10DEFAULT_PBKDF2_ITERATIONS = 600000 

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 = salt.encode("utf-8") 

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

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(password, salt=salt, n=n, r=r, p=p, maxmem=maxmem).hex(), 

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

45 ) 

46 elif method == "pbkdf2": 

47 len_args = len(args) 

48 

49 if len_args == 0: 

50 hash_name = "sha256" 

51 iterations = DEFAULT_PBKDF2_ITERATIONS 

52 elif len_args == 1: 

53 hash_name = args[0] 

54 iterations = DEFAULT_PBKDF2_ITERATIONS 

55 elif len_args == 2: 

56 hash_name = args[0] 

57 iterations = int(args[1]) 

58 else: 

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

60 

61 return ( 

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

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

64 ) 

65 else: 

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

67 

68 

69def generate_password_hash( 

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

71) -> str: 

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

73 using :func:`check_password_hash`. 

74 

75 The following methods are supported: 

76 

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

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

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

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

81 

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

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

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

85 users with a link to reset their password. 

86 

87 :param password: The plaintext password. 

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

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

90 

91 .. versionchanged:: 2.3 

92 Scrypt support was added. 

93 

94 .. versionchanged:: 2.3 

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

96 

97 .. versionchanged:: 2.3 

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

99 """ 

100 salt = gen_salt(salt_length) 

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

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

103 

104 

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

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

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

108 

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

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

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

112 

113 :param pwhash: The hashed password. 

114 :param password: The plaintext password. 

115 

116 .. versionchanged:: 2.3 

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

118 """ 

119 try: 

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

121 except ValueError: 

122 return False 

123 

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

125 

126 

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

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

129 directory to avoid escaping the base directory. 

130 

131 :param directory: The trusted base directory. 

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

133 base directory. 

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

135 """ 

136 if not directory: 

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

138 # otherwise the first untrusted part could become trusted. 

139 directory = "." 

140 

141 parts = [directory] 

142 

143 for filename in pathnames: 

144 if filename != "": 

145 filename = posixpath.normpath(filename) 

146 

147 if ( 

148 any(sep in filename for sep in _os_alt_seps) 

149 or os.path.isabs(filename) 

150 or filename == ".." 

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

152 ): 

153 return None 

154 

155 parts.append(filename) 

156 

157 return posixpath.join(*parts)