Skip to content

Commit

Permalink
Implement password hashing with bcrypt (#3793)
Browse files Browse the repository at this point in the history
* Use bcrypt for notebook.auth.security

* Add bcrypt to install_requires

* Add test for bcrypt

* Switch to argon2-cffi
  • Loading branch information
remram44 authored Jul 10, 2020
1 parent 0df10de commit 617f746
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 32 deletions.
74 changes: 48 additions & 26 deletions notebook/auth/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
-------
Expand All @@ -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):
Expand All @@ -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):
Expand Down
14 changes: 8 additions & 6 deletions notebook/auth/tests/test_security.py
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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¶ŧ←↓→")
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¶ŧ←↓→")
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 617f746

Please sign in to comment.