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