5

I have a field userid which has value between 10000 and 999999. I want to create a promotional code which is based on this field.

The promotional code will be used by the users to refer new users.

The promotional code characteristics:

  • no additional field in the database - through algorithm the promotional code can be decoded back to userid
  • hard to guess the userid
  • easy to share verbally

Can you suggest some ideas or examples how to tackle this issue?

  • do these ever expire? or are they good forever? – Joe Apr 18 '18 at 13:53
  • No the promotional code doesn't expire. – GeorgiY Apr 20 '18 at 5:25
3

no additional field in the database - through algorithm the promotional code can be decoded back to userid

While this would usually be solved through an additional table with cross references, ideally 1:n, so that one user id can have multiple valid promotional codes, this seems to be explicitly out of scope. This also precludes self-chosen promocodes, unfortunately.

hard to guess the userid

Ideally, it shouldn't be possible to guess the user id (and be sure you guessed right) at all.

easy to share verbally

This might be the biggest problem with not using an additional table.

I propose the following solution:

Encrypt your user ids.

There are different encryption schemes that may yield smaller results that can be used. But if, as suggested by your restriction on changes to the database, implementation cost is a driving factor, raw rsa might be worth a look as it keeps the ciphertext relatively small and encryption libraries are readily available.

The resulting bits could be translated into natural language words, for example by using a wordlist for diceware.

Using this approach, the ciphertext would be easy to communicate (as it's a string of words) and easy to decipher (reverse translation, decrypt resulting bits).

There is however a catch: those codes might get longer than expected (depending on the chosen encryption scheme) and might pose a time penalty when it comes to typing them in.

While this yields longer (in terms of string size) codes, they are way easier to transmit verbally especially for native speakers than the result of base32, base64 or any of the usual approaches for transport encoding.

Thus, even though a 1:n table would be ideal, using a fitting encryption scheme and ending up with maybe a 3 word code might be the next best thing - and definitely easier to share verbally than the usual suspects for encoding data.

2

I'd recommend the following approach:

  • Encrypt the userid with an algorithm that produces small outputs, e.g. has a small block size. One alterantive would be Hasty Pudding cipher whith say a 32 bit block size.
  • Encode the output with base32. You then get a seven letter long promo code.
  • To get a userid from a promo code, just reverse the process - decode and decrypt.

I should admit that I am not sure how the small block size affect the security of the cipher in your use case. So before you declare this water proof, you might want to look into that.

Note that this would be very easy to do in a secure way by just using a translation table, relating random promo codes with users. However, that would violate your requirement that no database modifications should be needed.

  • you probably want some kind of timestamp in there as well so you can expire these things... at which point they're going to get longer. i agree, just create another table. – Joe Apr 18 '18 at 13:52
  • 2
    Just a heads-up on base32 - it will sometimes produce words, and those words will sometimes be offensive. I once had to pull vowels out of a base64 encoding due to customer complaints. This is only a problem when you show these strings to users, but that is the use-case that is being discussed here. – Neil Smithline Apr 18 '18 at 15:19
1

Keeping the UserID hidden is easy. The difficulty is preventing collisions: most hash functions that are so short as to be easily communicated, or truncated to that length, will randomly collide.

The same applies to encryption functions: since strong encryption produces a random-like ciphertext, it will produce collisions due to the birthday paradox. So what you need is a one-to-one transformation.

One approach is using a PRNG with a deterministic and sufficient period and only its previous number as a state, like LFSR, with UserID as the number of rounds. The downside is that it can be slow unless written in a fast compiled language.

A more straightforward way is to encrypt the value with a very short block cipher. Skip32 can be implemented with almost no effort. Hasty Pudding is an even closer fit, as it can be set to a 20-bit block.

For verbal translation, 20-21 bits translates to 7 numerics or five 5-bit alphanumerics with 2-5 spare bits. The spare bits are needed as a check digit against typos. You can use just those 2-5 bits if your user base is small now and add one more check digit as your user base approaches a million.

Personally, I prefer a short alphanumeric to a passphrase - the latter create the temptation to rely on memory, and human memory often substitutes words for synonyms. Codes and numbers are easier to write down and people usually can do it. OTOH, a passphrase doesn't require check digits, since its limited dictionary is a check in itself. It comes down to your use case and your preference.

All of these approaches are insecure; LFSR isn't even a cipher, the other two are quite weak. But your security is limited by the invite code's short length and the need for guaranteed collision avoidance.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.