Skip to content

Commit abaa8cd

Browse files
authored
Merge pull request #450 from afshin/argon2-cffi
Implement password hashing with argon2-cffi
2 parents d24f9f6 + 054cc4a commit abaa8cd

File tree

3 files changed

+38
-12
lines changed

3 files changed

+38
-12
lines changed

jupyter_server/auth/security.py

+29-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
import traceback
1313
import warnings
1414

15+
import argon2
16+
import argon2.exceptions
17+
from argon2 import PasswordHasher
1518
from ipython_genutils.py3compat import cast_bytes, str_to_bytes, cast_unicode
1619
from traitlets.config import Config, ConfigFileNotFound, JSONFileConfigLoader
1720
from jupyter_core.paths import jupyter_config_dir
@@ -21,7 +24,7 @@
2124
salt_len = 12
2225

2326

24-
def passwd(passphrase=None, algorithm='sha1'):
27+
def passwd(passphrase=None, algorithm='argon2'):
2528
"""Generate hashed password and salt for use in server configuration.
2629
2730
In the server configuration, set `c.ServerApp.password` to
@@ -34,7 +37,7 @@ def passwd(passphrase=None, algorithm='sha1'):
3437
and verify a password.
3538
algorithm : str
3639
Hashing algorithm to use (e.g, 'sha1' or any argument supported
37-
by :func:`hashlib.new`).
40+
by :func:`hashlib.new`, or 'argon2').
3841
3942
Returns
4043
-------
@@ -59,6 +62,16 @@ def passwd(passphrase=None, algorithm='sha1'):
5962
else:
6063
raise ValueError('No matching passwords found. Giving up.')
6164

65+
if algorithm == 'argon2':
66+
ph = PasswordHasher(
67+
memory_cost=10240,
68+
time_cost=10,
69+
parallelism=8,
70+
)
71+
h = ph.hash(passphrase)
72+
73+
return ':'.join((algorithm, cast_unicode(h, 'ascii')))
74+
6275
h = hashlib.new(algorithm)
6376
salt = ('%0' + str(salt_len) + 'x') % random.getrandbits(4 * salt_len)
6477
h.update(cast_bytes(passphrase, 'utf-8') + str_to_bytes(salt, 'ascii'))
@@ -84,14 +97,24 @@ def passwd_check(hashed_passphrase, passphrase):
8497
Examples
8598
--------
8699
>>> from jupyter_server.auth.security import passwd_check
87-
>>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a',
88-
... 'mypassword')
100+
>>> passwd_check('argon2:...', 'mypassword')
89101
True
90102
91-
>>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a',
92-
... 'anotherpassword')
103+
>>> passwd_check('argon2:...', 'otherpassword')
93104
False
105+
106+
>>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a',
107+
... 'mypassword')
108+
True
94109
"""
110+
if hashed_passphrase.startswith('argon2:'):
111+
ph = argon2.PasswordHasher()
112+
113+
try:
114+
return ph.verify(hashed_passphrase[7:], passphrase)
115+
except argon2.exceptions.VerificationError:
116+
return False
117+
95118
try:
96119
algorithm, salt, pw_digest = hashed_passphrase.split(':', 2)
97120
except (ValueError, TypeError):

jupyter_server/tests/auth/test_security.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import pytest
22

3-
from jupyter_server.auth.security import passwd, passwd_check, salt_len
3+
from jupyter_server.auth.security import passwd, passwd_check
44

55

66
def test_passwd_structure():
77
p = passwd('passphrase')
8-
algorithm, salt, hashed = p.split(':')
9-
assert algorithm == 'sha1'
10-
assert len(salt) == salt_len
11-
assert len(hashed) == 40
8+
algorithm, hashed = p.split(':')
9+
assert algorithm == 'argon2', algorithm
10+
assert hashed.startswith('$argon2id$'), hashed
1211

1312

1413
def test_roundtrip():
@@ -26,4 +25,7 @@ def test_bad():
2625
def test_passwd_check_unicode():
2726
# GH issue #4524
2827
phash = u'sha1:23862bc21dd3:7a415a95ae4580582e314072143d9c382c491e4f'
29-
assert passwd_check(phash, u"łe¶ŧ←↓→")
28+
assert passwd_check(phash, u"łe¶ŧ←↓→")
29+
phash = (u'argon2:$argon2id$v=19$m=10240,t=10,p=8$'
30+
u'qjjDiZUofUVVnrVYxacnbA$l5pQq1bJ8zglGT2uXP6iOg')
31+
assert passwd_check(phash, u"łe¶ŧ←↓→")

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
'jinja2',
4343
'tornado>=6.1.0',
4444
'pyzmq>=17',
45+
'argon2-cffi',
4546
'ipython_genutils',
4647
'traitlets>=4.2.1',
4748
'jupyter_core>=4.4.0',

0 commit comments

Comments
 (0)