基本上你可以将你的操作分成几个部分:
- 以某种方式“加密”您的初始数字
n
,以便两个连续的数字产生(非常)不同的结果
- 从第 1 步的结果构建您的“人类可读”代码
对于第 1 步,我建议使用简单的分组密码(例如,具有您选择的轮函数的Feistel 密码)。另请参阅此问题。
Feistel 密码在几轮中工作。在每一轮中,将一些轮函数应用于输入的一半,将结果xor
与另一半相结合,然后交换两半。Feistel 密码的好处是轮函数不必是双向的(轮函数的输入在每一轮之后保持不变,因此可以在解密期间重建轮函数的结果)。因此,您可以选择您喜欢的任何疯狂操作:)。此外,Feistel 密码是对称的,可以满足您的第一个要求。
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));
}
(请注意,在最后一轮之后没有交换,在代码中交换只是在结果的构造中撤消)
除了满足您的要求 3 和 4(函数是total,因此对于不同的输入,您会得到不同的输出,并且根据您的非正式定义,输入是“完全打乱的”)它也是它自己的逆(因此隐含地满足要求 1),即对于输入域中crypt(crypt(x))==x
的每个(在此实现中)。就性能要求而言,它也很便宜。x
0..2^30-1
对于第 2 步,只需将结果编码为您选择的某个基础。例如,要编码 30 位数字,您可以使用 32 个字符的字母表中的 6 个“数字”(因此您可以编码 6*5=30 位)。
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;
}
对于输入 0 - 9,这会产生以下优惠券代码
0 => 5VZNKB
1 => HL766Z
2 => TMGSEY
3 => P28L4W
4 => EM5EWD
5 => WIACCZ
6 => 8DEPDA
7 => OQE33A
8 => 4SEQ5A
9 => AVAXS5
请注意,这种方法有两个不同的内部“秘密”:第一,轮函数以及使用的轮数,第二,用于编码加密结果的字母表。但还要注意,所示的实现在密码学意义上绝不是安全的!
另请注意,显示的函数是一个完全双射函数,从某种意义上说,每个可能的 6 字符代码(包含字母表之外的字符)都会产生一个唯一的数字。为了防止任何人只输入一些随机代码,您应该对输入数字定义某种限制。例如,仅为前 10.000 个号码发放优惠券。然后,一些随机优惠券代码有效的概率为 10000/2^30=0.00001(大约需要 50000 次尝试才能找到正确的优惠券代码)。如果您需要更多“安全性”,您可以增加位大小/优惠券代码长度(见下文)。
编辑:更改优惠券代码长度
更改生成的优惠券代码的长度需要一些数学运算:第一步(加密)仅适用于具有偶数位数的位串(这是 Feistel 密码工作所必需的)。
在第二步中,可以使用给定字母表编码的位数取决于所选字母表的“大小”和优惠券代码的长度。这个以比特为单位的“熵”通常不是整数,更不是偶数。例如:
使用 30 个字符字母的 5 位代码会产生 30^5 个可能的代码,这意味着ld(30^5)=24.53位/优惠券代码。
对于四位代码,有一个简单的解决方案:给定一个 32 个字符的字母表,您可以编码 *ld(32^4)=5*4=20* 位。因此,您只需将 设置BITCOUNT
为 20 并将for
代码第二部分中的循环更改为运行直到4
(而不是6
)
生成一个五位代码有点棘手,并且会“削弱”算法:您可以将 设置BITCOUNT
为 24,然后从 30 个字符的字母表中生成一个 5 位代码(从ALPHABET
字符串中删除两个字符并让for
循环运行直到5
)。
但这不会生成所有可能的 5 位代码:使用 24 位,您只能从加密阶段获得 16,777,216 个可能的值,5 位代码可以编码 24,300,000 个可能的数字,因此永远不会生成一些可能的代码。更具体地说,代码的最后位置永远不会包含字母表中的某些字符。这可以看作是一个缺点,因为它以一种明显的方式缩小了有效代码的范围。
解码优惠券代码时,您首先必须运行该codeFromCoupon
函数,然后检查是否设置了结果的第 25 位。这将标记您可以立即拒绝的无效代码。请注意,在实践中,这甚至可能是一个优势,因为它允许快速检查(例如在客户端)代码的有效性,而不会泄露算法的所有内部结构。
如果未设置第 25 位,您将调用该crypt
函数并获取原始编号。