0 j( g, m m/ G! s0 Y% e" Q MD5的全称是Message-Digest Algorithm 5,在90年代初由MIT的计算机科学实验室和RSA Data Security Inc发明,经MD2、MD3和MD4发展而来。 2 u) Z' E: m) v8 d# k$ ]+ C. M9 L3 w: o! [3 V& v; R
Message-Digest泛指字节串(Message)的Hash变换,就是把一个任意长度的字节串变换成一定长的大整数。请注意我使用了“字节串”而不是“字符串”这个词,是因为这种变换只与字节的值有关,与字符集或编码方式无关。 1 B- J0 U, |) ]$ u* z5 Q2 i, S. l
5 w; j; }0 A. f; L7 P4 n. C3 _ MD5将任意长度的“字节串”变换成一个128bit的大整数,并且它是一个不可逆的字符串变换算法,换句话说就是,即使你看到源程序和算法描述,也无法将一个MD5的值变换回原始的字符串,从数学原理上说,是因为原始的字符串有无穷多个,这有点象不存在反函数的数学函数。 ! z: G% W! _+ Y2 }7 ]! b
/ A' ]$ [ z* l7 h l$ a" m) `
MD5的典型应用是对一段Message(字节串)产生fingerprint(指纹),以防止被“篡改”。举个例子,你将一段话写在一个叫readme.txt文件中,并对这个readme.txt产生一个MD5的值并记录在案,然后你可以传播这个文件给别人,别人如果修改了文件中的任何内容,你对这个文件重新计算MD5时就会发现。如果再有一个第三方的认证机构,用MD5还可以防止文件作者的“抵赖”,这就是所谓的数字签名应用。 # n* d$ c/ f$ {1 w- B) j9 b4 h- z f3 e! c/ q9 M9 k
MD5还广泛用于加密和解密技术上,在很多操作系统中,用户的密码是以MD5值(或类似的其它算法)的方式保存的, 用户Login的时候,系统是把用户输入的密码计算成MD5值,然后再去和系统中保存的MD5值进行比较,而系统并不“知道”用户的密码是什么。 ) `$ x. B' Z3 q1 S. V1 k7 a6 ~ ) [/ Q0 }) P5 V0 K. q, A 一些黑客破获这种密码的方法是一种被称为“跑字典”的方法。有两种方法得到字典,一种是日常搜集的用做密码的字符串表,另一种是用排列组合方法生成的,先用MD5程序计算出这些字典项的MD5值,然后再用目标的MD5值在这个字典中检索。 # y A3 B# Z3 o e' S9 D7 ^" C
v/ v' p4 G L6 k- i
即使假设密码的最大长度为8,同时密码只能是字母和数字,共26+26+10=62个字符,排列组合出的字典的项数则是P(62,1)+P(62,2)….+P(62,8),那也已经是一个很天文的数字了,存储这个字典就需要TB级的磁盘组,而且这种方法还有一个前提,就是能获得目标账户的密码MD5值的情况下才可以。 * A1 C0 z, | p& r
) ~2 x- W8 ^8 k8 Z) o 在很多电子商务和社区应用中,管理用户的Account是一种最常用的基本功能,尽管很多Application Server提供了这些基本组件,但很多应用开发者为了管理的更大的灵活性还是喜欢采用关系数据库来管理用户,懒惰的做法是用户的密码往往使用明文或简单的变换后直接保存在数据库中,因此这些用户的密码对软件开发者或系统管理员来说可以说毫无保密可言,本文的目的是介绍MD5的Java Bean的实现,同时给出用MD5来处理用户的Account密码的例子,这种方法使得管理员和程序设计者都无法看到用户的密码,尽管他们可以初始化它们。但重要的一点是对于用户密码设置习惯的保护。 , t0 W! n5 Q( o: C ! J5 r& a9 {4 r( R1 u# ? 有兴趣的读者可以从这里取得MD5也就是RFC 1321的文本。http://www.ietf.org/rfc/rfc1321.txt ! J. l9 z6 m2 s4 D+ ~# \8 D, N m% F- a, v' ^' R6 K0 \ 实现策略 ) M4 ~; ]3 n$ ~8 M; J
# D8 i% _4 p# }* L, i% O/ W MD5的算法在RFC1321中实际上已经提供了C的实现,我们其实马上就能想到,至少有两种用Java实现它的方法,第一种是,用Java语言重新写整个算法,或者再说简单点就是把C程序改写成Java程序。第二种是,用JNI(Java Native Interface)来实现,核心算法仍然用这个C程序,用Java类给它包个壳。 q2 k9 {. b: i4 ?, F8 ~/ f ( T# N/ k4 x8 N4 f. D1 B 但我个人认为,JNI应该是Java为了解决某类问题时的没有办法的办法(比如与操作系统或I/O设备密切相关的应用),同时为了提供和其它语言的互操作性的一个手段。使用JNI带来的最大问题是引入了平台的依赖性,打破了SUN所鼓吹的“一次编写到处运行”的Java好处。因此,我决定采取第一种方法,一来和大家一起尝试一下“一次编写到处运行”的好处,二来检验一下Java 2现在对于比较密集的计算的效率问题。 8 Y2 N& R: H' F j' k) |1 @; n( v# h- E' y/ |5 k9 o ?. E# i
实现过程 " L# B' p/ c* W2 j # h9 `7 ?* n$ ]% c G 限于这篇文章的篇幅,同时也为了更多的读者能够真正专注于问题本身,我不想就某一种Java集成开发环境来介绍这个Java Bean的制作过程,介绍一个方法时我发现步骤和命令很清晰,我相信有任何一种Java集成环境三天以上经验的读者都会知道如何把这些代码在集成环境中编译和运行。用集成环境讲述问题往往需要配很多屏幕截图,这也是我一直对集成环境很头疼的原因。我使用了一个普通的文本编辑器,同时使用了Sun公司标准的JDK 1.3.0 for Windows NT。 J! L; x7 K4 ?. n) X( ~* s4 e" a( f' h% N
其实把C转换成Java对于一个有一定C语言基础的程序员并不困难,这两个语言的基本语法几乎完全一致.我大概花了一个小时的时间完成了代码的转换工作,我主要作了下面几件事: 7 j2 @- Y: ^7 m, U0 y: d1 C
: @) }; m+ m7 w3 T; f3 x: `9 A # Q5 i7 o# i5 J9 a& Z) {% K 把必须使用的一些#define的宏定义变成Class中的final static,这样保证在一个进程空间中的多个Instance共享这些数据 - z: N0 _; H5 Y1 Z
( i: f4 O9 p- Z7 u" w6 Q
删去了一些无用的#if define,因为我只关心MD5,这个推荐的C实现同时实现了MD2 MD3和 MD4,而且有些#if define还和C不同编译器有关 $ W, K: y. f: U
$ X+ A# h* \9 Q- U3 N 将一些计算宏转换成final static 成员函数。 & p R3 m1 G% C1 j: [; A6 Q. x ) Q( H7 D" A6 _1 u6 r; M 所有的变量命名与原来C实现中保持一致,在大小写上作一些符合Java习惯的变化,计算过程中的C函数变成了private方法(成员函数)。 4 I9 d5 Q- \1 v' y. G
5 }6 Q; \+ K) |4 b5 \. e 关键变量的位长调整 ; G+ D! }2 E' [" z N ' w# z' M; g- G 定义了类和方法 1 |/ s# H, W8 n ^% L" i: q: c! F. z, D, E& L! E/ j0 J$ m
需要注意的是,很多早期的C编译器的int类型是16 bit的,MD5使用了unsigned long int,并认为它是32bit的无符号整数。而在Java中int是32 bit的,long是64 bit的。在MD5的C实现中,使用了大量的位操作。这里需要指出的一点是,尽管Java提供了位操作,由于Java没有unsigned类型,对于右移位操作多提供了一个无符号右移:>>>,等价于C中的 >> 对于unsigned 数的处理。 5 |' F# w; M$ T: q# ?& e2 p2 H 9 i! e2 L' F$ d 因为Java不提供无符号数的运算,两个大int数相加就会溢出得到一个负数或异常,因此我将一些关键变量在Java中改成了long类型(64bit)。我个人认为这比自己去重新定义一组无符号数的类同时重载那些运算符要方便,同时效率高很多并且代码也易读,OO(Object Oriented)的滥用反而会导致效率低下。 ) C* w# {: I6 K I' H/ D/ N' n - P6 h% Z% ]2 Q/ g6 R" @ 限于篇幅,这里不再给出原始的C代码,有兴趣对照的读者朋友可以去看RFC 1321。MD5.java源代码 # |" @( k& |7 \) Q( G4 N1 F; {
( o, D* L @: m2 R& k9 _
测试 ! `9 K/ N# T" u' c: E+ U& Y, r) W' n
. }4 \# ]5 u# a2 d1 B. c6 ?
在RFC 1321中,给出了Test suite用来检验你的实现是否正确: 8 \3 V" W/ |; n4 {# t" p1 W8 ^8 X, R; \3 Y' ^3 B( J4 R9 N$ ]
MD5 ("") = d41d8cd98f00b204e9800998ecf8427e + Q' C; c! K8 {5 O, L. Y; p 9 h3 j5 S* Z6 o& U) D" R9 yMD5 ("a") = 0cc175b9c0f1b6a831c399e269772661 # i; r7 X, Z- u. _! B6 Y6 U& W8 g0 g p. G6 J9 }2 i" K
MD5 ("abc") = 900150983cd24fb0d6963f7d28e17f72 7 b& @4 a( |2 ^7 ?7 W1 l! p9 U% F8 k! U {3 G
MD5 ("message digest") = f96b697d7cb7938d525a2f31aaf161d0 ' d+ Z, `- b4 _8 G5 `2 l4 R+ m
. I9 v a* U y% q$ K% BMD5 ("abcdefghijklmnopqrstuvwxyz") = c3fcd3d76192e4007dfb496cca67e13b 6 E6 {- m8 h8 |) t9 M$ F- b$ }$ {( p, L3 R! p* Q# T* l, S" r* D
…… ) H J7 x- K+ c" l6 c
- A- [/ x+ E: l: l 这些输出结果的含义是指:空字符串””的MD5值是d41d8cd98f00b204e9800998ecf8427e,字符串”a”的MD5值是0cc175b9c0f1b6a831c399e269772661…… 0 [" a! w I5 a2 a! p8 q3 m $ |( |: W; I* r6 g' g 编译并运行我们的程序: . j6 ?4 X7 h1 `! Y4 C
( o; a( f5 N) L& V4 v+ X7 |1 F
javac –d . MD5.java & Z! b/ P- v5 J" o. U. Z) `' W: C, [
java beartool.MD5 % D' D2 r! M3 ~" n
5 T( C( M4 v6 C 为了将来不与别人的同名程序冲突,我在我的程序的第一行使用了package beartool; $ r* ?# e1 E( D5 `% e' ~ - R. H* A# z1 j( e. h 因此编译命令javac –d . MD5.java 命令在我们的工作目录下自动建立了一个beartool目录,目录下放着编译成功的 MD5.class 2 f/ ~& R9 |, z8 z' J; W3 j0 i5 t+ j! X) ?+ b# u
我们将得到和Test suite同样的结果。当然还可以继续测试你感兴趣的其它MD5变换,例如: , s1 d6 [6 C* c, S1 b- P- y5 m$ ?' D, h, I2 Z4 F
java beartool.MD5 1234 ' ^. g d( Z1 `3 n - }! t/ r0 q% i: l 将给出1234的MD5值。 $ W0 _+ j0 E) ?. Y) a2 m
o [4 a, H/ v 可能是我的计算机知识是从Apple II和Z80单板机开始的,我对大写十六进制代码有偏好,如果您想使用小写的Digest String只需要把byteHEX函数中的A、B、C、D、E、F改成a、b、 c、d、e、f就可以了。 0 g, a( d7 }# U9 W* e0 ?2 Z . J3 @+ S3 k6 W. c MD5据称是一种比较耗时的计算,我们的Java版MD5一闪就算出来了,没遇到什么障碍,而且用肉眼感觉不出来Java版的MD5比C版的慢。 - @7 f1 @ V' C 5 L& l. t3 w, o$ b3 ]( I: x# B 为了测试它的兼容性,我把这个MD5.class文件拷贝到我的另一台Linux+IBM JDK 1.3的机器上,执行后得到同样结果,确实是“一次编写到处运行了”。 2 x3 {+ `3 S* I( U$ ]: {