aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichał Górny <mgorny@gentoo.org>2013-08-21 23:14:40 +0200
committerMichał Górny <mgorny@gentoo.org>2013-08-21 23:54:06 +0200
commit0ba365730e33d30e24b77b440cea5ca3425d87aa (patch)
tree2364ac8a9fc71c3982f278e3b00e471db3f0c727 /okupy/common
parent'Pack' session id into binary string before encrypting. (diff)
downloadidentity.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.py179
-rw-r--r--okupy/common/ldap_helpers.py2
-rw-r--r--okupy/common/models.py87
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')