安全随机数
- 场景
在安全敏感的代码块(如算法实现时某些参数的生成,算法使用时密钥的产生,IV的生成,SessionID
的生成等场景),必须采用安全随机数。 - 重要性
密码学意义上的安全随机数产生器必须能够保证其产生的随机数很难被全部或部分预测到,否则会带来很大的安全隐患,使其成为整个安全链中最薄弱的一环。比如,如果密钥的产生使用了不安全的随机数,攻击者可以绕过算法本身直接去猜解密钥。 - 真随机数产生器
按照BSI
(德国信息安全办公室)分类标准,真随机数产生器分为物理真随机数产生器和非物理真随机数产生器。 - 物理真随机数产生器
其随机性来源于真正的物理随机过程,如热噪声,散粒噪声,二极管击穿熵、自由运行的振荡器等。各种商用硬件随机数产生器都属于物理真随机数产生器。 - 非物理真随机数产生器
其随机性来源于系统外部的随机事件,如磁盘I/O、中断、键盘鼠标操作时间等。 - 非物理真随机数产生器接口
已知的可使用的密码学安全的非物理真随机数产生器有: Linux
操作系统的/dev/random
设备接口Windows
操作系统的CryptGenRandom
接口- 产生安全随机数正常措施
- 直接使用真随机数产生器产生的随机数。
- 使用真随机数产生器产生的随机数做种子,输入密码学安全的伪随机数产生器产生密码学安全随机数。
GO
语言中的安全随机数
cryto/rand
包中提供了密码学安全的伪随机数生成器,它提供了Reader
变量,在Unix
系统中Reader
读取/dev/urandom
生成随机数、在Linux
系统中Reader
使用getrandom
生成随机数、在Windows
系统中Reader
使用CryptGenRandom
API生成随机数。
伪随机数
百度百科定义如下:
伪随机数是用确定性的算法计算出来自[0,1]均匀分布的随机数序列。 并不真正的随机,但具有类似于随机数的统计特征,如均匀性、独立性等。 在计算伪随机数时,若使用的初值(种子)不变,那么伪随机数的数序也不变。
即,伪随机数是通过一个固定的、可重现的计算方法产生的,如果不考虑这一点的话,伪随机数跟真随机数是没什么两样的。
在GoLang
中,"math/rand
" 包实现了伪随机数生成器。
代码
package main
import "fmt"
import "math/rand"
// import "crypto/rand"
func main() {
fmt.Println(rand.Intn(100)) // 产生0-100的随机整数
fmt.Println(rand.Intn(100)) // 产生0-100的随机整数
fmt.Println(rand.Intn(100)) // 产生0-100的随机整数
fmt.Println(rand.Intn(100)) // 产生0-100的随机整数
fmt.Println(rand.Intn(100)) // 产生0-100的随机整数
fmt.Println(rand.Intn(100)) // 产生0-100的随机整数
fmt.Println(rand.Float64()) // 产生0.0-1.0的随机浮点数
fmt.Println(rand.Float64()) // 产生0.0-1.0的随机浮点数
fmt.Println(rand.Float64()) // 产生0.0-1.0的随机浮点数
fmt.Println(rand.Float64()) // 产生0.0-1.0的随机浮点数
fmt.Println(rand.Float64()) // 产生0.0-1.0的随机浮点数
fmt.Println(rand.Float64()) // 产生0.0-1.0的随机浮点数
}
运行
可以看到,在第一次go run hello.go
的执行中,rand.Intn(100)
生成了81,87,47,59,81,10,从排列上来看,挺满足我们要的随机性需求的,同理rand.Float64()
也是,然而在第二次和第三次go run hello.go
的执行中,输出的结果跟第一次执行竟然完全一样(如果你现在在自己的电脑上运行一下上面代码也会跟我完全一样的结果),这的确让人费解,如果下一次运行跟上一次的输出轨迹一模一样,这种随机不是很容易被别人破解吗?而这样生成的随机数就是上文所述的伪随机数了。
随机种子的伪随机数
随机种子
我们在提到伪随机数的时候也有说到,“若使用的初值(种子)不变,那么伪随机数的数序也不变。”,这里的“初值”就是随机种子。
查看rand.Intn()
的源码:
继续查看r.Int31n()
的源码:
而r.Int31()
的默认执行源码如下:
可见,在"math/rand
" 包中,如果没有设置随机种子, Int()
函数自己初始化了一个 lockedSource
后产生伪随机数,并且初始化时随机种子被设置为1。因此不管重复执行多少次代码,每次随机种子都是固定值,输出的伪随机数数列也就固定了。
通常为了使得输出的数列达到密码学安全的伪随机,我们使用以时钟,输入输出等特殊节点作为参数来初始化随机种子。
代码
package main
import "fmt"
import "math/rand"
import "time"
// import "crypto/rand"
func main() {
s1 := rand.NewSource(27) // 用指定值创建一个随机数种子
r1 := rand.New(s1)
fmt.Println(r1.Intn(100)) // 产生0-100的随机整数
fmt.Println(r1.Intn(100)) // 产生0-100的随机整数
fmt.Println(r1.Float64()) // 产生0.0-1.0的随机浮点数
fmt.Println(r1.Float64()) // 产生0.0-1.0的随机浮点数
fmt.Println()
s2 := rand.NewSource(27) // 同前面一样的种子
r2 := rand.New(s2)
fmt.Println(r2.Intn(100)) // 产生0-100的随机整数
fmt.Println(r2.Intn(100)) // 产生0-100的随机整数
fmt.Println(r2.Float64()) // 产生0.0-1.0的随机浮点数
fmt.Println(r2.Float64()) // 产生0.0-1.0的随机浮点数
fmt.Println()
s3 := rand.NewSource(time.Now().UnixNano()) // 使用当前时间的时间戳作为随机种子
r3 := rand.New(s3)
fmt.Println(r3.Intn(100)) // 产生0-100的随机整数
fmt.Println(r3.Intn(100)) // 产生0-100的随机整数
fmt.Println(r3.Float64()) // 产生0.0-1.0的随机浮点数
fmt.Println(r3.Float64()) // 产生0.0-1.0的随机浮点数
fmt.Println()
s4 := rand.NewSource(time.Now().UnixNano()) // 使用当前时间的时间戳作为随机种子
r4 := rand.New(s4)
fmt.Println(r4.Intn(100)) // 产生0-100的随机整数
fmt.Println(r4.Intn(100)) // 产生0-100的随机整数
fmt.Println(r4.Float64()) // 产生0.0-1.0的随机浮点数
fmt.Println(r4.Float64()) // 产生0.0-1.0的随机浮点数
}
运行
可以看到,一开始设置固定随机种子42,因此输出数列不再是随机种子1的情况(即输出81,87等),然而由于随机种子是固定的,所以两次执行,四次调用的输出完全一致。
而用了当前时间戳作为随机种子(这样的随机种子又叫做活种)的四次调用输出完全不一致。
密码学安全的伪随机数生成器
cryto/rand
包中提供了密码学安全的伪随机数生成器,这个生成器在Windows
系统中使用CryptGenRandom
API生成随机数,这个API
可是“已知的可使用的密码学安全的非物理真随机数产生器”之一哇。
代码
package main
import "fmt"
import "crypto/rand"
func main() {
k := make([]byte, 32)
/** 【正确】使用rand.Read()生成安全随机数 **/
if _, err := rand.Read(k); err != nil {
fmt.Println("rand.Read() error : %v", err)
}
fmt.Println("rand.Read(): ", k)
}
执行
正如传闻所说,rand.Read()
很强大,能直接生成安全随机数。
那么问题来了,为什么用了“已知的可使用的密码学安全的非物理真随机数产生器”之一的cryto/rand
包不是真随机数生成器,而是密码学安全的“伪”随机数生成器呢????
其实,
我也母鸡。