diff --git a/notebook/auth/security.py b/notebook/auth/security.py index 548e1d6dff..6664137137 100644 --- a/notebook/auth/security.py +++ b/notebook/auth/security.py @@ -21,7 +21,7 @@ salt_len = 12 -def passwd(passphrase=None, algorithm='sha1'): +def passwd(passphrase=None, algorithm='argon2'): """Generate hashed password and salt for use in notebook configuration. In the notebook configuration, set `c.NotebookApp.password` to @@ -34,7 +34,7 @@ def passwd(passphrase=None, algorithm='sha1'): and verify a password. algorithm : str Hashing algorithm to use (e.g, 'sha1' or any argument supported - by :func:`hashlib.new`). + by :func:`hashlib.new`, or 'argon2'). Returns ------- @@ -59,11 +59,22 @@ def passwd(passphrase=None, algorithm='sha1'): 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')) + if algorithm == 'argon2': + from argon2 import PasswordHasher + ph = PasswordHasher( + memory_cost=10240, + time_cost=10, + parallelism=8, + ) + h = ph.hash(passphrase) + + return ':'.join((algorithm, cast_unicode(h, 'ascii'))) + else: + 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())) + return ':'.join((algorithm, salt, h.hexdigest())) def passwd_check(hashed_passphrase, passphrase): @@ -84,30 +95,41 @@ def passwd_check(hashed_passphrase, passphrase): Examples -------- >>> from notebook.auth.security import passwd_check - >>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a', - ... 'mypassword') + >>> passwd_check('argon2:...', 'mypassword') True - >>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a', - ... 'anotherpassword') + >>> passwd_check('argon2:...', 'otherpassword') 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 + >>> 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 + else: + 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): diff --git a/notebook/auth/tests/test_security.py b/notebook/auth/tests/test_security.py index a17e80087e..7b75c6a4be 100644 --- a/notebook/auth/tests/test_security.py +++ b/notebook/auth/tests/test_security.py @@ -1,13 +1,12 @@ # coding: utf-8 -from ..security import passwd, passwd_check, salt_len +from ..security import passwd, passwd_check 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) + algorithm, hashed = p.split(':') + nt.assert_equal(algorithm, 'argon2') + nt.assert_true(hashed.startswith('$argon2id$')) def test_roundtrip(): p = passwd('passphrase') @@ -22,4 +21,7 @@ def test_bad(): 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 + assert passwd_check(phash, u"łe¶ŧ←↓→") + phash = (u'argon2:$argon2id$v=19$m=10240,t=10,p=8$' + u'qjjDiZUofUVVnrVYxacnbA$l5pQq1bJ8zglGT2uXP6iOg') + assert passwd_check(phash, u"łe¶ŧ←↓→") diff --git a/setup.py b/setup.py index b30a44dad5..b6aab7718e 100755 --- a/setup.py +++ b/setup.py @@ -103,6 +103,7 @@ # pyzmq>=17 is not technically necessary, # but hopefully avoids incompatibilities with Tornado 5. April 2018 'pyzmq>=17', + 'argon2-cffi', 'ipython_genutils', 'traitlets>=4.2.1', 'jupyter_core>=4.6.1',