From 586e1f2519ae1ca8945a721d1397ebb95882d9e1 Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Tue, 24 Jul 2018 15:00:21 -0400 Subject: [PATCH 1/4] Use bcrypt for notebook.auth.security --- notebook/auth/security.py | 63 +++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/notebook/auth/security.py b/notebook/auth/security.py index 548e1d6dff..65c757957a 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='bcrypt'): """Generate hashed password and salt for use in notebook configuration. In the notebook configuration, set `c.NotebookApp.password` to @@ -59,11 +59,17 @@ 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 == 'bcrypt': + import bcrypt + h = bcrypt.hashpw(cast_bytes(passphrase, 'utf-8'), bcrypt.gensalt()) - return ':'.join((algorithm, salt, h.hexdigest())) + 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())) def passwd_check(hashed_passphrase, passphrase): @@ -84,30 +90,37 @@ def passwd_check(hashed_passphrase, passphrase): Examples -------- >>> from notebook.auth.security import passwd_check - >>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a', - ... 'mypassword') + >>> passwd_check('bcrypt:...', 'mypassword') True - >>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a', - ... 'anotherpassword') + >>> passwd_check('bcrypt:...', '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('bcrypt:'): + import bcrypt + return bcrypt.checkpw(cast_bytes(passphrase, 'utf-8'), + cast_bytes(hashed_passphrase[7:], 'ascii')) + 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): From 8be9284b26309957c4fb46bdfaa4840f163e5a18 Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Tue, 24 Jul 2018 15:01:33 -0400 Subject: [PATCH 2/4] Add bcrypt to install_requires --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index b30a44dad5..0525e65579 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', + 'bcrypt', 'ipython_genutils', 'traitlets>=4.2.1', 'jupyter_core>=4.6.1', From e55edce2658851c40be103e9652e02afdb47e29d Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Tue, 24 Jul 2018 15:11:06 -0400 Subject: [PATCH 3/4] Add test for bcrypt --- notebook/auth/tests/test_security.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/notebook/auth/tests/test_security.py b/notebook/auth/tests/test_security.py index a17e80087e..c4557333b9 100644 --- a/notebook/auth/tests/test_security.py +++ b/notebook/auth/tests/test_security.py @@ -4,10 +4,9 @@ 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, 'bcrypt') + nt.assert_true(hashed.startswith('$2b$')) 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'bcrypt:$2b$12$' + u'/IwxT/HWZRlDczICrZQVr.Mg0K5eu9Sv817lgf8qUd8goygYs1OlO') + assert passwd_check(phash, u"łe¶ŧ←↓→") From e76a9f685aa59c7c0d2ee2383bbe84b8d02b089f Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Wed, 8 Jul 2020 09:54:43 -0400 Subject: [PATCH 4/4] Switch to argon2-cffi --- notebook/auth/security.py | 31 ++++++++++++++++++---------- notebook/auth/tests/test_security.py | 10 ++++----- setup.py | 2 +- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/notebook/auth/security.py b/notebook/auth/security.py index 65c757957a..6664137137 100644 --- a/notebook/auth/security.py +++ b/notebook/auth/security.py @@ -21,7 +21,7 @@ salt_len = 12 -def passwd(passphrase=None, algorithm='bcrypt'): +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='bcrypt'): 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,9 +59,14 @@ def passwd(passphrase=None, algorithm='bcrypt'): else: raise ValueError('No matching passwords found. Giving up.') - if algorithm == 'bcrypt': - import bcrypt - h = bcrypt.hashpw(cast_bytes(passphrase, 'utf-8'), bcrypt.gensalt()) + 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: @@ -90,20 +95,24 @@ def passwd_check(hashed_passphrase, passphrase): Examples -------- >>> from notebook.auth.security import passwd_check - >>> passwd_check('bcrypt:...', 'mypassword') + >>> passwd_check('argon2:...', 'mypassword') True - >>> passwd_check('bcrypt:...', 'otherpassword') + >>> passwd_check('argon2:...', 'otherpassword') False >>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a', ... 'mypassword') True """ - if hashed_passphrase.startswith('bcrypt:'): - import bcrypt - return bcrypt.checkpw(cast_bytes(passphrase, 'utf-8'), - cast_bytes(hashed_passphrase[7:], 'ascii')) + 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) diff --git a/notebook/auth/tests/test_security.py b/notebook/auth/tests/test_security.py index c4557333b9..7b75c6a4be 100644 --- a/notebook/auth/tests/test_security.py +++ b/notebook/auth/tests/test_security.py @@ -1,12 +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, hashed = p.split(':') - nt.assert_equal(algorithm, 'bcrypt') - nt.assert_true(hashed.startswith('$2b$')) + nt.assert_equal(algorithm, 'argon2') + nt.assert_true(hashed.startswith('$argon2id$')) def test_roundtrip(): p = passwd('passphrase') @@ -22,6 +22,6 @@ def test_passwd_check_unicode(): # GH issue #4524 phash = u'sha1:23862bc21dd3:7a415a95ae4580582e314072143d9c382c491e4f' assert passwd_check(phash, u"łe¶ŧ←↓→") - phash = (u'bcrypt:$2b$12$' - u'/IwxT/HWZRlDczICrZQVr.Mg0K5eu9Sv817lgf8qUd8goygYs1OlO') + 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 0525e65579..b6aab7718e 100755 --- a/setup.py +++ b/setup.py @@ -103,7 +103,7 @@ # pyzmq>=17 is not technically necessary, # but hopefully avoids incompatibilities with Tornado 5. April 2018 'pyzmq>=17', - 'bcrypt', + 'argon2-cffi', 'ipython_genutils', 'traitlets>=4.2.1', 'jupyter_core>=4.6.1',