Skip to content

Instantly share code, notes, and snippets.

@treyhunner
Created December 10, 2010 06:16
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save treyhunner/735861 to your computer and use it in GitHub Desktop.
Save treyhunner/735861 to your computer and use it in GitHub Desktop.
Encrypt and decrypt Django model primary key values (useful for publicly viewable unique identifiers)
# 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)
@samos123
Copy link

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

@treyhunner
Copy link
Author

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'

@samos123
Copy link

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.

@treyhunner
Copy link
Author

@samos123 Yes your explanation is correct. All of these functions are called to resolve type/value expectation issues.

@parhammmm
Copy link

@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

@webtweakers
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment