+-
开源、先进、易用加密库 Libsodium 编程指南
首页 专栏 c 文章详情
0

开源、先进、易用加密库 Libsodium 编程指南

去旅行 发布于 2 月 4 日

💡 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、ChaCha20

3.3 MAC 报文鉴别码

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

GMAC CBC-MAC Poly1305

3.4 AE

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

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

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

不重数,即不重复的数,通常是从 0 开始递增的计数器,不需要保密。密钥和不重数的结合,相当于一次一密,能有效抵御「重放攻击」。

3.5 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

🔐 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
不重数 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,下同。
#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);

密钥、不重数、附加数据、明文等参数的含义同上。在合并模式下,报文鉴别码直接追加到密文后面,因此减少了 macmaclen 两个参数,但参数 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 Argon2

Argon2 是最新的算法,也是 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 字节的密码 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 表示失败(这通常是由于操作系统拒绝分配请求的内存)。

#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);

若位于 nnlen 个字节是否全为 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);

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

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);

位于 nnlen 字节的数字将按小端字节序处理。加密算法中经常提到的不重数 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);

位于 ab 的两个 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);

位于 ab 的两个 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指令集)
c chacha20-poly1305
阅读 11 发布于 2 月 4 日
收藏
分享
本作品系原创, 采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议
avatar
去旅行
0 声望
0 粉丝
关注作者
0 条评论
得票 时间
提交评论
avatar
去旅行
0 声望
0 粉丝
关注作者
宣传栏
目录

💡 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、ChaCha20

3.3 MAC 报文鉴别码

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

GMAC CBC-MAC Poly1305

3.4 AE

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

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

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

不重数,即不重复的数,通常是从 0 开始递增的计数器,不需要保密。密钥和不重数的结合,相当于一次一密,能有效抵御「重放攻击」。

3.5 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

🔐 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
不重数 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,下同。
#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);

密钥、不重数、附加数据、明文等参数的含义同上。在合并模式下,报文鉴别码直接追加到密文后面,因此减少了 macmaclen 两个参数,但参数 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 Argon2

Argon2 是最新的算法,也是 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 字节的密码 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 表示失败(这通常是由于操作系统拒绝分配请求的内存)。

#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);

若位于 nnlen 个字节是否全为 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);

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

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);

位于 nnlen 字节的数字将按小端字节序处理。加密算法中经常提到的不重数 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);

位于 ab 的两个 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);

位于 ab 的两个 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指令集)