开源、先进、易用 C 语言加密函数库 Libsodium 中文指南

Libsodium 是一个开源、跨平台、跨语言的加密库,提供了一组简单易用的函数,大大简化了加密、散列、签名、鉴别、解密等复杂工作。支持许多种主流的加密算法和散列算法,包括 AES256-GCM 和 ChaCha20-Poly1305 两种 AEAD 加密方案。此外还提供了一系列方便实用的函数,可完成随机数的生成、大数的计算、编码和解码等辅助性工作。

🧱 起步

执行以下命令,完成 Libsodium 的下载、解压、编译和安装。

1
2
3
4
5
6
7
$ 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 的程序将无法运行。

1
2
$ echo "/usr/local/lib" > /etc/ld.so.conf.d/usr-local-lib.conf
$ ldconfig

/usr/local/lib/pkgconfig 目录中,可以找到文件 libsodium.pc。为了能够通过命令 pkg-config 获取编译和链接所需参数,须将此文件复制到 pkg-config 命令的搜寻目录中。

1
$ cp /usr/local/lib/pkgconfig/libsodium.pc /usr/share/pkgconfig/

通过命令 pkg-config 获取编译和链接所需参数。

1
2
3
4
$ pkg-config --cflags libsodium
-I/usr/local/include
$ pkg-config --libs libsodium
-L/usr/local/lib -lsodium

如果使用了 make,应当在 Makefile 文件中使用上述两条命令。

1
2
CFLAGS = $(pkg-config --cflags libsodium)
LDFLAGS = $(pkg-config --libs libsodium)

而在程序中,只需包含头文件 sodium.h 即可。

1
2
3
4
5
6
7
8
9
#include <sodium.h>

int main(void)
{
if (sodium_init() == -1) {
return 1;
}
...
}

在使用 Libsodium 的其他函数之前,必须先调用函数 sodium_init()。该函数不需要任何参数,返回 0 表示成功,返回 -1 表示失败,返回 1 则表示已经初始化过了。

📖 知识储备

两种密码体制

当前有两种密码体制:一种称为对称密钥密码体制;另一种称为公钥密码体制。

在对称密钥密码体制中,加密和解密使用相同的密钥。密钥由通信双方事先约定。算法可以公开,而密钥需要保密。

公钥密码体制在加密和解密过程中使用不同的密钥。并且使用其中一个进行加密,则需要用另一个才能解密。这两个成对的密钥在使用时,一个密钥作为私钥,需要保密;另一个密钥作为公钥,可以公开。

对称加密算法

对称加密算法分为分组密码(又称块加密)和序列密码(又称流密码、流加密)两种。

  • 著名的分组密码:DES、AES
  • 常用的流密码:Salsa20、ChaCha20

MAC 报文鉴别码

MAC 是 Message Authentication Code 的缩写,即报文鉴别码。通常是经过加密的散列值。计算报文鉴别码的算法称为 MAC 算法。常用的 MAC 算法有:

  • GMAC
  • CBC-MAC
  • Poly1305

AE

AE 是 Authenticated encryption 的缩写。顾名思义,这种加密方案不仅能提供机密性,还能提供完整性。任何伪造或篡改都会被发现。

AE 实际上是对称加密算法和 MAC 算法的结合体。在加密一个报文时,需要一个密钥和一个不重数,加密后将得到密文和一个报文鉴别码。报文鉴别码须随同密文一起发送给接收方。

接收方收到报文鉴别码和密文后,须用相同的密钥和不重数才能进行解密。任何对密文和报文鉴别码的篡改都会导致解密失败。当然,只要确保密钥没有泄露,其他人也无法伪造出合法的密文和相应的报文鉴别码。

不重数,即不重复的数,不需要保密,通常是从 0 开始递增的计数器,位数足够多的时候也可以是随机数。密钥和不重数的结合,相当于一次一密,能有效抵御「重放攻击」。

AEAD

AEAD 是 Authenticated Encryption with Additional Data 的缩写。相比于 AE,AEAD 在加、解密时还可以选择性地给定一些没有保密性要求的「附加数据」,例如版本号、时间戳、报文的长度和编码方式等。这些附加数据会参与到报文鉴别码的计算中去,但不会被加密,也不会成为密文的一部分。附加数据可以随同密文一起发送。

常用的 AEAD 有以下两种:

  • AES256-GCM
  • ChaCha20-Poly1305

Intel 在 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

🔐 Libsodium 对 ChaCha20-Poly1305 的支持

Libsodium 为 ChaCha20-Poly1305 的三种版本分别提供了三组函数:

  • crypto_aead_chacha20poly1305_*()
  • crypto_aead_chacha20poly1305_ietf_*()
  • crypto_aead_xchacha20poly1305_ietf_*()

这三组函数在用法上完全一致,因此只要掌握了其中一种,自然也就掌握了其余两种。

加密

1
2
3
4
5
6
7
8
9
10
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
不重数 npubmlen 字节的报文 m 进行加密,并根据密文和 adlen 字节的附加数据 ad 计算报文鉴别码。密文将被写到 c,而报文鉴别码将被写到 macmaclen 会被设为 mac 的长度。

密文和明文等长。而密钥、不重数、报文鉴别码的长度都是固定的,它们分别等于:

  • crypto_aead_xchacha20poly1305_ietf_KEYBYTES
  • crypto_aead_xchacha20poly1305_ietf_NPUBBYTES
  • crypto_aead_xchacha20poly1305_ietf_ABYTES

若没有关联的数据,则把 ad 设为 NULL,并把 adlen 设为 0。

此处 nsec 必须始终设为 NULL,下同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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;
}
1
2
Ciphertext: 5abc40d737
MAC: 0be7cd4beaf9ec2a063170aab65fa5aa

函数 sodium_bin2hex() 是 Libsodium 提供的「辅助函数」,具体用法详见下文。

在密钥不变的情况下,不重数必须每次都不一样。建议用 randombytes_buf() 函数产生第一条报文的不重数,再用 sodium_increment() 函数对其进行递增。

解密

解密必须提供相同的密钥 k、不重数 npub 和附加数据 ad

1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#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;
}
1
Message: hello

合并模式

以上这种将密文和报文鉴别码分开储存的方式称为分开模式。由于大多数需求都是将报文鉴别码直接追加到密文后面,即合并模式。因此,Libsodium 实际上为每种 AEAD 方案都提供两组函数:一组实现分开模式;另一组实现合并模式。

为合并模式设计的函数,相比于分开模式的函数,函数名少了后缀 _detached

1
2
3
4
5
6
7
8
9
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);

密钥、不重数、附加数据、明文等参数的含义同上。在合并模式下,报文鉴别码直接追加到密文后面,因此减少了 macmaclen 两个参数,但参数 c 必须为报文鉴别码预留存储空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#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;
}
1
2
3
Ciphertext: 5abc40d737
MAC: 0be7cd4beaf9ec2a063170aab65fa5aa
Ciphertext: 5abc40d7370be7cd4beaf9ec2a063170aab65fa5aa

🔑 密钥的派生

在实际应用中,不应从始至终都使用同一个密钥,更不能直接使用密码(通常是简短的字符串)作为密钥,否则很容易遭受「字典攻击」。应当为每次会话专门准备一个子密钥。这就需要一种能够产生大量子密钥的机制。

KDF

KDF 是 Key Derivation Function 的缩写,即密钥派生函数。能够满足上述需求。这类函数通过引入随机数、增加散列迭代次数,增加暴力破解难度。常用的 KDF 有:

  • PBKDF2
  • Scrypt
  • Argon2

Argon2 是最新的算法,也是 Libsodium 首推及其底层默认使用的算法。

基于密码派生密钥

根据给定的密码和一个长度固定的随机数生成指定长度的密钥。

1
2
3
4
5
6
7
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 字节的密码 passwdcrypto_pwhash_SALTBYTES 字节的随机数 salt 派生出 outlen 字节的密钥并储存到 out 中。全部参数相同时,生成相同的密钥。

\ passwdlen outlen
最小值 crypto_pwhash_PASSWD_MIN crypto_pwhash_BYTES_MIN
最大值 crypto_pwhash_PASSWD_MAX crypto_pwhash_BYTES_MAX

倒数两个参数 opslimitmemlimit 与性能和内存占用有关,取值如下:

\ 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 表示失败(这通常是由于操作系统拒绝分配请求的内存)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#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;
}
1
key: a5c2d5ca23026834f7ff177fb8137b62

基于主密钥派生子密钥

根据一个主密钥生成多个子密钥。Libsodium 专门为此提供了两个函数 crypto_kdf_*()

这两个函数可以根据一个主密钥 key 和一个被称为上下文的参数 ctx 派生出 2^64 个密钥,并且单个子密钥的长度可以在 128(16 字节)到 512 位(64 字节)之间。

1
void crypto_kdf_keygen(uint8_t key[crypto_kdf_KEYBYTES]);

函数 crypto_kdf_keygen() 的作用是生成一个主密钥。

1
2
3
4
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,那也有防止密钥被不同程序重复使用的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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;
}
1
2
3
subkey1: 0440b65332dc5f6b4a46d262996af08e
subkey2: 73e6d9bbfb25c25d3898ba435f16b710
subkey3: 9fdffff7fd9d4ba7a8b1172c79cdf86b7a823256b418e9a61cb8e21f1170ef1f

🔩 辅助函数

尽可能使用这些函数,以抵御「时序攻击」。

测试字节序列

sodium_memcmp()

函数 sodium_memcmp() 可完成两个等长字节序列的对比。

1
int sodium_memcmp(const void * const b1_, const void * const b2_, size_t len);

如果位于 b1_len 个字节和位于 b2_len 个字节相同,函数返回 0,否则返回 -1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char b1_[6] = "hello";
char b2_[6] = "hello";
char b3_[6] = "Hello";

if (sodium_memcmp(b1_, b2_, 5) == -1) {
puts("Not match");
} else {
puts("Match");
}

if (sodium_memcmp(b1_, b3_, 5) == -1) {
puts("Not match");
} else {
puts("Match");
}
1
2
Match
No match

sodium_is_zero()

函数 sodium_is_zero() 可判断给定的字节序列是否全为 0

1
int sodium_is_zero(const unsigned char *n, const size_t nlen);

若位于 nnlen 个字节全为 0,则返回 1,否则返回 0

字节序列的十六进制表示

sodium_bin2hex()

函数 sodium_bin2hex() 可获取字节序列的十六进制表示,并由此得到一个字符串。

1
2
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

1
2
3
4
5
char hex[9]; // 2*4 + 1 = 9
char bin[5] = "AAAA";

sodium_bin2hex(hex, 9, bin, 4);
puts(hex);
1
41414141

sodium_hex2bin()

函数 sodium_hex2bin() 作用相反,通过解析字节序列的十六进制表示,还原该字节序列。

1
2
3
4
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);

函数将字节序列写到 binbin_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 总是会被设为下一个待解析的字符的地址。

1
2
3
4
5
6
7
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);
1
4: abcd, 7

Base64 编码/解码

sodium_bin2base64()

函数 sodium_bin2base64() 可获取字节序列的 Base64 编码。

1
2
3
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')的最小长度。

1
2
3
4
5
6
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);
1
9: aGVsbG8=

sodium_base642bin()

函数 sodium_base642bin() 可完成 Base64 解码工作。

1
2
3
4
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 会被设为解码得到的字节数,其他参数的含义参考前文。

1
2
3
4
5
6
7
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);
1
5: hello

大数的计算

sodium_increment()

函数 sodium_increment() 用来递增一个任意长度的无符号数。

1
void sodium_increment(unsigned char *n, const size_t nlen);

位于 nnlen 字节的数字将按小端字节序处理。加密算法中经常提到的不重数 nonce 就可用此函数进行递增。

1
2
3
4
5
6
7
8
9
10
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
1
2
3

sodium_add()

函数 sodium_add() 可完成大数的加法。

1
void sodium_add(unsigned char *a, const unsigned char *b, const size_t len);

位于 ab 的两个 nlen 字节的加数均按小端字节序的无符号数处理。计算结果将覆盖 a

1
2
3
4
5
6
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
1
2

sodium_sub()

函数 sodium_sub() 可完成大数减法。

1
void sodium_sub(unsigned char *a, const unsigned char *b, const size_t len);

位于 ab 的两个 nlen 字节的加数均按小端字节序的无符号数处理。计算结果将覆盖 a

sodium_compare()

函数 sodium_compare() 可完成两个大数的比较。两个大数均按小端字节序处理。

1
int sodium_compare(const void * const b1_, const void * const b2_, size_t len);

返回 0 表示相等,返回 -1 表示 b1_ 小于 b2_;返回 1 表示 b1_ 大于 b2_

🔗 参考文献

  1. libsodium 密码学库 中文文档
  2. 现代密码学实践指南[2015年]
  3. 【翻译】密码学一小时必知
  4. 加密解密学习笔记
  5. 密码学基础系列
  6. 实用密码学工具——KDF
  7. 如何存储密码(KDF)
  8. ldconfig命令
  9. pkg-config 详解
  10. 什么是 AES-NI(AES指令集)