Coverage for /pythoncovmergedfiles/medio/medio/src/jupyter_server/jupyter_server/auth/security.py: 23%
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
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
1"""
2Password generation for the Jupyter Server.
3"""
5import getpass
6import hashlib
7import json
8import os
9import random
10import traceback
11import warnings
12from contextlib import contextmanager
14from jupyter_core.paths import jupyter_config_dir
15from traitlets.config import Config
16from traitlets.config.loader import ConfigFileNotFound, JSONFileConfigLoader
18# Length of the salt in nr of hex chars, which implies salt_len * 4
19# bits of randomness.
20salt_len = 12
23def passwd(passphrase=None, algorithm="argon2"):
24 """Generate hashed password and salt for use in server configuration.
26 In the server configuration, set `c.ServerApp.password` to
27 the generated string.
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').
38 Returns
39 -------
40 hashed_passphrase : str
41 Hashed password, in the format 'hash_algorithm:salt:passphrase_hash'.
43 Examples
44 --------
45 >>> passwd("mypassword") # doctest: +ELLIPSIS
46 'argon2:...'
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)
61 if algorithm == "argon2":
62 import argon2
64 ph = argon2.PasswordHasher(
65 memory_cost=10240,
66 time_cost=10,
67 parallelism=8,
68 )
69 h_ph = ph.hash(passphrase)
71 return f"{algorithm}:{h_ph}"
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"))
77 return f"{algorithm}:{salt}:{h.hexdigest()}"
80def passwd_check(hashed_passphrase, passphrase):
81 """Verify that a given passphrase matches its hashed version.
83 Parameters
84 ----------
85 hashed_passphrase : str
86 Hashed password, in the format returned by `passwd`.
87 passphrase : str
88 Passphrase to validate.
90 Returns
91 -------
92 valid : bool
93 True if the passphrase matches the hash.
95 Examples
96 --------
97 >>> myhash = passwd("mypassword")
98 >>> passwd_check(myhash, "mypassword")
99 True
101 >>> passwd_check(myhash, "otherpassword")
102 False
104 >>> passwd_check("sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a", "mypassword")
105 True
106 """
107 if hashed_passphrase.startswith("argon2:"):
108 import argon2
109 import argon2.exceptions
111 ph = argon2.PasswordHasher()
113 try:
114 return ph.verify(hashed_passphrase[7:], passphrase)
115 except argon2.exceptions.VerificationError:
116 return False
118 try:
119 algorithm, salt, pw_digest = hashed_passphrase.split(":", 2)
120 except (ValueError, TypeError):
121 return False
123 try:
124 h = hashlib.new(algorithm)
125 except ValueError:
126 return False
128 if len(pw_digest) == 0:
129 return False
131 h.update(passphrase.encode("utf-8") + salt.encode("ascii"))
133 return h.hexdigest() == pw_digest
136@contextmanager
137def persist_config(config_file=None, mode=0o600):
138 """Context manager that can be used to modify a config object
140 On exit of the context manager, the config will be written back to disk,
141 by default with user-only (600) permissions.
142 """
144 if config_file is None:
145 config_file = os.path.join(jupyter_config_dir(), "jupyter_server_config.json")
147 os.makedirs(os.path.dirname(config_file), exist_ok=True)
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()
155 yield config
157 with open(config_file, "w", encoding="utf8") as f:
158 f.write(json.dumps(config, indent=2))
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 )
169def set_password(password=None, config_file=None):
170 """Ask user for password, store it in JSON configuration file"""
172 hashed_password = passwd(password)
174 with persist_config(config_file) as config:
175 config.IdentityProvider.hashed_password = hashed_password
176 return hashed_password