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
Add hashed password support. #1011
Changes from all commits
551f71e
3fe8501
8e03291
7ab92dd
c764ef0
fcb12a8
8aa26a2
8457e37
2e8339f
2ac6267
daaf07e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,102 @@ | |||
""" | |||
Password generation for the IPython notebook. | |||
""" | |||
|
|||
import hashlib | |||
import random | |||
import getpass | |||
|
|||
from IPython.core.error import UsageError | |||
|
|||
# Length of the salt in nr of hex chars, which implies salt_len * 4 | |||
# bits of randomness. | |||
salt_len = 12 | |||
|
|||
def passwd(passphrase=None, algorithm='sha1'): | |||
"""Generate hashed password and salt for use in notebook configuration. | |||
|
|||
In the notebook configuration, set `c.NotebookApp.password` to | |||
the generated string. | |||
|
|||
Parameters | |||
---------- | |||
passphrase : str | |||
Password to hash. If unspecified, the user is asked to input | |||
and verify a password. | |||
algorithm : str | |||
Hashing algorithm to use (e.g, 'sha1' or any argument supported | |||
by :func:`hashlib.new`). | |||
|
|||
Returns | |||
------- | |||
hashed_passphrase : str | |||
Hashed password, in the format 'hash_algorithm:salt:passphrase_hash'. | |||
|
|||
Examples | |||
-------- | |||
In [1]: passwd('mypassword') | |||
Out[1]: 'sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12' | |||
|
|||
""" | |||
if passphrase is None: | |||
for i in range(3): | |||
p0 = getpass.getpass('Enter password: ') | |||
p1 = getpass.getpass('Verify password: ') | |||
if p0 == p1: | |||
passphrase = p0 | |||
break | |||
else: | |||
print('Passwords do not match.') | |||
else: | |||
raise UsageError('No matching passwords found. Giving up.') | |||
|
|||
h = hashlib.new(algorithm) | |||
salt = ('%0' + str(salt_len) + 'x') % random.getrandbits(4 * salt_len) | |||
h.update(passphrase + salt) | |||
|
|||
return ':'.join((algorithm, salt, h.hexdigest())) | |||
|
|||
def passwd_check(hashed_passphrase, passphrase): | |||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW, it just occurred to me that we should probably add to this function a delay option, defaulting to 0.25 seconds at least... That will prevent DOS attacks and make brute-force rapid-fire guessing impossible. What do you guys think? |
|||
"""Verify that a given passphrase matches its hashed version. | |||
|
|||
Parameters | |||
---------- | |||
hashed_passphrase : str | |||
Hashed password, in the format returned by `passwd`. | |||
passphrase : str | |||
Passphrase to validate. | |||
|
|||
Returns | |||
------- | |||
valid : bool | |||
True if the passphrase matches the hash. | |||
|
|||
Examples | |||
-------- | |||
In [1]: from IPython.lib.security import passwd_check | |||
|
|||
In [2]: passwd_check('sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12', | |||
...: 'mypassword') | |||
Out[2]: True | |||
|
|||
In [3]: passwd_check('sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12', | |||
...: 'anotherpassword') | |||
Out[3]: 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 or len(salt) != salt_len: | |||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checking the salt length seems entirely unnecessary here. It has no valuable effect, because shorter or longer salts would still be perfectly valid. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. I won't revert it quite yet, let's see if @stefanv had a specific reason to check for that. But I can't think of one and unless Stefan has a good reason for wanting it there, we can remove the length check. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can take this out. We put in a number of tests to make sure the hash is
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 4cd1067 |
|||
return False | |||
|
|||
h.update(passphrase + salt) | |||
|
|||
return h.hexdigest() == pw_digest |
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,21 @@ | |||
from IPython.lib import passwd | |||
from IPython.lib.security import passwd_check, salt_len | |||
import nose.tools as nt | |||
|
|||
def test_passwd_structure(): | |||
p = passwd('passphrase') | |||
algorithm, salt, hashed = p.split(':') | |||
nt.assert_equals(algorithm, 'sha1') | |||
nt.assert_equals(len(salt), salt_len) | |||
nt.assert_equals(len(hashed), 40) | |||
|
|||
def test_roundtrip(): | |||
p = passwd('passphrase') | |||
nt.assert_equals(passwd_check(p, 'passphrase'), True) | |||
|
|||
def test_bad(): | |||
p = passwd('passphrase') | |||
nt.assert_equals(passwd_check(p, p), False) | |||
nt.assert_equals(passwd_check(p, 'a:b:c:d'), False) | |||
nt.assert_equals(passwd_check(p, 'a:b'), False) | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just occurred to me that this is not going to be portable to Python 3, because you can't directly hash unicode. For that matter, it will fail on non-ascii unicode strings in Python 2 as well.
I think the best thing is to do is:
py3compat.cast_bytes((passphrase + salt), 'utf-8')
.I realise this has been merged - I'll try to get round to doing another PR, unless someone else beats me to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry about that! We haven't gotten into a good swing of keeping py3 in mind, my fault...
BTW, if you see anything similarly problematic on #1012, let me know. It doesn't have similar low-level pieces so it shouldn't be much of a problem, but still, pitch in if you see anything amiss regarding py3.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have a check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mateusz just reminded me that allowing unicode for a password is probably a bad idea, so I think we'll try to detect that situation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My feeling is that if people want to use non-ascii characters in their password, that's up to them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So how about just doing .encode() on the string? That should work in both 2.7 and 3.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've done essentially that in PR #1016.