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

Add hashed password support. #1011

Merged
merged 11 commits into from Nov 19, 2011
16 changes: 13 additions & 3 deletions IPython/frontend/html/notebook/handlers.py
Expand Up @@ -28,6 +28,7 @@


from IPython.external.decorator import decorator from IPython.external.decorator import decorator
from IPython.zmq.session import Session from IPython.zmq.session import Session
from IPython.lib.security import passwd_check


try: try:
from docutils.core import publish_string from docutils.core import publish_string
Expand Down Expand Up @@ -166,16 +167,25 @@ def get(self):


class LoginHandler(AuthenticatedHandler): class LoginHandler(AuthenticatedHandler):


def get(self): def _render(self, message=''):
self.render('login.html', self.render('login.html',
next=self.get_argument('next', default='/'), next=self.get_argument('next', default='/'),
read_only=self.read_only, read_only=self.read_only,
message=message
) )


def get(self):
self._render()

def post(self): def post(self):
pwd = self.get_argument('password', default=u'') pwd = self.get_argument('password', default=u'')
if self.application.password and pwd == self.application.password: if self.application.password:
self.set_secure_cookie('username', str(uuid.uuid4())) if passwd_check(self.application.password, pwd):
self.set_secure_cookie('username', str(uuid.uuid4()))
else:
self._render(message='Invalid password')
return

self.redirect(self.get_argument('next', default='/')) self.redirect(self.get_argument('next', default='/'))




Expand Down
11 changes: 10 additions & 1 deletion IPython/frontend/html/notebook/notebookapp.py
Expand Up @@ -208,7 +208,16 @@ def _ip_changed(self, name, old, new):
) )


password = Unicode(u'', config=True, password = Unicode(u'', config=True,
help="""Password to use for web authentication""" help="""Hashed password to use for web authentication.

To generate, do:

from IPython.lib import passwd

passwd('mypassphrase')

The string should be of the form type:salt:hashed-password.
"""
) )


open_browser = Bool(True, config=True, open_browser = Bool(True, config=True,
Expand Down
12 changes: 12 additions & 0 deletions IPython/frontend/html/notebook/static/css/layout.css
Expand Up @@ -101,3 +101,15 @@
-moz-box-pack: center; -moz-box-pack: center;
box-pack: center; box-pack: center;
} }

#message {
border: 1px solid red;
background-color: #FFD3D1;
text-align: center;
padding: 0.5em;
margin: 0.5em;
}

#content_panel {
margin: 0.5em;
}
Expand Up @@ -31,7 +31,7 @@ body {
} }


#content_toolbar { #content_toolbar {
padding: 10px 5px 5px 5px; padding: 5px;
height: 25px; height: 25px;
line-height: 25px; line-height: 25px;
} }
Expand Down
6 changes: 6 additions & 0 deletions IPython/frontend/html/notebook/templates/login.html
Expand Up @@ -31,6 +31,12 @@
</div> </div>


<div id="content_panel"> <div id="content_panel">
{% if message %}
<div id="message">
{{message}}
</div>
{% end %}

<form action="/login?next={{url_escape(next)}}" method="post"> <form action="/login?next={{url_escape(next)}}" method="post">
Password: <input type="password" name="password"> Password: <input type="password" name="password">
<input type="submit" value="Sign in" id="signin"> <input type="submit" value="Sign in" id="signin">
Expand Down
2 changes: 2 additions & 0 deletions IPython/lib/__init__.py
Expand Up @@ -25,6 +25,8 @@
current_gui current_gui
) )


from IPython.lib.security import passwd

#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Code # Code
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
102 changes: 102 additions & 0 deletions IPython/lib/security.py
@@ -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)
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.


return ':'.join((algorithm, salt, h.hexdigest()))

def passwd_check(hashed_passphrase, passphrase):
Copy link
Member

Choose a reason for hiding this comment

The 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:
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
in the right format, but as all negative results return False it doesn't
matter.
On Nov 18, 2011 9:02 PM, "Fernando Perez" <
reply@reply.github.com>
wrote:

  • 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:

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.


Reply to this email directly or view it on GitHub:
https://github.com/ipython/ipython/pull/1011/files#r240467

Copy link
Member

Choose a reason for hiding this comment

The 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
21 changes: 21 additions & 0 deletions IPython/lib/tests/test_security.py
@@ -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)