Skip to content

Instantly share code, notes, and snippets.

@Kobold
Last active August 29, 2015 14:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kobold/999eba6b0d129a3994fc to your computer and use it in GitHub Desktop.
Save Kobold/999eba6b0d129a3994fc to your computer and use it in GitHub Desktop.
Obfuscate primary keys

Obfuscating primary keys

Often you'll want urls that are not authenticated, yet are not easily guessed. Giving each of your models a UUID is one approach. Another elegant approach is to use a block cipher to "encrypt" a unique identifier like the primary key (PK).

Let's sketch out what properties a good solution for PK obfuscation has:

  • Generates a one to one mapping between all PKs and obfuscated PKs, with no possibility of collision.
  • Not easily possible for an attacker to determine the obfuscated PK given a PK and vice versa.
  • Reversible, i.e. the originator can determine the original PK given an obfuscated PK.

Heyyy... that looks an awful lot like encryption!

You can use pretty much any encryption mechanism. In the case of int primary keys, using a 32 or 64 bit cipher has the nice property that the obfuscated PK can also be stored in 32 or 64 bits. The example below uses a skip32 cipher and converts it into a compressed number representation (OBFUSCATE_ALPHABET) for human readability. The example stores the obfuscated PK as well, but note that this is not strictly necessary, as long as you keep

The cipher key (SKIP32_KEY) determines the unique mapping of PKs to obfuscated PKs. Keep it secret! Keep it safe!

Primary Sources:

Other resources:

import skip32
def _int_str(val, keyspace):
"""Turn a positive integer into a string."""
assert val >= 0
out = ""
while val > 0:
val, digit = divmod(val, len(keyspace))
out += keyspace[digit]
return out[::-1]
def _str_int(val, keyspace):
"""Turn a string into a positive integer."""
out = 0
for c in val:
out = out * len(keyspace) + keyspace.index(c)
return out
def mangle_pk(pk):
"""Mangles pks into a string for use in URLs."""
SKIP32_KEY = "s\xfe\x06%$'l\x18\xd3k"
mapped = skip32.encrypt(SKIP32_KEY, pk)
OBFUSCATE_ALPHABET = 'VGT09xDByOYjEvKnSXHbalMgwPqdru81Uf5NcApQL4oF73Ch6ZWJm2RzeskIti'
return _int_str(mapped, OBFUSCATE_ALPHABET)
git+https://github.com/kbussell/pyskip32.git@3e3e32e8a7acfedece9f6463db2dd575044d2448#egg=pyskip32
diff --git a/thingy/models.py b/thingy/models.py
--- a/thingy/models.py
+++ b/thingy/models.py
@@ -17,6 +17,7 @@ from model_utils.fields import MonitorField
from model_utils.managers import QueryManager
from model_utils import Choices
from south.modelsinspector import add_introspection_rules
+from .utils import mangle_pk
import datetime
import os
import textwrap
@@ -36,6 +37,7 @@ RENT_STRUCTURE_CHOICES = [
class Building(models.Model):
name = models.CharField(max_length=200)
+ mangled_pk = models.CharField(max_length=200)
@@ -76,6 +78,11 @@ class Building(models.Model):
+ def save(self, *args, **kwargs):
+ super(Building, self).save(*args, **kwargs)
+
+ if not self.mangled_pk:
+ self.mangled_pk = mangle_pk(self.pk)
+ super(Building, self).save(*args, **kwargs)
diff --git a/thingy/views.py b/thingy/views.py
index dbb437c..688630d 100644
--- a/thingy/views.py
+++ b/thingy/views.py
@@ -64,8 +64,8 @@ def base_prospects(request):
})
-def building(request, pk):
- building = get_object_or_404(Building, pk=pk)
+def building(request, mangled_pk):
+ building = get_object_or_404(Building, mangled_pk=mangled_pk)
return render(request, 'thingy/building.html', {
diff --git a/main/urls.py b/main/urls.py
index c81f8a8..2b7cd7c 100644
--- a/main/urls.py
+++ b/main/urls.py
@@ -25,7 +25,7 @@ urlpatterns = patterns('',
- url(r'^building/(?P<pk>\d+)/$', thingy.views.building, name='building'),
+ url(r'^building/(?P<mangled_pk>[\d\w]+)/$', thingy.views.building, name='building'),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment