Basically, you can split the operation into parts:
- Somehow "encrypt" your starting number
n , so two consecutive numbers give (very) different results - Build your "readable" code from the result of step 1
For step 1, I would suggest using a simple block cipher (for example, a Feistel cipher with a round function of your choice). See also this question .
Feisty ciphers work in several rounds. During each round, one round function is applied to one half of the input signal, the result is xor ed with the other half, and the two halves are swapped. The best part of Feistel’s ciphers is that the round function should not be two-way (the entrance to the round function remains unchanged after each round, so the result of the cyclic function can be restored during decryption). Therefore, you can choose any crazy operation (s) that you like :). Feistel's ciphers are also symmetrical, which meets your first requirement.
A brief example in C #
const int BITCOUNT = 30; const int BITMASK = (1 << BITCOUNT/2) - 1; static uint roundFunction(uint number) { return (((number ^ 47894) + 25) << 1) & BITMASK; } static uint crypt(uint number) { uint left = number >> (BITCOUNT/2); uint right = number & BITMASK; for (int round = 0; round < 10; ++round) { left = left ^ roundFunction(right); uint temp = left; left = right; right = temp; } return left | (right << (BITCOUNT/2)); }
(Note that after the last round there is no swapping, in the code the replacement is simply canceled in the construction of the result)
In addition to fulfilling your requirements 3 and 4 (the function is common, so you get different outputs for different inputs, and the input is “completely scrambled” according to your informal definition), it is also its own inverse (thus implicitly fulfilling requirement 1), those. crypt(crypt(x))==x for each x in the input domain ( 0..2^30-1 in this implementation). It is also cheap in terms of performance requirements.
For step 2, simply write the result to some database of your choice. For example, to encode a 30-bit number, you can use 6 "digits" of an alphabet of 32 characters (so you can encode 6 * 5 = 30 bits).
An example of this step in C #:
const string ALPHABET= "AG8FOLE2WVTCPY5ZH3NIUDBXSMQK7946"; static string couponCode(uint number) { StringBuilder b = new StringBuilder(); for (int i=0; i<6; ++i) { b.Append(ALPHABET[(int)number&((1 << 5)-1)]); number = number >> 5; } return b.ToString(); } static uint codeFromCoupon(string coupon) { uint n = 0; for (int i = 0; i < 6; ++i) n = n | (((uint)ALPHABET.IndexOf(coupon[i])) << (5 * i)); return n; }
For inputs 0 - 9 this gives the following coupon codes
0 => 5VZNKB 1 => HL766Z 2 => TMGSEY 3 => P28L4W 4 => EM5EWD 5 => WIACCZ 6 => 8DEPDA 7 => OQE33A 8 => 4SEQ5A 9 => AVAXS5
Note that this approach has two different internal “secrets”: firstly, a round function along with the number of rounds used and the second alphabet that you use to encode a curved result. But also note that the shown implementation is in no way protected in a cryptographic sense!
Also note that the feature shown is a complete bijective function, in the sense that every possible 6-character code (with characters from your alphabet) will give a unique number. So that someone does not enter any random code, you must define some restrictions on the input number. For example. Only coupons are issued for the first 10,000 numbers. Then the probability that any random coupon code will be valid will be 10000/2 ^ 30 = 0.00001 (this will require about 50,000 attempts to find the correct coupon code). If you need more “security”, you can simply increase the bit size / coupon size (see below).
EDIT: change coupon code length
Changing the length of the resulting coupon code requires some mathematics: the first (encryption) step only works with a bit string with an even number of bits (this is necessary in order for the Feistel cipher to work).
In the second step, the number of bits that can be encoded using a given alphabet depends on the "size" of the selected alphabet and the length of the coupon code. This "entropy", given in bits, is, generally speaking, not an integer, but even more so an even integer. For example:
A 5-digit code using the 30-character alphabet gives 30 ^ 5 possible codes, which means ld (30 ^ 5) = 24.53 bits / coupon code.
There is a simple solution for a four-digit code: given the 32-character alphabet, you can encode * ld (32 ^ 4) = 5 * 4 = 20 * Bits. So you can just set BITCOUNT to 20 and change the for loop in the second part of the code to run up to 4 (instead of 6 )
Generating a five-digit code is a bit more complicated and somehow “weakens” the algorithm: you can set BITCOUNT to 24 and just generate a 5-digit code from an alphabet of 30 characters (remove two characters from ALPHABET and run the for loop to 5 ).
But this will not generate all possible 5-digit codes: with 24 bits you can only get 16,777,216 possible values from the encryption step, 5-digit codes can encode 24,300,000 possible numbers, so some possible codes will never be generated. More specifically, the last position of the code will never contain some alphabetical characters. This can be considered a drawback, as it narrows the set of valid codes in an obvious way.
When decoding a coupon code, you first need to run the codeFromCoupon function, and then check if bit 25 of the result is set. That would mean a wrong code that you can immediately reject. Please note that in practice this can even be an advantage, since it allows you to quickly check (for example, on the client side) the correctness of the code without discarding all the internal functions of the algorithm.
If bit 25 is not set, you will call the crypt function and get the original number.