开源、先进、易用加密库 Libsodium 编程指南
💡 1. 简介
Libsodium 是一个开源、跨平台、跨语言的加密库,提供了一组简单易用的函数,大大简化了加密、散列、签名、鉴别、解密等复杂工作。支持许多种主流的加密算法和散列算法,包括 AES256-GCM 和 ChaCha20-Poly1305 两种 AEAD 加密方案。此外还提供了一系列方便实用的函数,可完成随机数的生成、大数的计算、编码和解码等辅助性工作。
源码:GitHub | 码云 文档:GitHub | 码云 | 官网🧱 2. 起步
执行以下命令,完成 Libsodium 的下载、解压、编译和安装。
$ yum -y groupinstall "Development Tools" # apt install -y build-essential
$ wget -N --no-check-certificate https://download.libsodium.org/libsodium/releases/libsodium-1.0.17.tar.gz
$ tar -zxf libsodium-1.0.17.tar.gz
$ cd libsodium-1.0.17
$ ./configure
$ make && make check
$ make install
Libsodium 的动态链接库 lib*.so*
位于 /usr/local/lib
目录中。须将此目录设为动态库的搜寻目录之一,否则依赖于 Libsodium 的程序将无法运行。
$ echo "/usr/local/lib" > /etc/ld.so.conf.d/usr-local-lib.conf
$ ldconfig
在 /usr/local/lib/pkgconfig
目录中,可以找到文件 libsodium.pc
。为了能够通过命令 pkg-config
获取编译和链接所需参数,须将此文件复制到 pkg-config
命令的搜寻目录中。
$ cp /usr/local/lib/pkgconfig/libsodium.pc /usr/share/pkgconfig/
通过命令 pkg-config
获取编译和链接所需参数。
$ pkg-config --cflags libsodium
-I/usr/local/include
$ pkg-config --libs libsodium
-L/usr/local/lib -lsodium
如果使用了 make,应当在 Makefile
文件中使用上述两条命令。
CFLAGS = $(pkg-config --cflags libsodium)
LDFLAGS = $(pkg-config --libs libsodium)
而在程序中,只需包含头文件 sodium.h
即可。
#include <sodium.h>
int main(void)
{
if (sodium_init() == -1) {
return 1;
}
...
}
在使用 Libsodium 的其他函数之前,必须先调用函数 sodium_init()
。该函数不需要任何参数,返回 0
表示成功,返回 -1
表示失败,返回 1
则表示已经初始化过了。
📖 3. 知识储备
3.1 两种密码体制
当前有两种密码体制:一种称为对称密钥密码体制;另一种称为公钥密码体制。
在对称密钥密码体制中,加密和解密使用相同的密钥。密钥由通信双方事先约定。算法可以公开,而密钥需要保密。
公钥密码体制在加密和解密过程中使用不同的密钥。并且使用其中一个进行加密,则需要用另一个才能解密。这两个成对的密钥在使用时,一个密钥作为私钥,需要保密;另一个密钥作为公钥,可以公开。
3.2 对称加密算法
对称加密算法分为分组密码(又称块加密)和序列密码(又称流密码、流加密)两种。
著名的分组密码:DES、AES 常用的流密码:Salsa20、ChaCha203.3 MAC 报文鉴别码
MAC 是 Message Authentication Code 的缩写,即报文鉴别码。通常是经过加密的散列值。计算报文鉴别码的算法称为 MAC 算法。常用的 MAC 算法有:
GMAC CBC-MAC Poly13053.4 AE
AE 是 Authenticated encryption 的缩写。顾名思义,这种加密方案不仅能提供机密性,还能提供完整性。任何伪造或篡改都会被发现。
AE 实际上是对称加密算法和 MAC 算法的结合体。在加密一个报文时,需要一个密钥和一个不重数,加密后将得到密文和一个报文鉴别码。报文鉴别码须随同密文一起发送给接收方。
接收方收到报文鉴别码和密文后,须用相同的密钥和不重数才能进行解密。任何对密文和报文鉴别码的篡改都会导致解密失败。当然,只要确保密钥没有泄露,其他人也无法伪造出合法的密文和相应的报文鉴别码。
不重数,即不重复的数,通常是从 0 开始递增的计数器,不需要保密。密钥和不重数的结合,相当于一次一密,能有效抵御「重放攻击」。
3.5 AEAD
AEAD 是 Authenticated Encryption with Additional Data 的缩写。相比于 AE,AEAD 在加、解密时还可以选择性地给定一些没有保密性要求的「附加数据」,例如版本号、时间戳、报文的长度和编码方式等。这些附加数据会参与到报文鉴别码的计算中去,但不会被加密,也不会成为密文的一部分。附加数据可以随同密文一起发送。
常用的 AEAD 有以下两种:
AES256-GCM ChaCha20-Poly1305Intel 在 2008 年推出新的指令集——AES-NI,为 AES 算法提供了硬件层面上的支持。但在其他平台(ARM)上,针对移动互联网优化的 ChaCha20 的速度大约是 AES 的三倍。
ChaCha20-Poly1305 最初在 2014 年提出,在 2015 年成为 IETF 标准,即 ChaCha20-Poly1305-IETF。后来,又通过对 ChaCha20 的改进,形成 XChaCha20-Poly1305-IETF。这一版本有望成为新的 IETF 标准,也是 Libsodium 目前首推的加密方案。
不同 AEAD 的密钥、不重数和报文鉴别码的长度(单位:位):
AEAD Key 密钥 Nonce 不重数 MAC 报文鉴别码 AES256-GCM 256 96 128 ChaCha20-Poly1305 256 64 128 ChaCha20-Poly1305-IETF 256 96 128 XChaCha20-Poly1305-IETF 256 192 128🔐 4. Libsodium 对 ChaCha20-Poly1305 的支持
Libsodium 为 ChaCha20-Poly1305 的三种版本分别提供了三组函数:
crypto_aead_chacha20poly1305_*()
crypto_aead_chacha20poly1305_ietf_*()
crypto_aead_xchacha20poly1305_ietf_*()
这三组函数在用法上完全一致,因此只要掌握了其中一种,自然也就掌握了其余两种。
4.1 加密
int crypto_aead_xchacha20poly1305_ietf_encrypt_detached(unsigned char *c,
unsigned char *mac,
unsigned long long *maclen_p,
const unsigned char *m,
unsigned long long mlen,
const unsigned char *ad,
unsigned long long adlen,
const unsigned char *nsec,
const unsigned char *npub,
const unsigned char *k);
函数 crypto_aead_xchacha20poly1305_ietf_encrypt_detached()
使用密钥 k
和
不重数 npub
对 mlen
字节的报文 m
进行加密,并根据密文和 adlen
字节的附加数据 ad
计算报文鉴别码。密文将被写到 c
,而报文鉴别码将被写到 mac
,maclen
会被设为 mac
的长度。
密文和明文等长。而密钥、不重数、报文鉴别码的长度都是固定的,它们分别等于:
crypto_aead_xchacha20poly1305_ietf_KEYBYTES
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES
crypto_aead_xchacha20poly1305_ietf_ABYTES
若没有关联的数据,则把 ad
设为 NULL
,并把 adlen
设为 0。
此处nsec
必须始终设为NULL
,下同。
#include <sodium.h>
int main(void)
{
if (sodium_init() == -1) {
return 1;
}
// 密钥
unsigned char k[crypto_aead_xchacha20poly1305_ietf_KEYBYTES] = "123456";
// 不重数
unsigned char npub[crypto_aead_xchacha20poly1305_ietf_NPUBBYTES] = {0};
// 明文
int mlen = 5;
unsigned char m[6] = "hello";
// 附加的数据
int adlen = 4;
unsigned char ad[5] = "2020";
// 密文
unsigned char c[6];
// 报文鉴别码
unsigned char mac[crypto_aead_xchacha20poly1305_ietf_ABYTES];
unsigned long long maclen;
// 加密
crypto_aead_xchacha20poly1305_ietf_encrypt_detached(c,
mac, &maclen,
m, mlen,
ad, adlen, NULL,
npub, k);
// 获取密文和报文鉴别码的十六进制表示
char buf[1024];
sodium_bin2hex(buf, sizeof buf, c, 5);
printf("Ciphertext: %s\n", buf);
sodium_bin2hex(buf, sizeof buf, mac, maclen);
printf("MAC: %s\n", buf);
return 0;
}
Ciphertext: 5abc40d737
MAC: 0be7cd4beaf9ec2a063170aab65fa5aa
函数
sodium_bin2hex()
是 Libsodium 提供的「辅助函数」,具体用法详见下文。
在密钥不变的情况下,不重数必须每次都不一样。建议用 randombytes_buf()
函数产生第一条报文的不重数,再用 sodium_increment()
函数对其进行递增。
4.2 解密
解密必须提供相同的密钥 k
、不重数 npub
和附加数据 ad
。
int crypto_aead_xchacha20poly1305_ietf_decrypt_detached(unsigned char *m,
unsigned char *nsec,
const unsigned char *c,
unsigned long long clen,
const unsigned char *mac,
const unsigned char *ad,
unsigned long long adlen,
const unsigned char *npub,
const unsigned char *k);
函数 crypto_aead_xchacha20poly1305_ietf_decrypt_detached()
首先验证 c
中包含的 tag
是否合法。若函数返回 -1
表示验证未通过;若验证通过,则返回 0
,并将解密得到的报文写到 m
。
#include <sodium.h>
int main(void)
{
if (sodium_init() == -1) {
return 1;
}
// 密钥
unsigned char k[crypto_aead_xchacha20poly1305_ietf_KEYBYTES] = "123456";
// 不重数
unsigned char npub[crypto_aead_xchacha20poly1305_ietf_NPUBBYTES] = {0};
// 明文
unsigned char m[6];
// 附加的数据
int adlen = 4;
unsigned char ad[5] = "2020";
// 密文
int clen = 5;
unsigned char c[6];
sodium_hex2bin(c, clen, "5abc40d737", 10, NULL, NULL, NULL);
// 报文鉴别码
unsigned char mac[crypto_aead_xchacha20poly1305_ietf_ABYTES];
sodium_hex2bin(mac, crypto_aead_xchacha20poly1305_ietf_ABYTES,
"0be7cd4beaf9ec2a063170aab65fa5aa", 32, NULL, NULL, NULL);
// 解密
crypto_aead_xchacha20poly1305_ietf_decrypt_detached(m,
NULL,
c, clen,
mac,
ad, adlen,
npub, k);
printf("Message: %s\n", m);
return 0;
}
Message: hello
4.3 合并模式
以上这种将密文和报文鉴别码分开储存的方式称为分开模式。由于大多数需求都是将报文鉴别码直接追加到密文后面,即合并模式。因此,Libsodium 实际上为每种 AEAD 方案都提供两组函数:一组实现分开模式;另一组实现合并模式。
为合并模式设计的函数,相比于分开模式的函数,函数名少了后缀 _detached
。
int crypto_aead_xchacha20poly1305_ietf_encrypt(unsigned char *c,
unsigned long long *clen_p,
const unsigned char *m,
unsigned long long mlen,
const unsigned char *ad,
unsigned long long adlen,
const unsigned char *nsec,
const unsigned char *npub,
const unsigned char *k);
密钥、不重数、附加数据、明文等参数的含义同上。在合并模式下,报文鉴别码直接追加到密文后面,因此减少了 mac
和 maclen
两个参数,但参数 c
必须为报文鉴别码预留存储空间。
#include <sodium.h>
int main(void)
{
if (sodium_init() == -1) {
return 1;
}
// 密钥
unsigned char k[crypto_aead_xchacha20poly1305_ietf_KEYBYTES] = "123456";
// 不重数
unsigned char npub[crypto_aead_xchacha20poly1305_ietf_NPUBBYTES] = {0};
// 明文
int mlen = 5;
unsigned char m[6] = "hello";
// 附加的数据
int adlen = 4;
unsigned char ad[5] = "2020";
// 密文
unsigned char c1[6];
unsigned char c2[6 + crypto_aead_xchacha20poly1305_ietf_ABYTES];
unsigned long long clen;
// 报文鉴别码
unsigned char mac[crypto_aead_xchacha20poly1305_ietf_ABYTES];
unsigned long long maclen;
// 加密(分开模式)
crypto_aead_xchacha20poly1305_ietf_encrypt_detached(c1,
mac, &maclen,
m, mlen,
ad, adlen, NULL,
npub, k);
char buf[1024];
sodium_bin2hex(buf, sizeof buf, c1, 5);
printf("Ciphertext: %s\n", buf);
sodium_bin2hex(buf, sizeof buf, mac, maclen);
printf("MAC: %s\n", buf);
// 加密(合并模式)
crypto_aead_xchacha20poly1305_ietf_encrypt(c2,
&clen,
m, mlen,
ad, adlen, NULL,
npub, k);
sodium_bin2hex(buf, sizeof buf, c2, clen);
printf("Ciphertext: %s\n", buf);
return 0;
}
Ciphertext: 5abc40d737
MAC: 0be7cd4beaf9ec2a063170aab65fa5aa
Ciphertext: 5abc40d7370be7cd4beaf9ec2a063170aab65fa5aa
🔑 5. 密钥的派生
在实际应用中,不应从始至终都使用同一个密钥,更不能直接使用密码(通常是简短的字符串)作为密钥,否则很容易遭受「字典攻击」。应当为每次会话专门准备一个子密钥。这就需要一种能够产生大量子密钥的机制。
5.1 KDF
KDF 是 Key Derivation Function 的缩写,即密钥派生函数。能够满足上述需求。这类函数通过引入随机数、增加散列迭代次数,增加暴力破解难度。常用的 KDF 有:
PBKDF2 Scrypt Argon2Argon2 是最新的算法,也是 Libsodium 首推及其底层默认使用的算法。
5.2 基于密码派生密钥
根据给定的密码和一个长度固定的随机数生成指定长度的密钥。
int crypto_pwhash(unsigned char * const out,
unsigned long long outlen,
const char * const passwd,
unsigned long long passwdlen,
const unsigned char * const salt,
unsigned long long opslimit,
size_t memlimit, int alg);
函数 crypto_pwhash()
根据 passwdlen
字节的密码 passwd
和 crypto_pwhash_SALTBYTES
字节的随机数 salt
派生出 outlen
字节的密钥并储存到 out
中。全部参数相同时,生成相同的密钥。
passwdlen
outlen
最小值
crypto_pwhash_PASSWD_MIN
crypto_pwhash_BYTES_MIN
最大值
crypto_pwhash_PASSWD_MAX
crypto_pwhash_BYTES_MAX
倒数两个参数 opslimit
和 memlimit
与性能和内存占用有关,取值如下:
opslimit
memlimit
最小值
crypto_pwhash_OPSLIMIT_MIN
crypto_pwhash_MEMLIMIT_MIN
较快/小
crypto_pwhash_OPSLIMIT_INTERACTIVE
crypto_pwhash_MEMLIMIT_INTERACTIVE
中等
crypto_pwhash_OPSLIMIT_MODERATE
crypto_pwhash_MEMLIMIT_MODERATE
较慢/大
crypto_pwhash_OPSLIMIT_SENSITIVE
crypto_pwhash_MEMLIMIT_SENSITIVE
最大值
crypto_pwhash_OPSLIMIT_MAX
crypto_pwhash_MEMLIMIT_MAX
最后一个参数 alg
决定选用的算法,只有下列 3 种取值可选:
crypto_pwhash_ALG_DEFAULT
Libsodium 推荐的选项。
crypto_pwhash_ALG_ARGON2I13
Argon2i 1.3。
crypto_pwhash_ALG_ARGON2ID13
Argon2id 1.3。
函数返回 0
表示成功;返回 -1
表示失败(这通常是由于操作系统拒绝分配请求的内存)。
#include <sodium.h>
int main(void)
{
if (sodium_init() == -1) {
return 1;
}
// 密码
unsigned char passwd[] = "secret";
// 长度固定的随机数
unsigned char salt[crypto_pwhash_SALTBYTES] = {0};
// 密钥
unsigned char key[16];
crypto_pwhash(key, sizeof key, passwd, strlen(passwd), salt,
crypto_pwhash_OPSLIMIT_INTERACTIVE,
crypto_pwhash_MEMLIMIT_INTERACTIVE,
crypto_pwhash_ALG_DEFAULT);
char buf[1024];
sodium_bin2hex(buf, sizeof buf, key, sizeof key);
printf("key: %s\n", buf);
return 0;
}
key: a5c2d5ca23026834f7ff177fb8137b62
5.3 基于主密钥派生子密钥
根据一个主密钥生成多个子密钥。Libsodium 专门为此提供了两个函数 crypto_kdf_*()
。
这两个函数可以根据一个主密钥 key
和一个被称为上下文的参数 ctx
派生出 2^64 个密钥,并且单个子密钥的长度可以在 128(16 字节)到 512 位(64 字节)之间。
void crypto_kdf_keygen(uint8_t key[crypto_kdf_KEYBYTES]);
函数 crypto_kdf_keygen()
的作用是生成一个主密钥。
int crypto_kdf_derive_from_key(unsigned char *subkey, size_t subkey_len,
uint64_t subkey_id,
const char ctx[crypto_kdf_CONTEXTBYTES],
const unsigned char key[crypto_kdf_KEYBYTES]);
函数 crypto_kdf_derive_from_key()
可以根据主密钥 key
和上下文 ctx
派生出长度为 subkey_len
字节的子密钥。subkey_id
是子密钥的编号,可以是不大于 2^64 - 1
的任意值。
主密钥的长度必须是 crypto_kdf_KEYBYTES
。子密钥的长度 subkey_len
必须介于 crypto_kdf_BYTES_MIN
(含)和 crypto_kdf_BYTES_MAX
(含)之间。
上下文 ctx
是一个 8 字符的字符串,应能描述子密钥的用途。不需要保密,并且强度可以很低。比如 "UserName"
、"__auth__"
、"pictures"
和 "userdata"
等。但其长度必须是 crypto_kdf_CONTEXTBYTES
字节。
使用相同的密钥,但使用不同的 ctx
,就会得到不同的输出。正如其名,ctx
可以和程序的上下文对应。当然,就算一个程序从头到尾只使用一个 ctx
,那也有防止密钥被不同程序重复使用的作用。
#include <sodium.h>
int main(void)
{
if (sodium_init() == -1) {
return 1;
}
char ctx[] = "Examples";
uint8_t master_key[crypto_kdf_KEYBYTES];
uint8_t subkey1[16];
uint8_t subkey2[16];
uint8_t subkey3[32];
// 创建主密钥
crypto_kdf_keygen(master_key);
// 派生子密钥
crypto_kdf_derive_from_key(subkey1, sizeof subkey1, 1, ctx, master_key);
crypto_kdf_derive_from_key(subkey2, sizeof subkey2, 2, ctx, master_key);
crypto_kdf_derive_from_key(subkey3, sizeof subkey3, 3, ctx, master_key);
// 获取子密钥的十六进制表示
char buf[1024];
sodium_bin2hex(buf, sizeof buf, subkey1, sizeof subkey1);
printf("subkey1: %s\n", buf);
sodium_bin2hex(buf, sizeof buf, subkey2, sizeof subkey2);
printf("subkey2: %s\n", buf);
sodium_bin2hex(buf, sizeof buf, subkey3, sizeof subkey3);
printf("subkey3: %s\n", buf);
return 0;
}
subkey1: 0440b65332dc5f6b4a46d262996af08e
subkey2: 73e6d9bbfb25c25d3898ba435f16b710
subkey3: 9fdffff7fd9d4ba7a8b1172c79cdf86b7a823256b418e9a61cb8e21f1170ef1f
🔩 6. 辅助函数
尽可能使用这些函数,以抵御「时序攻击」。
6.1 测试字节序列
sodium_memcmp()
函数 sodium_memcmp()
可完成两个等长字节序列的对比。
int sodium_memcmp(const void * const b1_, const void * const b2_, size_t len);
如果位于 b1_
的 len
个字节和位于 b2_
的 len
个字节相同,函数返回 0
,否则返回 -1
。
char b1_[6] = "hello";
char b2_[6] = "hello";
char b3_[6] = "Hello";
if (sodium_memcmp(b1_, b2_, 5) == -1) {
puts("No match");
} else {
puts("Match");
}
if (sodium_memcmp(b1_, b3_, 5) == -1) {
puts("No match");
} else {
puts("Match");
}
Match
No match
sodium_is_zero()
函数 sodium_is_zero()
可判断给定的字节序列是否全为 0
。
int sodium_is_zero(const unsigned char *n, const size_t nlen);
若位于 n
的 nlen
个字节是否全为 0
,则返回 1
,否则返回 0
。
6.2 字节序列的十六进制表示
sodium_bin2hex()
函数 sodium_bin2hex()
可获取字节序列的十六进制表示,并由此得到一个字符串。
char *sodium_bin2hex(char * const hex, const size_t hex_maxlen,
const unsigned char * const bin, const size_t bin_len);
函数将字符串写到 hex
,这个字符串就是从 bin
开始的 bin_len
个字节的十六进制表示,包括 '\0'
,故 hex_maxlen
至少为 2*bin_len + 1
。该函数始终返回 hex
。
char hex[9]; // 2*4 + 1 = 9
char bin[5] = "AAAA";
sodium_bin2hex(hex, 9, bin, 4);
puts(hex);
41414141
sodium_hex2bin()
函数 sodium_hex2bin()
作用相反,通过解析字节序列的十六进制表示,还原该字节序列。
int sodium_hex2bin(unsigned char * const bin, const size_t bin_maxlen,
const char * const hex, const size_t hex_len,
const char * const ignore, size_t * const bin_len,
const char ** const hex_end);
函数将字节序列写到 bin
。bin_maxlen
表示允许写入的最大字节数。而位于 hex
的字符串应当是一个字节序列的十六进制表示,可以没有 '\0'
结尾,需要解析的长度由 hex_len
指定。
ignore
是需要跳过的字符组成的字符串。比如 ": "
表示跳过冒号和空格。此时 "69:FC"
、"69 FC"
、"69 : FC"
和 "69FC"
都视为合法的输入,并产生相同的输出。ignore
可以设为 NULL
,表示不允许任何非法的字符出现。
函数返回 0
表示转换成功,同时 bin_len
会被设为解析得到的字节数;返回 -1
则表示失败。失败的情况有以下两种:
bin_maxlen
字节; 遇到非法字符时,如果前面的字符都能顺利解析,函数仍然返回
0
,否则返回
-1
。
无论如何 hex_end
总是会被设为下一个待解析的字符的地址。
char bin[5] = {0};
char hex[12] = "61*62636472";
size_t bin_len = 0;
const char * hex_end;
sodium_hex2bin(bin, 4, hex, 9, "*", &bin_len, &hex_end);
printf("%d: %s, %c\n", bin_len, bin, *hex_end);
4: abcd, 7
6.3 Base64 编码/解码
sodium_bin2base64()
函数 sodium_bin2base64()
可获取字节序列的 Base64 编码。
char *sodium_bin2base64(char * const b64, const size_t b64_maxlen,
const unsigned char * const bin, const size_t bin_len,
const int variant);
Base64 编码有多种变体,采用哪种变体由 variant
指定,有下列 4 种取值可选:
sodium_base64_VARIANT_ORIGINAL
sodium_base64_VARIANT_ORIGINAL_NO_PADDING
sodium_base64_VARIANT_URLSAFE
sodium_base64_VARIANT_URLSAFE_NO_PADDING
这些 Base64 编码并不提供任何形式的加密;就像十六进制编码一样,任何人都可以对它们进行解码。
可以令 b64_maxlen
等于宏 sodium_base64_ENCODED_LEN(BIN_LEN, VARIANT)
,它表示使用 VARIANT
这种变体时,BIN_LEN
个字节的 Base64 编码(包括 '\0'
)的最小长度。
char bin[6] = "hello";
int b64_len = sodium_base64_ENCODED_LEN(5, sodium_base64_VARIANT_ORIGINAL);
char b64[b64_len];
sodium_bin2base64(b64, b64_len, bin, 5, sodium_base64_VARIANT_ORIGINAL);
printf("%d: %s\n", b64_len, b64);
9: aGVsbG8=
sodium_base642bin()
函数 sodium_base642bin()
可完成 Base64 解码工作。
int sodium_base642bin(unsigned char * const bin, const size_t bin_maxlen,
const char * const b64, const size_t b64_len,
const char * const ignore, size_t * const bin_len,
const char ** const b64_end, const int variant);
返回 -1
表示错误,返回 0
表示解码成功,同时 bin_len
会被设为解码得到的字节数,其他参数的含义参考前文。
size_t bin_len;
char bin[6];
char b64[9] = "aGVsbG8=";
sodium_base642bin(bin, sizeof bin,
b64, strlen(b64), "", &bin_len, NULL,
sodium_base64_VARIANT_ORIGINAL);
printf("%d: %s\n", bin_len, bin);
5: hello
6.4 大数的计算
sodium_increment()
函数 sodium_increment()
用来递增一个任意长度的无符号数。
void sodium_increment(unsigned char *n, const size_t nlen);
位于 n
的 nlen
字节的数字将按小端字节序处理。加密算法中经常提到的不重数 nonce
就可用此函数进行递增。
unsigned char nonce[8] = {0};
sodium_increment(nonce, sizeof(nonce));
printf("%d\n", *(int *)nonce);
sodium_increment(nonce, sizeof(nonce));
printf("%d\n", *(int *)nonce);
sodium_increment(nonce, sizeof(nonce));
printf("%d\n", *(int *)nonce);
1
2
3
sodium_add()
函数 sodium_add()
可完成大数的加法。
void sodium_add(unsigned char *a, const unsigned char *b, const size_t len);
位于 a
和 b
的两个 nlen
字节的加数均按小端字节序的无符号数处理。计算结果将覆盖 a
。
unsigned char a[8] = {1};
unsigned char b[8] = {1};
printf("%d\n", *(int *)a);
sodium_add(a, b, sizeof a); // a = a + b
printf("%d\n", *(int *)a);
1
2
sodium_sub()
函数 sodium_sub()
可完成大数减法。
void sodium_sub(unsigned char *a, const unsigned char *b, const size_t len);
位于 a
和 b
的两个 nlen
字节的加数均按小端字节序的无符号数处理。计算结果将覆盖 a
。
sodium_compare()
函数 sodium_compare()
可完成两个大数的比较。两个大数均按小端字节序处理。
int sodium_compare(const void * const b1_, const void * const b2_, size_t len);
返回 0
表示相等,返回 -1
表示 b1_
小于 b2_
;返回 1
表示 b1_
大于 b2_
。
🔗 参考文献
libsodium 密码学库 中文文档 [现代密码学实践指南[2015年]](https://byronhe.com/post/2015... 【翻译】密码学一小时必知 加密解密学习笔记 密码学基础系列 实用密码学工具——KDF 如何存储密码(KDF) ldconfig命令 pkg-config 详解 什么是 AES-NI(AES指令集)去旅行
去旅行
宣传栏
目录
💡 1. 简介
Libsodium 是一个开源、跨平台、跨语言的加密库,提供了一组简单易用的函数,大大简化了加密、散列、签名、鉴别、解密等复杂工作。支持许多种主流的加密算法和散列算法,包括 AES256-GCM 和 ChaCha20-Poly1305 两种 AEAD 加密方案。此外还提供了一系列方便实用的函数,可完成随机数的生成、大数的计算、编码和解码等辅助性工作。
源码:GitHub | 码云 文档:GitHub | 码云 | 官网🧱 2. 起步
执行以下命令,完成 Libsodium 的下载、解压、编译和安装。
$ yum -y groupinstall "Development Tools" # apt install -y build-essential
$ wget -N --no-check-certificate https://download.libsodium.org/libsodium/releases/libsodium-1.0.17.tar.gz
$ tar -zxf libsodium-1.0.17.tar.gz
$ cd libsodium-1.0.17
$ ./configure
$ make && make check
$ make install
Libsodium 的动态链接库 lib*.so*
位于 /usr/local/lib
目录中。须将此目录设为动态库的搜寻目录之一,否则依赖于 Libsodium 的程序将无法运行。
$ echo "/usr/local/lib" > /etc/ld.so.conf.d/usr-local-lib.conf
$ ldconfig
在 /usr/local/lib/pkgconfig
目录中,可以找到文件 libsodium.pc
。为了能够通过命令 pkg-config
获取编译和链接所需参数,须将此文件复制到 pkg-config
命令的搜寻目录中。
$ cp /usr/local/lib/pkgconfig/libsodium.pc /usr/share/pkgconfig/
通过命令 pkg-config
获取编译和链接所需参数。
$ pkg-config --cflags libsodium
-I/usr/local/include
$ pkg-config --libs libsodium
-L/usr/local/lib -lsodium
如果使用了 make,应当在 Makefile
文件中使用上述两条命令。
CFLAGS = $(pkg-config --cflags libsodium)
LDFLAGS = $(pkg-config --libs libsodium)
而在程序中,只需包含头文件 sodium.h
即可。
#include <sodium.h>
int main(void)
{
if (sodium_init() == -1) {
return 1;
}
...
}
在使用 Libsodium 的其他函数之前,必须先调用函数 sodium_init()
。该函数不需要任何参数,返回 0
表示成功,返回 -1
表示失败,返回 1
则表示已经初始化过了。
📖 3. 知识储备
3.1 两种密码体制
当前有两种密码体制:一种称为对称密钥密码体制;另一种称为公钥密码体制。
在对称密钥密码体制中,加密和解密使用相同的密钥。密钥由通信双方事先约定。算法可以公开,而密钥需要保密。
公钥密码体制在加密和解密过程中使用不同的密钥。并且使用其中一个进行加密,则需要用另一个才能解密。这两个成对的密钥在使用时,一个密钥作为私钥,需要保密;另一个密钥作为公钥,可以公开。
3.2 对称加密算法
对称加密算法分为分组密码(又称块加密)和序列密码(又称流密码、流加密)两种。
著名的分组密码:DES、AES 常用的流密码:Salsa20、ChaCha203.3 MAC 报文鉴别码
MAC 是 Message Authentication Code 的缩写,即报文鉴别码。通常是经过加密的散列值。计算报文鉴别码的算法称为 MAC 算法。常用的 MAC 算法有:
GMAC CBC-MAC Poly13053.4 AE
AE 是 Authenticated encryption 的缩写。顾名思义,这种加密方案不仅能提供机密性,还能提供完整性。任何伪造或篡改都会被发现。
AE 实际上是对称加密算法和 MAC 算法的结合体。在加密一个报文时,需要一个密钥和一个不重数,加密后将得到密文和一个报文鉴别码。报文鉴别码须随同密文一起发送给接收方。
接收方收到报文鉴别码和密文后,须用相同的密钥和不重数才能进行解密。任何对密文和报文鉴别码的篡改都会导致解密失败。当然,只要确保密钥没有泄露,其他人也无法伪造出合法的密文和相应的报文鉴别码。
不重数,即不重复的数,通常是从 0 开始递增的计数器,不需要保密。密钥和不重数的结合,相当于一次一密,能有效抵御「重放攻击」。
3.5 AEAD
AEAD 是 Authenticated Encryption with Additional Data 的缩写。相比于 AE,AEAD 在加、解密时还可以选择性地给定一些没有保密性要求的「附加数据」,例如版本号、时间戳、报文的长度和编码方式等。这些附加数据会参与到报文鉴别码的计算中去,但不会被加密,也不会成为密文的一部分。附加数据可以随同密文一起发送。
常用的 AEAD 有以下两种:
AES256-GCM ChaCha20-Poly1305Intel 在 2008 年推出新的指令集——AES-NI,为 AES 算法提供了硬件层面上的支持。但在其他平台(ARM)上,针对移动互联网优化的 ChaCha20 的速度大约是 AES 的三倍。
ChaCha20-Poly1305 最初在 2014 年提出,在 2015 年成为 IETF 标准,即 ChaCha20-Poly1305-IETF。后来,又通过对 ChaCha20 的改进,形成 XChaCha20-Poly1305-IETF。这一版本有望成为新的 IETF 标准,也是 Libsodium 目前首推的加密方案。
不同 AEAD 的密钥、不重数和报文鉴别码的长度(单位:位):
AEAD Key 密钥 Nonce 不重数 MAC 报文鉴别码 AES256-GCM 256 96 128 ChaCha20-Poly1305 256 64 128 ChaCha20-Poly1305-IETF 256 96 128 XChaCha20-Poly1305-IETF 256 192 128🔐 4. Libsodium 对 ChaCha20-Poly1305 的支持
Libsodium 为 ChaCha20-Poly1305 的三种版本分别提供了三组函数:
crypto_aead_chacha20poly1305_*()
crypto_aead_chacha20poly1305_ietf_*()
crypto_aead_xchacha20poly1305_ietf_*()
这三组函数在用法上完全一致,因此只要掌握了其中一种,自然也就掌握了其余两种。
4.1 加密
int crypto_aead_xchacha20poly1305_ietf_encrypt_detached(unsigned char *c,
unsigned char *mac,
unsigned long long *maclen_p,
const unsigned char *m,
unsigned long long mlen,
const unsigned char *ad,
unsigned long long adlen,
const unsigned char *nsec,
const unsigned char *npub,
const unsigned char *k);
函数 crypto_aead_xchacha20poly1305_ietf_encrypt_detached()
使用密钥 k
和
不重数 npub
对 mlen
字节的报文 m
进行加密,并根据密文和 adlen
字节的附加数据 ad
计算报文鉴别码。密文将被写到 c
,而报文鉴别码将被写到 mac
,maclen
会被设为 mac
的长度。
密文和明文等长。而密钥、不重数、报文鉴别码的长度都是固定的,它们分别等于:
crypto_aead_xchacha20poly1305_ietf_KEYBYTES
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES
crypto_aead_xchacha20poly1305_ietf_ABYTES
若没有关联的数据,则把 ad
设为 NULL
,并把 adlen
设为 0。
此处nsec
必须始终设为NULL
,下同。
#include <sodium.h>
int main(void)
{
if (sodium_init() == -1) {
return 1;
}
// 密钥
unsigned char k[crypto_aead_xchacha20poly1305_ietf_KEYBYTES] = "123456";
// 不重数
unsigned char npub[crypto_aead_xchacha20poly1305_ietf_NPUBBYTES] = {0};
// 明文
int mlen = 5;
unsigned char m[6] = "hello";
// 附加的数据
int adlen = 4;
unsigned char ad[5] = "2020";
// 密文
unsigned char c[6];
// 报文鉴别码
unsigned char mac[crypto_aead_xchacha20poly1305_ietf_ABYTES];
unsigned long long maclen;
// 加密
crypto_aead_xchacha20poly1305_ietf_encrypt_detached(c,
mac, &maclen,
m, mlen,
ad, adlen, NULL,
npub, k);
// 获取密文和报文鉴别码的十六进制表示
char buf[1024];
sodium_bin2hex(buf, sizeof buf, c, 5);
printf("Ciphertext: %s\n", buf);
sodium_bin2hex(buf, sizeof buf, mac, maclen);
printf("MAC: %s\n", buf);
return 0;
}
Ciphertext: 5abc40d737
MAC: 0be7cd4beaf9ec2a063170aab65fa5aa
函数
sodium_bin2hex()
是 Libsodium 提供的「辅助函数」,具体用法详见下文。
在密钥不变的情况下,不重数必须每次都不一样。建议用 randombytes_buf()
函数产生第一条报文的不重数,再用 sodium_increment()
函数对其进行递增。
4.2 解密
解密必须提供相同的密钥 k
、不重数 npub
和附加数据 ad
。
int crypto_aead_xchacha20poly1305_ietf_decrypt_detached(unsigned char *m,
unsigned char *nsec,
const unsigned char *c,
unsigned long long clen,
const unsigned char *mac,
const unsigned char *ad,
unsigned long long adlen,
const unsigned char *npub,
const unsigned char *k);
函数 crypto_aead_xchacha20poly1305_ietf_decrypt_detached()
首先验证 c
中包含的 tag
是否合法。若函数返回 -1
表示验证未通过;若验证通过,则返回 0
,并将解密得到的报文写到 m
。
#include <sodium.h>
int main(void)
{
if (sodium_init() == -1) {
return 1;
}
// 密钥
unsigned char k[crypto_aead_xchacha20poly1305_ietf_KEYBYTES] = "123456";
// 不重数
unsigned char npub[crypto_aead_xchacha20poly1305_ietf_NPUBBYTES] = {0};
// 明文
unsigned char m[6];
// 附加的数据
int adlen = 4;
unsigned char ad[5] = "2020";
// 密文
int clen = 5;
unsigned char c[6];
sodium_hex2bin(c, clen, "5abc40d737", 10, NULL, NULL, NULL);
// 报文鉴别码
unsigned char mac[crypto_aead_xchacha20poly1305_ietf_ABYTES];
sodium_hex2bin(mac, crypto_aead_xchacha20poly1305_ietf_ABYTES,
"0be7cd4beaf9ec2a063170aab65fa5aa", 32, NULL, NULL, NULL);
// 解密
crypto_aead_xchacha20poly1305_ietf_decrypt_detached(m,
NULL,
c, clen,
mac,
ad, adlen,
npub, k);
printf("Message: %s\n", m);
return 0;
}
Message: hello
4.3 合并模式
以上这种将密文和报文鉴别码分开储存的方式称为分开模式。由于大多数需求都是将报文鉴别码直接追加到密文后面,即合并模式。因此,Libsodium 实际上为每种 AEAD 方案都提供两组函数:一组实现分开模式;另一组实现合并模式。
为合并模式设计的函数,相比于分开模式的函数,函数名少了后缀 _detached
。
int crypto_aead_xchacha20poly1305_ietf_encrypt(unsigned char *c,
unsigned long long *clen_p,
const unsigned char *m,
unsigned long long mlen,
const unsigned char *ad,
unsigned long long adlen,
const unsigned char *nsec,
const unsigned char *npub,
const unsigned char *k);
密钥、不重数、附加数据、明文等参数的含义同上。在合并模式下,报文鉴别码直接追加到密文后面,因此减少了 mac
和 maclen
两个参数,但参数 c
必须为报文鉴别码预留存储空间。
#include <sodium.h>
int main(void)
{
if (sodium_init() == -1) {
return 1;
}
// 密钥
unsigned char k[crypto_aead_xchacha20poly1305_ietf_KEYBYTES] = "123456";
// 不重数
unsigned char npub[crypto_aead_xchacha20poly1305_ietf_NPUBBYTES] = {0};
// 明文
int mlen = 5;
unsigned char m[6] = "hello";
// 附加的数据
int adlen = 4;
unsigned char ad[5] = "2020";
// 密文
unsigned char c1[6];
unsigned char c2[6 + crypto_aead_xchacha20poly1305_ietf_ABYTES];
unsigned long long clen;
// 报文鉴别码
unsigned char mac[crypto_aead_xchacha20poly1305_ietf_ABYTES];
unsigned long long maclen;
// 加密(分开模式)
crypto_aead_xchacha20poly1305_ietf_encrypt_detached(c1,
mac, &maclen,
m, mlen,
ad, adlen, NULL,
npub, k);
char buf[1024];
sodium_bin2hex(buf, sizeof buf, c1, 5);
printf("Ciphertext: %s\n", buf);
sodium_bin2hex(buf, sizeof buf, mac, maclen);
printf("MAC: %s\n", buf);
// 加密(合并模式)
crypto_aead_xchacha20poly1305_ietf_encrypt(c2,
&clen,
m, mlen,
ad, adlen, NULL,
npub, k);
sodium_bin2hex(buf, sizeof buf, c2, clen);
printf("Ciphertext: %s\n", buf);
return 0;
}
Ciphertext: 5abc40d737
MAC: 0be7cd4beaf9ec2a063170aab65fa5aa
Ciphertext: 5abc40d7370be7cd4beaf9ec2a063170aab65fa5aa
🔑 5. 密钥的派生
在实际应用中,不应从始至终都使用同一个密钥,更不能直接使用密码(通常是简短的字符串)作为密钥,否则很容易遭受「字典攻击」。应当为每次会话专门准备一个子密钥。这就需要一种能够产生大量子密钥的机制。
5.1 KDF
KDF 是 Key Derivation Function 的缩写,即密钥派生函数。能够满足上述需求。这类函数通过引入随机数、增加散列迭代次数,增加暴力破解难度。常用的 KDF 有:
PBKDF2 Scrypt Argon2Argon2 是最新的算法,也是 Libsodium 首推及其底层默认使用的算法。
5.2 基于密码派生密钥
根据给定的密码和一个长度固定的随机数生成指定长度的密钥。
int crypto_pwhash(unsigned char * const out,
unsigned long long outlen,
const char * const passwd,
unsigned long long passwdlen,
const unsigned char * const salt,
unsigned long long opslimit,
size_t memlimit, int alg);
函数 crypto_pwhash()
根据 passwdlen
字节的密码 passwd
和 crypto_pwhash_SALTBYTES
字节的随机数 salt
派生出 outlen
字节的密钥并储存到 out
中。全部参数相同时,生成相同的密钥。
passwdlen
outlen
最小值
crypto_pwhash_PASSWD_MIN
crypto_pwhash_BYTES_MIN
最大值
crypto_pwhash_PASSWD_MAX
crypto_pwhash_BYTES_MAX
倒数两个参数 opslimit
和 memlimit
与性能和内存占用有关,取值如下:
opslimit
memlimit
最小值
crypto_pwhash_OPSLIMIT_MIN
crypto_pwhash_MEMLIMIT_MIN
较快/小
crypto_pwhash_OPSLIMIT_INTERACTIVE
crypto_pwhash_MEMLIMIT_INTERACTIVE
中等
crypto_pwhash_OPSLIMIT_MODERATE
crypto_pwhash_MEMLIMIT_MODERATE
较慢/大
crypto_pwhash_OPSLIMIT_SENSITIVE
crypto_pwhash_MEMLIMIT_SENSITIVE
最大值
crypto_pwhash_OPSLIMIT_MAX
crypto_pwhash_MEMLIMIT_MAX
最后一个参数 alg
决定选用的算法,只有下列 3 种取值可选:
crypto_pwhash_ALG_DEFAULT
Libsodium 推荐的选项。
crypto_pwhash_ALG_ARGON2I13
Argon2i 1.3。
crypto_pwhash_ALG_ARGON2ID13
Argon2id 1.3。
函数返回 0
表示成功;返回 -1
表示失败(这通常是由于操作系统拒绝分配请求的内存)。
#include <sodium.h>
int main(void)
{
if (sodium_init() == -1) {
return 1;
}
// 密码
unsigned char passwd[] = "secret";
// 长度固定的随机数
unsigned char salt[crypto_pwhash_SALTBYTES] = {0};
// 密钥
unsigned char key[16];
crypto_pwhash(key, sizeof key, passwd, strlen(passwd), salt,
crypto_pwhash_OPSLIMIT_INTERACTIVE,
crypto_pwhash_MEMLIMIT_INTERACTIVE,
crypto_pwhash_ALG_DEFAULT);
char buf[1024];
sodium_bin2hex(buf, sizeof buf, key, sizeof key);
printf("key: %s\n", buf);
return 0;
}
key: a5c2d5ca23026834f7ff177fb8137b62
5.3 基于主密钥派生子密钥
根据一个主密钥生成多个子密钥。Libsodium 专门为此提供了两个函数 crypto_kdf_*()
。
这两个函数可以根据一个主密钥 key
和一个被称为上下文的参数 ctx
派生出 2^64 个密钥,并且单个子密钥的长度可以在 128(16 字节)到 512 位(64 字节)之间。
void crypto_kdf_keygen(uint8_t key[crypto_kdf_KEYBYTES]);
函数 crypto_kdf_keygen()
的作用是生成一个主密钥。
int crypto_kdf_derive_from_key(unsigned char *subkey, size_t subkey_len,
uint64_t subkey_id,
const char ctx[crypto_kdf_CONTEXTBYTES],
const unsigned char key[crypto_kdf_KEYBYTES]);
函数 crypto_kdf_derive_from_key()
可以根据主密钥 key
和上下文 ctx
派生出长度为 subkey_len
字节的子密钥。subkey_id
是子密钥的编号,可以是不大于 2^64 - 1
的任意值。
主密钥的长度必须是 crypto_kdf_KEYBYTES
。子密钥的长度 subkey_len
必须介于 crypto_kdf_BYTES_MIN
(含)和 crypto_kdf_BYTES_MAX
(含)之间。
上下文 ctx
是一个 8 字符的字符串,应能描述子密钥的用途。不需要保密,并且强度可以很低。比如 "UserName"
、"__auth__"
、"pictures"
和 "userdata"
等。但其长度必须是 crypto_kdf_CONTEXTBYTES
字节。
使用相同的密钥,但使用不同的 ctx
,就会得到不同的输出。正如其名,ctx
可以和程序的上下文对应。当然,就算一个程序从头到尾只使用一个 ctx
,那也有防止密钥被不同程序重复使用的作用。
#include <sodium.h>
int main(void)
{
if (sodium_init() == -1) {
return 1;
}
char ctx[] = "Examples";
uint8_t master_key[crypto_kdf_KEYBYTES];
uint8_t subkey1[16];
uint8_t subkey2[16];
uint8_t subkey3[32];
// 创建主密钥
crypto_kdf_keygen(master_key);
// 派生子密钥
crypto_kdf_derive_from_key(subkey1, sizeof subkey1, 1, ctx, master_key);
crypto_kdf_derive_from_key(subkey2, sizeof subkey2, 2, ctx, master_key);
crypto_kdf_derive_from_key(subkey3, sizeof subkey3, 3, ctx, master_key);
// 获取子密钥的十六进制表示
char buf[1024];
sodium_bin2hex(buf, sizeof buf, subkey1, sizeof subkey1);
printf("subkey1: %s\n", buf);
sodium_bin2hex(buf, sizeof buf, subkey2, sizeof subkey2);
printf("subkey2: %s\n", buf);
sodium_bin2hex(buf, sizeof buf, subkey3, sizeof subkey3);
printf("subkey3: %s\n", buf);
return 0;
}
subkey1: 0440b65332dc5f6b4a46d262996af08e
subkey2: 73e6d9bbfb25c25d3898ba435f16b710
subkey3: 9fdffff7fd9d4ba7a8b1172c79cdf86b7a823256b418e9a61cb8e21f1170ef1f
🔩 6. 辅助函数
尽可能使用这些函数,以抵御「时序攻击」。
6.1 测试字节序列
sodium_memcmp()
函数 sodium_memcmp()
可完成两个等长字节序列的对比。
int sodium_memcmp(const void * const b1_, const void * const b2_, size_t len);
如果位于 b1_
的 len
个字节和位于 b2_
的 len
个字节相同,函数返回 0
,否则返回 -1
。
char b1_[6] = "hello";
char b2_[6] = "hello";
char b3_[6] = "Hello";
if (sodium_memcmp(b1_, b2_, 5) == -1) {
puts("No match");
} else {
puts("Match");
}
if (sodium_memcmp(b1_, b3_, 5) == -1) {
puts("No match");
} else {
puts("Match");
}
Match
No match
sodium_is_zero()
函数 sodium_is_zero()
可判断给定的字节序列是否全为 0
。
int sodium_is_zero(const unsigned char *n, const size_t nlen);
若位于 n
的 nlen
个字节是否全为 0
,则返回 1
,否则返回 0
。
6.2 字节序列的十六进制表示
sodium_bin2hex()
函数 sodium_bin2hex()
可获取字节序列的十六进制表示,并由此得到一个字符串。
char *sodium_bin2hex(char * const hex, const size_t hex_maxlen,
const unsigned char * const bin, const size_t bin_len);
函数将字符串写到 hex
,这个字符串就是从 bin
开始的 bin_len
个字节的十六进制表示,包括 '\0'
,故 hex_maxlen
至少为 2*bin_len + 1
。该函数始终返回 hex
。
char hex[9]; // 2*4 + 1 = 9
char bin[5] = "AAAA";
sodium_bin2hex(hex, 9, bin, 4);
puts(hex);
41414141
sodium_hex2bin()
函数 sodium_hex2bin()
作用相反,通过解析字节序列的十六进制表示,还原该字节序列。
int sodium_hex2bin(unsigned char * const bin, const size_t bin_maxlen,
const char * const hex, const size_t hex_len,
const char * const ignore, size_t * const bin_len,
const char ** const hex_end);
函数将字节序列写到 bin
。bin_maxlen
表示允许写入的最大字节数。而位于 hex
的字符串应当是一个字节序列的十六进制表示,可以没有 '\0'
结尾,需要解析的长度由 hex_len
指定。
ignore
是需要跳过的字符组成的字符串。比如 ": "
表示跳过冒号和空格。此时 "69:FC"
、"69 FC"
、"69 : FC"
和 "69FC"
都视为合法的输入,并产生相同的输出。ignore
可以设为 NULL
,表示不允许任何非法的字符出现。
函数返回 0
表示转换成功,同时 bin_len
会被设为解析得到的字节数;返回 -1
则表示失败。失败的情况有以下两种:
bin_maxlen
字节; 遇到非法字符时,如果前面的字符都能顺利解析,函数仍然返回
0
,否则返回
-1
。
无论如何 hex_end
总是会被设为下一个待解析的字符的地址。
char bin[5] = {0};
char hex[12] = "61*62636472";
size_t bin_len = 0;
const char * hex_end;
sodium_hex2bin(bin, 4, hex, 9, "*", &bin_len, &hex_end);
printf("%d: %s, %c\n", bin_len, bin, *hex_end);
4: abcd, 7
6.3 Base64 编码/解码
sodium_bin2base64()
函数 sodium_bin2base64()
可获取字节序列的 Base64 编码。
char *sodium_bin2base64(char * const b64, const size_t b64_maxlen,
const unsigned char * const bin, const size_t bin_len,
const int variant);
Base64 编码有多种变体,采用哪种变体由 variant
指定,有下列 4 种取值可选:
sodium_base64_VARIANT_ORIGINAL
sodium_base64_VARIANT_ORIGINAL_NO_PADDING
sodium_base64_VARIANT_URLSAFE
sodium_base64_VARIANT_URLSAFE_NO_PADDING
这些 Base64 编码并不提供任何形式的加密;就像十六进制编码一样,任何人都可以对它们进行解码。
可以令 b64_maxlen
等于宏 sodium_base64_ENCODED_LEN(BIN_LEN, VARIANT)
,它表示使用 VARIANT
这种变体时,BIN_LEN
个字节的 Base64 编码(包括 '\0'
)的最小长度。
char bin[6] = "hello";
int b64_len = sodium_base64_ENCODED_LEN(5, sodium_base64_VARIANT_ORIGINAL);
char b64[b64_len];
sodium_bin2base64(b64, b64_len, bin, 5, sodium_base64_VARIANT_ORIGINAL);
printf("%d: %s\n", b64_len, b64);
9: aGVsbG8=
sodium_base642bin()
函数 sodium_base642bin()
可完成 Base64 解码工作。
int sodium_base642bin(unsigned char * const bin, const size_t bin_maxlen,
const char * const b64, const size_t b64_len,
const char * const ignore, size_t * const bin_len,
const char ** const b64_end, const int variant);
返回 -1
表示错误,返回 0
表示解码成功,同时 bin_len
会被设为解码得到的字节数,其他参数的含义参考前文。
size_t bin_len;
char bin[6];
char b64[9] = "aGVsbG8=";
sodium_base642bin(bin, sizeof bin,
b64, strlen(b64), "", &bin_len, NULL,
sodium_base64_VARIANT_ORIGINAL);
printf("%d: %s\n", bin_len, bin);
5: hello
6.4 大数的计算
sodium_increment()
函数 sodium_increment()
用来递增一个任意长度的无符号数。
void sodium_increment(unsigned char *n, const size_t nlen);
位于 n
的 nlen
字节的数字将按小端字节序处理。加密算法中经常提到的不重数 nonce
就可用此函数进行递增。
unsigned char nonce[8] = {0};
sodium_increment(nonce, sizeof(nonce));
printf("%d\n", *(int *)nonce);
sodium_increment(nonce, sizeof(nonce));
printf("%d\n", *(int *)nonce);
sodium_increment(nonce, sizeof(nonce));
printf("%d\n", *(int *)nonce);
1
2
3
sodium_add()
函数 sodium_add()
可完成大数的加法。
void sodium_add(unsigned char *a, const unsigned char *b, const size_t len);
位于 a
和 b
的两个 nlen
字节的加数均按小端字节序的无符号数处理。计算结果将覆盖 a
。
unsigned char a[8] = {1};
unsigned char b[8] = {1};
printf("%d\n", *(int *)a);
sodium_add(a, b, sizeof a); // a = a + b
printf("%d\n", *(int *)a);
1
2
sodium_sub()
函数 sodium_sub()
可完成大数减法。
void sodium_sub(unsigned char *a, const unsigned char *b, const size_t len);
位于 a
和 b
的两个 nlen
字节的加数均按小端字节序的无符号数处理。计算结果将覆盖 a
。
sodium_compare()
函数 sodium_compare()
可完成两个大数的比较。两个大数均按小端字节序处理。
int sodium_compare(const void * const b1_, const void * const b2_, size_t len);
返回 0
表示相等,返回 -1
表示 b1_
小于 b2_
;返回 1
表示 b1_
大于 b2_
。