2

In my Django application I have hierarchical URL structure:

webpage.com/property/PK/sub-property/PK/ etc...

I do not want to expose primary keys and create a vulnerability. Therefore I am encrypting all PKs into strings in all templates and URLs. This is done by the wonderful library django-encrypted-id written by this SO user. However, the library supports up to 2^64 long integers and produces 24 characters output (22 + 2 padding). This results in huge URLs in my nested structure.

Therefore, I would like to patch the encrypting and decrypting functions and try to shorten the output. Here is the original code (+ padding handling which I added):

# Remove the padding after encode and add it on decode
PADDING = '=='

def encode(the_id):
    assert 0 <= the_id < 2 ** 64

    crc = binascii.crc32(bytes(the_id)) & 0xffffff

    message = struct.pack(b"<IQxxxx", crc, the_id)
    assert len(message) == 16

    cypher = AES.new(
        settings.SECRET_KEY[:24], AES.MODE_CBC,
        settings.SECRET_KEY[-16:]
    )

    return base64.urlsafe_b64encode(cypher.encrypt(message)).rstrip(PADDING)


def decode(e):
    if isinstance(e, basestring):
        e = bytes(e.encode("ascii"))

    try:
        e += str(PADDING)
        e = base64.urlsafe_b64decode(e)
    except (TypeError, AttributeError):
        raise ValueError("Failed to decrypt, invalid input.")

    for skey in getattr(settings, "SECRET_KEYS", [settings.SECRET_KEY]):
        cypher = AES.new(skey[:24], AES.MODE_CBC, skey[-16:])
        msg = cypher.decrypt(e)

        crc, the_id = struct.unpack("<IQxxxx", msg)

        if crc != binascii.crc32(bytes(the_id)) & 0xffffff:
            continue

        return the_id
    raise ValueError("Failed to decrypt, CRC never matched.")

# Lets test with big numbers
for x in range(100000000, 100000003):
    ekey = encode(x)
    pk =  decode(ekey)
    print "Pk: %s Ekey: %s" % (pk, ekey)

Output (I changed the strings a bit, so don't try to hack me :P):

Pk: 100000000 Ekey: GNtOHji8rA42qfq3p5gNMI
Pk: 100000001 Ekey: tK6RcAZ2MrWmR3nB5qkQDe
Pk: 100000002 Ekey: a7VXIf8pEB6R7XvqwGQo6W

I have tried to modify everything in the encode() function but without any success. The produced string has always the length of 22.

Here is what I want:

  • Keep the encryption strength near to the original level or at least do not decrease it dramatically
  • Support integers up to 2^48 (~281 trillions), or 2^40, because as it is now with 2^64 is too much, I do not think that we will ever have such huge PKs in the database.
  • I will be happy with string length between 14-20. If its 20.. then yeah, its still 2 chars less..
Community
  • 1
  • 1
katericata
  • 1,008
  • 3
  • 14
  • 33
  • Out of curiosity, why encrypt and decrypt every time? Are you just trying to prevent accessing a random property by switching out the key in the URL? In that situation I've generated a UUID for each object and stored it on the model, then used that to do the lookup... a bit more efficient. – neomanic Feb 23 '17 at 23:04
  • Yes, that is why I am doing this :). We used to have UUIDs, but we didn't store them in the database. Also, what I am targeting here is to get as short string as possible. For collision safety, UUIDs are pretty long I believe. And I've read that truncating them is not safe. – katericata Feb 23 '17 at 23:13
  • Whatever method you use (as long as it's good), your chances of collision are going to be pretty much dependent on the length of the identifier. It's going to be a space/time tradeoff too: I personally would just generate a random id to save onto the model, (and check to make sure it's not in the db already and generate another if so). I'd take the extra storage space over having to encrypt/decrypt on every access to the objects URL (and even more so since you have sub-properties as well). – neomanic Feb 24 '17 at 00:11

1 Answers1

1

Currently you are using CBC mode with a static IV, so the code you have isn't secure anyway and, like you say, produces rather large ciphertexts.

I would recommend swapping from CBC mode to CTR mode, which lets you have a variable length IV. The normal recommended length for the IV (or nonce) in CTR mode, I think, is 12, but you can reduce this up or down as needed. CTR is also a stream cipher which means what you put in is what you get out in terms of size. With AES, CBC mode will always return you ciphertexts in blocks of 16 bytes so even if you are encrypting 6 bytes, you get 16 bytes out, so isn't ideal for you.

If you make your IV say... 48 bits long and aim to encrypt no larger than 48 bits, you'll be able to produce a raw output of 6 + 6 = 12 bytes, or with base64, (4*(12/3)) = 16 bytes. You will be able to get a lower output than this by further reducing your IV and/or input size (2^40?). You can lower possible values of your input as much as you want without damaging the security.

Keep in mind that CTR does have pitfalls. Producing two ciphertexts that share the same IV and key means that they can be trivially broken, so always randomly generate your IV (and don't reduce it in size too much).

Luke Joshua Park
  • 9,527
  • 5
  • 27
  • 44
  • 1
    Thanks for the explanation. I am trying to generate random IV/counter, but the decode function will need it to decrypt the ciphertext. The decrypt function can be called in different session or in different part of the application. How can I keep the IV then. Shall I store it in the database as additional fields of the PK's table? Or is there a way to securely decrypt by using only the ciphertext? – katericata Feb 23 '17 at 22:50
  • Append it to the ciphertext itself! The IV doesn't need to be secret, just random. I included it as part of the 16 bytes total. That's 6 bytes of ciphertext, 6 bytes of IV and then extended to 16 because you use base64. – Luke Joshua Park Feb 23 '17 at 22:56
  • Great, I will give it a try now. – katericata Feb 23 '17 at 23:04
  • I fully understand your proposal, but I fail to implement it in Python. As a reference, I checked the answer of [this](http://stackoverflow.com/questions/3154998/pycrypto-problem-using-aesctr) question, but if I try to reduce the secret from 16 to 6, I get this: `TypeError: CTR counter function returned string not of length 16`. I do not know how to proceed, and I would really appreciate some code snippets to get me on the right direction. – katericata Feb 27 '17 at 22:06
  • I don't know much Python so I'm likely not going to be able to give you a code snippet. If I was you, I'd implement the CTR mode myself. It sounds like they are still expecting CTR to return a full block... which is unnecessary. Have you tried adjusting the feedback size? – Luke Joshua Park Feb 27 '17 at 22:08