diff --git a/jupyter_notebook/auth/__init__.py b/jupyter_notebook/auth/__init__.py index e69de29bb..9f84fa2e9 100644 --- a/jupyter_notebook/auth/__init__.py +++ b/jupyter_notebook/auth/__init__.py @@ -0,0 +1 @@ +from .security import passwd diff --git a/jupyter_notebook/auth/login.py b/jupyter_notebook/auth/login.py index 6c52486bf..edbbb3162 100644 --- a/jupyter_notebook/auth/login.py +++ b/jupyter_notebook/auth/login.py @@ -7,7 +7,7 @@ import uuid from tornado.escape import url_escape -from IPython.lib.security import passwd_check +from ..auth.security import passwd_check from ..base.handlers import IPythonHandler diff --git a/jupyter_notebook/auth/security.py b/jupyter_notebook/auth/security.py new file mode 100644 index 000000000..ee0e7e62f --- /dev/null +++ b/jupyter_notebook/auth/security.py @@ -0,0 +1,101 @@ +""" +Password generation for the Notebook. +""" +import getpass +import hashlib +import random + +from ipython_genutils.py3compat import cast_bytes, str_to_bytes + +# 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='sha1'): + """Generate hashed password and salt for use in notebook configuration. + + In the notebook configuration, set `c.NotebookApp.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`). + + Returns + ------- + hashed_passphrase : str + Hashed password, in the format 'hash_algorithm:salt:passphrase_hash'. + + Examples + -------- + >>> passwd('mypassword') + 'sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12' + + """ + 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.') + + 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 + -------- + >>> from jupyter_notebook.auth.security import passwd_check + >>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a', + ... 'mypassword') + True + + >>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a', + ... 'anotherpassword') + 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 diff --git a/jupyter_notebook/auth/tests/__init__.py b/jupyter_notebook/auth/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/jupyter_notebook/auth/tests/test_security.py b/jupyter_notebook/auth/tests/test_security.py new file mode 100644 index 000000000..a17e80087 --- /dev/null +++ b/jupyter_notebook/auth/tests/test_security.py @@ -0,0 +1,25 @@ +# coding: utf-8 +from ..security import passwd, passwd_check, salt_len +import nose.tools as nt + +def test_passwd_structure(): + p = passwd('passphrase') + algorithm, salt, hashed = p.split(':') + nt.assert_equal(algorithm, 'sha1') + nt.assert_equal(len(salt), salt_len) + nt.assert_equal(len(hashed), 40) + +def test_roundtrip(): + p = passwd('passphrase') + nt.assert_equal(passwd_check(p, 'passphrase'), True) + +def test_bad(): + p = passwd('passphrase') + nt.assert_equal(passwd_check(p, p), False) + nt.assert_equal(passwd_check(p, 'a:b:c:d'), False) + nt.assert_equal(passwd_check(p, 'a:b'), False) + +def test_passwd_check_unicode(): + # GH issue #4524 + phash = u'sha1:23862bc21dd3:7a415a95ae4580582e314072143d9c382c491e4f' + assert passwd_check(phash, u"łe¶ŧ←↓→") \ No newline at end of file diff --git a/jupyter_notebook/notebookapp.py b/jupyter_notebook/notebookapp.py index ae98e3b6d..31dc1327d 100644 --- a/jupyter_notebook/notebookapp.py +++ b/jupyter_notebook/notebookapp.py @@ -483,7 +483,7 @@ class NotebookApp(BaseIPythonApplication): To generate, type in a python/IPython shell: - from IPython.lib import passwd; passwd() + from jupyter_notebook.auth import passwd; passwd() The string should be of the form type:salt:hashed-password. """