Skip to content

Commit 617f746

Browse files
authored
Implement password hashing with bcrypt (#3793)
* Use bcrypt for notebook.auth.security * Add bcrypt to install_requires * Add test for bcrypt * Switch to argon2-cffi
1 parent 0df10de commit 617f746

File tree

3 files changed

+57
-32
lines changed

3 files changed

+57
-32
lines changed

notebook/auth/security.py

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
salt_len = 12
2222

2323

24-
def passwd(passphrase=None, algorithm='sha1'):
24+
def passwd(passphrase=None, algorithm='argon2'):
2525
"""Generate hashed password and salt for use in notebook configuration.
2626
2727
In the notebook configuration, set `c.NotebookApp.password` to
@@ -34,7 +34,7 @@ def passwd(passphrase=None, algorithm='sha1'):
3434
and verify a password.
3535
algorithm : str
3636
Hashing algorithm to use (e.g, 'sha1' or any argument supported
37-
by :func:`hashlib.new`).
37+
by :func:`hashlib.new`, or 'argon2').
3838
3939
Returns
4040
-------
@@ -59,11 +59,22 @@ def passwd(passphrase=None, algorithm='sha1'):
5959
else:
6060
raise ValueError('No matching passwords found. Giving up.')
6161

62-
h = hashlib.new(algorithm)
63-
salt = ('%0' + str(salt_len) + 'x') % random.getrandbits(4 * salt_len)
64-
h.update(cast_bytes(passphrase, 'utf-8') + str_to_bytes(salt, 'ascii'))
62+
if algorithm == 'argon2':
63+
from argon2 import PasswordHasher
64+
ph = PasswordHasher(
65+
memory_cost=10240,
66+
time_cost=10,
67+
parallelism=8,
68+
)
69+
h = ph.hash(passphrase)
70+
71+
return ':'.join((algorithm, cast_unicode(h, 'ascii')))
72+
else:
73+
h = hashlib.new(algorithm)
74+
salt = ('%0' + str(salt_len) + 'x') % random.getrandbits(4 * salt_len)
75+
h.update(cast_bytes(passphrase, 'utf-8') + str_to_bytes(salt, 'ascii'))
6576

66-
return ':'.join((algorithm, salt, h.hexdigest()))
77+
return ':'.join((algorithm, salt, h.hexdigest()))
6778

6879

6980
def passwd_check(hashed_passphrase, passphrase):
@@ -84,30 +95,41 @@ def passwd_check(hashed_passphrase, passphrase):
8495
Examples
8596
--------
8697
>>> from notebook.auth.security import passwd_check
87-
>>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a',
88-
... 'mypassword')
98+
>>> passwd_check('argon2:...', 'mypassword')
8999
True
90100
91-
>>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a',
92-
... 'anotherpassword')
101+
>>> passwd_check('argon2:...', 'otherpassword')
93102
False
94-
"""
95-
try:
96-
algorithm, salt, pw_digest = hashed_passphrase.split(':', 2)
97-
except (ValueError, TypeError):
98-
return False
99103
100-
try:
101-
h = hashlib.new(algorithm)
102-
except ValueError:
103-
return False
104-
105-
if len(pw_digest) == 0:
106-
return False
107-
108-
h.update(cast_bytes(passphrase, 'utf-8') + cast_bytes(salt, 'ascii'))
109-
110-
return h.hexdigest() == pw_digest
104+
>>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a',
105+
... 'mypassword')
106+
True
107+
"""
108+
if hashed_passphrase.startswith('argon2:'):
109+
import argon2
110+
import argon2.exceptions
111+
ph = argon2.PasswordHasher()
112+
try:
113+
return ph.verify(hashed_passphrase[7:], passphrase)
114+
except argon2.exceptions.VerificationError:
115+
return False
116+
else:
117+
try:
118+
algorithm, salt, pw_digest = hashed_passphrase.split(':', 2)
119+
except (ValueError, TypeError):
120+
return False
121+
122+
try:
123+
h = hashlib.new(algorithm)
124+
except ValueError:
125+
return False
126+
127+
if len(pw_digest) == 0:
128+
return False
129+
130+
h.update(cast_bytes(passphrase, 'utf-8') + cast_bytes(salt, 'ascii'))
131+
132+
return h.hexdigest() == pw_digest
111133

112134
@contextmanager
113135
def persist_config(config_file=None, mode=0o600):

notebook/auth/tests/test_security.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
# coding: utf-8
2-
from ..security import passwd, passwd_check, salt_len
2+
from ..security import passwd, passwd_check
33
import nose.tools as nt
44

55
def test_passwd_structure():
66
p = passwd('passphrase')
7-
algorithm, salt, hashed = p.split(':')
8-
nt.assert_equal(algorithm, 'sha1')
9-
nt.assert_equal(len(salt), salt_len)
10-
nt.assert_equal(len(hashed), 40)
7+
algorithm, hashed = p.split(':')
8+
nt.assert_equal(algorithm, 'argon2')
9+
nt.assert_true(hashed.startswith('$argon2id$'))
1110

1211
def test_roundtrip():
1312
p = passwd('passphrase')
@@ -22,4 +21,7 @@ def test_bad():
2221
def test_passwd_check_unicode():
2322
# GH issue #4524
2423
phash = u'sha1:23862bc21dd3:7a415a95ae4580582e314072143d9c382c491e4f'
25-
assert passwd_check(phash, u"łe¶ŧ←↓→")
24+
assert passwd_check(phash, u"łe¶ŧ←↓→")
25+
phash = (u'argon2:$argon2id$v=19$m=10240,t=10,p=8$'
26+
u'qjjDiZUofUVVnrVYxacnbA$l5pQq1bJ8zglGT2uXP6iOg')
27+
assert passwd_check(phash, u"łe¶ŧ←↓→")

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
# pyzmq>=17 is not technically necessary,
104104
# but hopefully avoids incompatibilities with Tornado 5. April 2018
105105
'pyzmq>=17',
106+
'argon2-cffi',
106107
'ipython_genutils',
107108
'traitlets>=4.2.1',
108109
'jupyter_core>=4.6.1',

0 commit comments

Comments
 (0)