Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement password hashing with argon2-cffi #450

Merged
merged 1 commit into from
Mar 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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