我想生成优惠券代码,例如AYB4ZZ2
。但是,我还希望能够标记使用过的优惠券并限制其全局编号,比方说N
。天真的方法类似于“生成N
个唯一的字母数字代码,将它们放入数据库并在每个优惠券操作上执行数据库搜索。”
但是,据我所知,我们还可以尝试找到一个函数 MakeCoupon(n)
,它将给定的数字转换为具有预定义长度的类似优惠券的字符串。
据我了解,MakeCoupon
应满足以下要求:
是双射的。它的倒数MakeNumber(coupon)
应该是可以有效计算的。
MakeCoupon(n)
的输出应为字母数字,并且应具有较小且常量长度 - 以便可以将其称为人类可读。例如。 SHA1
摘要无法通过此要求。
实用的独特性。每个自然MakeCoupon(n)
的{{1}}结果应该完全唯一或唯一,例如,n <= N
是唯一的(具有相同的极小碰撞概率)。
(这个定义很棘手)如何从单个优惠券代码中枚举所有剩余的优惠券应该不明显 - 让我们说MD5
和{{1视觉上应该不同。
E.g。简单输出
MakeCoupon(n)
填充零的MakeCoupon(n + 1)
将无法满足此要求,因为MakeCoupon(n),
和n
实际上并没有“视觉上”不同。
是否存在满足以下要求的任何函数或函数生成器?我的搜索尝试只引导我[CPAN]
CouponCode,但它没有满足相应函数的要求
答案 0 :(得分:71)
基本上你可以将操作分成几部分:
n
,以便两个连续的号码产生(非常)不同的结果对于第1步,我建议使用简单的分组密码(例如Feistel cipher,其中包含您选择的轮功能)。另请参阅this question。
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(该功能总计,因此对于不同的输入,您将获得不同的输出,并且输入根据您的非正式定义“完全扰乱”)它也是它的自己的逆(因此暗示满足要求1),即输入域中每个crypt(crypt(x))==x
的{{1}}(在此实现中为x
)。此外,它在性能要求方面也很便宜。
对于第2步,只需将结果编码为您选择的某个基础。例如,要对30位数字进行编码,您可以使用32个字符的6位“数字”(因此您可以编码6 * 5 = 30位)。
C#中此步骤的示例:
0..2^30-1
对于输入0 - 9,这会产生以下优惠券代码
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;
}
注意,这种方法有两个不同的内部“秘密”:第一,圆函数和使用的轮数,第二,你用来编码encyrpted结果的字母。但请注意,所显示的实现在加密意义上绝不安全!
另请注意,显示的函数是全双射函数,在某种意义上,每个可能的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 * Bits。因此,您只需将0 => 5VZNKB
1 => HL766Z
2 => TMGSEY
3 => P28L4W
4 => EM5EWD
5 => WIACCZ
6 => 8DEPDA
7 => OQE33A
8 => 4SEQ5A
9 => AVAXS5
设置为20,然后将代码第二部分中的BITCOUNT
循环更改为for
(而不是4
)
生成一个五位数代码有点棘手,而且somhow“弱化”算法:您可以将6
设置为24,只需从30个字符的字母表生成一个5位数的代码(删除两个字符)从BITCOUNT
字符串开始,让ALPHABET
循环运行到for
)。
但这不会生成所有可能的5位数代码:24位只能从加密阶段获得16,777,216个可能的值,5位数代码可以编码24,300,000个可能的数字,因此永远不会生成一些可能的代码。更具体地说,代码的最后位置将永远不会包含字母表中的某些字符。这可以被视为一个缺点,因为它以明显的方式缩小了有效代码集。
解码优惠券代码时,首先必须运行5
函数,然后检查是否设置了结果的第25位。这将标记您可以立即拒绝的无效代码。请注意,在实践中,这甚至可能是一个优点,因为它允许快速检查(例如在客户端)代码的有效性,而不会泄露算法的所有内部。
如果未设置第25位,您将调用codeFromCoupon
函数并获取原始数字。
答案 1 :(得分:12)
虽然我可能会接受这个答案,但我觉得我需要回应 - 我真的希望你能听到我说的话,因为它来自很多痛苦的经历。
虽然这项任务非常在学术上具有挑战性,而且软件工程师倾向于<强烈>挑战他们的互动与解决问题,但如果我需要为此提供一些指导可以。世界上没有零售商店,无论如何都有任何成功,并没有很好地跟踪所生成的每一个实体;从每件库存到他们发出的每张优惠券或礼品卡。如果你是这样的话,那就不是一个好的管家了,因为如果人们想要欺骗你,那就不是时候了,所以如果你的武器库里有你所有可能的物品,你就会做好准备。
现在,让我们来谈谈在您的方案中使用优惠券的过程。
当客户兑换优惠券时,前面会有某种POS系统吗?这甚至可能是一个在线业务,然后他们就可以输入他们的优惠券代码而不是收银员扫描条形码的寄存器(我假设这是我们在这里处理的)?所以现在,作为供应商,你说如果你有一个有效的优惠券代码,我会给你一些折扣 和 ,因为我们的目标是为了生成可逆的优惠券代码我们不需要数据库来验证代码,我们可以正确地反转它!我的意思是这只是数学吗? 嗯,是的,不是。
是的,你是对的,这只是数学。事实上,这也是问题,因为cracking SSL也是如此。但是,我将假设我们都认识到SSL中使用的数学比这里使用的任何东西都要复杂和密钥基本更大。
你不应该这样做,也不是明智的尝试提出某种方案,你肯定没有人愿意打破,特别是当涉及到钱时。你正在努力解决一个你不应该试图解决的问题,因为你需要保护自己免受使用优惠券代码的人的影响。
因此,这个问题不必要地复杂化,可以像这样解决。
// insert a record into the database for the coupon
// thus generating an auto-incrementing key
var id = [some code to insert into database and get back the key]
// base64 encode the resulting key value
var couponCode = Convert.ToBase64String(id);
// truncate the coupon code if you like
// update the database with the coupon code
答案 2 :(得分:5)
您想要的是Format-preserving encryption。
不失一般性,通过在基数36中编码,我们可以假设我们在讨论0..M-1
中的整数而不是符号串。 M
应该是2的力量。
选择密钥并指定M
后,FPE会为您提供0..M-1
encrypt
及其反decrypt
的伪随机排列。
string GenerateCoupon(int n) {
Debug.Assert(0 <= n && n < N);
return Base36.Encode(encrypt(n));
}
boolean IsCoupon(string code) {
return decrypt(Base36.Decode(code)) < N;
}
如果你的FPE是安全的,这个方案是安全的:即使他设法猜出与每个优惠券相关的数字,任何攻击者都不会产生概率高于O(N / M)的其他优惠券代码。他知道。
这仍然是一个相对较新的领域,因此这种加密方案的实现很少。 This crypto.SE question只提到Botan,一个带有Perl / Python绑定的C ++库,但没有提到C#。
提醒:除了还没有公认的FPE标准这一事实外,您必须考虑实施中存在错误的可能性。如果线上有很多钱,你需要权衡这个风险与避免数据库相对较小的好处。
答案 3 :(得分:3)
您可以使用base-36号码系统。假设您在coupen输出中需要6个字符。
MakeCoupon的伪代码
MakeCoupon(n)的 {
具有固定大小的字节数组,例如6.将所有值初始化为0。 将数字转换为base - 36并将'digits'存储在数组中 (使用整数除法和mod运算) 现在,对于每个'数字',找到相应的ascii代码假设 数字从0..9开始,A..Z 这个对流输出六位数作为字符串。
}
现在计算返回的数字与此操作相反。
这适用于非常大的数字(35 ^ 6)和6个允许的字符。
答案 4 :(得分:2)
选择加密函数c
。对c有一些要求,但现在我们来看看SHA1。
选择一个密钥k
。
您的优惠券代码生成功能可以是号码n
:
"n"+"k"
(这在密码管理中称为salting)s
printf "%09d%s" n s
,即零填充n
和截断哈希s
的串联。是的,猜测n
优惠券代码的数量是微不足道的(但请参见下文)。但是很难生成另一个有效的代码。
您的要求得到满足:
一些评论:
n
过于明显,您可以轻轻地对其进行模糊处理,例如base64编码,并在代码中交替使用n
和s
字符