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

51 statements  

« prev     ^ index     » next       coverage.py v7.0.1, created at 2022-12-25 06:11 +0000

1import hashlib 

2import hmac 

3import os 

4import posixpath 

5import secrets 

6import typing as t 

7 

8if t.TYPE_CHECKING: 

9 pass 

10 

11SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 

12DEFAULT_PBKDF2_ITERATIONS = 260000 

13 

14_os_alt_seps: t.List[str] = list( 

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

16) 

17 

18 

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

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

21 if length <= 0: 

22 raise ValueError("Salt length must be positive") 

23 

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

25 

26 

27def _hash_internal(method: str, salt: str, password: str) -> t.Tuple[str, str]: 

28 """Internal password hash helper. Supports plaintext without salt, 

29 unsalted and salted passwords. In case salted passwords are used 

30 hmac is used. 

31 """ 

32 if method == "plain": 

33 return password, method 

34 

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

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

37 

38 if method.startswith("pbkdf2:"): 

39 if not salt: 

40 raise ValueError("Salt is required for PBKDF2") 

41 

42 args = method[7:].split(":") 

43 

44 if len(args) not in (1, 2): 

45 raise ValueError("Invalid number of arguments for PBKDF2") 

46 

47 method = args.pop(0) 

48 iterations = int(args[0] or 0) if args else DEFAULT_PBKDF2_ITERATIONS 

49 return ( 

50 hashlib.pbkdf2_hmac(method, password, salt, iterations).hex(), 

51 f"pbkdf2:{method}:{iterations}", 

52 ) 

53 

54 if salt: 

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

56 

57 return hashlib.new(method, password).hexdigest(), method 

58 

59 

60def generate_password_hash( 

61 password: str, method: str = "pbkdf2:sha256", salt_length: int = 16 

62) -> str: 

63 """Hash a password with the given method and salt with a string of 

64 the given length. The format of the string returned includes the method 

65 that was used so that :func:`check_password_hash` can check the hash. 

66 

67 The format for the hashed string looks like this:: 

68 

69 method$salt$hash 

70 

71 This method can **not** generate unsalted passwords but it is possible 

72 to set param method='plain' in order to enforce plaintext passwords. 

73 If a salt is used, hmac is used internally to salt the password. 

74 

75 If PBKDF2 is wanted it can be enabled by setting the method to 

76 ``pbkdf2:method:iterations`` where iterations is optional:: 

77 

78 pbkdf2:sha256:80000$salt$hash 

79 pbkdf2:sha256$salt$hash 

80 

81 :param password: the password to hash. 

82 :param method: the hash method to use (one that hashlib supports). Can 

83 optionally be in the format ``pbkdf2:method:iterations`` 

84 to enable PBKDF2. 

85 :param salt_length: the length of the salt in letters. 

86 """ 

87 salt = gen_salt(salt_length) if method != "plain" else "" 

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

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

90 

91 

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

93 """Check a password against a given salted and hashed password value. 

94 In order to support unsalted legacy passwords this method supports 

95 plain text passwords, md5 and sha1 hashes (both salted and unsalted). 

96 

97 Returns `True` if the password matched, `False` otherwise. 

98 

99 :param pwhash: a hashed string like returned by 

100 :func:`generate_password_hash`. 

101 :param password: the plaintext password to compare against the hash. 

102 """ 

103 if pwhash.count("$") < 2: 

104 return False 

105 

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

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

108 

109 

110def safe_join(directory: str, *pathnames: str) -> t.Optional[str]: 

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

112 directory to avoid escaping the base directory. 

113 

114 :param directory: The trusted base directory. 

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

116 base directory. 

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

118 """ 

119 if not directory: 

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

121 # otherwise the first untrusted part could become trusted. 

122 directory = "." 

123 

124 parts = [directory] 

125 

126 for filename in pathnames: 

127 if filename != "": 

128 filename = posixpath.normpath(filename) 

129 

130 if ( 

131 any(sep in filename for sep in _os_alt_seps) 

132 or os.path.isabs(filename) 

133 or filename == ".." 

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

135 ): 

136 return None 

137 

138 parts.append(filename) 

139 

140 return posixpath.join(*parts)