diff options
author | Michał Górny <mgorny@gentoo.org> | 2013-08-21 23:14:40 +0200 |
---|---|---|
committer | Michał Górny <mgorny@gentoo.org> | 2013-08-21 23:54:06 +0200 |
commit | 0ba365730e33d30e24b77b440cea5ca3425d87aa (patch) | |
tree | 2364ac8a9fc71c3982f278e3b00e471db3f0c727 /okupy/common | |
parent | 'Pack' session id into binary string before encrypting. (diff) | |
download | identity.gentoo.org-0ba365730e33d30e24b77b440cea5ca3425d87aa.tar.gz identity.gentoo.org-0ba365730e33d30e24b77b440cea5ca3425d87aa.tar.bz2 identity.gentoo.org-0ba365730e33d30e24b77b440cea5ca3425d87aa.zip |
Move crypto-related stuff to okupy.crypto.
Diffstat (limited to 'okupy/common')
-rw-r--r-- | okupy/common/crypto.py | 179 | ||||
-rw-r--r-- | okupy/common/ldap_helpers.py | 2 | ||||
-rw-r--r-- | okupy/common/models.py | 87 |
3 files changed, 1 insertions, 267 deletions
diff --git a/okupy/common/crypto.py b/okupy/common/crypto.py deleted file mode 100644 index 1efb00f..0000000 --- a/okupy/common/crypto.py +++ /dev/null @@ -1,179 +0,0 @@ -# vim:fileencoding=utf8:et:ts=4:sts=4:sw=4:ft=python - -from django.conf import settings -from django.contrib.sessions.backends.cache import SessionStore - -from Crypto.Cipher.AES import AESCipher -from Crypto.Hash.SHA256 import SHA256Hash - -import Crypto.Random - -import base64 -import struct - - -def ub32encode(text): - """ Encode text as unpadded base32. """ - return base64.b32encode(text).rstrip('=') - - -def ub32decode(text): - """ Decode text from unpadded base32. """ - # add missing padding if necessary - text += '=' * (-len(text) % 8) - return base64.b32decode(text, casefold=True) - - -def ub64encode(text): - """ Encode text as unpadded base64. """ - return base64.b64encode(text).rstrip('=') - - -def ub64decode(text): - """ decode text from unpadded base64. """ - # add missing padding if necessary - text += '=' * (-len(text) % 4) - return base64.b64decode(text) - - -class OkupyCipher(object): - """ Symmetric cipher using django's SECRET_KEY. """ - - _hasher_algo = SHA256Hash - _cipher_algo = AESCipher - - def __init__(self): - hasher = self._hasher_algo() - hasher.update(settings.SECRET_KEY) - key_hash = hasher.digest() - self.cipher = self._cipher_algo(key_hash) - self.rng = Crypto.Random.new() - - @property - def block_size(self): - """ - Cipher's block size. - """ - return self.cipher.block_size - - def encrypt(self, data): - """ - Encrypt random-length data block padding it with random data - if necessary. - """ - - # ensure it's bytestring before we append random bits - data = bytes(data) - # minus is intentional. (-X % S) == S - (X % S) - padding = -len(data) % self.block_size - if padding: - data += self.rng.read(padding) - return self.cipher.encrypt(data) - - def decrypt(self, data, length): - """ - Decrypt the data block of given length. Removes padding if any. - """ - - if len(data) < length: - raise ValueError('Ciphertext too short for requested length') - return self.cipher.decrypt(data)[:length] - - -class IDCipher(object): - """ - A cipher to create 'encrypted database IDs'. It is specifically fit - to encrypt an integer into constant-length hexstring. - """ - - def encrypt(self, id): - byte_id = struct.pack('!I', id) - byte_eid = cipher.encrypt(byte_id) - return ub32encode(byte_eid).lower() - - def decrypt(self, eid): - byte_eid = ub32decode(eid) - byte_id = cipher.decrypt(byte_eid, 4) - id = struct.unpack('!I', byte_id)[0] - return id - - -class SessionRefCipher(object): - """ - A cipher to provide encrypted identifiers to sessions. - - The encrypted session ID is stored in session for additional - security. Only previous encryption result may be used in decrypt(). - """ - - cache_key_prefix = 'django.contrib.sessions.cache' - session_id_length = 32 - random_prefix_bytes = 4 - ciphertext_length = session_id_length*3/4 + random_prefix_bytes - - def encrypt(self, session): - """ - Return an encrypted reference to the session. The encrypted - identifier will be stored in the session for verification - and caching. Therefore, further calls to this method will reuse - the previously cached identifier. - """ - - if 'encrypted_id' not in session: - # .cache_key is a very good property since it ensures - # that the cache is actually created, and works from first - # request - session_id = session.cache_key - - # since it always starts with the backend module name - # and __init__() expects pure id, we can strip that - assert(session_id.startswith(self.cache_key_prefix)) - session_id = session_id[len(self.cache_key_prefix):] - assert(len(session_id) == self.session_id_length) - - # now's another curious trick: session id consists - # of [a-z][0-9]. it's basically base36 but since decoding - # that is harsh, let's just treat it as base64. that's - # going to pack it into 3/4 original size, that is 24 bytes. - # then, with random prefix prepended we will fit into one - # block of ciphertext less. - session_id = ub64decode(session_id) - - data = (cipher.rng.read(self.random_prefix_bytes) - + session_id) - assert(len(data) == self.ciphertext_length) - session['encrypted_id'] = ub32encode( - cipher.encrypt(data)).lower() - session.save() - return session['encrypted_id'] - - def decrypt(self, eid): - """ - Return the SessionStore to which the encrypted identifier is - pointing. Raises ValueError if the identifier is invalid. - """ - - try: - session_id = cipher.decrypt(ub32decode(eid), - self.ciphertext_length) - except (TypeError, ValueError): - pass - else: - session_id = session_id[self.random_prefix_bytes:] - session_id = ub64encode(session_id) - session = SessionStore(session_key=session_id) - if session.get('encrypted_id') == eid: - # circular import - from .models import RevokedToken - - # revoke to prevent replay attacks - if RevokedToken.add(eid): - del session['encrypted_id'] - session.save() - return session - raise ValueError('Invalid session id') - - -cipher = OkupyCipher() -idcipher = IDCipher() -sessionrefcipher = SessionRefCipher() diff --git a/okupy/common/ldap_helpers.py b/okupy/common/ldap_helpers.py index 1bcfa69..4970e6a 100644 --- a/okupy/common/ldap_helpers.py +++ b/okupy/common/ldap_helpers.py @@ -4,7 +4,7 @@ from base64 import b64encode from Crypto import Random from passlib.hash import ldap_md5_crypt -from .crypto import cipher +from ..crypto.ciphers import cipher from ..accounts.models import LDAPUser diff --git a/okupy/common/models.py b/okupy/common/models.py deleted file mode 100644 index 545d369..0000000 --- a/okupy/common/models.py +++ /dev/null @@ -1,87 +0,0 @@ -# vim:fileencoding=utf8:et:ts=4:sts=4:sw=4:ft=python - -from django.conf import settings -from django.contrib.auth.models import User -from django.db import models, IntegrityError -from django.utils.timezone import now - -from .crypto import idcipher - -from datetime import timedelta - - -# based on https://gist.github.com/treyhunner/735861 - -class EncryptedPKModelManager(models.Manager): - def get(self, *args, **kwargs): - eid = kwargs.pop('encrypted_id', None) - if eid is not None: - kwargs['id'] = idcipher.decrypt(eid) - return super(EncryptedPKModelManager, self).get(*args, **kwargs) - - -class EncryptedPKModel(models.Model): - """ - A model with built-in identifier encryption (for secure tokens). - """ - - objects = EncryptedPKModelManager() - - @property - def encrypted_id(self): - """ - The object identifier encrypted using IDCipher, as a hex-string. - """ - if self.id is None: - return None - return idcipher.encrypt(self.id) - - class Meta: - abstract = True - - -class RevokedToken(models.Model): - """ - A model that guarantees atomic token revocation. - - We can use a single table for various kinds of tokens as long - as they don't interfere (e.g. are of different length). - """ - - user = models.ForeignKey(User, db_index=False, null=True) - token = models.CharField(max_length=64) - ts = models.DateTimeField(auto_now_add=True) - - @classmethod - def cleanup(cls): - """ - Remove tokens old enough to be no longer valid. - """ - - # we use this just to enforce atomicity and prevent replay - # for SOTP, we can clean up old tokens quite fast - # (as soon as .delete() is effective) - # for TOTP, we should wait till the token drifts away - old = now() - timedelta(minutes=3) - cls.objects.filter(ts__lt=old).delete() - - @classmethod - def add(cls, token, user=None): - """ - Use and revoke the given token, for the given user. User - can be None if irrelevant. - - Returns True if the token is fine, False if it was used - already. - """ - cls.cleanup() - - t = cls(user=user, token=token) - try: - t.save() - except IntegrityError: - return False - return True - - class Meta: - unique_together = ('user', 'token') |