礼品卡代码算法

Posted

技术标签:

【中文标题】礼品卡代码算法【英文标题】:Algorithm for Gift Card Codes 【发布时间】:2011-01-06 10:00:21 【问题描述】:

I recently posted this question 关于用户可以在线兑换的类似礼品卡的优惠券的代码。我想在大键空间、低猜测性和人类可读性之间找到最佳折衷。现在我开始实施了,我意识到我遇到了另一个问题,更多的是算法挑战。

假设我采用某种代码格式 - 为简单起见,假设从 A 到 Z 有 10 个字符,然后我开始生成凭证。这样做的正确算法是什么?!

我的第一种方法是从 0 到 308,915,776 对所有可能的代码进行编号,然后开始生成该范围内的随机数。不过,这显然有一个大问题——我必须对照所有以前生成的凭证代码检查我的随机数,如果它与现有的代码冲突,我将不得不丢弃代码并尝试另一个代码。随着系统积累更多数据,它会变慢。在只剩下一个代码的极端情况下,系统几乎不可能正确猜测它。

我可以预先生成所有代码并打乱它们,然后按顺序使用它们。但这意味着我必须存储许多代码,实际上我的键空间比我描述的要大,所以我们谈论的是非常大量的数据。所以这也不太理想。

所以这让我可以按顺序使用代码。我不想要可猜测的优惠券代码。购买“AAAAAAAAAY”代金券的用户如果输入“AAAAAAAAAZ”,应该不太可能获得另一个有效代码。

我可以改变我的字母表和位置,而不是

我使用'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

'LYFZTGKBNDRAPWEOXQHVJSUMIC'

因此而不是位置

9 8 7 6 5 4 3 2 1 0 职位是

1 8 0 7 5 4 3 9 2 6

使用这个逻辑,给定代码

LNWHDTECMA

下一个代码是

LNEHDTECMA

这绝对是不可猜测的方式。但它们之间的距离仍然只有一个字符,并且只需其中两张凭证,您就会知道哪个位置在增加,并且您将有 90% 的机会在 24 次或更少的猜测中获得下一个代码。

我的“逃生舱”是抛弃所有这些并使用 GUID。它们的字符比我希望我的用户输入的字符多,并且包含类似的字符,例如 I/1 和 O/0,但它们神奇地使上述所有令人头疼的问题都消失了。不过,我觉得这个很有趣,也许你也是。我很想听听一些替代建议。你有什么?

谢谢!

【问题讨论】:

“随着系统积累更多数据,它会变慢。”嗯,有点。 log N 可能会减慢速度。如果是这样,当你有 1,000 张卡时需要 10 毫秒的检查,而当你有 十亿 卡时需要 30 毫秒 - 几乎不值得担心。请记住,每次使用礼品卡时,您都会遇到完全相同的查找,而您绝对无法避免这种情况。 GUID 绝对不会“神奇地”使碰撞成为不可能——它们足够长,以至于碰撞变得如此不可能,以至于几乎对所有目的都无关紧要——并且有不同类型的 GUID,其中一些这是完全可以预测的。 M. Borgwardt,我并不是说 GUID 实际上使用魔法来使碰撞变得不可能。每个人都知道他们使用精灵粉。 嗯,我该选择哪个答案,技术上最正确、最有创意的答案,还是我真正会使用的答案?无论如何,每个人都会获得支持!谢谢。 请注意,在您的乱序字母示例中,两个生成的代码只有 1 个字母不同,并且两个字母(“W”和“E”)在标准美式键盘上彼此相邻!我的意思是,您可能需要注意可能被单个字符错误输入的代码。也许确保生成的每个代码与其他代码至少有 2 个字符不同。这需要时间,但现在计算机速度非常快...... 【参考方案1】:

两个随机生成的代码发生冲突的可能性与用户猜测有效代码的可能性基本相同 - 您无法阻止用户猜测。所以你必须有一个比实际使用的代码数量大得多的密钥空间,随机冲突也极不可能发生(尽管,由于生日悖论,可能不太可能完全忽略它们,至少如果您希望您的代码相当短),并且检查现有代码并在发生冲突时重新生成是一个完全可行的策略。

【讨论】:

+1 用于在碰撞时存储和重新生成。要获得 50% 的机会重新生成仅包含 10 个大写字母的单个密钥,您必须存储 (26^10)/2 个密钥,大约 70.5 万亿,因此必须重新生成这么多次的可能性它变得不可行是可以忽略不计的。 90% 的时间(编造的数字)你不必再生。 如果您必须重新生成甚至 10% 的时间,这意味着随机猜测有 10% 的机会命中有效代码 - 您希望这种可能性要低得多。我想说的是,使代码空间比您理论上预期需要的代码数量大 1000 倍应该没问题。 这将是有趣的代码测试。一生中执行一次,然后可能会损坏。【参考方案2】:

使用 N 位序列号 R,结合串联对 (R, S) 的 M 位哈希 H,其中 S 是您执行 发布的某个秘密“盐”S .然后以您喜欢的任何可逆方式对 (R,H) 对进行字母数字编码。如果你喜欢 MD5* 或 SHA 之类的算法,但位数太高,那么只需取标准哈希算法的 M 个最低有效位即可。

您可以轻松验证:解码字母数字编码,以便您可以看到 R 和 H。然后计算 H' = hash(R+S) 并验证 H = H'。

编辑: R 可以是递增的序列号或随机数或其他任何值,只要确保每个值不超过一次。

*在有人说“MD5 已损坏”之前,让我提醒您,MD5 的已知弱点是碰撞攻击,不是preimage attacks。此外,通过使用未发布的秘密盐值,您拒绝攻击者测试您的安全机制的能力,除非他/她可以猜测盐值。如果您感到偏执,请选择两个盐值 Sprefix 和 Ssuffix,并计算串联三元组 (Sprefix,R,Ssuffix) 的哈希值。

【讨论】:

但是加密哈希比随机数有什么优势? 简单!用于此目的的伪随机数可能无法猜测,但它们仍然需要是唯一且可验证的。【参考方案3】:

一些随机数生成器有一个有趣的特性:使用得当,它们不会在很长一段时间内生成重复的数字。他们产生了一种叫做full cycle的东西。 使用其中描述的一种算法,为其播种,您将拥有许多唯一数字,

添加一种将数字映射到字符的智能方法,您就得到了代码。

【讨论】:

请注意,在某些特定情况下,这可能会带来严重的安全风险。如果攻击者设法获得了他知道随后生成的许多凭证,他可以通过伪 RNG 的“逆向工程”(可能不是技术术语)获得所有凭证的代码。 @Andreas - 如果你使用盐值:凭证(previousVoucher + salt)? t。 jung - 你能在不丢失算法的“全周期”属性的情况下加盐吗? @Barry - 如果:voucher(previousVoucher, salt) = rng(previousVoucher) + salt,它不会与 RNG 混淆。问题是这是否使该方法更安全。如果您只使用 RNG,并且已知一张凭证已损坏。 @Thomas 比在代码末尾添加一些随机垃圾然后在检查之前去掉这些数字更好吗?【参考方案4】:

我会说使用“完美哈希” - http://en.wikipedia.org/wiki/Perfect_hash_function 结合 4 位随机数...

因此,只需每次增加您的优惠券代码,然后对其进行哈希处理,添加一个 4 位随机数,然后我还会在末尾添加一个校验位(如 Alix Axel 建议的那样)。

这将非常安全,不会发生冲突 - 例如,如果有人计算出您的哈希算法,他们还必须猜测最后的 4 位数代码...

【讨论】:

【参考方案5】:

Programming Pearls 有几个生成随机数集的算法示例,如果您对此类问题感兴趣,请阅读它。

书中显示,如果你生成的m随机数的值小于n,那么简单的生成数字并丢弃重复项的方法将生成不超过2m的随机数,如果m < n / 2。在这里,在 C++ 中:

void gensets(int m, int n)

    set<int> S;
    set<int>::iterator i;
    while (S.size() < m) 
        int t = bigrand() % n;
        S.insert(t);
    
    for (i = S.begin(); i != S.end(); ++i)
        cout << *i << "\n";

显然,如果您担心人们猜测值,您会希望 m 远小于 n / 2

甚至还有一个基于集合的算法来生成小于nm 随机数,每个值的可能性相同,没有重复,并且保证不会生成超过m 的随机数:

void genfloyd(int m, int n)

    set<int> S;
    set<int>::iterator i;
    for (int j = n-m; j < n; j++) 
        int t = bigrand() % (j+1);
        if (S.find(t) == S.end())
            S.insert(t); // t not in S
        else
            S.insert(j); // t in S
    
    for (i = S.begin(); i != S.end(); ++i)
        cout << *i << "\n";

不过,数字的顺序不是随机的,所以这对你来说可能不是一个好的选择。

【讨论】:

【参考方案6】:

我阅读了整条评论,发现很多人在保护他人时使用了非常聪明和复杂的手段。猜测我的算法的机会是 1/2600000 你所要做的就是在每一代之后改变盐前缀盐后缀

我选择了 4 个数字的盐前缀 和4个数字的后缀 那么主码是9个数字可以互换 然后使用这种格式sprefix +random_numbers+ssuffix 我会立即将存储它的格式散列到数据库中 查询可以帮助去除相似代码 一旦你打印了 9,就应该更改后缀和前缀! (362880) 次。

【讨论】:

请将您的答案格式化为可读性;这还不错,但目前都集中在一堵文字中。【参考方案7】:

我也回答了另一个问题:P

最好的方法是一次随机生成一个字母数字字符,直到你有 8 个。这将成为您的代金券。

理想情况下,最好的方法是选择一个足够长的序列,以便您可以安全地假设是否会有任何重复。请注意,由于Birthday problem,这种情况发生的频率可能比您想象的要多。

例如,对于 8 个字符,您有 1785793904896 种可能的组合,但如果您只生成 1,573,415 个凭证,您将有 50% 的机会出现重复。

因此,这完全取决于您要生成多少,以及您能接受的代码的最大长度。如果您要生成很多并且希望保持简短,则应保存之前生成的那些,并检查数据库是否有重复项。

【讨论】:

你好,安德烈亚斯!我同意,对于足够大的键空间,赔率开始看起来相当不错。我想我现在只是戴上了计算机科学家的帽子,并且想要一种保证有效的通用方法 - 而不仅仅是在当前输入下可能有效的方法。 至少对于 v1 算法,GUID 并不完全正确,除非您每毫秒生成超过(我认为)4 个,并且假设生成它的机器有一个具有有效 MAC 地址的设备由于 GUID 的时间戳组件以及 MAC 地址组件(然后是一些随机的东西),保证了唯一性。其他版本可能没有严格的保证,并且可能确实足够接近 GUID 池的绝对大小来保证。 如果有1785793904896个可能的组合,那么在随机生成大约1,573,415个之后,没有重复测试,碰撞概率达到0.5。见en.wikipedia.org/wiki/Birthday_problem。 -1:生日悖论意味着发生冲突的概率随着使用代码的数量而急剧增加,并且在大约 130 万个代码的某个地方开始接近 50%。 GUID 通过长得离谱来解决这个问题(对于这个目的来说太长了) @Davy8:好吧,还是看看这里:en.wikipedia.org/wiki/…【参考方案8】:

这是所有其他答案中最好的部分的总结。 :)

您需要生成以下礼品卡号:

独一无二的 猜不透

随机数是不可猜测的,但不一定是唯一的。各种算法产生的数字是唯一的,但可以猜测(该算法可以进行逆向工程)。我不知道有一种算法可以同时提供这两种属性,而且由于需要对抗逆向工程,它属于密码学领域。当然,非专家不应该尝试设计密码系统。

幸运的是,您不必从同一个算法中获取这两个属性。您的礼品卡代码可以由两部分组成:唯一的部分(使用linear congruential generator,也许,或模算术,甚至只是您每次递增的整数)和不可猜测的部分(只是随机数)。

【讨论】:

您已经注意到“独特”和“不可猜测”,但您缺少可验证性的属性。您的解决方案可以工作,尽管它需要一个数据库存储来将每个唯一 ID 映射到一个有效的随机数。 Jason S:无论如何你都得把代码存入数据库,记录礼物的金额和已经消费的金额。【参考方案9】:

我认为最好的方法是 Andreas 建议的方法。但我的回答是关于一个有趣的相关讨论。

您想生成一个数字序列,这些数字共同构成 S = 1, ..., MAX 的排列。一种方法是在 S 上取循环群的元素。例如,数字 R = x modulo p, x^2 modulo p, x^3 modulo p, ..., x^(p-1) modulo p1, ..., p-1 上形成循环群,前提是 p 是质数,x 是 @987654325 的互质数@。因此,如果您选择 MAX 作为质数,则确实使用此序列。

您想要一个“难以破解”的序列。足够难以破解的序列的生成器称为伪随机生成器(当然你可能不需要那个难以破解的)。一个例子是上面R 中元素的最后一位,前提是p 是保密的(我说的对吗?)。但是安德烈亚斯的回答已经使用了(伪)随机数的来源,因此不能称为伪随机生成器。

如果您对伪随机生成器感兴趣,可以在 Knuth 著名著作的第 2 卷中详细讨论它们。

【讨论】:

【参考方案10】:

基于Jason Orendoff's answer,我整理了一个算法来生成礼品卡代码。 基本上,它有两个 40 位数字:一个确保它是唯一的,另一个确保它难以猜测。

40 位随机数部分足以满足 1 in 2^40 的机会 猜测; 40位序号部分足够34.8 years 唯一性(假设我们每毫秒生成一张礼品卡)

然后使用Base32 将总的 80 位序列转换为 16 个字符的字符串。

import java.security.SecureRandom;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.commons.codec.binary.Base32;

public class GiftCardUtil 

    private AtomicLong sequence;
    private Random random;

    public GiftCardUtil() 
        // 1325383200000L == 1 Jan 2012
        sequence = new AtomicLong(System.currentTimeMillis() - 1325383200000L);
        random = new SecureRandom();
    

    public String generateCode() 
        System.out.println(sequence.get());
        byte[] id = new byte[10];
        longTo5ByteArray(sequence.incrementAndGet(), id);
        byte[] rnd = new byte[5];
        random.nextBytes(rnd);
        System.arraycopy(rnd, 0, id, 5, 5);
        return new Base32().encodeAsString(id);
    

    private void longTo5ByteArray(long l, byte[] b) 
        b[0] = (byte) (l >>> 32);
        b[1] = (byte) (l >>> 24);
        b[2] = (byte) (l >>> 16);
        b[3] = (byte) (l >>> 8);
        b[4] = (byte) (l >>> 0);
    

【讨论】:

【参考方案11】:

有效的方法是简单地利用创作时间来发挥自己的优势。比如说,年的最后两位数,两位数的月份,两位数的日,两位数的小时,两位数的分钟,两位数的秒,然后将秒数进行到微秒。如果需要进一步混淆,请将它们预先加扰(例如 MYmdshhdMmYs 而不是 YYMMddhmmss)。然后更改基数(也许是五进制)以进一步拒绝任何猜测尝试。 这有两个主要好处: 1-使用包括年份在内的日期将破坏任何重复,因为同一时间不会经过两次。一百年后才有风险。唯一需要担心的是可能会在同一微秒内创建两个,因此禁止一次创建多个是一项简单的任务。毫秒延迟可以解决问题。

2-猜测将非常困难。不仅要弄清楚数字(和字母!)的基数和顺序将是一项艰巨的任务,而且精确到微秒会使序列在很大程度上无关紧要。更不用说客户很难弄清楚他们以多少微秒的时间购买以及他们的时钟与您的时钟如何匹配。

反对意见可能是“等等!那是 17 位数字 (YYMMDDhhmmss.sssss),但之后带入更大的基数会减少它。使用 10 个数字和 26 个字母以 36 为基数,意味着 11 位代码将涵盖每种可能性。如果大写和小写不能互换,则可以将数据压缩到 10 位的目标,并且零问题。

【讨论】:

Nichi,这是一个有趣的答案。我的程序将分批生成凭证,因此我可能会一次运行 1000 个凭证,并且它们的创建时间将非常接近(但可能至少相隔一微秒!)如果我通过 Perfect Hash 运行时间码,则可能是一个合理的解决方案。【参考方案12】:

这里是一个虽然:

ID = 每张优惠券都有一个唯一的(自动递增的?)ID CHECKSUM = 对 ID 应用 Verhoeff 或 Luhn 算法的 N 次迭代 VOUCHER = base 将生成的 CHECKSUM 从基数 10 转换为基数 36

另请参阅此相关 SO 问题:Ideas to create a small (<10 digits), not (very) secure “hash”


使此方法更安全的一种简单方法是使用非自动递增的 ID 值,一种选择可能是将 ID 用作 UNIX 时间戳的最后 6 或 7 位数字并计算校验和。

【讨论】:

我不熟悉这些算法,但我会去阅读它们,谢谢。 所以基本上你依赖于试图猜测代码的人不知道 N 以及你使用的是哪种算法?这被称为“通过默默无闻的安全”,并不好。 @Michael:确实,但他说他想要“低可猜测性”而不是“根本没有可猜测性”。 如果你能想出如何随意生成有效代码,那就不用猜测了——安全性简直被打破了。只需要一个人来解决(或知道它,例如心怀不满的员工),几天后,10,000 人下载了凭证生成器...... 使用时间戳更糟糕——同样可预测,但现在您一直在生成重复。【参考方案13】:

我支持使用加密哈希——从 MD5 中获取位非常简单。 为了使事情可读,我想到了以下想法:获取单词列表,并使用键的位来索引单词列表。我的单词列表大约有 100 000 个单词,所以每个单词大约 16 位,对于四个单词,这提供了一个 64 位的键空间。结果通常非常可读。

例如上一段的密码签名是

神风敢死队的新鲜宅邸正在咳咳

(我的单词列表倾向于更大的键空间;如果你想要更短的短语,你的单词就更少。)

如果你手边有一个 MD5 库,那么这个策略很容易实现——我用大约 40 行 Lua 来实现。

【讨论】:

以上是关于礼品卡代码算法的主要内容,如果未能解决你的问题,请参考以下文章

礼品卡

访问 iTunes 礼品卡余额

如何仅在角度中为礼品卡模块添加提供者、服务和拦截器?

用于将 iTunes 礼品卡转移给用户的 API?

在woocommerce中手动对优惠券应用电子邮件限制的代码

免费iTunes礼品卡生成器应用程序