Skip to content

Commit

Permalink
Merge pull request #450 from afshin/argon2-cffi
Browse files Browse the repository at this point in the history
Implement password hashing with argon2-cffi
  • Loading branch information
blink1073 committed Mar 19, 2021
2 parents d24f9f6 + 054cc4a commit abaa8cd
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 12 deletions.
35 changes: 29 additions & 6 deletions jupyter_server/auth/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
-------
Expand All @@ -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'))
Expand All @@ -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):
Expand Down
14 changes: 8 additions & 6 deletions jupyter_server/tests/auth/test_security.py
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -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¶ŧ←↓→")
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 @@ -42,6 +42,7 @@
'jinja2',
'tornado>=6.1.0',
'pyzmq>=17',
'argon2-cffi',
'ipython_genutils',
'traitlets>=4.2.1',
'jupyter_core>=4.4.0',
Expand Down

0 comments on commit abaa8cd

Please sign in to comment.