| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- """
- 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
- # 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 _ 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
|