-
-
Save treyhunner/735861 to your computer and use it in GitHub Desktop.
# This code is under the MIT license. | |
# Inspired by this StackOverflow question: | |
http://stackoverflow.com/questions/3295405/creating-django-objects-with-a-random-primary-key | |
import struct | |
from Crypto.Cipher import DES | |
from django.db import models | |
def base36encode(number): | |
"""Encode number to string of alphanumeric characters (0 to z). (Code taken from Wikipedia).""" | |
if not isinstance(number, (int, long)): | |
raise TypeError('number must be an integer') | |
if number < 0: | |
raise ValueError('number must be positive') | |
alphabet = '0123456789abcdefghijklmnopqrstuvwxyz' | |
base36 = '' | |
while number: | |
number, i = divmod(number, 36) | |
base36 = alphabet[i] + base36 | |
return base36 or alphabet[0] | |
def base36decode(numstr): | |
"""Convert a base-36 string (made of alphanumeric characters) to its numeric value.""" | |
return int(numstr,36) | |
class EncryptedPKModelManager(models.Manager): | |
"""This manager allows models to be identified based on their encrypted_pk value.""" | |
def get(self, *args, **kwargs): | |
encrypted_pk = kwargs.pop('encrypted_pk', None) | |
if encrypted_pk: | |
# If found, decrypt encrypted_pk argument and set pk argument to the appropriate value | |
kwargs['pk'] = struct.unpack('<Q', self.model.encryption_obj.decrypt( | |
struct.pack('<Q', base36decode(encrypted_pk)) | |
))[0] | |
return super(EncryptedPKModelManager, self).get(*args, **kwargs) | |
class EncryptedPKModel(models.Model): | |
"""Adds encrypted_pk property to children which returns the encrypted value of the primary key.""" | |
encryption_obj = DES.new('8charkey') # This 8 character secret key should be changed! | |
def __init__(self, *args, **kwargs): | |
super(EncryptedPKModel, self).__init__(*args, **kwargs) | |
setattr( | |
self.__class__, | |
"encrypted_%s" % (self._meta.pk.name,), | |
property(self.__class__._encrypted_pk) | |
) | |
def _encrypted_pk(self): | |
return base36encode(struct.unpack('<Q', self.encryption_obj.encrypt( | |
str(struct.pack('<Q', self.pk)) | |
))[0]) | |
encrypted_pk = property(_encrypted_pk) | |
class Meta: | |
abstract = True | |
class ExampleModelManager(EncryptedPKModelManager): | |
pass | |
class ExampleModel(EncryptedPKModel): | |
objects = ExampleModelManager() | |
example_field = models.CharField(max_length=32) | |
# Example usage: | |
# example_instance = ExampleModel.objects.get(pk=1) | |
# url_pk = example_instance.encrypted_pk | |
# ExampleModel.objects.get(encrypted_pk=url_pk) |
base36encode(self.id)
would encode the key but it wouldn't encrypt it.
If someone guessed that a base 36 encoder function may have been used to encode the key they would be able to decode the key by reversing the base 36 encoding (using a base 36 decoder).
The secret to the encryption lies in the EncryptedPKModel.encryption_obj
which creates a DES cipher with a secret key (that only you know) and uses this to encrypt the message. At this point base36encode
is only used to keep the message to alphanumeric characters only.
As you can see from the example below each
>>> pk = 1
>>> d = DES.new('8charkey')
>>> s = d.encrypt(str(struct.pack('<Q', pk))) # Encrypt pk with DES using key '8charkey'
>>> s
'\x92\xf1R\x85\x18b!\xfc'
>>>
>>> t = struct.unpack('<Q', s) # Turn encrypted key into a tuple of longs
>>> t
(18167910229244834194L,)
>>>
>>> base36encode(t[0]) # Turn a number into a base 36 encoded string (0-9 and a-z)
'3u14ioidz0o7m'
Thanks for the great explanation!! The struct part was getting me confused, you have to use <Q unsigned long long, because of this? ValueError: Strings for DES must be a multiple of 8 in length.
Am I getting closer to understanding? DES requires a multiple of 8 so we pack the PK in a C struct of unsigned long long of 8 bytes. But the base36encode cant accept those C structs, so we unpack it again into a Python long value. Yea it's all starting to make sense now or at least I feel that way hehe.
@samos123 Yes your explanation is correct. All of these functions are called to resolve type/value expectation issues.
@treyhunner Just a suggested improvement why not, instead of storing the secret key in the abstract EncryptedPKModel store it in the concret ExampleModel and recreate the encryption_obj using that key when need be, so that each model can have their own unique secret key; it should end up looking like this: https://gist.github.com/4241508
Based on the version by @parhammmm here is a python3 fork: https://gist.github.com/webtweakers/b75b98cfa22cfcfa9fe479ba67fed851
Why can't we just return base36encode(self.id) in the model ?
Is this because of Query objects? I don't see the reason to unpack the queryset in the model? It's probably because I dont understand, just trying to learn more about Django hehe