""" Password generation for the Jupyter Server. """ import getpass import hashlib import io import json import os import random import traceback import warnings from contextlib import contextmanager from ipython_genutils.py3compat import cast_bytes from ipython_genutils.py3compat import cast_unicode from ipython_genutils.py3compat import str_to_bytes from jupyter_core.paths import jupyter_config_dir from traitlets.config import Config from traitlets.config import ConfigFileNotFound from traitlets.config import JSONFileConfigLoader # Length of the salt in nr of hex chars, which implies salt_len * 4 # bits of randomness. 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 i in range(3): p0 = getpass.getpass("Enter password: ") p1 = getpass.getpass("Verify password: ") if p0 == p1: passphrase = p0 break else: print("Passwords do not match.") else: raise ValueError("No matching passwords found. Giving up.") if algorithm == "argon2": import argon2 ph = argon2.PasswordHasher( memory_cost=10240, time_cost=10, parallelism=8, ) h = ph.hash(passphrase) return ":".join((algorithm, cast_unicode(h, "ascii"))) h = hashlib.new(algorithm) salt = ("%0" + str(salt_len) + "x") % random.getrandbits(4 * salt_len) h.update(cast_bytes(passphrase, "utf-8") + str_to_bytes(salt, "ascii")) return ":".join((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(cast_bytes(passphrase, "utf-8") + cast_bytes(salt, "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 io.open(config_file, "w", encoding="utf8") as f: f.write(cast_unicode(json.dumps(config, indent=2))) try: os.chmod(config_file, mode) except Exception as e: tb = traceback.format_exc() warnings.warn("Failed to set permissions on %s:\n%s" % (config_file, tb), RuntimeWarning) 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.ServerApp.password = hashed_password