EntropyString for Swift
从各种字符集高效生成具有指定熵的加密强随机字符串。
安装
Carthage
-
将项目添加到你的Cartfile中。
github "EntropyString/EntropyString-Swift.git"
-
运行
carthage update
并遵循Carthage入门步骤。 -
导入模块EntropyString
import EntropyString
CocoaPods
-
将项目添加到你的Podfile中。
use_frameworks! pod 'EntropyString', '~> 3.0'
-
运行
pod install
并打开.xcworkspace
文件以启动Xcode。 -
导入模块EntropyString
import EntropyString
Swift Package Manager
-
将项目添加到你的
Package.swift
中。import PackageDescription let package = Package( name: "YourProject", dependencies: [ .Package(url: "https://github.com/EntropyString/EntropyString-Swift.git", majorVersion: 1) ] )
-
导入模块EntropyString
import EntropyString
此README的其余部分作为Swift playground包含在项目中。
TL;DR
import EntropyString
生成最多1百万个随机字符串,重复概率为1亿分之一。
let bits = Entropy.bits(for: 1.0e6, risk: 1.0e9)
let entropy = Entropy()
var string = entropy.string(bits: bits)
2tF6bMNPqQ82Qj
请参阅实际需求了解熵比特表示的含义。
EntropyString
默认使用预定义的charset32
字符(参考字符集)。要获取与上述熵比特相同的随机十六进制字符串
entropy.use(.charset16)
string = entropy.string(bits: bits)
a946ff97a1c4e64e79
可以指定自定义字符。使用大写十六进制字符
try! entropy.use("0123456789ABCDEF")
string = entropy.string(bits: bits)
78E3ACABE544EBA7DF
方便的函数smallID
、mediumID
、largeID
、sessionID
和token
提供用于各种预定义熵比特的随机字符串。例如,小ID代表30个字符串的可能性,重复概率为百万分之一
string = entropy.smallID()
7A81129D
或者,生成一个OWASP会话ID
string = entropy.sessionID()
CC287C99158BF152DF98AF36D5E92AE3
或者你可能需要一个256位的令牌,使用RFC 4648文件系统和URL安全字符
string = entropy.token(.charset64)
X2AZRHuNN3mFUhsYzHSE_r2SeZJ_9uqdw-j9zvPqU2O
概述
EntropyString
可以方便地利用不同的字符集创建具有特定熵的随机字符串。例如在生成随机ID时需要唯一的标识符,但又不想使用UUID这样的过度方案。
在生成此类字符串时,一个关键问题是它们必须是唯一的。然而,保证唯一性需要要么使用确定的生成方式(如计数器),这种方式不随机,要么需要将每个新创建的随机字符串与前所有字符串进行比较。当需要随机性时,存储和比较字符串的开销往往太大,因此我们选择不同的策略。
一种常见的策略是用较弱的但通常足够用的策略替换唯一的保证,即所谓的《概率唯一性》。具体来说,我们不是绝对确信唯一性,而是满足于这样的说法:“有两个字符串相同的概率小于十亿的分之一”。我们每次使用这种策略时都隐含地假设,使用我们的值时不会有哈希冲突,但我们并没有真正的唯一性保证。
幸运的是,概率唯一性策略所需的资源远低于唯一性保证。但这也要求我们必须以某种方式来界定我们所说的“在十亿分之一的情况下,一百万个这样的字符串中会有重复”。
理解随机字符串的概率唯一性需要理解 熵 以及估计 冲突(即在随机生成的字符串集中的两个字符串可能相同)的概率。博客文章Hash Collision Probabilities 详细介绍了使用具有N位输出的完美哈希表达式计算哈希冲突概率的推导。这对于理解给定固定N位输出的哈希冲突概率是足够的,但它没有提供关于我们如何界定“在十亿分之一的情况下,一百万个这样的字符串中会有重复”的答案。下文中的熵位部分描述了EntropyString
如何提供这个界定度量。
我们将从考虑生成随机字符串时的实际需求来开始研究EntropyString
。
实际需求
让我们先从思考常见的说法开始:我需要一个16字节的随机字符串。
当然。有现成的库可以满足这种需求。但是,从这种需求出发,产生了一些问题,比如
- 你想使用哪些字符?
- 你需要多少这样的字符串?
- 为什么你需要这些字符串?
可用的库通常让你指定要使用的字符。所以我们可以假设目前问题1得到了回答。
十六进制将就可以了。.
至于问题2,开发者可能回答:
我需要10,000个这样的东西。.
啊,我们终于有点眉目了。对于问题3的答案可能会导致进一步的规定。
我需要生成10,000个随机、唯一的ID。.
现在我们从箱子里取出猫。我们正在找到真正需要的东西,它与原始声明不同。开发者需要在一定数量的字符串中实现唯一性,字符串长度是唯一性的附带产品,不是目标,不应成为随机字符串的主要指定。
如概述中所述,保证唯一性是困难的,所以我们将此声明替换为概率唯一性声明,通过提出第四个问题。
- 你能接受重复的风险有多大?
概率唯一性包含风险。这是我们放弃更强声明性唯一性的代价。但是,开发者可以用类似以下声明的形式为特定场景量化一个适当的风险。
我想我可以接受一个百万分之一的机会重复。.
所以现在我们终于得到了开发者的真正需求。
我需要10,000个随机十六进制ID,任何重复的机会不超过百万分之一。.
这项声明不仅更具体,而且没有提到字符串长度。开发者需要概率唯一性,字符串是用来捕捉随机性的目的。因此,字符串的长度只是将所需唯一性表示为字符串时的编码的附带产品。
如何使用设计用于生成指定长度字符串的库来满足这一需求?好吧,你不这样做,因为这个库是设计来回答最初声明的需求,而不是我们现在发现的真正需求。我们需要一个处理一定数量字符串的概率唯一性的库。这正是EntropyString
所做的事情。
让我们使用EntropyString
来帮助开发者从潜在10,000个ID的池中生成5个十六进制ID,重复的机会为百万分之一。
import EntropyString
let bits = Entropy.bits(for: 10000, risk: 1.0e6)
let entropy = Entropy(.charset16)
var strings = [String]()
for i in 0 ..< 5 {
let string = entropy.string(bits: bits)
strings.append(string)
}
print("Strings: \(strings)")
字符串:["85e442fa0e83", "a74dc126af1e", "368cd13b1f6e", "81bf94e1278d", "fe7dec099ac9"]
观察上面的代码,
let bits = Entropy.bits(for: 10000, risk: 1.0e6)
用于确定为了满足在总共10,000个字符串中重复的风险为百万分之一所需要的多少熵。我们没有打印结果,但如果你做了,你会看到大约是45.51位。接着
let entropy = Entropy(.charset16)
创建一个配置为使用预定义十六进制字符生成字符串的Entropy
实例。最后
let string = entropy.string(bits: bits)
用于实际上生成特定熵的随机字符串。
观察ID,我们可以看到每个都是12个字符长。字符串长度再次是表示所需熵的字符的附带产品。看来开发者根本不需要16个字符。
考虑到字符串是12个十六进制数长,每个字符串实际上具有48位熵的信息携带能力(一个十六进制字符携带4位)。这很好。假设所有字符都是等可能的,字符串只能携带等于表示每个字符的熵的数量的熵。 EntropyString
产生超过指定熵的最小字符串。
更多示例
在实际需求中,我们的开发者使用了十六进制字符。让我们看看使用其他字符。
我们从使用32个字符开始。您可能会问,什么是32个字符?字符集部分讨论了在EntropyString
中可用的预定义字符,而自定义字符部分描述了如何使用您想要的任何字符。默认情况下,EntropyString
使用charset32
字符,因此我们不需要将此参数传递给Entropy()
。
import EntropyString
let entropy = Entropy(.charset32)
let bits = Entropy.bits(for: 10000, risk: 1.0e6)
var string = entropy.string(bits: bits)
print("String: \(string)\n")
字符串:PmgMJrdp9h
我们使用相同的强�数计算,因为我们没有改变ID的数量或概率唯一性的可接受风险。但这次我们使用了32个字符,我们的结果ID只需要10个字符(并且可以携带50位的熵)。
作为另一个示例,让我们假设我们需要确保几个项目的名称是唯一的。EntropyString.smallID
生成的字符串在30个字符串中有百万分之一次的重复概率。
string = entropy.smallID()
print("String: \(string)\n")
字符串:ThfLbm
我们可以使用相同的Entropy
实例,切换到默认的charset4
字符
entropy.use(charset: .charset64)
string = entropy.smallID()
print("String: \(string)\n")
字符串:CCCCTCAGGCATAGG
好吧,我们可能不会使用4个字符(而且这些字符是什么意思呢?),但您应该明白了。
假设我们有一个更极端的需求。我们希望10亿个字符串重复的概率低于万亿分之一。让我们想一下,我们的风险(万亿)是10的12次方,我们的总数(10亿)是10的10次方,所以
entropy.use(.charset32)
bits = Entropy.bits(for: 1.0e10, risk: 1.0e12)
string = entropy.string(bits: bits)
字符串:F78PmfGRNfJrhHGTqpt6Hn
最后,让我们假设我们正在生成会话ID。我们不是对唯一性本身感兴趣,而是要确保我们的ID是不可预测的,因为我们不能让坏家伙猜测有效的会话ID。在这种情况下,我们使用熵作为ID不可预测性的度量。我们不计算熵,而是声明它需要128位(因为我们从OWASP网站上了解到会话ID应该是128位)。
string = entropy.sessionID(.charset64)
字符串:b0Gnh6H5cKCjWrCLwKoeuN
使用64个字符,我们字符串的长度是22个字符。实际上22*6 = 132
位,所以我们已经满足了OWASP的要求!
注意,我们使用只有22个字符长度的字符串就满足了我们的需求。因此,我们可以告别仅携带122位熵(常见版本4)且长度为36个字符的GUID字符串(带有连字符的十六进制),使用字符串表示形式。
字符集
正如我们在前面的几节中看到的,EntropyString
为每个支持的字符集提供了预定义的字符。让我们看看内部的情况。可用的CharSet
有.charset64、.charset32、.charset16、.charset8、.charset4和.charset2。
import EntropyString
print("Base 64: \(CharSet.charset64.chars)\n")
print("Base 32: \(CharSet.charset32.chars)\n")
print("Base 16: \(CharSet.charset16.chars)\n")
print("Base 8: \(CharSet.charset8.chars)\n")
print("Base 4: \(CharSet.charset4.chars)\n")
print("Base 2: \(CharSet.charset2.chars)\n")
每个字符集的字符选择如下
- 字符集 64:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_
- 文件系统和URL安全字符集来自 RFC 4648。
- CharSet 32: 2346789bdfghjmnpqrtBDFGHJLMNPQRT
- 删除所有大小写元音字母(包括 y)
- 删除所有看起来像字母的数字
- 删除所有看起来像数字的字母
- 删除所有大小写区分度差的字母。生成的字符串看起来不像英文单词,并且易于视觉解析。
- CharSet 16: 0123456789abcdef
- 十六进制
- CharSet 8: 01234567
- 八进制
- CharSet 4: ATCG
- DNA 字母表。没有特别的原因;只是想避开显而易见的。
- CharSet 2: 01
- 二进制
您当然可以选择使用的字符,这将在下一节中介绍 自定义字符。
自定义字符
虽然轻松生成随机字符串很棒,但如果您想指定自己的字符怎么办?例如,假设您想用10位熵结果来模拟抛硬币。
import EntropyString
let entropy = Entropy(.charset2)
var flips = entropy.string(bits: 10)
print("flips: \(flips)\n")
抛掷:0101001110
生成的由 0 和 1 组成的字符串看起来并不很合适。也许您想使用字符 H 和 T。
try! entropy.use("HT")
flips = entropy.string(bits: 10)
print("flips: \(flips)\n")
抛掷:HTTTHHTTHH
作为另一个例子,我们看到了在 字符集 中定义的 charset16
的预设十六进制字符为小写。假设您喜欢大写十六进制字母。
try! entropy.use("0123456789ABCDEF")
let hex = entropy.string(bits: 48)
print("hex: \(hex)\n")
十六进制:4D20D9AA862C
Entropy
构造函数允许三种不同的用例:
- 无参数默认为
charset32
字符。 - 可以指定六个预定义的
CharSet
中的一个。 - 可以指定代表要使用字符的字符串。
上述最后一个选项如果字符字符串不适用于创建 CharSet
,将会抛出 EntropyStringError
。
do {
try entropy.use("abcdefg")
}
catch {
print(error)
}
invalidCharCount
do {
try entropy.use("01233210")
}
catch {
print(error)
}
charsNotUnique
效率
为了有效地生成随机字符串,EntropyString
生成每个字符串所需的随机字节数,并使用那些字节通过位移方案在字符集中索引。例如,要从 charset32 字符集中的 32 个字符生成字符串,每个索引需要在 [0,31]
范围内。因此,生成 charset32 字符串被简化为生成 [0,31]
范围内的随机索引序列。
为了生成索引,EntropyString
只从随机字节中截取足够的位来创建每个索引。在当前的例子中,需要5位来在范围[0,31]
中创建索引。EntropyString
一次处理5位字节来创建索引。第一个索引来自第一个字节的第一个5位,第二个索引来自第一个字节的最后3位和第二个字节的第一个2位,依此类推,以系统性地截取字节以形成字符集中的索引。因为位移和字节值相加运算非常高效,所以这种方案相当快。
EntropyString
方案在所用随机数数量方面也非常高效。考虑以下常见的Swift生成随机字符串的解决方案。为了生成一个字符,使用arc4random_uniform
在可用字符的索引中创建索引。代码看起来像是:
for _ in 0 ..< len {
let offset = Int(arc4random_uniform(charCount))
let index = chars.index(chars.startIndex, offsetBy: offset)
let char = chars[index]
string += String(char)
}
在上面的代码中,arc4random_uniform
每次生成32位随机数,作为UInt32
返回。该返回值用于创建索引。假设我们正在创建长度为16、字符数为32的字符串。生成每个字符串字符消耗32位随机数,而只将5位(log2(32)
)的熵注入到最终的随机字符串中。结果字符串的携带信息容量为16 * 5 = 80位,因此创建每个字符串需要整个总共 512位随机数,而实际上只有80位熵。这意味着生成的随机数中,有432位(84%)只是浪费了。
对比一下上面的EntropyString
方案。在上述例子中,每次切割5位需要总共80位(10字节)。创建上述相同的字符串,EntropyString
每个字符串使用80位随机数,没有浪费的位。一般来说,EntropyString
方案每个字符串可以浪费高达7位,但这是最坏的情况,并且是每个字符串,而不是每字符!
幸运的是,您无需真正理解如何高效地切割和拼接字节以获取字符串。但您可能想了解使用的是Secure Bytes,这是下一个话题。
Secure Bytes
如效率部分所述,EntropyString
使用一个基础的字节数组来生成字符串。结果字符串的熵当然直接与所用字节的随机性相关。这一点很重要。字符串只能携带信息(熵);实际的随机字节提供了熵本身。
EntropyString
自动生成创建随机字符串所需的字节。在Apple操作系统上,EntropyString
使用SecRandomCopyBytes
或arc4random_buf
中的一种,这两者都是加密安全的随机数生成器。SecRandomCopyBytes
是两者中较强的一个,但如果系统熵池缺乏足够的随机性,则可能失败。而不是传播这个失败,如果SecRandomCopyBytes
失败,则EntropyString
回退并使用arc4random_buf
来生成字节。虽然不这么强,但arc4random_buf
不会失败。
当然,您可能想知道何时或是否会出现SecRandomCopyBytes
失败的情况。Entropy.string(bits:secRand)
提供了一个inout
参数secRand
,如果SecRandomCopyBytes
调用失败,它将作为标志。
在 Linux 操作系统中,EntropyString
总是使用 arc4random_buf
。参数 secRand
被忽略。
import EntropyString
let entropy = Entropy()
var secRand = true
entropy.string(bits: 20, secRand: &secRand)
secRand: true
如果使用 SecRandomCopyBytes
,则 secRand 参数将保持为 true
;否则将设置为 false
。
您也可以传入 secRand 为 false
,在这种情况下,entropy
调用将不会尝试使用 SecRandomCopyBytes
,而是使用 arc4random_buf
。
secRand = false
entropy.string(bits: 20, secRand: &secRand)
而不是让 EntropyString
自动生成字节,您可以为创建字符串提供自己的 自定义字节,这是下一节内容。
自定义字节
如 安全字节 中所述,EntropyString
自动使用 SecRandomCopyBuf
或 arc4random_buf
生成随机字节。这些函数很好,但您可能需要为确定性测试或使用专门的字节生成器提供自己的字节。函数 entropy.string(bits:using)
允许指定自己的字节来创建字符串。
假设我们想要一个可以使用 32 个字符表示 30 比特熵的字符串。我们传入 4 个字节(以覆盖 30 比特)
import EntropyString
let entropy = Entropy()
let bytes: [UInt8] = [250, 200, 150, 100]
let string = try! entropy.string(bits: 30, using: bytes)
print("String: \(string)\n")
字符串:Th7fjL
提供的字节可以来自任何来源。但是,如果在 效率 部分中描述的生成字符串数量不足以生成字符串,则会引发 EntropyStringError.tooFewBytes
。
do {
try entropy.string(bits: 32, using: bytes)
}
catch {
print(error)
}
错误:tooFewBytes
请注意,所需字节的数量取决于字符集的字符数。对于熵的字符串表示,我们只能得到字符的熵比特数的倍数。在上面的示例中,每个字符代表 5 比特熵。所以我们不能得到正好 32 比特,我们通过每个字符的比特数向上舍入到总比特数 35。我们需要 5 个字节(40 比特),而不是 4 个(32 比特)。
CharSet.bytes_needed(bits)
可以用来确定给定字符集需要多少字节来覆盖指定量的熵。
let bytes_needed = entropy.charset.bytesNeeded(bits: 32)
print("\nBytes needed: \(bytes_needed)\n")
所需字节:5
熵比特
到目前为止,我们避免了计算所需熵比特数以指定一些随机字符串将不会重复的风险的数学。如 概览 中所述,帖子 哈希碰撞概率 基于著名的 生日问题 推导出一个表达式,用于计算使用输出为 M
比特的完美哈希在某个数量(表示为 k
)的哈希中发生碰撞的概率.
与所引用的帖子中的方程式相比,此方程式有两个细微的调整。M
用于总的可能的哈希数,并通过明确指定帖子中的表达式约等于 1/n
来形成方程式。
更重要的是,上述方程不在有利于我们熵字符串需求的格式中。该方程是为了特定数量的可能散列值而推导出来的,因此它产生了一个概率,这对于散列冲突来说是可接受的,但对于计算所需随机字符串的熵位并不完全正确。
我们将首先更改的是使用 M = 2^N
,其中 N
是熵位数。这仅仅是表述可能的字符串数量等于使用 N
位能表示的可能值的数量。
现在我们将方程修改以将 N
表示为 k
和 n
的函数
最后一行表示熵位数 N
作为潜在字符串数量 k
和 n
中 1 重复的风险的函数,这正是我们想要的。此外,方程的形式避免了在计算 N
时的真正大数,因为我们立即对每个大值 k
和 n
取对数。
升级到版本 3
EntropyString 版本 3 并没有引入任何新的功能。版本 3 发布的唯一目的是简化并加强 API。在这项工作中作出的向后不兼容的变更迫使进行了一次语义重大版本发布。
两个主要变更包括
- 将类
EntropyString.Random
替换为类EntropyString.Entropy
将所有new Random()
出现的地方改为new Entropy()
- 将所有
charSetNN
替换为charsetNN
将所有.charSetNN
出现的地方改为.charsetNN
例如,
import EntropyString
let bits = Entropy.bits(for: 10000, risk: 1.0e6)
let random = Random(.charSet16)
let string = random.sessionID()
变为
import EntropyString
let bits = Entropy.bits(for: 10000, risk: 1.0e6)
let entropy = Entropy(.charset16)
let string = entropy.sessionID()
TL;DR 2
要点
- 您不需要长度为 L 的随机字符串。
- 字符串长度是副产品,而不是目标。
- 您不需要真正唯一的字符串。
- 唯一性太强。您可以用概率唯一性就足够了。
- 概率唯一性包含测量风险。
- 风险以 "每 n 次机会生成重复的概率为 1" 来衡量。
- 熵位给出这个度量。
- 您需要总共 N 个具有重复风险 1/n 的字符串。
- 字符是任意的。
- 您需要
EntropyString
。
一百万个潜在字符串,重复概率为一千亿
import EntropyString
let entropy = Entropy()
let bits = Entropy.bits(for: 1.0e6, risk: 1.0e9)
let string = entropy.string(bits: bits)
DdHrT2NdrHf8tM