aboutsummaryrefslogtreecommitdiff
blob: 545d369860f6561d2bac05ab841aaaa6ef883811 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# 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')