SessionID
的生成等场景),必须采用安全随机数。BSI
(德国信息安全办公室)分类标准,真随机数产生器分为物理真随机数产生器和非物理真随机数产生器。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()
的源码:
可见,在”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
包不是真随机数生成器,而是密码学安全的“伪”随机数生成器呢????
其实,
我也母鸡。