目录
1 引言
WebRTC的rtc_base/helpers.h 和 rtc_base/helpers.cc提供了生成随机值的各种方法:
- CreateRandomString:生成一定长度的随机字符串;
- CreateRandomUuid:生成UUID字符串,"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"格式,其中’x’ is a hex digit, and ‘y’ is 8, 9, a or b;
- CreateRandomId && CreateRandomId64 && CreateRandomNonZeroId:生成随机整数ID值;
- CreateRandomDouble:创建随机浮点值。
这些多样随机值产生的方法都离不开RandomGenerator接口提供的基础功能。在rtc_base/helpers.cc文件中定义了RandomGenerator接口,也提供了两个实现类SecureRandomGenerator、TestRandomGenerator。由于后者是做测试用的,因此本文将不做分析。
2 RandomGenerator && SecureRandomGenerator
class RandomGenerator {
public:
virtual ~RandomGenerator() {}
virtual bool Init(const void* seed, size_t len) = 0;
virtual bool Generate(void* buf, size_t len) = 0;
};
// The OpenSSL RNG.
class SecureRandomGenerator : public RandomGenerator {
public:
SecureRandomGenerator() {}
~SecureRandomGenerator() override {}
bool Init(const void* seed, size_t len) override { return true; }
bool Generate(void* buf, size_t len) override {
return (RAND_bytes(reinterpret_cast<unsigned char*>(buf), len) > 0);
}
};
由上源码可知:随机数生成器提供了两个基本方法,初始化方法Init、生成一定长度随机值方法Generate。SecureRandomGenerator类的定义上方有一行英文注释:// The OpenSSL RNG. ,也即告知SecureRandomGenerator实现随机值的生成是利用的OpenSSL库中的方法。
- SecureRandomGenerator.Init():什么也做,也不会保存传入的初始种子。因为在OpenSSL库中会有跨平台的种子获取函数重新获取,不依赖于外部传入的初始种子。
- SecureRandomGenerator.Generate():利用RAND_bytes()来生成随机值。注意reinterpret_cast<unsigned char*>强制性地将一块无具体类型的数据块转为unsigned char*类型的数据块。
2.1 RAND_bytes——boringSSL库中的随机值生成函数
看本节标题会发现RAND_bytes()是boringSSL库的方法,说好的OpenSSL呢?其实,boringSSL脱胎于OpenSSL,是OpenSSL的一个分支,由Google开发的。可能是因为OpenSSL出过几次安全泄漏BUG,Google转而开分支自己维护从而形成了boringSSL库。Google很多项目包括WebRTC和Chrome都使用了boringSSL。比如RAND_bytes()就位于third_party/boringssl/src/crypto/fipsmodule/rand/rand.c文件,即WebRTC的第三方库boringssl中。
按理说,想要研究透随机值产生的原理需要更深的挖掘RAND_bytes()方法的代码以及其中包含的数学原理。但是,每个人的精力是有限的,或许以后有时间会这么做,当前姑且先放过自己吧。
RAND_bytes() 源码如下:
int RAND_bytes(uint8_t *out, size_t out_len) {
static const uint8_t kZeroAdditionalData[32] = {0};
RAND_bytes_with_additional_data(out, out_len, kZeroAdditionalData);
return 1;
}
3 InitRandom——初始化
我们的计算机系统是无法提供真随机数的,都是利用算法提供的伪随机序列(给同样的初值,后续将生成同样的随机值序列),因此,并不真正的随机。但是可以通过初始种子的随机性 和 伪随机数算法让产生的数看起来像是随机的。
算法就是上面boringSSL库RAND_bytes()提供的。而初始种子是调用InitRandom方法时外部提供的。其中的Rng()返回的就是一个全局唯一的SecureRandomGenerator对象,根据上面对SecureRandomGenerator的源码分析:外部传入的这个种子没有得到使用的。
bool InitRandom(int seed) {
return InitRandom(reinterpret_cast<const char*>(&seed), sizeof(seed));
}
bool InitRandom(const char* seed, size_t len) {
if (!Rng().Init(seed, len)) {
RTC_LOG(LS_ERROR) << "Failed to init random generator!";
return false;
}
return true;
}
3.1 WebRTC随机系统初始化时机
虽然外部种子没有得到使用,但是我们还是来看下WebRTC中何时调用的InitRandom(),传入的种子一般会给什么样的值:
bool PeerConnectionFactory::Initialize() {
RTC_DCHECK(signaling_thread_->IsCurrent());
rtc::InitRandom(rtc::Time32());
...
}
由源码可知:
rtc::InitRandom()方法会在PeerConnectionFactory.Initialize()方法中第一时间被调用,但请注意一点,在调用rtc::InitRandom()之前,都会先调用rtc::InitializeSSL(),确保SSL库先初始化完毕。毕竟,随机值系统是依赖于SSL库的。
另外,传入的初始化种子由rtc::Time32()产生,也即当前的系统时间,记住这个常用的做法。
4 随机字符串的生成
4.1 CreateRandomString
源码如下:
std::string CreateRandomString(size_t len) {
std::string str;
RTC_CHECK(CreateRandomString(len, &str));
return str;
}
bool CreateRandomString(size_t len, std::string* str) {
return CreateRandomString(len, kBase64, 64, str);
}
bool CreateRandomString(size_t len,
const std::string& table,
std::string* str) {
return CreateRandomString(len, table.c_str(), static_cast<int>(table.size()),
str);
}
static bool CreateRandomString(size_t len,
const char* table,
int table_size,
std::string* str) {
str->clear();
// table的长度必须能被256整除,比如base64,为什么是256?因为单个字符8bit,能表示的最大值就是256.
// 为了避免后续生成的字符串映射到table中字符时有偏模除法,此处做这个检测。
// Avoid biased modulo division below.
if (256 % table_size) {
RTC_LOG(LS_ERROR) << "Table size must divide 256 evenly!";
return false;
}
// 利用RandomGenerator生成len长度的随机串
std::unique_ptr<uint8_t[]> bytes(new uint8_t[len]);
if (!Rng().Generate(bytes.get(), len)) {
RTC_LOG(LS_ERROR) << "Failed to generate random string!";
return false;
}
// 若str的容量capacity不够len,则扩展std::string的capacity以容纳len长度的字符串
str->reserve(len);
// 让生成的随机串的每个字符都映射到table的中的一员,直接取余映射到表中,呼应一开始被256整除
for (size_t i = 0; i < len; ++i) {
str->push_back(table[bytes[i] % table_size]);
}
return true;
}
由上4个源码可知,最终的实现在于最后一个静态CreateRandomString方法,该方法依赖一个字符串table的入参。提供如此多的CreateRandomString方法,就是为了给这个table参数提供一个的默认实现,让一般的用户使用更方便;也为了增强灵活性,让用户可以自行提供自己想要table的实现。注意这种设计思路。
默认的table,是Base64算法提供的64个 可打印的 基础字符。关于Base64算法和应用,可见 https://baijiahao.baidu.com/s?id=1644892102150918183&wfr=spider&for=pc
static const char kBase64[64] = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
如同静态CreateRandomString源码上的注释所写,基本的思路就是利用RandomGenerator生成长度为目标长度的随机字符串,由于这些字符串的单个字符范围是0~255,在ASII表中并不都是可打印的字符,因此需要转为可打印的字符,也即此处提供的默认字符表——base64编码算法的基础字符表。这与WebRTC中CreateRandomString的用途有关系,很多时候都有打印这些随机串到日志的需求。
4.2 WebRTC中随机串的用途
- Candidate.id_:Candidate的标识id值为8位长字符串,采用rtc::CreateRandomString(8)产生;
- STUN协议的Bing Request请求的事务id:12位长随机字符串;
- ICE协议中的身份验证信息,uflag和pwd:分别为4位、24位随机字符串;
- 轨道id:8位随机串;
- CNAME:16位随机串;
- MediaTransportSettings.pre_shared_key:媒体传输设置的预设共享key长度为32位随机字符串。
5 随机UUID生成
5.1 CreateRandomUuid
源码如下:
// Version 4 UUID is of the form:
// xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
// Where 'x' is a hex digit, and 'y' is 8, 9, a or b.
std::string CreateRandomUuid() {
std::string str;
std::unique_ptr<uint8_t[]> bytes(new uint8_t[31]);
RTC_CHECK(Rng().Generate(bytes.get(), 31));
str.reserve(36);
for (size_t i = 0; i < 8; ++i) {
str.push_back(kHex[bytes[i] % 16]);
}
str.push_back('-');
for (size_t i = 8; i < 12; ++i) {
str.push_back(kHex[bytes[i] % 16]);
}
str.push_back('-');
str.push_back('4');
for (size_t i = 12; i < 15; ++i) {
str.push_back(kHex[bytes[i] % 16]);
}
str.push_back('-');
str.push_back(kUuidDigit17[bytes[15] % 4]);
for (size_t i = 16; i < 19; ++i) {
str.push_back(kHex[bytes[i] % 16]);
}
str.push_back('-');
for (size_t i = 19; i < 31; ++i) {
str.push_back(kHex[bytes[i] % 16]);
}
return str;
}
随机UUID的生成与随机串生成本质上没有区别,都是利用RandomGenerator来生成一定长度的串后,将串中的单个字符映射到对应的表中,从而形成自己想要的串。
随机UUID生成过程中用到两个表:kHex 和 kUuidDigit17
static const char kHex[16] = {'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
static const char kUuidDigit17[4] = {'8', '9', 'a', 'b'};
PS: UUID长度36位的串,但其中有5个字符是固定的:4个’-’ 和 1个’4’。因此,生成随机串时,只需要生成31位的,然后按位映射到"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"这个格式即可。
5.2 WebRTC中UUID的用途
- RtpSender && RtpReceiver的id标识;
- MediaStream的id标识;
6 随机整数的生成
6.1 CreateRandomId
源码如下:
uint32_t CreateRandomId() {
uint32_t id;
RTC_CHECK(Rng().Generate(&id, sizeof(id)));
return id;
}
相当于直接填充32位的buffer,然后强制按照无符号的32位整型进行解释。
6.2 CreateRandomId64
源码如下:
uint64_t CreateRandomId64() {
return static_cast<uint64_t>(CreateRandomId()) << 32 | CreateRandomId();
}
生成32位无符号随机整数;然后使用static_cast<uint64_t>强制显示的转换为64位无符号整数,之前的32位随机数将占据64位整数的低32位;然后进行左移32后占据高32位;然后生成新的32位无符号随机整数,进行位或,新的32位无符号随机整数将占据低32位。从而得到的64位无符号整数都是随机产生的。
有个疑问:为什么要如此绕着处理得到64位的值,而不是直接一次性随机产生64位的值呢?
6.3 CreateRandomNonZeroId
uint32_t CreateRandomNonZeroId() {
uint32_t id;
do {
id = CreateRandomId();
} while (id == 0);
return id;
}
显而易见,不做多余的解释
6.4 WebRTC中随机整数的用途
- 发送STUN PING的ConnectionRequest请求所在的连接对象Connection的32位唯一标识id值,使用CreateRandomId()来产生。
- WebRTC的64位会话id,即PeerConnection.session_id_使用CreateRandomId64()来产生。
- WebRTC的内部jsep会话控制器对象的ice_tiebreaker_使用CreateRandomId64()来产生。
- RTP时钟RtpClock的初始序列号以及初始时间值都是采用CreateRandomNonZeroId()产生的32位无符号随机值,然后在该随机值上进行递增的。
7 随机浮点数的生成
源码如下:
double CreateRandomDouble() {
return CreateRandomId() / (std::numeric_limits<uint32_t>::max() +
std::numeric_limits<double>::epsilon());
}
获取32位无符号整型随机数、32位无符号整型最大值、double类型的比1.0略多的一个浮点数。
然后先都转换成浮点数,进行运算,得到随机浮点数double。
目前WebRTC源码中尚无使用。
PS: 生成64位随机无符号整数、生成double类型的随机浮点数时,为什么不直接生成64位的随机值,然后再进行转换了?当前做法是生成32位的无符号随机整数,然后再向64位的随机整数/double浮点数转换?是因为ssl库中直接生成64位随机数所耗性能 >两次生成32位随机数?——这个留待以后有时间去研究RAND_bytes()方法实现原理再来深究吧
8 总结
行文至此,大致将WebRTC的基础模块rtc_base/helpers.cc下的各类随机值生成方法介绍完毕了。回顾下要点:
- RandomGenerator && SecureRandomGenerator是提供随机值的重要接口和实现类。本质上,随机值的产生依赖于底层的ssl库——boringSSL。因此,在初始化随机值系统之前,即调用rtc::InitRandom()之前需要确保调用了rtc::InitializeSSL();
- 生成随机字符串 && UUID过程中,先利用RandomGenerator生成一定长度的uint8_t[len]数组,然后挨个将数组中的字符映射到某个字符表中,比如随机字符串生成默认映射到base64的字符表中。这样使得生成的字符串 && UUID称为可打印的。
- 生成整数 && 浮点数过程中有个疑问:即在生成64位随机无符号整数、生成double类型的随机浮点数时,为什么不直接生成64位的随机值,然后再进行转换了?当前做法是生成32位的无符号随机整数,然后再向64位的随机整数/double浮点数转换?是因为ssl库中直接生成64位随机数所耗性能 >两次生成32位随机数?