From 551f71ec6c94325619225b6a08753f9ad120e913 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 18 Nov 2011 10:50:03 -0800 Subject: [PATCH 01/11] Add hashed passphrase generation and verification. --- IPython/lib/__init__.py | 2 + IPython/lib/security.py | 79 ++++++++++++++++++++++++++++++ IPython/lib/tests/test_security.py | 21 ++++++++ 3 files changed, 102 insertions(+) create mode 100644 IPython/lib/security.py create mode 100644 IPython/lib/tests/test_security.py 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..2c283a8b0a3 --- /dev/null +++ b/IPython/lib/security.py @@ -0,0 +1,79 @@ +""" +Password generation for the IPython notebook. +""" + +import hashlib +import random + +def passwd(passphrase): + """Generate hashed password and salt for use in notebook configuration. + + Parameters + ---------- + passphrase : str + Password to hash. + + Returns + ------- + hashed_passphrase : str + Hashed password, in the format 'hash_algorithm:salt:passphrase_hash'. + + Examples + -------- + In [1]: passwd('mypassword') + Out[1]: 'sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12' + + """ + algorithm = 'sha1' + + h = hashlib.new(algorithm) + salt = hex(int(random.getrandbits(16)))[2:] + 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 + + """ + # Algorithm and hash length + supported_algorithms = {'sha1': 40} + + try: + algorithm, salt, pw_digest = hashed_passphrase.split(':', 2) + except (ValueError, TypeError): + return False + + if not (algorithm in supported_algorithms and \ + len(pw_digest) == supported_algorithms[algorithm] and \ + len(salt) == 4): + return False + + h = hashlib.new(algorithm) + 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..38bad4b5a67 --- /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 +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), 4) + 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) + From 3fe8501ccf31ee224ad6c218538e7de76f2d5459 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 18 Nov 2011 10:53:09 -0800 Subject: [PATCH 02/11] Integrate hashed passwords into the notebook. --- IPython/frontend/html/notebook/handlers.py | 4 +++- IPython/frontend/html/notebook/notebookapp.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index 15d3561ae10..d6c5b19124c 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 @@ -174,7 +175,8 @@ def get(self): def post(self): pwd = self.get_argument('password', default=u'') - if self.application.password and pwd == self.application.password: + if self.application.password and \ + passwd_check(self.application.password, pwd): self.set_secure_cookie('username', str(uuid.uuid4())) 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, From 8e032917b7fae9f3b97468a5907b99b7d17a4e23 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 18 Nov 2011 11:34:47 -0800 Subject: [PATCH 03/11] In passwd, mention which variable in the notebook config to update. --- IPython/lib/security.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IPython/lib/security.py b/IPython/lib/security.py index 2c283a8b0a3..f84c7e0cc83 100644 --- a/IPython/lib/security.py +++ b/IPython/lib/security.py @@ -8,6 +8,9 @@ def passwd(passphrase): """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 From 7ab92dd62acf90a644fec3c620146ad6ae5622f7 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 18 Nov 2011 12:04:17 -0800 Subject: [PATCH 04/11] Notify user about invalid password. --- IPython/frontend/html/notebook/handlers.py | 16 ++++++++++++---- .../frontend/html/notebook/static/css/layout.css | 12 ++++++++++++ .../notebook/static/css/projectdashboard.css | 2 +- .../frontend/html/notebook/templates/login.html | 6 ++++++ 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index d6c5b19124c..7822118c5d8 100644 --- a/IPython/frontend/html/notebook/handlers.py +++ b/IPython/frontend/html/notebook/handlers.py @@ -167,17 +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 \ - passwd_check(self.application.password, pwd): - 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/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: From c764ef0df5680dde898b0713d227b8e0325b2009 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 18 Nov 2011 17:25:28 -0800 Subject: [PATCH 05/11] Allow any hashing algorithm. --- IPython/lib/security.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/IPython/lib/security.py b/IPython/lib/security.py index f84c7e0cc83..2730b143430 100644 --- a/IPython/lib/security.py +++ b/IPython/lib/security.py @@ -5,7 +5,7 @@ import hashlib import random -def passwd(passphrase): +def passwd(passphrase, algorithm='sha1'): """Generate hashed password and salt for use in notebook configuration. In the notebook configuration, set `c.NotebookApp.password` to @@ -15,6 +15,8 @@ def passwd(passphrase): ---------- passphrase : str Password to hash. + algorithm : str + Hashing algorithm to use. Returns ------- @@ -27,8 +29,6 @@ def passwd(passphrase): Out[1]: 'sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12' """ - algorithm = 'sha1' - h = hashlib.new(algorithm) salt = hex(int(random.getrandbits(16)))[2:] h.update(passphrase + salt) @@ -63,20 +63,19 @@ def passwd_check(hashed_passphrase, passphrase): Out[3]: False """ - # Algorithm and hash length - supported_algorithms = {'sha1': 40} - try: algorithm, salt, pw_digest = hashed_passphrase.split(':', 2) except (ValueError, TypeError): return False - if not (algorithm in supported_algorithms and \ - len(pw_digest) == supported_algorithms[algorithm] and \ - len(salt) == 4): + try: + h = hashlib.new(algorithm) + except ValueError: + return False + + if len(pw_digest) == 0 or len(salt) != 4: return False - h = hashlib.new(algorithm) h.update(passphrase + salt) return h.hexdigest() == pw_digest From fcb12a88e399a0294f6a045b61d7f0b9086db07b Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 18 Nov 2011 17:28:26 -0800 Subject: [PATCH 06/11] Always produce a salt of length 4. --- IPython/lib/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/lib/security.py b/IPython/lib/security.py index 2730b143430..fb50f116359 100644 --- a/IPython/lib/security.py +++ b/IPython/lib/security.py @@ -30,7 +30,7 @@ def passwd(passphrase, algorithm='sha1'): """ h = hashlib.new(algorithm) - salt = hex(int(random.getrandbits(16)))[2:] + salt = '%04x' % random.getrandbits(16) h.update(passphrase + salt) return ':'.join((algorithm, salt, h.hexdigest())) From 8aa26a2171db550e3ce8e75298c08e4e83ade5d8 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 18 Nov 2011 17:54:20 -0800 Subject: [PATCH 07/11] Update docstring to refer to hashlib. --- IPython/lib/security.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IPython/lib/security.py b/IPython/lib/security.py index fb50f116359..880bee344f4 100644 --- a/IPython/lib/security.py +++ b/IPython/lib/security.py @@ -16,7 +16,8 @@ def passwd(passphrase, algorithm='sha1'): passphrase : str Password to hash. algorithm : str - Hashing algorithm to use. + Hashing algorithm to use (e.g, 'sha1' or any argument supported + by :func:`hashlib.new`). Returns ------- From 8457e379ecad4645461c424c0f3f92ab80345671 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 18 Nov 2011 18:02:29 -0800 Subject: [PATCH 08/11] Use a global variable to adjust the number of bits used to generate the salt. --- IPython/lib/security.py | 8 ++++++-- IPython/lib/tests/test_security.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/IPython/lib/security.py b/IPython/lib/security.py index 880bee344f4..40e1b3d3e12 100644 --- a/IPython/lib/security.py +++ b/IPython/lib/security.py @@ -5,6 +5,10 @@ import hashlib import random +# Length of the salt in nr of hex chars, which implies salt_len * 4 +# bits of randomness. +salt_len = 12 + def passwd(passphrase, algorithm='sha1'): """Generate hashed password and salt for use in notebook configuration. @@ -31,7 +35,7 @@ def passwd(passphrase, algorithm='sha1'): """ h = hashlib.new(algorithm) - salt = '%04x' % random.getrandbits(16) + salt = ('%0' + str(salt_len) + 'x') % random.getrandbits(4 * salt_len) h.update(passphrase + salt) return ':'.join((algorithm, salt, h.hexdigest())) @@ -74,7 +78,7 @@ def passwd_check(hashed_passphrase, passphrase): except ValueError: return False - if len(pw_digest) == 0 or len(salt) != 4: + if len(pw_digest) == 0 or len(salt) != salt_len: return False h.update(passphrase + salt) diff --git a/IPython/lib/tests/test_security.py b/IPython/lib/tests/test_security.py index 38bad4b5a67..1b58a5fc5c2 100644 --- a/IPython/lib/tests/test_security.py +++ b/IPython/lib/tests/test_security.py @@ -1,12 +1,12 @@ from IPython.lib import passwd -from IPython.lib.security import passwd_check +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), 4) + nt.assert_equals(len(salt), salt_len) nt.assert_equals(len(hashed), 40) def test_roundtrip(): From 2e8339fd5d4f094919ec65d1489d7975e25c0fd0 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 18 Nov 2011 18:10:11 -0800 Subject: [PATCH 09/11] If no password is given, ask for one on the prompt. --- IPython/lib/security.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/IPython/lib/security.py b/IPython/lib/security.py index 40e1b3d3e12..059cd1c52a6 100644 --- a/IPython/lib/security.py +++ b/IPython/lib/security.py @@ -4,12 +4,13 @@ import hashlib import random +import getpass # Length of the salt in nr of hex chars, which implies salt_len * 4 # bits of randomness. salt_len = 12 -def passwd(passphrase, algorithm='sha1'): +def passwd(passphrase='', algorithm='sha1'): """Generate hashed password and salt for use in notebook configuration. In the notebook configuration, set `c.NotebookApp.password` to @@ -18,7 +19,8 @@ def passwd(passphrase, algorithm='sha1'): Parameters ---------- passphrase : str - Password to hash. + 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`). @@ -34,6 +36,14 @@ def passwd(passphrase, algorithm='sha1'): Out[1]: 'sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12' """ + if not passphrase: + p0 = getpass.getpass('Enter password: ') + p1 = getpass.getpass('Verify password: ') + if (p0 == p1): + passphrase = p0 + else: + raise ValueError('Passwords did not match.') + h = hashlib.new(algorithm) salt = ('%0' + str(salt_len) + 'x') % random.getrandbits(4 * salt_len) h.update(passphrase + salt) From 2ac6267c3ace0f0f64aa3e10379028bda24cdd52 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 18 Nov 2011 18:23:26 -0800 Subject: [PATCH 10/11] Try to get password from user three times. --- IPython/lib/security.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/IPython/lib/security.py b/IPython/lib/security.py index 059cd1c52a6..e7c444b4c52 100644 --- a/IPython/lib/security.py +++ b/IPython/lib/security.py @@ -10,7 +10,7 @@ # bits of randomness. salt_len = 12 -def passwd(passphrase='', algorithm='sha1'): +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 @@ -36,13 +36,17 @@ def passwd(passphrase='', algorithm='sha1'): Out[1]: 'sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12' """ - if not passphrase: - p0 = getpass.getpass('Enter password: ') - p1 = getpass.getpass('Verify password: ') - if (p0 == p1): - passphrase = p0 + 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 ValueError('Passwords did not match.') + raise ValueError('No matching passwords found. Giving up.') h = hashlib.new(algorithm) salt = ('%0' + str(salt_len) + 'x') % random.getrandbits(4 * salt_len) From daaf07ea9cec5657e64aadcbddecb0be65cf4c25 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 18 Nov 2011 18:31:29 -0800 Subject: [PATCH 11/11] Throw UsageError instead of ValueError. --- IPython/lib/security.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IPython/lib/security.py b/IPython/lib/security.py index e7c444b4c52..4f15e564721 100644 --- a/IPython/lib/security.py +++ b/IPython/lib/security.py @@ -6,6 +6,8 @@ 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 @@ -46,7 +48,7 @@ def passwd(passphrase=None, algorithm='sha1'): else: print('Passwords do not match.') else: - raise ValueError('No matching passwords found. Giving up.') + raise UsageError('No matching passwords found. Giving up.') h = hashlib.new(algorithm) salt = ('%0' + str(salt_len) + 'x') % random.getrandbits(4 * salt_len)