 | 级别: 中级 Vladimir Silva (vladimir_silva@yahoo.com), 计算机科学家, Freelance Developer
2006 年 2 月 06 日 UNIX®/Linux® PAM 兼容系统使用了这样一种认证机制,即以 crypt() 系统调用的 GNU MD5 扩展为基础。本文对这些扩展进行了介绍,并向您展示了 MD5 加密的一种 Java™ 实现,它可以与 UNIX/Linux 系统兼容。
如果您是一名安全性开发人员,需要为 Java 应用程序提供本地操作系统用户注册接口,那么应该怎样做呢?本文为您提供了答案:这就是 UNIX/Linux PAM(Pluggable Authentication Module,可插入认证模块)兼容的系统,它使用了以 crypt() 系统调用的 GNU MD5 扩展为基础的认证机制。在本文中我们将对这些扩展进行介绍,并展示 MD5 加密的一种 Java 实现(我们使用了 FreeBSD 作为 UNIX 平台)。
MD5(消息摘要算法 5;RFC 1321)是一种广泛应用的加密散列函数,使用了 128 位的散列值。这个算法可以接收任意长度的消息作为输入,并对输入生成一个 128 位的 “指纹(fingerprint)” 或 “消息摘要(message digest)” 作为输出。这个算法并不需要很大的置换表。在这个算法中密钥的概念是,我们不可能产生两条具有相同消息摘要的消息。
旧风格的 crypt(3) 函数
crypt() 是 UNIX 系统上使用的密码加密函数。它基于数据加密标准(Data Encryption Standard)算法,并具有一些用来禁用密钥搜索的硬件实现的变种。这个函数的原型如下:
char *crypt(const char *key, const char *salt)
其中 key 是要被加密的密码字符串,salt 是从 [a-A-0-./] 子集中选择的一个包含两个字符的字符串。这个字符串用来以 4,096 种不同的方法对这个算法进行初始化。这个算法使用密码中前 8 个字符的低 7 位来生成一个密钥。这个密钥用来反复对一个常量字符串(包含 0 )进行转换。返回值是一个包含 13 个可打印字符的字符串,用来表示加密后的密码。
这个函数具有以下缺点:
- 只使用了密码的前 8 个字符和每个字符的低 7 位。
- 这 56 位的密钥很容易被目前的计算能力破解。
- 这个密钥空间包含 2^56 = 7.2e16 个可能值。使用并行计算机对这个密钥空间进行遍历搜索是可能的。目前有一些这方面的开源软件,例如 MD5 Crack 项目(请参阅 参考资料);MD5 Crack 可以对密钥空间的可能值进行搜索,以得到密码。
- DES 算法本身有一些怪癖,它使用了 crypt(3) 接口,而该接口对于除密码认证之外的任何功能都不是什么好选择。
GNU 扩展:MD5 加密
GNU 对 crypt() 函数提供了以下扩展:如果 salt 是一个以 3 个字符 "$1$" 开始的字符串,后面是前 8 个字符(可以以 "$" 结束),那么 crypt() 函数就不使用 DES 算法,而是使用基于 MD5 的算法,并输出 34 个字节的内容。具体来说,这些字节是 $1$[string]$[encrypted-string],其中 [string] 代表 salt 中 "$1$" 后面的 8 个字符,后面是 22 个从 [a-zA-Z0-9./] 集合中选出的字符。
这些扩展具有以下优点:
- 基于 MD5 的加密比已经过时的 DES 提供了更好的安全性和更大的密钥空间。
- 整个密码都非常重要,而不仅仅是前 8 个字节。
算法的伪代码
这个算法使用两个 MD5 散列值对密码和 salt 进行一系列转换来进行工作。返回值是一组包含 34 个可打印的 ASCII 字符的组合,其格式如下:
$1$[salt]$[encrypted]
其中:
- "$1$" 是一个魔力字符串,它告诉 OS 使用 MD5 算法,而不是使用 DES 算法。
-
[salt] 是该算法的参数,是包含 8 个字符的字符串。
-
[encrypted] 是一个 22 字节的转换字符,代表加密后的密码。
这个算法的伪代码如清单 1 所示。
清单 1. MD5 加密的伪代码
// md5_crypt P-Code
// Inputs:
// password: Text to be encrypted
// salt: 8 character ramdom string from the set [a-zA-Z0-9]
// magic_string: The string used for encryption
// $1$ for Linux/BSD compatible encryption
// $apr1$ for Apache's MD5Crypt
// Output: Password hash
sub md5_crypt( password, salt, magic_string )
{
MD5 h1;
MD5 h2;
byte[] finalState;
// MD5 Hash #1: pwd, magic string and salt
h1.update( password );
h1.update( magic_string );
h1.update( salt );
// MD5 Hash #2: password, salt, password
h2.update( password );
h2.update( salt );
h2.update( password );
finalState = h2.digest();
//
// Two sets of transformations based on the length of the password
//
for ( int i = password.length(); i > 0; i -= 16 )
{
// Update h1 from offset value (i > 16 ? 16 : i)
h1.update( finalState, 0, i > 16 ? 16 : i );
}
for ( int i = password.length(); i != 0; i >> >= 1 )
{
if ( ( i & 1 ) != 0 )
{
h1.update( finalState, 0, 1 );
}
else
{
h1.update( password, 0, 1 );
}
}
finalState = h1.digest();
// Make an output string
String encoded;
//
// Salt is part of the encoded password ($1$<string>$)
//
encoded.append( magic_string );
encoded.append( salt );
encoded.append( "$" );
//
// Hash 22 bytes chosen from the set [a-zA-Z0-9./]
//
// Transform the 1st 4 bytes from h1's final state into the set [a-zA-Z0-9./]
//
l = ( bytes2u( finalState[0] ) << 16 ) | ( bytes2u( finalState[6] ) << 8 )
| bytes2u( finalState[12] );
encoded.append( to64( l, 4 ) );
// 4 bytes (8 so far...)
l = ( bytes2u( finalState[1] ) << 16 ) | ( bytes2u( finalState[7] ) << 8 )
| bytes2u( finalState[13] );
encoded.append( to64( l, 4 ) );
// 4 bytes ( 12 so far)
l = ( bytes2u( finalState[2] ) << 16 ) | ( bytes2u( finalState[8] ) << 8 )
| bytes2u( finalState[14] );
encoded.append( to64( l, 4 ) );
l = ( bytes2u( finalState[3] ) << 16 ) | ( bytes2u( finalState[9] ) << 8 )
| bytes2u( finalState[15] );
encoded.append( to64( l, 4 ) );
// 20 bytes
l = ( bytes2u( finalState[4] ) << 16 ) | ( bytes2u( finalState[10] ) << 8 )
| bytes2u( finalState[5] );
encoded.append( to64( l, 4 ) );
// 22 bytes from the set [a-zA-Z0-9./]
l = bytes2u( finalState[11] );
encoded.append( to64( l, 2 ) );
return encoded;
}
|

 |

|
MD5 加密的一个纯 Java 实现
纯 Java 实现具有很多优势,它提供了一个简单的接口,Web 和其他 Java 应用程序可以对本地 UNIX 注册系统进行认证。您可以从本文最后的 下载 一节下载这个实现。
认证是如何工作的?
图 1 给出了 UNIX 系统中典型的认证过程。
图 1. UNIX 系统中的密码认证
- 用户的密码是从终端读取的,并与从 /etc/shadow 中读取出来的文件的散列值(H1)一起发送给 MD5Crypt 子例程。H1 表示传递给 MD5Crypt 的 salt。
- 上面这个步骤的输出是第二个散列值(H2)。
- 对 H1 和 H2 进行比较。如果它们相等,那么认证就成功了。
对本地 Linux 注册系统进行测试
您可以使用下面这个简单的 shell 脚本来测试 MD5Crypt Java 程序在本地 Linux 注册系统上工作的情况。这个脚本接收一个密码作为惟一的参数。然后它会从 shadow 密码文件(/etc/shadow)中检索当前用户 H1(由 $USER 环境变量指定)的加密密码。这个脚本然后会运行 MD5Crypt 程序来获得 H2,并对这两个散列值进行比较。如果 H1 与 H2 相等,那么认证就成功了。
清单 2. 对本地注册系统测试 MD5Crypt 的 Java 实现
#!/bin/bash
#
# Simple authentication test for the pure java
# Implementation of MD5Crypt
#
# Password is the 1st argument
PWD=$1
if [ "$PWD" == "" ]
then
echo "$0 <Password>"
exit -1
fi
#
# Need super user access to perform this step
# Get the user's salt from /etc/shadow
#
SALT=`grep $USER /etc/shadow | cut -d: -f2`
echo "User: $USER"
echo "Salt(/etc/shadow): $SALT"
#
# Hash the user's pwd using the salt
#
HASH=`java grid.security.MD5Crypt $PWD $SALT`
# check exit status
if [ $? != 0 ] ; then
echo "Error hashing user $USER password."
exit -1
fi
echo "MD5Crypt Hash: $HASH"
#
# Compare both hashes. If they match we have a valid login
#
if [ $HASH == $SALT ]
then
echo Authentication succeeded
else
echo Authentication failed
fi
|
结束语
MD5 加密的一个纯 Java 实现可以提供一个简单的接口,Web 应用程序可以使用这个接口对本地 UNIX 注册系统进行认证。在本文中,我们介绍了 GNU 对 crypt() 系统调用的扩展,并给出了一个可以在 Java 应用程序中使用的 MD5 加密的实现。
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| Java implementation of MD5 crypt | l-md5crypt.zip | 3KB | HTTP |
|---|
参考资料 学习
获得产品和技术
讨论
关于作者  | |  | Vladimir Silva 出生在厄瓜多尔的基多。他于 1994 年从 Polytechnic Institute of the Army 获得系统分析员(System's Analyst)学位。同年,他作为交换学生来到美国,在 Middle Tennessee State University 学习计算机科学。毕业后,他加入了 IBM "Web-Ahead" 技术智囊团。他的兴趣包括网格计算、神经网络和人工智能。他还拥有包括 OCP、MCSD 和 MCP 在内的众多 IT 证书。 |
对本文的评价
|  |