1. 前言
The GNU C Library Reference Manual for version 2.35
2. 加密函数
Cryptographic Functions
GNU C 库仅包含一些特殊用途的加密函数:用于密码存储的单向哈希函数,以及对加密随机源(如果操作系统提供)的访问。需要通用加密的程序应该使用专用的加密库,例如 libgcrypt。
许多国家对加密软件的进口、出口、拥有或使用施加了法律限制。我们谴责这些限制,但我们仍然必须警告您,GNU C 库可能会受到这些限制,即使您自己不使用本章中的函数。这些限制因地而异,并且经常更改,因此我们无法提供比此警告更具体的建议。
2.1. 密码存储
Passphrase Storage
有时有必要确保用户被授权使用机器提供的某些服务——例如,以特定用户 ID 登录(请参阅用户和组)。这样做的一种传统方式是让每个用户选择一个秘密密码。然后,系统可以询问自称用户的用户的密码是什么,如果该人提供了正确的密码,则系统可以授予适当的权限。(传统上,这些被称为“密码”,但现在一个单词太容易猜到了。)
处理密码短语的程序必须特别注意不要将密码透露给任何人,无论如何。将它们保存在只能通过特殊权限访问的文件中是不够的。该文件可能通过错误或错误配置“泄露”,系统管理员不应该了解每个人的密码,即使他们出于某种原因必须编辑该文件。为避免这种情况,密码短语还应在存储之前使用单向函数转换为单向哈希。
单向函数很容易计算,但没有已知的方法来计算它的逆。这意味着系统可以通过对密码进行散列并将结果与存储的散列进行比较来轻松检查密码。但是发现某人密码哈希的攻击者只能通过猜测和检查来发现它对应的密码。单向函数旨在使这个过程变得不切实际地缓慢,除了最明显的猜测。(不要使用字典中的单词作为密码。)
GNU C 库为基于 SHA-2-512、SHA-2-256、MD5 和 DES 加密原语的四个单向函数提供了一个接口。新的密码短语应该使用任何一个基于 SHA 的函数进行散列。其他的对于新设置的密码短语来说太弱了,但我们继续支持它们验证旧密码短语。基于 DES 的散列特别弱,因为它忽略了输入的前八个字符以外的所有字符。
函数:char * crypt (const char *phrase, const char *salt)
Preliminary: | MT-Unsafe race:crypt | AS-Unsafe corrupt lock heap dlopen | AC-Unsafe lock mem | See POSIX Safety Concepts.
函数 crypt 将密码字符串、phrase 转换为适合存储在用户数据库中的单向散列。它返回的字符串将完全由可打印的 ASCII 字符组成。它不会包含空格,也不会包含任何字符“:”、“;”、“*”、“!”或“\”。
salt 参数控制使用哪个单向函数,它还确保每个用户的单向函数的输出都是不同的,即使他们具有相同的密码。这使得从大型用户数据库中猜测密码短语变得更加困难。如果没有盐,攻击者可以进行猜测,对其运行一次 crypt,并将结果与所有哈希值进行比较。Salt 迫使攻击者为每个用户单独调用 crypt。
要验证密码短语,请将先前散列的密码短语作为盐传递。要散列一个新的密码以进行存储,请将 salt 设置为由前缀和随机选择的字符序列组成的字符串,如下表所示:
单向函数 前缀 随机序列
SHA-2-512 '$6$' 16 characters
SHA-2-256 '$5$' 16 characters
MD5 '$1$' 8 characters
DES '' 2 characters
在所有情况下,应从字母表 ./0-9A-Za-z 中选择随机字符。
对于除 DES 之外的所有散列函数,短语可以任意长,并且每个字节的所有八位都是有效的。使用 DES,只有短语的前 8 个字符影响输出,每个字节的第 8 位也被忽略。
crypt 可能会失败。一些实现在失败时返回 NULL,而另一些则返回一个无效的散列密码,它以“*”开头,与 salt 不同。在任何一种情况下,都会设置 errno 来指示问题。一些可能的错误代码是:
EINVAL
盐是无效的;既不是以前散列的密码,也不是任何受支持的散列函数的格式良好的新盐。
EPERM
系统配置禁止使用 salt 选择的哈希函数。
ENOMEM
未能分配内部暂存存储。
ENOSYS
EOPNOTSUPP
根本不支持散列密码,或者不支持盐选择的散列函数。GNU C 库不使用这些错误代码,但它们可能会在其他操作系统上遇到。
crypt 对内部临时工作和它返回的字符串使用静态存储。同时从多个线程调用 crypt 是不安全的,并且它返回的字符串将被任何后续对 crypt 的调用覆盖。
crypt 在 X/Open Portability Guide 中指定,几乎所有历史上的 Unix 系统上都有。但是,XPG 没有指定任何单向函数。
crypt 在 unistd.h 中声明。GNU C 库也在 crypt.h 中声明了这个函数。
函数:char * crypt_r (const char *phrase, const char *salt, struct crypt_data *data)
Preliminary: | MT-Safe | AS-Unsafe corrupt lock heap dlopen | AC-Unsafe lock mem | See POSIX Safety Concepts.
函数 crypt_r 是 crypt 的线程安全版本。它不是静态存储,而是使用其数据参数指向的内存来存储临时文件和它返回的字符串。只要在每个线程中使用不同的数据对象,它就可以安全地从多个线程中使用。它返回的字符串仍将被具有相同数据的另一个调用覆盖。
data 必须指向调用者分配的 struct crypt_data 对象。struct crypt_data 的所有字段都是私有的,但在第一次使用这些对象中的一个之前,必须使用 memset 或类似方法将其初始化为全零。之后,它可以在多次调用 crypt_r 时重复使用,而无需再次擦除。struct crypt_data 非常大,所以最好用 malloc 分配,而不是作为局部变量。请参阅为程序数据分配存储空间。
crypt_r 是一个 GNU 扩展。它在 crypt.h 中声明,结构 crypt_data 也是如此。
以下程序显示了如何在第一次输入密码时使用 crypt。它使用 getentropy 使盐尽可能地不可预测;请参阅生成不可预测的字节。
#include <stdio.h>
#include <unistd.h>
#include <crypt.h>
int
main(void)
{
unsigned char ubytes[16];
char salt[20];
const char *const saltchars =
"./0123456789ABCDEFGHIJKLMNOPQRST"
"UVWXYZabcdefghijklmnopqrstuvwxyz";
char *hash;
int i;
/* Retrieve 16 unpredictable bytes from the operating system. */
if (getentropy (ubytes, sizeof ubytes))
{
perror ("getentropy");
return 1;
}
/* Use them to fill in the salt string. */
salt[0] = '$';
salt[1] = '5'; /* SHA-256 */
salt[2] = '$';
for (i = 0; i < 16; i++)
salt[3+i] = saltchars[ubytes[i] & 0x3f];
salt[3+i] = '\0';
/* Read in the user’s passphrase and hash it. */
hash = crypt (getpass ("Enter new passphrase: "), salt);
if (!hash || hash[0] == '*')
{
perror ("crypt");
return 1;
}
/* Print the results. */
puts (hash);
return 0;
}
下一个程序演示如何验证密码。它检查硬编码到程序中的哈希,因为查找真实用户的哈希密码可能需要特殊权限(请参阅用户数据库)。它还表明,不同的单向函数会为相同的密码生成不同的哈希值。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <crypt.h>
/* ‘GNU's Not Unix’ hashed using SHA-256, MD5, and DES. */
static const char hash_sha[] =
"$5$DQ2z5NHf1jNJnChB$kV3ZTR0aUaosujPhLzR84Llo3BsspNSe4/tsp7VoEn6";
static const char hash_md5[] = "$1$A3TxDv41$rtXVTUXl2LkeSV0UU5xxs1";
static const char hash_des[] = "FgkTuF98w5DaI";
int
main(void)
{
char *phrase;
int status = 0;
/* Prompt for a passphrase. */
phrase = getpass ("Enter passphrase: ");
/* Compare against the stored hashes. Any input that begins with
‘GNU's No’ will match the DES hash, but the other two will
only match ‘GNU's Not Unix’. */
if (strcmp (crypt (phrase, hash_sha), hash_sha))
{
puts ("SHA: not ok");
status = 1;
}
else
puts ("SHA: ok");
if (strcmp (crypt (phrase, hash_md5), hash_md5))
{
puts ("MD5: not ok");
status = 1;
}
else
puts ("MD5: ok");
if (strcmp (crypt (phrase, hash_des), hash_des))
{
puts ("DES: not ok");
status = 1;
}
else
puts ("DES: ok");
return status;
}
2.2. 生成不可预测的字节
Generating Unpredictable Bytes
加密应用程序通常需要一些随机数据,这些数据对于恶意窃听者来说是尽可能难以猜测的。例如,加密密钥应该随机选择,crypt 使用的“salt”字符串(参见密码存储)也应该随机选择。
一些伪随机数生成器不能为密码应用程序提供足够不可预测的输出;请参阅伪随机数。此类应用程序需要使用加密随机数生成器 (CRNG),有时也称为加密强伪随机数生成器 (CSPRNG) 或确定性随机位生成器 (DRBG)。
目前,GNU C 库不提供加密随机数生成器,但它提供了从操作系统提供的随机源读取随机数据的函数。随机源本质上是一个 CRNG,但它也不断地从物理随机源“重新播种”自身,例如电子噪声和时钟抖动。这意味着应用程序不需要做任何事情来确保它产生的随机数在每次运行时都不同。
然而,问题是这些函数在任何一次调用中都只会产生相对较短的随机字符串。通常这不是问题,但是需要超过几千字节的加密强随机数据的应用程序应该调用这些函数一次,并使用它们的输出来播种 CRNG。
大多数应用程序应该使用 getentropy。getrandom 函数适用于需要额外控制阻塞行为的低级应用程序。
函数:int getentropy (void *buffer, size_t length)
| MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数从缓冲区开始将长度字节的随机数据写入数组。长度不能超过 256。成功时返回零。失败时,它返回 -1,并设置 errno 以指示问题。下面列出了一些可能的错误。
ENOSYS
操作系统没有实现随机源,或者不支持这种访问方式。(例如,这个函数使用的系统调用是在 3.17 版本的 Linux 内核中添加的。)
EFAULT
缓冲区和长度参数的组合指定了无效的内存范围。
EIO
长度大于 256,或内核熵池发生灾难性故障。
仅当系统刚刚启动并且随机源尚未初始化时,才能阻止对 getentropy 的调用。但是,如果它确实阻塞,它不能被信号或线程取消中断。打算在启动过程的早期阶段运行的程序可能需要在非阻塞模式下使用 getrandom,并准备好应对根本不可用的随机数据。
getentropy 函数在头文件 sys/random.h 中声明。它源自 OpenBSD。
函数:ssize_t getrandom (void *buffer, size_t length, unsigned int flags)
| MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数从缓冲区开始将最多长度字节的随机数据写入数组。flags 参数应该是零,或者是以下一些标志的按位或:
GRND_RANDOM
使用 /dev/random(阻塞)源而不是 /dev/urandom(非阻塞)源来获得随机性。
如果指定了此标志,则调用可能会阻塞,可能会阻塞相当长的一段时间,即使在随机源已初始化之后也是如此。如果未指定,则调用只能在系统刚刚启动且随机源尚未初始化时阻塞。
GRND_NONBLOCK
如果没有可用数据,则立即返回调用方,而不是阻塞。
GRND_INSECURE
写入可能不是加密安全的随机数据。
与 getentropy 不同,getrandom 函数是一个取消点,如果它阻塞,它可以被信号中断。
成功时,getrandom 返回已写入缓冲区的字节数,可能小于长度。出错时返回 -1,并设置 errno 以指示问题。一些可能的错误是:
ENOSYS
操作系统没有实现随机源,或者不支持这种访问方式。(例如,这个函数使用的系统调用是在 3.17 版本的 Linux 内核中添加的。)
EAGAIN
没有可用的随机数据,并且在标志中指定了 GRND_NONBLOCK。
EFAULT
缓冲区和长度参数的组合指定了无效的内存范围。
EINTR
系统调用被中断。在系统引导过程中,在初始化内核随机池之前,即使 flags 为零也可能发生这种情况。
EINVAL
flags 参数包含无效的标志组合。
getrandom 函数在头文件 sys/random.h 中声明。它是一个 GNU 扩展。