某些应用程序可能会要求用户对自身进行认证,通常会采取(用户名)登录名+密码的认证方式。出于这一目的,应用程序可能会维护其自有的用户名和密码数据库。然而,有时需要让要让用户输入标准的用户名/密码以登录到远程系统的网络应用程序,诸如 ssh 和 ftp,此时,必须按照标准的login程序那样,对用户名和密码加以验证
由于安全方面的原因,Unix系统采用单向加密算法对密码进行加密,这意味着由密码的加密形式将无法还原出原始密码。因此,验证候选密码的唯一方法就是使用同一算法对其进行加密,并将加密结果与存储于/etc/shadow中的密码进行匹配。加密算法封装于crypt()函数中
NAME
crypt, crypt_r - password and data encryption
SYNOPSIS
#define _XOPEN_SOURCE /* See feature_test_macros(7) */
#include <unistd.h>
char *crypt(const char *key, const char *salt);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <crypt.h>
char *crypt_r(const char *key, const char *salt,
struct crypt_data *data);
Link with -lcrypt.
DESCRIPTION
crypt() 是密码加密功能。 它基于数据加密标准算法,该算法具有(除其他目的外)旨在阻止使用密钥搜索的硬件实现的变体。
key是用户键入的密码。
salt是从[a–zA–Z0–9./]中选择的两个字符的字符串。 该字符串用于以4096种不同方式之一干扰算法。
返回值
成功后,该函数会返回一个指针,指向长度为 13 个字符的字符串,该字符串为静态分配而成,内容即为经过加密处理的密码。
错误时,返回NULL。
crypt()算法会接受一个最长可达8字符的密码,并施以数据加密算法(DES)的一种变体。salt参数指向一个两字符的字符串,用来扰动DES 算法,设计该技术,意在使得经过加密的密码更加难以破解。
- salt 参数和经过加密的密码,其组成成员均取自同一字符集合,范围在[a-zA-Z0-9/.]之间,共计 64 个字符。因此,两个字符的 salt 参数可使加密算法产生 4096(64×64)种不同变化。这意味着,预先对整部字典进行加密,再以其中的每个单词与经过加密处理的密码进行比对的做法并不可行,破解程序需要对照字典的 4096 种加密版本来检查密码
- 由 crypt()所返回的经过加密的密码中,头两个字符是对原始 salt 值的拷贝。也就是说,加密候选密码时,能够从已加密密码(存储于/etc/shadow 内)中获取 salt 值。(加密新密码时,passwd(1)这样的程序会生成一个随机 salt 值。)事实上,在 salt 字符串中,只有前两个字符对crypt()函数有意义。因此,可以直接将已加密密码指定为 salt 参数。
要想在 Linux 中使用 crypt(),在编译程序时需开启–lcrypt 选项,以便程序链接 crypt 库。
#include <pwd.h>
#include <grp.h>
#include <ctype.h>
#include <stdlib.h>
#if ! defined(__sun)
#define _BSD_SOURCE /* Get getpass() declaration from <unistd.h> */
#ifndef _XOPEN_SOURCE
#define _XOPEN_SOURCE /* Get crypt() declaration from <unistd.h> */
#endif
#endif
#include <unistd.h>
#include <limits.h>
#include <pwd.h>
#include <shadow.h>
#include <jmorecfg.h>
#include <stdio.h>
#include <string.h>
#include <uv/errno.h>
int
main(int argc, char *argv[])
{
char *username, *password, *encrypted, *p;
struct passwd *pwd;
struct spwd *spwd;
boolean authOk;
size_t len;
long lnmax;
/* 该调用获取了主机系统上用户名字符串的最大长度*/
lnmax = sysconf(_SC_LOGIN_NAME_MAX);
if (lnmax == -1) /* If limit is indeterminate */
lnmax = 256; /* make a guess */
username = static_cast<char *>(malloc(lnmax));
if (username == NULL){
perror("malloc");
exit(0);
}
// 1. 读取用户名,
printf("Username: ");
fflush(stdout);
if (fgets(username, lnmax, stdin) == NULL)
exit(EXIT_FAILURE); /* Exit on EOF */
len = strlen(username);
if (username[len - 1] == '\n')
username[len - 1] = '\0'; /* Remove trailing '\n' */
// 2. 会获取相应的密码记录以及(如开启了 shadow 密码功能)shadow 密码记录
pwd = getpwnam(username);
if (pwd == NULL){ // 程序没有权限
perror("couldn't get password record");
exit(0);
};
spwd = getspnam(username);
if (spwd == NULL && errno == EACCES){ // 程序没有权限--- (需要超级用户权限,或具有 shadow 组成员资格)
perror("no permission to read shadow password file");
exit(0);
}
// 若未能发现密码记录
if (spwd != NULL) /* If there is a shadow password record */
pwd->pw_passwd = spwd->sp_pwdp; /* Use the shadow password */
// getpass()函数首先会屏蔽回显功能,并停止对终端特殊字符的处理(诸如中断字符,一般为 Control-C)
//然后,该函数会打印出 prompt 所指向的字符串,读取一行输入,返回以 NULL 结尾的输入字符串(剥离尾部的换行符)
// 作为函数结果。(该字符串由静态分配而成,故而后续对 getpass()的调用会覆盖其原有内容。)返回结果之前,
// getpass()会将终端设置还原。
password = getpass("Password: ");
// 使用 crypt()加密密码:读取密码的程序应立即加密密码,并尽快将密码的明文从内存中抹去。
//只有这样,才能基本杜绝如下事件的发生:恶意之徒借程序崩溃之机,读取内核转储文件以获取密码
encrypted = crypt(password, pwd->pw_passwd);
for (p = password; *p != '\0'; )
*p++ = '\0';
if (encrypted == NULL){
printf("crypt\n");
exit(EXIT_FAILURE);
}
//并将结果与 shadow 密码文件中经过加密的密码记录进行比对
authOk = strcmp(encrypted, pwd->pw_passwd) == 0;
if (!authOk) {
printf("Incorrect password\n");
exit(EXIT_FAILURE);
}
// 若两者匹配,则显示用户 ID
printf("Successfully authenticated: UID=%ld\n", (long) pwd->pw_uid);
exit(EXIT_SUCCESS);
}
总结
crypt()函数加密密码的方式与标准的 login 程序相同,这对需要认证用户的程序来说极为有用。