From 79fd245e0961ad62985e50b772b3217696f46cc0 Mon Sep 17 00:00:00 2001 From: "Afshin T. Darian" Date: Fri, 19 Mar 2021 13:36:31 +0000 Subject: [PATCH] Implement password hashing with argon2-cffi --- jupyter_server/auth/security.py | 35 ++++++++++++++++++---- jupyter_server/tests/auth/test_security.py | 14 +++++---- setup.py | 1 + 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/jupyter_server/auth/security.py b/jupyter_server/auth/security.py index 0fdb8eb5f0..ff78f4a050 100644 --- a/jupyter_server/auth/security.py +++ b/jupyter_server/auth/security.py @@ -12,6 +12,9 @@ import traceback import warnings +import argon2 +import argon2.exceptions +from argon2 import PasswordHasher from ipython_genutils.py3compat import cast_bytes, str_to_bytes, cast_unicode from traitlets.config import Config, ConfigFileNotFound, JSONFileConfigLoader from jupyter_core.paths import jupyter_config_dir @@ -21,7 +24,7 @@ salt_len = 12 -def passwd(passphrase=None, algorithm='sha1'): +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 @@ -34,7 +37,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,6 +62,16 @@ def passwd(passphrase=None, algorithm='sha1'): else: raise ValueError('No matching passwords found. Giving up.') + if algorithm == 'argon2': + ph = 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')) @@ -84,14 +97,24 @@ def passwd_check(hashed_passphrase, passphrase): Examples -------- >>> from jupyter_server.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 + + >>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a', + ... 'mypassword') + True """ + if hashed_passphrase.startswith('argon2:'): + 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): diff --git a/jupyter_server/tests/auth/test_security.py b/jupyter_server/tests/auth/test_security.py index 85a4ead118..ff4ea669ec 100644 --- a/jupyter_server/tests/auth/test_security.py +++ b/jupyter_server/tests/auth/test_security.py @@ -1,14 +1,13 @@ import pytest -from jupyter_server.auth.security import passwd, passwd_check, salt_len +from jupyter_server.auth.security import passwd, passwd_check def test_passwd_structure(): p = passwd('passphrase') - algorithm, salt, hashed = p.split(':') - assert algorithm == 'sha1' - assert len(salt) == salt_len - assert len(hashed) == 40 + algorithm, hashed = p.split(':') + assert algorithm == 'argon2', algorithm + assert hashed.startswith('$argon2id$'), hashed def test_roundtrip(): @@ -26,4 +25,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 c65b72c8ab..8c12e08d53 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ 'jinja2', 'tornado>=6.1.0', 'pyzmq>=17', + 'argon2-cffi', 'ipython_genutils', 'traitlets>=4.2.1', 'jupyter_core>=4.4.0',