diff --git a/VERSION b/VERSION index 9cbdcbf7..d1814950 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-06-10 00:32:10) dev +Version 2.00.0 (2012-06-10 01:18:28) dev diff --git a/gluon/contrib/aes.py b/gluon/contrib/aes.py new file mode 100644 index 00000000..24e40878 --- /dev/null +++ b/gluon/contrib/aes.py @@ -0,0 +1,502 @@ +"""Simple AES cipher implementation in pure Python following PEP-272 API + +Homepage: https://bitbucket.org/intgr/pyaes/ + +The goal of this module is to be as fast as reasonable in Python while still +being Pythonic and readable/understandable. It is licensed under the permissive +MIT license. + +Hopefully the code is readable and commented enough that it can serve as an +introduction to the AES cipher for Python coders. In fact, it should go along +well with the Stick Figure Guide to AES: +http://www.moserware.com/2009/09/stick-figure-guide-to-advanced.html + +Contrary to intuition, this implementation numbers the 4x4 matrices from top to +bottom for efficiency reasons:: + + 0 4 8 12 + 1 5 9 13 + 2 6 10 14 + 3 7 11 15 + +Effectively it's the transposition of what you'd expect. This actually makes +the code simpler -- except the ShiftRows step, but hopefully the explanation +there clears it up. + +""" + +#### +# Copyright (c) 2010 Marti Raudsepp +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +#### + + +from array import array + +# Globals mandated by PEP 272: +# http://www.python.org/dev/peps/pep-0272/ +MODE_ECB = 1 +MODE_CBC = 2 +#MODE_CTR = 6 + +block_size = 16 +key_size = None + +def new(key, mode=MODE_CBC, IV=None): + if mode == MODE_ECB: + return ECBMode(AES(key)) + elif mode == MODE_CBC: + if IV is None: + raise ValueError, "CBC mode needs an IV value!" + + return CBCMode(AES(key), IV) + else: + raise NotImplementedError + +#### AES cipher implementation + +class AES(object): + block_size = 16 + + def __init__(self, key): + self.setkey(key) + + def setkey(self, key): + """Sets the key and performs key expansion.""" + + self.key = key + self.key_size = len(key) + + if self.key_size == 16: + self.rounds = 10 + elif self.key_size == 24: + self.rounds = 12 + elif self.key_size == 32: + self.rounds = 14 + else: + raise ValueError, "Key length must be 16, 24 or 32 bytes" + + self.expand_key() + + def expand_key(self): + """Performs AES key expansion on self.key and stores in self.exkey""" + + # The key schedule specifies how parts of the key are fed into the + # cipher's round functions. "Key expansion" means performing this + # schedule in advance. Almost all implementations do this. + # + # Here's a description of AES key schedule: + # http://en.wikipedia.org/wiki/Rijndael_key_schedule + + # The expanded key starts with the actual key itself + exkey = array('B', self.key) + + # extra key expansion steps + if self.key_size == 16: + extra_cnt = 0 + elif self.key_size == 24: + extra_cnt = 2 + else: + extra_cnt = 3 + + # 4-byte temporary variable for key expansion + word = exkey[-4:] + # Each expansion cycle uses 'i' once for Rcon table lookup + for i in xrange(1, 11): + + #### key schedule core: + # left-rotate by 1 byte + word = word[1:4] + word[0:1] + + # apply S-box to all bytes + for j in xrange(4): + word[j] = aes_sbox[word[j]] + + # apply the Rcon table to the leftmost byte + word[0] = word[0] ^ aes_Rcon[i] + #### end key schedule core + + for z in xrange(4): + for j in xrange(4): + # mix in bytes from the last subkey + word[j] ^= exkey[-self.key_size + j] + exkey.extend(word) + + # Last key expansion cycle always finishes here + if len(exkey) >= (self.rounds+1) * self.block_size: + break + + # Special substitution step for 256-bit key + if self.key_size == 32: + for j in xrange(4): + # mix in bytes from the last subkey XORed with S-box of + # current word bytes + word[j] = aes_sbox[word[j]] ^ exkey[-self.key_size + j] + exkey.extend(word) + + # Twice for 192-bit key, thrice for 256-bit key + for z in xrange(extra_cnt): + for j in xrange(4): + # mix in bytes from the last subkey + word[j] ^= exkey[-self.key_size + j] + exkey.extend(word) + + self.exkey = exkey + + def add_round_key(self, block, round): + """AddRoundKey step in AES. This is where the key is mixed into plaintext""" + + offset = round * 16 + exkey = self.exkey + + for i in xrange(16): + block[i] ^= exkey[offset + i] + + #print 'AddRoundKey:', block + + def sub_bytes(self, block, sbox): + """SubBytes step, apply S-box to all bytes + + Depending on whether encrypting or decrypting, a different sbox array + is passed in. + """ + + for i in xrange(16): + block[i] = sbox[block[i]] + + #print 'SubBytes :', block + + def shift_rows(self, b): + """ShiftRows step. Shifts 2nd row to left by 1, 3rd row by 2, 4th row by 3 + + Since we're performing this on a transposed matrix, cells are numbered + from top to bottom:: + + 0 4 8 12 -> 0 4 8 12 -- 1st row doesn't change + 1 5 9 13 -> 5 9 13 1 -- row shifted to left by 1 (wraps around) + 2 6 10 14 -> 10 14 2 6 -- shifted by 2 + 3 7 11 15 -> 15 3 7 11 -- shifted by 3 + """ + + b[1], b[5], b[ 9], b[13] = b[ 5], b[ 9], b[13], b[ 1] + b[2], b[6], b[10], b[14] = b[10], b[14], b[ 2], b[ 6] + b[3], b[7], b[11], b[15] = b[15], b[ 3], b[ 7], b[11] + + #print 'ShiftRows :', b + + def shift_rows_inv(self, b): + """Similar to shift_rows above, but performed in inverse for decryption.""" + + b[ 5], b[ 9], b[13], b[ 1] = b[1], b[5], b[ 9], b[13] + b[10], b[14], b[ 2], b[ 6] = b[2], b[6], b[10], b[14] + b[15], b[ 3], b[ 7], b[11] = b[3], b[7], b[11], b[15] + + #print 'ShiftRows :', b + + def mix_columns(self, block): + """MixColumns step. Mixes the values in each column""" + + # Cache global multiplication tables (see below) + mul_by_2 = gf_mul_by_2 + mul_by_3 = gf_mul_by_3 + + # Since we're dealing with a transposed matrix, columns are already + # sequential + for i in xrange(4): + col = i * 4 + + #v0, v1, v2, v3 = block[col : col+4] + v0, v1, v2, v3 = (block[col], block[col + 1], block[col + 2], + block[col + 3]) + + block[col ] = mul_by_2[v0] ^ v3 ^ v2 ^ mul_by_3[v1] + block[col+1] = mul_by_2[v1] ^ v0 ^ v3 ^ mul_by_3[v2] + block[col+2] = mul_by_2[v2] ^ v1 ^ v0 ^ mul_by_3[v3] + block[col+3] = mul_by_2[v3] ^ v2 ^ v1 ^ mul_by_3[v0] + + #print 'MixColumns :', block + + def mix_columns_inv(self, block): + """Similar to mix_columns above, but performed in inverse for decryption.""" + + # Cache global multiplication tables (see below) + mul_9 = gf_mul_by_9 + mul_11 = gf_mul_by_11 + mul_13 = gf_mul_by_13 + mul_14 = gf_mul_by_14 + + # Since we're dealing with a transposed matrix, columns are already + # sequential + for i in xrange(4): + col = i * 4 + + v0, v1, v2, v3 = (block[col], block[col + 1], block[col + 2], + block[col + 3]) + #v0, v1, v2, v3 = block[col:col+4] + + block[col ] = mul_14[v0] ^ mul_9[v3] ^ mul_13[v2] ^ mul_11[v1] + block[col+1] = mul_14[v1] ^ mul_9[v0] ^ mul_13[v3] ^ mul_11[v2] + block[col+2] = mul_14[v2] ^ mul_9[v1] ^ mul_13[v0] ^ mul_11[v3] + block[col+3] = mul_14[v3] ^ mul_9[v2] ^ mul_13[v1] ^ mul_11[v0] + + #print 'MixColumns :', block + + def encrypt_block(self, block): + """Encrypts a single block. This is the main AES function""" + + # For efficiency reasons, the state between steps is transmitted via a + # mutable array, not returned. + self.add_round_key(block, 0) + + for round in xrange(1, self.rounds): + self.sub_bytes(block, aes_sbox) + self.shift_rows(block) + self.mix_columns(block) + self.add_round_key(block, round) + + self.sub_bytes(block, aes_sbox) + self.shift_rows(block) + # no mix_columns step in the last round + self.add_round_key(block, self.rounds) + + def decrypt_block(self, block): + """Decrypts a single block. This is the main AES decryption function""" + + # For efficiency reasons, the state between steps is transmitted via a + # mutable array, not returned. + self.add_round_key(block, self.rounds) + + # count rounds down from 15 ... 1 + for round in xrange(self.rounds-1, 0, -1): + self.shift_rows_inv(block) + self.sub_bytes(block, aes_inv_sbox) + self.add_round_key(block, round) + self.mix_columns_inv(block) + + self.shift_rows_inv(block) + self.sub_bytes(block, aes_inv_sbox) + self.add_round_key(block, 0) + # no mix_columns step in the last round + + +#### ECB mode implementation + +class ECBMode(object): + """Electronic CodeBook (ECB) mode encryption. + + Basically this mode applies the cipher function to each block individually; + no feedback is done. NB! This is insecure for almost all purposes + """ + + def __init__(self, cipher): + self.cipher = cipher + self.block_size = cipher.block_size + + def ecb(self, data, block_func): + """Perform ECB mode with the given function""" + + if len(data) % self.block_size != 0: + raise ValueError, "Plaintext length must be multiple of 16" + + block_size = self.block_size + data = array('B', data) + + for offset in xrange(0, len(data), block_size): + block = data[offset : offset+block_size] + block_func(block) + data[offset : offset+block_size] = block + + return data.tostring() + + def encrypt(self, data): + """Encrypt data in ECB mode""" + + return self.ecb(data, self.cipher.encrypt_block) + + def decrypt(self, data): + """Decrypt data in ECB mode""" + + return self.ecb(data, self.cipher.decrypt_block) + +#### CBC mode + +class CBCMode(object): + """Cipher Block Chaining (CBC) mode encryption. This mode avoids content leaks. + + In CBC encryption, each plaintext block is XORed with the ciphertext block + preceding it; decryption is simply the inverse. + """ + + # A better explanation of CBC can be found here: + # http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Cipher-block_chaining_.28CBC.29 + + def __init__(self, cipher, IV): + self.cipher = cipher + self.block_size = cipher.block_size + self.IV = array('B', IV) + + def encrypt(self, data): + """Encrypt data in CBC mode""" + + block_size = self.block_size + if len(data) % block_size != 0: + raise ValueError, "Plaintext length must be multiple of 16" + + data = array('B', data) + IV = self.IV + + for offset in xrange(0, len(data), block_size): + block = data[offset : offset+block_size] + + # Perform CBC chaining + for i in xrange(block_size): + block[i] ^= IV[i] + + self.cipher.encrypt_block(block) + data[offset : offset+block_size] = block + IV = block + + self.IV = IV + return data.tostring() + + def decrypt(self, data): + """Decrypt data in CBC mode""" + + block_size = self.block_size + if len(data) % block_size != 0: + raise ValueError, "Ciphertext length must be multiple of 16" + + data = array('B', data) + IV = self.IV + + for offset in xrange(0, len(data), block_size): + ctext = data[offset : offset+block_size] + block = ctext[:] + self.cipher.decrypt_block(block) + + # Perform CBC chaining + #for i in xrange(block_size): + # data[offset + i] ^= IV[i] + for i in xrange(block_size): + block[i] ^= IV[i] + data[offset : offset+block_size] = block + + IV = ctext + #data[offset : offset+block_size] = block + + self.IV = IV + return data.tostring() + +#### + +def galois_multiply(a, b): + """Galois Field multiplicaiton for AES""" + p = 0 + while b: + if b & 1: + p ^= a + a <<= 1 + if a & 0x100: + a ^= 0x1b + b >>= 1 + + return p & 0xff + +# Precompute the multiplication tables for encryption +gf_mul_by_2 = array('B', [galois_multiply(x, 2) for x in range(256)]) +gf_mul_by_3 = array('B', [galois_multiply(x, 3) for x in range(256)]) +# ... for decryption +gf_mul_by_9 = array('B', [galois_multiply(x, 9) for x in range(256)]) +gf_mul_by_11 = array('B', [galois_multiply(x, 11) for x in range(256)]) +gf_mul_by_13 = array('B', [galois_multiply(x, 13) for x in range(256)]) +gf_mul_by_14 = array('B', [galois_multiply(x, 14) for x in range(256)]) + +#### + +# The S-box is a 256-element array, that maps a single byte value to another +# byte value. Since it's designed to be reversible, each value occurs only once +# in the S-box +# +# More information: http://en.wikipedia.org/wiki/Rijndael_S-box + +aes_sbox = array('B', + '637c777bf26b6fc53001672bfed7ab76' + 'ca82c97dfa5947f0add4a2af9ca472c0' + 'b7fd9326363ff7cc34a5e5f171d83115' + '04c723c31896059a071280e2eb27b275' + '09832c1a1b6e5aa0523bd6b329e32f84' + '53d100ed20fcb15b6acbbe394a4c58cf' + 'd0efaafb434d338545f9027f503c9fa8' + '51a3408f929d38f5bcb6da2110fff3d2' + 'cd0c13ec5f974417c4a77e3d645d1973' + '60814fdc222a908846eeb814de5e0bdb' + 'e0323a0a4906245cc2d3ac629195e479' + 'e7c8376d8dd54ea96c56f4ea657aae08' + 'ba78252e1ca6b4c6e8dd741f4bbd8b8a' + '703eb5664803f60e613557b986c11d9e' + 'e1f8981169d98e949b1e87e9ce5528df' + '8ca1890dbfe6426841992d0fb054bb16'.decode('hex') +) + +# This is the inverse of the above. In other words: +# aes_inv_sbox[aes_sbox[val]] == val + +aes_inv_sbox = array('B', + '52096ad53036a538bf40a39e81f3d7fb' + '7ce339829b2fff87348e4344c4dee9cb' + '547b9432a6c2233dee4c950b42fac34e' + '082ea16628d924b2765ba2496d8bd125' + '72f8f66486689816d4a45ccc5d65b692' + '6c704850fdedb9da5e154657a78d9d84' + '90d8ab008cbcd30af7e45805b8b34506' + 'd02c1e8fca3f0f02c1afbd0301138a6b' + '3a9111414f67dcea97f2cfcef0b4e673' + '96ac7422e7ad3585e2f937e81c75df6e' + '47f11a711d29c5896fb7620eaa18be1b' + 'fc563e4bc6d279209adbc0fe78cd5af4' + '1fdda8338807c731b11210592780ec5f' + '60517fa919b54a0d2de57a9f93c99cef' + 'a0e03b4dae2af5b0c8ebbb3c83539961' + '172b047eba77d626e169146355210c7d'.decode('hex') +) + +# The Rcon table is used in AES's key schedule (key expansion) +# It's a pre-computed table of exponentation of 2 in AES's finite field +# +# More information: http://en.wikipedia.org/wiki/Rijndael_key_schedule + +aes_Rcon = array('B', + '8d01020408102040801b366cd8ab4d9a' + '2f5ebc63c697356ad4b37dfaefc59139' + '72e4d3bd61c29f254a943366cc831d3a' + '74e8cb8d01020408102040801b366cd8' + 'ab4d9a2f5ebc63c697356ad4b37dfaef' + 'c5913972e4d3bd61c29f254a943366cc' + '831d3a74e8cb8d01020408102040801b' + '366cd8ab4d9a2f5ebc63c697356ad4b3' + '7dfaefc5913972e4d3bd61c29f254a94' + '3366cc831d3a74e8cb8d010204081020' + '40801b366cd8ab4d9a2f5ebc63c69735' + '6ad4b37dfaefc5913972e4d3bd61c29f' + '254a943366cc831d3a74e8cb8d010204' + '08102040801b366cd8ab4d9a2f5ebc63' + 'c697356ad4b37dfaefc5913972e4d3bd' + '61c29f254a943366cc831d3a74e8cb'.decode('hex') +) diff --git a/gluon/globals.py b/gluon/globals.py index f265c1da..63e22e0d 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -36,6 +36,13 @@ import os import sys import traceback import threading +import hmac +import base64 + +try: + from Crypto.Cipher import AES +except ImportError: + from contrib import aes as AES try: from gluon.contrib.minify import minify @@ -409,6 +416,7 @@ class Session(Storage): migrate=True, separate = None, check_client=False, + cookie_key=None, ): """ separate can be separate=lambda(session_name): session_name[-2:] @@ -421,9 +429,30 @@ class Session(Storage): if not masterapp: masterapp = request.application response.session_id_name = 'session_id_%s' % masterapp.lower() + + # Load session data from cookie + + if cookie_key: + response.session_cookie_key = cookie_key + response.session_cookie_key2 = hashlib.md5(cookie_key).digest() + cookie_name = request.application.lower()+'_session_data' + response.session_cookie_name = cookie_name + if cookie_data in request.cookies: + cookie_value = request.cookies[cookie_name].value + cookie_parts = cookie_value.split(":") + enc = cookie_parts[2] + cipher = AES.new(cookie_key) + decrypted = cipher.decrypt(base64.b64decode(enc)).rstrip('{') + check = hmac.new(response.session_cookie_key2,enc).hexdigest() + if cookie_parts[0] == check: + session_data = cPickle.loads(decrypted) + self.update(session_data) + else: + return if not db: - if global_settings.db_sessions is True or masterapp in global_settings.db_sessions: + if global_settings.db_sessions is True \ + or masterapp in global_settings.db_sessions: return response.session_new = False client = request.client and request.client.replace(':', '.') @@ -441,11 +470,13 @@ class Session(Storage): response.session_file = \ open(response.session_filename, 'rb+') try: - portalocker.lock(response.session_file,portalocker.LOCK_EX) + portalocker.lock(response.session_file, + portalocker.LOCK_EX) response.session_locked = True self.update(cPickle.load(response.session_file)) response.session_file.seek(0) - oc = response.session_filename.split('/')[-1].split('-')[0] + oc = response.session_filename.split('/')[-1]\ + .split('-')[0] if check_client and client!=oc: raise Exception, "cookie attack" finally: @@ -459,7 +490,8 @@ class Session(Storage): response.session_id = '%s-%s' % (client, uuid) if separate: prefix = separate(response.session_id) - response.session_id = '%s/%s' % (prefix,response.session_id) + response.session_id = '%s/%s' % \ + (prefix,response.session_id) response.session_filename = \ os.path.join(up(request.folder), masterapp, 'sessions', response.session_id) @@ -492,16 +524,25 @@ class Session(Storage): migrate=table_migrate, ) try: + + # Get session data out of the database + + # Key comes from the cookie key = request.cookies[response.session_id_name].value (record_id, unique_key) = key.split(':') if record_id == '0': raise Exception, 'record_id == 0' + # Select from database. rows = db(table.id == record_id).select() + + # Make sure the session data exists in the database if len(rows) == 0 or rows[0].unique_key != unique_key: raise Exception, 'No record' # rows[0].update_record(locked=True) + + # Unpickle the data session_data = cPickle.loads(rows[0].session_data) self.update(session_data) except Exception: @@ -540,8 +581,18 @@ class Session(Storage): self._close(response) self._forget = True - def _try_store_in_db(self, request, response): + def _try_store_in_cookie(self, request, response): + pad = lambda s: s + (32 - len(s) % 32) * '{' + data = cPickle.dumps(dict(self)) + cipher = AES.new(response.session_cookie_key) + encrypted_data = base64.b64encode(cipher.encrypt(pad(data))) + signature = hmac.new(response.session_cookie_key2,encrypted_data)\ + .hexdigest() + value = signature+':'+encrypted_data + response.cookies[response.session_cookie_name] = value + response.cookies[response.session_cookie_name]['path'] = '/' + def _try_store_in_db(self, request, response): # don't save if file-based sessions, no session id, or session being forgotten if not response.session_db or not response.session_id or self._forget: return @@ -618,4 +669,3 @@ class Session(Storage): -