Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jupyter_server/auth/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

76 statements  

1""" 

2Password generation for the Jupyter Server. 

3""" 

4 

5import getpass 

6import hashlib 

7import json 

8import os 

9import random 

10import traceback 

11import warnings 

12from contextlib import contextmanager 

13 

14from jupyter_core.paths import jupyter_config_dir 

15from traitlets.config import Config 

16from traitlets.config.loader import ConfigFileNotFound, JSONFileConfigLoader 

17 

18# Length of the salt in nr of hex chars, which implies salt_len * 4 

19# bits of randomness. 

20salt_len = 12 

21 

22 

23def passwd(passphrase=None, algorithm="argon2"): 

24 """Generate hashed password and salt for use in server configuration. 

25 

26 In the server configuration, set `c.ServerApp.password` to 

27 the generated string. 

28 

29 Parameters 

30 ---------- 

31 passphrase : str 

32 Password to hash. If unspecified, the user is asked to input 

33 and verify a password. 

34 algorithm : str 

35 Hashing algorithm to use (e.g, 'sha1' or any argument supported 

36 by :func:`hashlib.new`, or 'argon2'). 

37 

38 Returns 

39 ------- 

40 hashed_passphrase : str 

41 Hashed password, in the format 'hash_algorithm:salt:passphrase_hash'. 

42 

43 Examples 

44 -------- 

45 >>> passwd("mypassword") # doctest: +ELLIPSIS 

46 'argon2:...' 

47 

48 """ 

49 if passphrase is None: 

50 for _ in range(3): 

51 p0 = getpass.getpass("Enter password: ") 

52 p1 = getpass.getpass("Verify password: ") 

53 if p0 == p1: 

54 passphrase = p0 

55 break 

56 warnings.warn("Passwords do not match.", stacklevel=2) 

57 else: 

58 msg = "No matching passwords found. Giving up." 

59 raise ValueError(msg) 

60 

61 if algorithm == "argon2": 

62 import argon2 

63 

64 ph = argon2.PasswordHasher( 

65 memory_cost=10240, 

66 time_cost=10, 

67 parallelism=8, 

68 ) 

69 h_ph = ph.hash(passphrase) 

70 

71 return f"{algorithm}:{h_ph}" 

72 

73 h = hashlib.new(algorithm) 

74 salt = ("%0" + str(salt_len) + "x") % random.getrandbits(4 * salt_len) 

75 h.update(passphrase.encode("utf-8") + salt.encode("ascii")) 

76 

77 return f"{algorithm}:{salt}:{h.hexdigest()}" 

78 

79 

80def passwd_check(hashed_passphrase, passphrase): 

81 """Verify that a given passphrase matches its hashed version. 

82 

83 Parameters 

84 ---------- 

85 hashed_passphrase : str 

86 Hashed password, in the format returned by `passwd`. 

87 passphrase : str 

88 Passphrase to validate. 

89 

90 Returns 

91 ------- 

92 valid : bool 

93 True if the passphrase matches the hash. 

94 

95 Examples 

96 -------- 

97 >>> myhash = passwd("mypassword") 

98 >>> passwd_check(myhash, "mypassword") 

99 True 

100 

101 >>> passwd_check(myhash, "otherpassword") 

102 False 

103 

104 >>> passwd_check("sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a", "mypassword") 

105 True 

106 """ 

107 if hashed_passphrase.startswith("argon2:"): 

108 import argon2 

109 import argon2.exceptions 

110 

111 ph = argon2.PasswordHasher() 

112 

113 try: 

114 return ph.verify(hashed_passphrase[7:], passphrase) 

115 except argon2.exceptions.VerificationError: 

116 return False 

117 

118 try: 

119 algorithm, salt, pw_digest = hashed_passphrase.split(":", 2) 

120 except (ValueError, TypeError): 

121 return False 

122 

123 try: 

124 h = hashlib.new(algorithm) 

125 except ValueError: 

126 return False 

127 

128 if len(pw_digest) == 0: 

129 return False 

130 

131 h.update(passphrase.encode("utf-8") + salt.encode("ascii")) 

132 

133 return h.hexdigest() == pw_digest 

134 

135 

136@contextmanager 

137def persist_config(config_file=None, mode=0o600): 

138 """Context manager that can be used to modify a config object 

139 

140 On exit of the context manager, the config will be written back to disk, 

141 by default with user-only (600) permissions. 

142 """ 

143 

144 if config_file is None: 

145 config_file = os.path.join(jupyter_config_dir(), "jupyter_server_config.json") 

146 

147 os.makedirs(os.path.dirname(config_file), exist_ok=True) 

148 

149 loader = JSONFileConfigLoader(os.path.basename(config_file), os.path.dirname(config_file)) 

150 try: 

151 config = loader.load_config() 

152 except ConfigFileNotFound: 

153 config = Config() 

154 

155 yield config 

156 

157 with open(config_file, "w", encoding="utf8") as f: 

158 f.write(json.dumps(config, indent=2)) 

159 

160 try: 

161 os.chmod(config_file, mode) 

162 except Exception: 

163 tb = traceback.format_exc() 

164 warnings.warn( 

165 f"Failed to set permissions on {config_file}:\n{tb}", RuntimeWarning, stacklevel=2 

166 ) 

167 

168 

169def set_password(password=None, config_file=None): 

170 """Ask user for password, store it in JSON configuration file""" 

171 

172 hashed_password = passwd(password) 

173 

174 with persist_config(config_file) as config: 

175 config.IdentityProvider.hashed_password = hashed_password 

176 return hashed_password