diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index 15d3561ae10..7822118c5d8 100644 --- a/IPython/frontend/html/notebook/handlers.py +++ b/IPython/frontend/html/notebook/handlers.py @@ -28,6 +28,7 @@ from IPython.external.decorator import decorator from IPython.zmq.session import Session +from IPython.lib.security import passwd_check try: from docutils.core import publish_string @@ -166,16 +167,25 @@ def get(self): class LoginHandler(AuthenticatedHandler): - def get(self): + def _render(self, message=''): self.render('login.html', next=self.get_argument('next', default='/'), read_only=self.read_only, + message=message ) + def get(self): + self._render() + def post(self): pwd = self.get_argument('password', default=u'') - if self.application.password and pwd == self.application.password: - self.set_secure_cookie('username', str(uuid.uuid4())) + if self.application.password: + 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='/')) diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py index 6c4926952d0..157e856b5b1 100644 --- a/IPython/frontend/html/notebook/notebookapp.py +++ b/IPython/frontend/html/notebook/notebookapp.py @@ -208,7 +208,16 @@ def _ip_changed(self, name, old, new): ) 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, diff --git a/IPython/frontend/html/notebook/static/css/layout.css b/IPython/frontend/html/notebook/static/css/layout.css index e80cca12e41..b9e0b086c3e 100644 --- a/IPython/frontend/html/notebook/static/css/layout.css +++ b/IPython/frontend/html/notebook/static/css/layout.css @@ -101,3 +101,15 @@ -moz-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; +} \ No newline at end of file diff --git a/IPython/frontend/html/notebook/static/css/projectdashboard.css b/IPython/frontend/html/notebook/static/css/projectdashboard.css index 9d730969deb..0ca74842f0b 100644 --- a/IPython/frontend/html/notebook/static/css/projectdashboard.css +++ b/IPython/frontend/html/notebook/static/css/projectdashboard.css @@ -31,7 +31,7 @@ body { } #content_toolbar { - padding: 10px 5px 5px 5px; + padding: 5px; height: 25px; line-height: 25px; } diff --git a/IPython/frontend/html/notebook/templates/login.html b/IPython/frontend/html/notebook/templates/login.html index 03b7b1c2490..b719c5b8816 100644 --- a/IPython/frontend/html/notebook/templates/login.html +++ b/IPython/frontend/html/notebook/templates/login.html @@ -31,6 +31,12 @@
+ {% if message %} +
+ {{message}} +
+ {% end %} +
Password: diff --git a/IPython/lib/__init__.py b/IPython/lib/__init__.py index 65b0ae3dbf4..dc07221dcf6 100644 --- a/IPython/lib/__init__.py +++ b/IPython/lib/__init__.py @@ -25,6 +25,8 @@ current_gui ) +from IPython.lib.security import passwd + #----------------------------------------------------------------------------- # Code #----------------------------------------------------------------------------- diff --git a/IPython/lib/security.py b/IPython/lib/security.py new file mode 100644 index 00000000000..4f15e564721 --- /dev/null +++ b/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) + + return ':'.join((algorithm, salt, h.hexdigest())) + +def passwd_check(hashed_passphrase, passphrase): + """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: + return False + + h.update(passphrase + salt) + + return h.hexdigest() == pw_digest diff --git a/IPython/lib/tests/test_security.py b/IPython/lib/tests/test_security.py new file mode 100644 index 00000000000..1b58a5fc5c2 --- /dev/null +++ b/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) +