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的字符,但是他们神奇地让所有上述麻烦都消失了。不过,考虑到这一点我很开心,也许你也是。我很想听听其他一些建议。你有什么?
谢谢!
答案 0 :(得分:14)
两个随机生成的代码冲突的可能性与用户猜测有效代码的可能性基本相同 - 并且您无法阻止用户猜测。所以你必须的密钥空间比实际使用的代码数量大得多,随机冲突也是不太可能的(但是,由于生日悖论,可能不太可能完全忽略它们,至少如果你希望你的代码合理地短,并且检查现有代码并在发生碰撞时重新生成是一种非常可行的策略。
答案 1 :(得分:10)
使用N位序列号R,结合串联对(R,S)的M位散列H,其中S是一些秘密的“盐”S,你做 NOT 发布。然后以您喜欢的任何可逆方式按字母顺序编码对(R,H)。如果您喜欢MD5 *或SHA等算法,但位数太高,那么只需采用标准哈希算法的M个最低有效位。
您可以轻松验证:解码字母数字编码,以便您可以看到R和H.然后计算H'=哈希(R + S)并验证H = H'。
编辑:R可以是递增的序列号或随机或其他,只需确保您使用的每个值不超过一次。
*在有人说“MD5坏了”之前,让我提醒你,MD5的已知弱点是碰撞攻击,而不是 preimage attacks。此外,通过使用未发布的秘密盐值,您拒绝攻击者测试您的安全机制的能力,除非他/她可以猜测盐值。如果您感到偏执,请选择两个盐值Sprefix和Ssuffix,并计算连接三元组的散列值(Sprefix,R,Ssuffix)。
答案 2 :(得分:5)
某些随机数生成器具有一个有趣的属性:使用它们不会在很长时间内生成重复的数字。他们产生一种叫做full cycle的东西。 使用其中描述的算法之一,播种它,你将拥有许多唯一的数字,
添加一种智能方法,将数字映射到字符,然后就可以获得代码了。
答案 3 :(得分:4)
我会说使用“完美哈希” - http://en.wikipedia.org/wiki/Perfect_hash_function结合4位数的随机数......
所以每次只增加你的优惠券代码,然后哈希,添加一个4位数的随机数,我还会在末尾添加一个校验位(如Alix Axel建议的那样)。
这是非常安全的,没有冲突 - 例如,如果有人制定了你的哈希算法,他们也必须在最后猜测4位数代码......
答案 4 :(得分:4)
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
。
甚至还有一种基于集合的算法来生成m
小于n
的随机数,每个值都具有相同的可能性,没有重复,并且保证不会生成超过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";
}
但是数字的顺序并不是随机的,所以这对你来说可能不是一个好选择。
答案 5 :(得分:2)
我也回答了另一个问题:P
最好的方法是随机生成一个字母数字字符,直到你有8个字符。这将是您的优惠券。
理想情况下,最好的方法是选择一个足够长的序列,以便您可以安全地假设是否会有任何重复。请注意,或许与直觉相反,由于Birthday problem,这种情况比你想象的更频繁。
例如,对于8个字符,您有1785793904896种可能的组合,但如果您只生成1,573,415个优惠券,则有50%的机会获得重复。
所以,这一切都取决于你想要生成多少,以及你喜欢的代码的最大长度。如果您要生成许多并且想要保持简短,则应保存先前生成的那些,并检查数据库是否有重复项。
答案 6 :(得分:2)
这是所有其他答案的最佳位的摘要。 :)
您需要生成以下礼品卡编号:
随机数是不可取的,但不一定是唯一的。各种算法产生的数字是唯一但可猜测的(算法可以进行逆向工程)。我不知道同时提供两种属性的算法,并且由于需要违反逆向工程,因此它属于加密领域。当然,非专家不应该尝试设计密码系统。
幸运的是,您不必从同一算法中获取这两个属性。您的礼品卡代码可以由两部分组成:一个独特的部分(使用linear congruential generator生成,可能或模运算,或者甚至只是每次递增的整数)和一个不可评论的部分(只是随机数)。
答案 7 :(得分:2)
我阅读了整个评论,我发现很多人在其他方面保护使用非常聪明和复杂的手段。猜测我的算法的几率是1/2600000 您所要做的就是在每一代之后更改salt前缀salt后缀
sprefix +random_numbers+ssuffix
答案 8 :(得分:1)
我认为最好的方法是安德烈亚斯建议的。但我的回答是一个有趣的相关讨论。
您希望生成一系列数字,这些数字一起形成S = {1,...,MAX}的排列。一种方法是将循环组的元素放在S上。例如,数字R = {x modulo p, x^2 modulo p, x^3 modulo p, ..., x^(p-1) modulo p}
形成一个超过{1, ..., p-1}
的循环组,前提是p
是素数和{{ 1}}是x
的互质。因此,如果您选择MAX作为素数,则使用此序列。
你想要一个“难以破解”的序列。用于足够难以破解的序列的生成器被称为伪随机生成器(当然,您可能不需要 难以破解)。一个例子是上面p
中元素的最后一位数,只要R
保密(我是否正确?)。但安德烈亚斯的答案已经使用了(伪)随机数的来源,所以不能称之为伪随机数。
如果您对伪随机生成器感兴趣,可以在Knuth着名书籍的第2卷中详细讨论它们。
答案 9 :(得分:1)
基于Jason Orendoff's answer,我汇总了一个生成礼品卡代码的算法。 基本上,它有两个40位数字:其中一个确保它是唯一的,另一个确保它很难猜测。
然后使用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);
}
}
答案 10 :(得分:1)
有效的方法是简单地利用创作时间。比如说,年份的最后两位数字,两位数的月份,两位数的日期,两位数的小时,两位数的分钟,两位数的秒数,然后将秒数输出到比如微秒。如果需要进一步混淆,请将它们预先打扰(例如MYmdshhdMmYs而不是YYMMddhmmss)。然后改变基数(可能是十五进制)以进一步拒绝任何猜测尝试。 这有两大好处: 1 - 使用日期(包括年份)将销毁任何重复,因为同一时间不会通过两次。只有一百年后才有风险。唯一的问题是可能在相同的微秒上创建两个,为此,一次不允许创建多个是一项简单的任务。毫秒延迟可以解决问题。
猜测2猜测将非常困难。不仅要弄清楚数字(和字母!)的基数和顺序将是一项艰巨的任务,但是走出微秒使得序列基本上无关紧要。没有提到客户如何努力计算他们购买的微秒以及他们的时钟如何与你的相匹配。反对意见可能是“等等!这是17位数字(YYMMDDhhmmss.sssss),但随后将它带到更大的基数会减少它。使用10个数字和26个字母进入36基数意味着将覆盖11位数的代码如果大写和小写不可互换,数据可以压缩到10位数的目标,没有问题。
答案 11 :(得分:0)
以下是:
另请参阅此相关SO问题: Ideas to create a small (<10 digits), not (very) secure “hash” 。
使此方法更安全的一种简单方法是使用非自动递增的ID值,一个选项可能是使用ID作为UNIX时间戳的最后6或7位数并计算校验和。
答案 12 :(得分:0)
我第二次使用MD5的加密哈希比特非常简单。 为了使事情可读,我提出了以下想法:获取单词列表,并使用键的位来索引单词列表。我的单词列表大约是10万个字,因此每个字大约16位,对于4个字,它给出了64位密钥空间。结果通常很可读。
例如,前一段的加密签名是
kamikaze的freshet豪宅憧憬
(我的单词列表向较大的键空间倾斜;如果您想要较短的短语,则单词较少。)
如果你有一个方便的MD5库,这个策略很容易实现 - 我在大约40行的Lua中做到了。