Code

Ticket #1534: secure_login.diff

File secure_login.diff, 13.9 KB (added by SmileyChris, 8 years ago)
Line 
1Index: django/contrib/admin/media/js/login_encrypt.js
2===================================================================
3--- django/contrib/admin/media/js/login_encrypt.js      (revision 0)
4+++ django/contrib/admin/media/js/login_encrypt.js      (revision 0)
5@@ -0,0 +1,165 @@
6+var login_form = document.getElementById('id_login_form');
7+if (login_form) {
8+       var salt = document.getElementById('id_login_salt');
9+       if (salt && salt.value) {
10+               // hide "sending plain text" message
11+               var no_encryption = document.getElementById('id_no_encryption');
12+               if (no_encryption) {
13+                       no_encryption.style.display = 'none';
14+               }
15+               // add encryption method
16+               addEvent(login_form, 'submit', encrypt_password);
17+       }
18+}
19+
20+function encrypt_password() {
21+       // encrypt with login salt
22+       var login_salt = document.getElementById('id_login_salt');
23+       var username = document.getElementById('id_username');
24+       var password = document.getElementById('id_password');
25+       var salt = hex_sha1(username.value).substring(0,5);
26+       var hsh = hex_sha1(password.value+salt);
27+       // make random salt
28+       var salt = hex_sha1(Math.random().toString()).substring(0,5);
29+       // put the hashed hash in password & new salt value
30+       password.value = '';
31+       login_salt.value = salt+'$'+hex_sha1(hsh+login_salt.value+salt);
32+}
33+
34+
35+// cut down version of sha1.js from http://pajhome.org.uk/crypt/md5/sha1src.html
36+
37+/*
38+ * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
39+ * in FIPS PUB 180-1
40+ * Version 2.1a Copyright Paul Johnston 2000 - 2002.
41+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
42+ * Distributed under the BSD License
43+ * See http://pajhome.org.uk/crypt/md5 for details.
44+ */
45+
46+/*
47+ * Configurable variables. You may need to tweak these to be compatible with
48+ * the server-side, but the defaults work in most cases.
49+ */
50+var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */
51+
52+/*
53+ * These are the functions you'll usually want to call
54+ */
55+function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));}
56+
57+/*
58+ * Calculate the SHA-1 of an array of big-endian words, and a bit length
59+ */
60+function core_sha1(x, len)
61+{
62+  /* append padding */
63+  x[len >> 5] |= 0x80 << (24 - len % 32);
64+  x[((len + 64 >> 9) << 4) + 15] = len;
65+
66+  var w = Array(80);
67+  var a =  1732584193;
68+  var b = -271733879;
69+  var c = -1732584194;
70+  var d =  271733878;
71+  var e = -1009589776;
72+
73+  for(var i = 0; i < x.length; i += 16)
74+  {
75+    var olda = a;
76+    var oldb = b;
77+    var oldc = c;
78+    var oldd = d;
79+    var olde = e;
80+
81+    for(var j = 0; j < 80; j++)
82+    {
83+      if(j < 16) w[j] = x[i + j];
84+      else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
85+      var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
86+                       safe_add(safe_add(e, w[j]), sha1_kt(j)));
87+      e = d;
88+      d = c;
89+      c = rol(b, 30);
90+      b = a;
91+      a = t;
92+    }
93+
94+    a = safe_add(a, olda);
95+    b = safe_add(b, oldb);
96+    c = safe_add(c, oldc);
97+    d = safe_add(d, oldd);
98+    e = safe_add(e, olde);
99+  }
100+  return Array(a, b, c, d, e);
101+
102+}
103+
104+/*
105+ * Perform the appropriate triplet combination function for the current
106+ * iteration
107+ */
108+function sha1_ft(t, b, c, d)
109+{
110+  if(t < 20) return (b & c) | ((~b) & d);
111+  if(t < 40) return b ^ c ^ d;
112+  if(t < 60) return (b & c) | (b & d) | (c & d);
113+  return b ^ c ^ d;
114+}
115+
116+/*
117+ * Determine the appropriate additive constant for the current iteration
118+ */
119+function sha1_kt(t)
120+{
121+  return (t < 20) ?  1518500249 : (t < 40) ?  1859775393 :
122+         (t < 60) ? -1894007588 : -899497514;
123+}
124+
125+/*
126+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
127+ * to work around bugs in some JS interpreters.
128+ */
129+function safe_add(x, y)
130+{
131+  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
132+  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
133+  return (msw << 16) | (lsw & 0xFFFF);
134+}
135+
136+/*
137+ * Bitwise rotate a 32-bit number to the left.
138+ */
139+function rol(num, cnt)
140+{
141+  return (num << cnt) | (num >>> (32 - cnt));
142+}
143+
144+/*
145+ * Convert an 8-bit or 16-bit string to an array of big-endian words
146+ * In 8-bit function, characters >255 have their hi-byte silently ignored.
147+ */
148+function str2binb(str)
149+{
150+  var bin = Array();
151+  var mask = (1 << chrsz) - 1;
152+  for(var i = 0; i < str.length * chrsz; i += chrsz)
153+    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32);
154+  return bin;
155+}
156+
157+/*
158+ * Convert an array of big-endian words to a hex string.
159+ */
160+function binb2hex(binarray)
161+{
162+  var hex_tab = "0123456789abcdef";
163+  var str = "";
164+  for(var i = 0; i < binarray.length * 4; i++)
165+  {
166+    str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
167+           hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8  )) & 0xF);
168+  }
169+  return str;
170+}
171Index: django/contrib/admin/templates/admin/login.html
172===================================================================
173--- django/contrib/admin/templates/admin/login.html     (revision 2524)
174+++ django/contrib/admin/templates/admin/login.html     (working copy)
175@@ -9,7 +9,7 @@
176 <p class="errornote">{{ error_message }}</p>
177 {% endif %}
178 <div id="content-main">
179-<form action="{{ app_path }}" method="post">
180+<form action="{{ app_path }}" method="post" id="id_login_form">
181 
182 <p class="aligned">
183 <label for="id_username">{% trans 'Username:' %}</label> <input type="text" name="username" id="id_username" />
184@@ -17,8 +17,10 @@
185 <p class="aligned">
186 <label for="id_password">{% trans 'Password:' %}</label> <input type="password" name="password" id="id_password" />
187 <input type="hidden" name="this_is_the_login_form" value="1" />
188+{% if login_salt %}<input type="hidden" name="login_salt" value="{{ login_salt }}" id="id_login_salt" />{% endif %}
189 <input type="hidden" name="post_data" value="{{ post_data }}" />{% comment %} <span class="help">{% trans 'Have you <a href="/password_reset/">forgotten your password</a>?' %}</span>{% endcomment %}
190 </p>
191+<p class="help" id="id_no_encryption">{% trans 'Your password will be sent without encryption' %}<noscript> ({% trans 'Javascript is turned off' %})</noscript></p>
192 
193 <div class="aligned ">
194 <label>&nbsp;</label><input type="submit" value="{% trans 'Log in' %}" />
195@@ -28,5 +30,7 @@
196 <script type="text/javascript">
197 document.getElementById('id_username').focus()
198 </script>
199+<script type="text/javascript" src="{% load adminmedia %}{% admin_media_prefix %}js/core.js"></script>
200+<script type="text/javascript" src="{% admin_media_prefix %}js/login_encrypt.js"></script>
201 </div>
202 {% endblock %}
203Index: django/contrib/admin/views/decorators.py
204===================================================================
205--- django/contrib/admin/views/decorators.py    (revision 2524)
206+++ django/contrib/admin/views/decorators.py    (working copy)
207@@ -1,13 +1,15 @@
208 from django.core.extensions import DjangoContext, render_to_response
209 from django.conf.settings import SECRET_KEY
210-from django.models.auth import users
211+from django.models.auth import users, CURRENT_ALGORITHM
212 from django.utils import httpwrappers
213 from django.utils.translation import gettext_lazy
214 import base64, datetime, md5
215 import cPickle as pickle
216+import random
217 
218 ERROR_MESSAGE = gettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
219 LOGIN_FORM_KEY = 'this_is_the_login_form'
220+LOGIN_SALT_KEY = '_session_salt'
221 
222 def _display_login_form(request, error_message=''):
223     request.session.set_test_cookie()
224@@ -23,7 +25,8 @@
225         'title': _('Log in'),
226         'app_path': request.path,
227         'post_data': post_data,
228-        'error_message': error_message
229+        'error_message': error_message,
230+        'login_salt': request.session.get(LOGIN_SALT_KEY)
231     }, context_instance=DjangoContext(request))
232 
233 def _encode_post_data(post_data):
234@@ -68,6 +71,11 @@
235             message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
236             return _display_login_form(request, message)
237 
238+        # Get the previous login salt in case this is a login attempt
239+        # then generate a new login salt.
240+        last_login_salt = request.session.get(LOGIN_SALT_KEY, '')
241+        request.session[LOGIN_SALT_KEY] = users.do_hash(CURRENT_ALGORITHM, str(random.random()))[:5]
242+
243         # Check the password.
244         username = request.POST.get('username', '')
245         try:
246@@ -86,7 +94,24 @@
247 
248         # The user data is correct; log in the user in and continue.
249         else:
250-            if user.check_password(request.POST.get('password', '')):
251+            is_correct = False
252+           
253+            incoming_salt = request.POST.get('login_salt', '')
254+            if '$' in incoming_salt:
255+                # Javascript encrypted hash and changed salt.
256+                incoming_salt, incoming_hash = incoming_salt.split('$',1)
257+                is_correct = user.check_password_hash(incoming_hash, last_login_salt+incoming_salt)
258+                algo, nothing = user.password.split('$', 1)
259+                if algo != CURRENT_ALGORITHM:
260+                    message = _("Your password needs to be updated to a more secure format, please type your password again.")
261+                    # Set empty salt so next password won't be encrypted.
262+                    request.session[LOGIN_SALT_KEY] = ''
263+                    return _display_login_form(request, message)
264+
265+            if not is_correct:
266+                is_correct = user.check_password(request.POST.get('password', ''))
267+
268+            if is_correct:
269                 request.session[users.SESSION_KEY] = user.id
270                 user.last_login = datetime.datetime.now()
271                 user.save()
272Index: django/models/auth.py
273===================================================================
274--- django/models/auth.py       (revision 2524)
275+++ django/models/auth.py       (working copy)
276@@ -2,6 +2,8 @@
277 from django.models import core
278 from django.utils.translation import gettext_lazy as _
279 
280+CURRENT_ALGORITHM = 'sha1un'   #user name
281+
282 class Permission(meta.Model):
283     name = meta.CharField(_('name'), maxlength=50)
284     package = meta.ForeignKey(core.Package, db_column='package')
285@@ -78,10 +80,12 @@
286         return full_name.strip()
287 
288     def set_password(self, raw_password):
289-        import sha, random
290-        algo = 'sha1'
291-        salt = sha.new(str(random.random())).hexdigest()[:5]
292-        hsh = sha.new(salt+raw_password).hexdigest()
293+        import random
294+        from django.models.auth import users, CURRENT_ALGORITHM
295+        algo = CURRENT_ALGORITHM
296+        #salt = do_hash(algo, str(random.random()))[:5]
297+        salt = users.do_hash(algo, self.username)[:5]
298+        hsh = users.do_hash(algo, raw_password, salt)
299         self.password = '%s$%s$%s' % (algo, salt, hsh)
300 
301     def check_password(self, raw_password):
302@@ -89,24 +93,33 @@
303         Returns a boolean of whether the raw_password was correct. Handles
304         encryption formats behind the scenes.
305         """
306-        # Backwards-compatibility check. Older passwords won't include the
307+        from django.models.auth import users, CURRENT_ALGORITHM
308+        # Backwards-compatibility check. Original MD5 passwords won't include the
309         # algorithm or salt.
310-        if '$' not in self.password:
311-            import md5
312-            is_correct = (self.password == md5.new(raw_password).hexdigest())
313-            if is_correct:
314-                # Convert the password to the new, more secure format.
315-                self.set_password(raw_password)
316-                self.save()
317-            return is_correct
318-        algo, salt, hsh = self.password.split('$')
319-        if algo == 'md5':
320-            import md5
321-            return hsh == md5.new(salt+raw_password).hexdigest()
322-        elif algo == 'sha1':
323-            import sha
324-            return hsh == sha.new(salt+raw_password).hexdigest()
325-        raise ValueError, "Got unknown password algorithm type in password."
326+        algo = 'md5'
327+        salt = ''
328+        hsh = self.password
329+        if '$' in self.password:
330+            algo, salt, hsh = self.password.split('$', 2)
331+        is_correct = (hsh == users.do_hash(algo, raw_password, salt))
332+        # Make sure we are using the current algorithm. Otherwise upgrade.
333+        if algo != CURRENT_ALGORITHM and is_correct:
334+            # Convert the password to the new, more secure format.
335+            self.set_password(raw_password)
336+            self.save()
337+        return is_correct
338+   
339+    def check_password_hash(self, incoming_hsh, incoming_salt):
340+        """
341+        Returns a boolean of whether the hash (done by javascript in the login
342+        form) matches.
343+        The javascript uses the login_salt + password to get the database hash.
344+        Then it hashes this result with it's own random salt.
345+        """
346+        from django.models.auth.users import do_hash
347+        algo, salt, hsh = self.password.split('$', 2)
348+        hashed_hash = do_hash(algo, hsh, incoming_salt)
349+        return incoming_hsh == hashed_hash
350 
351     def get_group_permissions(self):
352         "Returns a list of permission strings that this user has through his/her groups."
353@@ -211,6 +224,21 @@
354         from random import choice
355         return ''.join([choice(allowed_chars) for i in range(length)])
356 
357+    def _module_do_hash(algo, value, salt=''):
358+        "Hash a value using the given algorithm."
359+        if algo == 'md5':
360+            import md5
361+            return md5.new(salt+value).hexdigest()
362+        elif algo == 'sha1':
363+            import sha
364+            return sha.new(salt+value).hexdigest()
365+        # Psuedorandom username salted hash (different algo so existing names
366+        # get updated).
367+        elif algo == 'sha1un':
368+            import sha
369+            return sha.new(value+salt).hexdigest()  # note: salt last
370+        raise ValueError, "Got unknown algorithm type."
371+
372 class Message(meta.Model):
373     user = meta.ForeignKey(User)
374     message = meta.TextField(_('Message'))