|
""" |
|
Password generation for the Jupyter Server. |
|
""" |
|
|
|
import getpass |
|
import hashlib |
|
import json |
|
import os |
|
import random |
|
import traceback |
|
import warnings |
|
from contextlib import contextmanager |
|
|
|
from jupyter_core.paths import jupyter_config_dir |
|
from traitlets.config import Config |
|
from traitlets.config.loader import ConfigFileNotFound, JSONFileConfigLoader |
|
|
|
|
|
|
|
salt_len = 12 |
|
|
|
|
|
def passwd(passphrase=None, algorithm="argon2"): |
|
"""Generate hashed password and salt for use in server configuration. |
|
|
|
In the server configuration, set `c.ServerApp.password` to |
|
the generated string. |
|
|
|
Parameters |
|
---------- |
|
passphrase : str |
|
Password to hash. If unspecified, the user is asked to input |
|
and verify a password. |
|
algorithm : str |
|
Hashing algorithm to use (e.g, 'sha1' or any argument supported |
|
by :func:`hashlib.new`, or 'argon2'). |
|
|
|
Returns |
|
------- |
|
hashed_passphrase : str |
|
Hashed password, in the format 'hash_algorithm:salt:passphrase_hash'. |
|
|
|
Examples |
|
-------- |
|
>>> passwd("mypassword") # doctest: +ELLIPSIS |
|
'argon2:...' |
|
|
|
""" |
|
if passphrase is None: |
|
for _ in range(3): |
|
p0 = getpass.getpass("Enter password: ") |
|
p1 = getpass.getpass("Verify password: ") |
|
if p0 == p1: |
|
passphrase = p0 |
|
break |
|
warnings.warn("Passwords do not match.", stacklevel=2) |
|
else: |
|
msg = "No matching passwords found. Giving up." |
|
raise ValueError(msg) |
|
|
|
if algorithm == "argon2": |
|
import argon2 |
|
|
|
ph = argon2.PasswordHasher( |
|
memory_cost=10240, |
|
time_cost=10, |
|
parallelism=8, |
|
) |
|
h_ph = ph.hash(passphrase) |
|
|
|
return f"{algorithm}:{h_ph}" |
|
|
|
h = hashlib.new(algorithm) |
|
salt = ("%0" + str(salt_len) + "x") % random.getrandbits(4 * salt_len) |
|
h.update(passphrase.encode("utf-8") + salt.encode("ascii")) |
|
|
|
return f"{algorithm}:{salt}:{h.hexdigest()}" |
|
|
|
|
|
def passwd_check(hashed_passphrase, passphrase): |
|
"""Verify that a given passphrase matches its hashed version. |
|
|
|
Parameters |
|
---------- |
|
hashed_passphrase : str |
|
Hashed password, in the format returned by `passwd`. |
|
passphrase : str |
|
Passphrase to validate. |
|
|
|
Returns |
|
------- |
|
valid : bool |
|
True if the passphrase matches the hash. |
|
|
|
Examples |
|
-------- |
|
>>> myhash = passwd("mypassword") |
|
>>> passwd_check(myhash, "mypassword") |
|
True |
|
|
|
>>> passwd_check(myhash, "otherpassword") |
|
False |
|
|
|
>>> passwd_check("sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a", "mypassword") |
|
True |
|
""" |
|
if hashed_passphrase.startswith("argon2:"): |
|
import argon2 |
|
import argon2.exceptions |
|
|
|
ph = argon2.PasswordHasher() |
|
|
|
try: |
|
return ph.verify(hashed_passphrase[7:], passphrase) |
|
except argon2.exceptions.VerificationError: |
|
return False |
|
|
|
try: |
|
algorithm, salt, pw_digest = hashed_passphrase.split(":", 2) |
|
except (ValueError, TypeError): |
|
return False |
|
|
|
try: |
|
h = hashlib.new(algorithm) |
|
except ValueError: |
|
return False |
|
|
|
if len(pw_digest) == 0: |
|
return False |
|
|
|
h.update(passphrase.encode("utf-8") + salt.encode("ascii")) |
|
|
|
return h.hexdigest() == pw_digest |
|
|
|
|
|
@contextmanager |
|
def persist_config(config_file=None, mode=0o600): |
|
"""Context manager that can be used to modify a config object |
|
|
|
On exit of the context manager, the config will be written back to disk, |
|
by default with user-only (600) permissions. |
|
""" |
|
|
|
if config_file is None: |
|
config_file = os.path.join(jupyter_config_dir(), "jupyter_server_config.json") |
|
|
|
os.makedirs(os.path.dirname(config_file), exist_ok=True) |
|
|
|
loader = JSONFileConfigLoader(os.path.basename(config_file), os.path.dirname(config_file)) |
|
try: |
|
config = loader.load_config() |
|
except ConfigFileNotFound: |
|
config = Config() |
|
|
|
yield config |
|
|
|
with open(config_file, "w", encoding="utf8") as f: |
|
f.write(json.dumps(config, indent=2)) |
|
|
|
try: |
|
os.chmod(config_file, mode) |
|
except Exception: |
|
tb = traceback.format_exc() |
|
warnings.warn( |
|
f"Failed to set permissions on {config_file}:\n{tb}", RuntimeWarning, stacklevel=2 |
|
) |
|
|
|
|
|
def set_password(password=None, config_file=None): |
|
"""Ask user for password, store it in JSON configuration file""" |
|
|
|
hashed_password = passwd(password) |
|
|
|
with persist_config(config_file) as config: |
|
config.IdentityProvider.hashed_password = hashed_password |
|
return hashed_password |
|
|