当数据(可能是一个文件,一个程序,一段文字等)从A传递到B时,通过第2和3章的加密可以保证只有拥有密钥者(B)才可读取这段信息,实现了安全性编程中内容的保密性要求。但安全性编程还要求具有不可篡改性(B接收到数据后需要确认数据在传输过程中是否被别人修改过,发生纠纷时A需要检查B是否修改过原始数据)、身份的确定性(B要能够确认数据确实是A发来的)以及不可否认性(发生纠纷时,B能够证明数据确实是由A发来的)。
通过消息摘要、消息验证码、数字签名和数字证书等技术可以实现这些功能,Java支持这些技术,本章将介绍Java中如何使用消息摘要、消息验证码和数字签名,同时给出消息摘要在口令验证中的应用。在下一章则将介绍数字证书。
本章主要内容:
通过消息摘要验证数据是否被篡改
通过消息验证码证数据是否被篡改
通过数字签名验证数据的身份
使用消息摘要保存和验证口令
字典式攻击和加盐技术
4.1使用消息摘要验证数据未被篡改
消息摘要是对原始数据按照一定算法进行计算得到的计算结果,它主要用于检验原始数据是否被修改过。例如对于字符串,我们可以简单地将各个字符的ASCII码的值累加起来作为其消息摘要,这样,字符“Hello World!”的消息摘要是:72+101+108+108+111 +32+87+111+114+108+100+33=1085。这样,如果接收者对收到的字符串作同样计算发现计算结果不是1085,则可以确信收到的字符串在传输过程中被篡改了。
从这个简单的例子可以看出消息摘要和加密不同,从加密的结果可以得到原始数据,而从消息摘要中不可能得到原始数据。消息摘要长度比原始数据短得多(所以称为原始数据的“摘要”),实际使用中,原始数据不管多长,消息摘要一般是固定的16或20个字节长。
实际使用中消息摘要有许多成熟的算法,这些算法不仅处理效率高,而且不同的原始数据计算出相同的消息摘要的概率极其低,因此消息摘要可以看作原始数据的指纹,指纹不同则原始数据不同。本节介绍Java中如何使用这些成熟的消息摘要算法。
4.1.1计算消息摘要
★ 实例说明
本实例使用最简洁的编程步骤计算出指定字符串的消息摘要。
★ 编程思路:
java.security包中的MessageDigest类提供了计算消息摘要的方法, 首先生成对象,执行其update( )方法可以将原始数据传递给该对象,然后执行其digest( )方法即可得到消息摘要。具体步骤如下:
(1)生成MessageDigest对象
MessageDigest m=MessageDigest.getInstance("MD5");
分析:和2.2.1小节的KeyGenerator类一样。MessageDigest类也是一个工厂类,其构造器是受保护的,不允许直接使用new MessageDigist( )来创建对象,而必须通过其静态方法getInstance( )生成MessageDigest对象。其中传入的参数指定计算消息摘要所使用的算法,常用的有"MD5","SHA"等。若对MD5算法的细节感兴趣可参考
http://www.ietf.org/rfc/rfc1321.txt
。
(2)传入需要计算的字符串
m.update(x.getBytes("UTF8" ));
分析:x为需要计算的字符串,update传入的参数是字节类型或字节类型数组,对于字符串,需要先使用getBytes( )方法生成字符串数组。
(3)计算消息摘要
byte s[ ]=m.digest( );
分析:执行MessageDigest对象的digest( )方法完成计算,计算的结果通过字节类型的数组返回。
(4)处理计算结果
必要的话可以使用如下代码将计算结果s转换为字符串。
String result="";
for (int i=0; i
result+=Integer.toHexString((0x000000ff & s[i]) | 0xffffff00).substring(6);
}
★代码与分析:
完整程序如下:
import java.security.*;
public class DigestPass{
public static void main(String args[ ]) throws Exception{
String x=args[0];
MessageDigest m=MessageDigest.getInstance("MD5");
m.update(x.getBytes("UTF8"));
byte s[ ]=m.digest( );
String result="";
for (int i=0; i
result+=Integer.toHexString((0x000000ff & s[i]) | 0xffffff00).substring(6);
}
System.out.println(result);
}
}
★运行程序
输入java DigestCalc abc来运行程序,其中命令行参数abc是原始数据,屏幕输出计算后的消息摘要:900150983cd24fb0d6963f7d28e17f72。
根据
http://www.ietf.org/rfc/rfc1321.txt
,可测试以下字符串及输出结果:
输入字符串
|
程序输出
|
""
|
d41d8cd98f00b204e9800998ecf8427e
|
"a"
|
0cc175b9c0f1b6a831c399e269772661
|
"abc"
|
900150983cd24fb0d6963f7d28e17f72
|
"message digest"
|
f96b697d7cb7938d525a2f31aaf161d0
|
"abcdefghijklmnopqrstuvwxyz"
|
c3fcd3d76192e4007dfb496cca67e13b
|
"ABCDEFGHIJKLMNOPQRSTUVW XYZabcdefghijklmnopqrstuvwxyz0123456789" |
d174ab98d277d9f5a5611c2c9f419d9f
|
"1234567890123456789012345678901234567890 1234567890123456789012345678901234567890" |
57edf4a22be3c955ac49da2e2107b67a
|
如果A欲向B发送信息:“I have got your $800”,A可输入“java DigestCalc "I have got your $800"”来运行程序,将得到消息摘要:“d9c17e68da7ee9b24e8929f150f56fe9”,A将消息摘要和原始数据都发送给B。如果B收到数据后原始数据已经被篡改成:“I have got your $400”,B可以类似地用自己的程序计算其消息摘要(消息摘要算法是公开的),如输入“java DigestCalc "I have got your $400"”来运行程序,将得到消息摘要:“62069826e27c7e0b60a044e412f66b2b”,发现A发来的消息摘要不同,从而知道数据已经被篡改。
4.1.2基于输入流的消息摘要
4.1.1小节给出了计算字符串的消息摘要的编程方法,实际使用中经常要对流(如文件流)计算消息摘要,这时虽然可以从流中读出所有字节然后计算,但是使用DigestInputStream类更加方便。
★ 实例说明
本实例使用DigestInputStream对象计算文件输入流的消息摘要,它可以在一边读入数据一边将数据传递给MessageDigest对象以计算消息摘要。
★ 编程思路
Java中DigestInputStream类可以在读取输入流的同时将所读的字节传递给MessageDigest对象计算消息摘要,编程步骤如下:
(1)生成MessageDigest对象
MessageDigest m=MessageDigest.getInstance("MD5");
分析:和4.1.1小节第1步一样,其中传入的参数指定计算消息摘要所使用的算法,常用的有"MD5","SHA"等。
(2)生成需要计算的输入流
FileInputStream fin=new FileInputStream(args[0]);
分析:本实例针对文件输入流计算消息摘要,因此这里先创建文件输入流。文件名称不妨从命令行参数传入。
(3)生成DigestInputStream对象
DigestInputStream din=new DigestInputStream(fin,m);
分析:DigestInputStream类的构造器传入两个参数,第一个是所要计算的输入流,即第2步得到的fin,第二个是第1步生成的MessageDigest对象。
(4)从DigestInputStream流中读取数据
while(din.read()!=-1);
分析:该步骤和读取一般的输入流的方法一样,实际读取的是上一步创建DigestInputStream对象时从构造器传入的输入流。一般的输入流在该while循环中应该对读取的字节进行处理,对于DigestInputStream,读取过程中所读的字节除了通过read( )方法返回外,将传递给上一步穿入的MessageDigest对象,因此这里的while循环体可以为空。
(5)计算消息摘要
byte s[ ]=m.digest( );
分析:执行MessageDigest对象的digest( )方法完成计算,计算的结果通过字节类型的数组返回。
★代码与分析:
完整程序如下:
import java.security.*;
import java.io.*;
public class DigestInput{
public static void main(String args[ ]) throws Exception{
MessageDigest m=MessageDigest.getInstance("MD5");
FileInputStream fin=new FileInputStream(args[0]);
DigestInputStream din=new DigestInputStream(fin,m);
while(din.read()!=-1);
byte s[ ]=m.digest( );
String result="";
for (int i=0; i
result+=Integer.toHexString((0x000000ff & s[i]) |
0xffffff00).substring(6);
}
System.out.println(result);
}
}
程序最后和4.1.1小节的程序一样将消息摘要转换为字符串打印出来。
★运行程序
可以计算该字节码文件自身的消息摘要:输入“java DigestInput DigestInput.class”来运行程序,输出如下:
1ae0bb2f0c1bcb983060800730154b43
也可以计算运行Java程序的java.exe的消息摘要,如果J2SDK是安装在c:/j2sdk1.4.0目录下,可输入“java DigestInput c:/j2sdk1.4.0/bin/java.exe”来运行程序,输出如下:
6dcabd700656987230089b3c262b0249
可见不管原始数据多长,按照MD5算法计算出的消息摘要长度是相同的。此外,如果你的java.exe与我的完全相同的话,则计算出的消息摘要结果也必然是“6dcabd700656987230089b3c262b0249”,否则说明我们用的不是同一个java.exe。
4.1.3输入流中指定内容的消息摘要
4.1.2小节中对给定的文件总是计算所有文件内容的消息摘要,有时只需要文件中指定内容的消息摘要,本小节给出一个实例。
★ 实例说明
本实例使用DigestInputStream类计算文件输入流中第一次出现“$”以后的内容的消息摘要。
★ 编程思路
Java中DigestInputStream类在读取输入流时可以通过方法on( )随时控制是否将所读的字节传递给MessageDigest对象计算消息摘要,这样可以按照所需要的条件关闭或打开消息摘要功能。其编程方法和4.1.2小节类似,只要在while循环中根据所读内容调用on( )方法即可。
(1)生成MessageDigest和DigestInputStream对象
MessageDigest m=MessageDigest.getInstance("MD5");
FileInputStream fin=new FileInputStream(args[0]);
DigestInputStream din=new DigestInputStream(fin,m);
分析:该步骤和4.1.2小节的1至3步相同。
(2)先关闭消息摘要功能
din.on(false);
分析:din为上一步得到的DigestInputStream对象,在其on( )方法中传入false作为参数,则以后通过din从输入流读取字节时将不会把读到的字节传递给MessageDigest对象计算消息摘要。
(3)从DigestInputStream流中读取数据
int b;
while ( (b = din.read( )) != -1){
}
分析:该步骤和读取一般的输入流的方法一样。由于要根据所读到的字节控制是否开启消息摘要功能,因此将read( )方法返回的内容传递给整型变量b。
(4)若读到的内容为“$'”,则开启消息摘要功能
if(b=='$'){
din.on(true);
}
分析:该段代码放在上一步while语句的循环体中,当读到的内容是“$”时,则执行DigestInputStream对象的on( )方法,传入true作为参数。这样上一步以后再通过read( )方法读出的字节将自动传递给MessageDigest对象计算消息摘要。
(5)计算消息摘要
byte s[ ]=m.digest( );
分析:执行MessageDigest对象的digest( )方法完成计算,计算的结果通过字节类型的数组返回。
★代码与分析:
完整程序如下:
import java.security.*;
import java.io.*;
public class DigestInputLine{
public static void main(String args[ ]) throws Exception{
MessageDigest m=MessageDigest.getInstance("MD5");
FileInputStream fin=new FileInputStream(args[0]);
DigestInputStream din=new DigestInputStream(fin,m);
din.on(false);
int b;
while ( (b = din.read( )) != -1){
if(b=='$'){
din.on(true);
}
}
byte s[ ]=m.digest( );
String result="";
for (int i=0; i
result+=Integer.toHexString((0x000000ff & s[i]) |
0xffffff00).substring(6);
}
System.out.println(result);
}
}
程序最后和4.1.1小节的程序一样将消息摘要转换为字符串打印出来。
★运行程序
在当前目录存放三个文本文件,1.txt,2.txt和3.txt,内容分别为:
文件1.txt:
I'll lend u $200
文件2.txt:
As for many reasons,
I won't lend u $200
文件3.txt:
As for many reasons,
I won't lend u $100
则输入“java DigestInputLine 1.txt”运行程序,得到的结果为:
91f23d7175d3b3c2ea1ae301528f53c2
输入“java DigestInputLine 2.txt”运行程序,得到的结果同样为:
91f23d7175d3b3c2ea1ae301528f53c2
输入“java DigestInputLine 3.txt”运行程序,得到的结果则为:
b8a4f2f99c387b80cda72f6b43079b8b
可见程序只计算第一次出现“$”符号以后的内容。
4.1.4基于输入流的消息摘要
4.1.2小节给出了基于输入流的消息摘要,本小节介绍基于输出流的消息摘要。
★ 实例说明
本实例从键盘读入数据,然后使用DigestOutputStream对象将数据写入文件输出流,同时计算其消息摘要。
★ 编程思路
Java中DigestOutputStream类可以在向输出流写数据的同时将所写的字节传递给MessageDigest对象以便计算消息摘要,编程步骤如下:
(1)生成MessageDigest对象
MessageDigest m=MessageDigest.getInstance("MD5");
分析:和4.1.2小节第1步一样,其中传入的参数指定计算消息摘要所使用的算法,常用的有"MD5","SHA"等。
(2)生成需要的输出流
FileOutputStream fout=new FileOutputStream(args[0]);
分析:本实例以文件输出流为例,文件名称不妨从命令行参数传入。
(3)生成DigestOutputStream对象
DigestOutputStream dout=new DigestOutputStream(fout,m);
分析:DigestOutputStream类的构造器传入两个参数,第一个是所要处理的输入流,即第2步得到的fout,第二个是第1步生成的MessageDigest对象。
(4) 向DigestOutputStream流中写数据
int b;
while ((b = System.in.read( )) != -1) {
dout.write(b);
}
分析:该步骤和一般的输出流用法类似,使用DigestOutputStream的write( )方法写数据,这里一次写一个字节,也可以一次将一个字节类型数组中的内容写入DigestOutputStream流。在执行write( )方法时,相应的数据实际上写入了上一步所传入的文件输出流,同时传递给上一步穿入的MessageDigest对象。和4.1.3小节一样,可以使用on( )方法传入true或false的值控制是否将write( )方法中的数据传递给MessageDigest对象。
所写入的数据可以通过各种方式得到,这里使用System.in.read( )从键盘读入数据。
(5) 关闭DigestOutputStream流
dout.close();
分析:和一般的输出流一样使用close( )方法关闭流。
(6) 计算消息摘要
byte s[ ]=m.digest( );
分析:执行MessageDigest对象的digest( )方法完成计算,计算的结果通过字节类型的数组返回。
★代码与分析:
完整程序如下:
import java.security.*;
import java.io.*;
public class DigestOutput{
public static void main(String args[ ]) throws Exception{
MessageDigest m=MessageDigest.getInstance("MD5");
FileOutputStream fout=new FileOutputStream(args[0]);
DigestOutputStream dout=new DigestOutputStream(fout,m);
int b;
while ((b = System.in.read( )) != -1) {
dout.write(b);
}
dout.close();
byte s[ ]=m.digest( );
String result="";
for (int i=0; i
result+=Integer.toHexString((0x000000ff & s[i]) |
0xffffff00).substring(6);
}
System.out.println(result);
}
}
程序最后和4.1.2小节的程序一样将消息摘要转换为字符串打印出来。
★运行程序
输入java DigestOutput tmp.txt运行程序,然后通过键盘输入几行文本,最后同时按下Ctrl和Z键结束输入,这时屏幕上将显示所输入文本的消息摘要,如:
java DigestOutput tmp.txt
Hi
How a you!
This is a test!
9dd3424b1b9f4cdb7f8bb028362011e5
打开文件tmp.txt,将看到键盘输入的内容已经写入了文件tmp.txt。
4.2使用消息验证码
根据4.1节的内容,当A将数据传递给B时,可以同时将对应的消息摘要传递给B。B收到后可以用消息摘要验证数据在传输过程中是否被篡改过。但这样做的前提是A传递给B的消息摘要正确无误。如果攻击者在修改原始数据的同时重新计算一下消息摘要,然后将A传递给B的消息摘要替换掉,则B通过消息摘要就无法验证出原始数据是否被修改过了。
消息验证码可以解决这一问题。使用消息验证码的前提是A和B双方有一个共同的密钥,这样A可以将消息摘要加密发送给B,防止消息摘要被篡改。由于使用了共同的密钥,接收者可以在一定程度上验证发送者的身份:一定是和自己拥有共同的密钥的人。所以称为“验证码”。本章介绍其编程方法。
★ 实例说明
本实例使用2.2节得到的密钥计算一段字符串的消息验证码,并用于验证字符串是否被篡改过。
★ 编程思路:
javax.crypto包中的Mac类提供了计算消息验证码的方法。首先生成密钥对象和Mac类型的对象,Mac对象的init( )方法传入密钥,执行其update( )方法可以将原始数据传递给Mac对象,然后执行其doFinal( ) 方法即可得到消息验证码。具体步骤如下:
(1)生成密钥对象
byte [] kb={11,-105,-119,50,4,-105,16,38,-14,-111,21,-95,70,
-15,76,-74,67,-88,59,-71,55,-125,104,42};
SecretKeySpec k=new SecretKeySpec(kb,"HMACSHA1");
分析:这里使用2.2节得到的密钥。可以和2.3.1小节中的第1步那样从文件key1.dat中直接读取密钥对象,也可以像2.3.2小节中的第2步那样从文件keykb1.dat中读取密钥的字节,然后生成密钥对象。这里为简便起见直接将文件keykb1.dat中的内容赋值给字节数组kb,然后使用它生成密钥对象。密钥算法名称为“HMACSHA1”。
(2)生成Mac对象
Mac m=Mac.getInstance("HmacMD5")
分析:Mac类也是一个工厂类,通过其静态方法getInstance( )生成MessageDigest对象。其中传入的参数指定计算消息验证码所使用的算法,常用的有"HmacMD5"和"HmacSHA1"等
(3)传入需要计算的字符串
m.update(x.getBytes("UTF8" ));
分析:x为需要计算的字符串,update传入的参数是字节类型或字节类型数组,对于字符串,需要先使用getBytes( )方法生成字符串数组。
(4)计算消息验证码
byte s[ ]=m. doFinal( );
分析:执行Mac对象的doFinal( ) 方法完成计算,计算的结果通过字节类型的数组返回。
(5)处理计算结果
必要的话可以使用如下代码将计算结果s转换为字符串。
String result="";
for (int i=0; i
result+=Integer.toHexString((0x000000ff & s[i]) | 0xffffff00).substring(6);
}
★代码与分析:
完整程序如下:
import java.io.*;
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;
public class MyMac{
public static void main(String args[ ]) throws Exception{
//获取密钥
byte [] kb={11,-105,-119,50,4,-105,16,38,-14,-111,
21,-95,70,-15,76,-74,67,-88,59,-71,55,-125,104,42};
SecretKeySpec k=new SecretKeySpec(kb,"HMACSHA1");
//获取Mac对象
Mac m=Mac.getInstance("HmacMD5");
m.init(k);
String x=args[0];
m.update(x.getBytes("UTF8"));
byte s[ ]=m.doFinal( );
String result="";
for (int i=0; i
result+=Integer.toHexString((0x000000ff & s[i]) |
0xffffff00).substring(6);
}
System.out.println(result);
}
}
★运行程序
输入java MyMac "How are you!"来运行程序,其中命令行参数“How are you!”是原始数据,屏幕输出计算后的消息摘验证码:e0973b3fb96da6010b5f59f81194e3e9。
如果A欲向B发送信息:“I have got your $800”,A可输入“java MyMac "I have got your $800"”来运行程序,将得到消息验证码:“10e431a267e586a43affb575e7a7c974”。A将消息验证码和原始数据都发送给B。
原始数据和消息验证码在传输过程中都受到了攻击,攻击者将原始数据篡改成:“I have got your $400”,则B只要使用同样的密钥来计算消息验证码(消息验证码的算法是公开的),如输入“java MyMac "I have got your $400"”来运行程序,将得到消息验证码:“a4a53ffec37332a3542653e0904e2391”,发现和A发来的消息验证码不同,从而知道数据已经被篡改。
如果攻击者想把消息验证码也替换掉,尽管攻击者知道消息验证码的算法,但是由于攻击者没有A和B共有的密钥:“11,-105,-119,50,4,-105,16,38,-14,-111,21,-95,70, -15,76,-74,67,-88, 59,-71,55,-125,104,42”,因而无法计算出正确的值“a4a53ffec37332a3542653e0904e2391”,因此将无法得逞。正是这一点,使得消息验证码更加安全。
4.3使用数字签名确定数据的来源
使用消息摘要和消息验证码保证了数据未经过篡改,但接收者尚无法确定数据是否确实是某个人发来的。尽管消息验证码可以确定数据是某个拥有同样密钥的人发来的,但这要求双方具有共享的密钥,当数据要提供给一组用户、这一组用户都需要确定数据的来源时,消息验证码就不方便了。
数字签名可以解决这一问题。消息验证码的基础是基于公钥和私钥的非对称加密,发送者使用私钥加密消息摘要(签名),接收者使用公钥解密消息摘要以验证签名是否是某个人的。这和2.8节中的用法正好相反:2.8节中使用公钥进行加密,只有拥有私钥者才可以解密。由2.7和2.8节可知,私钥和公钥是成对的,私钥由拥有者秘密保存,对应的公钥则完全公开。因此每个人都可以用公钥尝试能否解密,若可以解密,则这个消息摘要必然是对应的私钥加密的。由于私钥只有加密者才拥有,因此如果接收者用某个公钥解密了某个消息摘要,就可以确定这段消息摘要必然是对应的私钥持有者发来的。
可见私钥就像一个人的笔迹或印章,是每个人独有的,同时又是人人可以检验的。使用私钥加密消息摘要,就像在文件上签名或盖章,确认了数据的身份。这里之所以不直接对原始数据加密而是对消息摘要加密,是因为非对称算法一般计算速度较慢,这样加密很长的原始数据较耗时;而消息摘要既简短,又足以代表原始数据。同时无论原始数据多长,消息摘要的长度都固定。
本节先介绍Java中如何用自己的私钥进行数字签名,然后介绍接收者如何用发送者提供的公钥验证数字签名。
4.3.1使用私钥进行数字签名
★ 实例说明
本实例使用2.7节得到的私钥文件Skey_RSA_priv.dat对文件msg.dat中的信息进行签名。签名将保存在文件sign.dat中。
★ 编程思路:
javax. security包中的Signature类提供了进行数字签名的方法。Signature对象的initSign( )方法传入私钥,执行其update( )方法可以将原始数据传递给Signature对象,然后执行其sign( ) 方法即可得到消息验证码。具体步骤如下:
(1)获取要签名的数据
FileInputStream f=new FileInputStream("msg.dat");
int num=f.available();
byte[ ] data=new byte[num];
f.read(data);
分析:不妨将需要签名的数据放在msg.dat文件中,通过文件输入流将其读入字节类型数组data中。
(2)获取私钥
FileInputStream f2=new FileInputStream("Skey_RSA_priv.dat");
ObjectInputStream b=new ObjectInputStream(f2);
RSAPrivateKey prk=(RSAPrivateKey)b.readObject( );
分析:这里使用2.7节生成的私钥文件Skey_RSA_priv.dat ,通过文件输入流读入私钥存放在RSAPrivateKey 类型的变量prk中。
(3)获取Signature对象
Signature s=Signature.getInstance("MD5WithRSA");
分析:Signature类是工厂类,需要使用getInstance( )方法获取对象,方法的参数指定签名所用的算法,参数中包含了计算消息摘要所用的算法和加密消息摘要所用的算法。如“SHA1withRSA”、“MD5withDSA”、“SHA15withDSA”等。
(4)用私钥初始化Signature对象
s.initSign(prk);
分析:使用Signature对象的initSign( )方法初始化Signature对象,其参数为第2步得到的私钥。这样,以后可以用这个私钥加密消息摘要。
(5)传入要签名的数据
s.update(data);
分析:执行Signature对象的update( )方法,其参数是第1步获得的需要签名的数据。
(6)执行签名
byte[ ] signeddata=s.sign( );
分析:使用Signature对象的sign( )方法,将自动使用前几步的设置进行计算,计算的结果以字节数组的类型通过方法返回。
★代码与分析:
完整程序如下:
import java.io.*;
import java.security.*;
import java.security.spec.*;
import java.security.interfaces.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.crypto.interfaces.*;
public class Sign{
public static void main(String args[ ]) throws Exception{
//获取要签名的数据,放在data数组
FileInputStream f=new FileInputStream("msg.dat");
int num=f.available();
byte[ ] data=new byte[num];
f.read(data);
//获取私钥
FileInputStream f2=
new FileInputStream("Skey_RSA_priv.dat");
ObjectInputStream b=new ObjectInputStream(f2);
RSAPrivateKey prk=(RSAPrivateKey)b.readObject( );
Signature s=Signature.getInstance("MD5WithRSA");
s.initSign(prk);
s.update(data);
System.out.println("");
byte[ ] signeddata=s.sign( );
// 打印签名
for(int i=0;i
System.out.print(signeddata[i]+",");
}
//保存签名
FileOutputStream f3=new FileOutputStream("Sign.dat");
f3.write(signeddata);
}
}
程序最后将签名结果在屏幕上显示,并保存在文件Sign.dat中。
★运行程序
在当前目录中存放三个文件:本小节的程序:Sign.class、秘密保存的私钥Skey_RSA_priv.dat和要签名的文件:msg.dat。msg.dat中不妨输入一段内容:
I have got your $800
输入java sign来运行程序,则得到如下结果:
49,-7,-48,-119,-14,68,-65,-27,24,-22,-128,54,-30,39,120,-99,56,92,14,21,85,106,
这个就是文件msg.dat签名的结果,它同时保存在文件Sign.dat中。
当发送者做完这些后,可以将msg.dat和Sign.dat同时提供给需要的人。提供时发送者可以放心地将文件通过Internet让接收者下载,或E-mail给接收者,甚至拷贝在软盘上由其他人转交接收者。
4.3.2使用公钥验证数字签名
当接收者接收到发送者发来的文件msg.dat及其签名Sign.dat后,可以对进行验证。其前提是接收者拥有发送者的公钥。本节介绍Java中如何验证数字签名。
★ 实例说明
本实例使用4.3.1小节所使用的私钥对应的公钥,即2.7节得到的公钥文件Skey_RSA_pub.dat对收到的文件msg.dat及其签名文件Sign.dat进行验证。确保msg.dat未被修改过,并且确实是发送者发来的。
★ 编程思路
javax. security包中的Signature类除了用于签名外,还可用于验证数字签名。Signature对象的initVerify ( )方法传入公钥,执行其verify ( )方法用其参数中的签名信息验证原始数据。具体步骤如下:
(1)获取要签名的数据
FileInputStream f=new FileInputStream("msg.dat");
int num=f.available();
byte[ ] data=new byte[num];
f.read(data);
分析:和4.3.1小节一样,从msg.dat文件读取需要验证的数据,存放在字节数组data中。
(2)获取签名
FileInputStream f2=new FileInputStream("Sign.dat");
int num2=f2.available();
byte[ ] signeddata=new byte[num2];
f2.read(signeddata);
分析:从Sign.dat文件中读取数字签名,存放在字节数组signeddata中。
(3)读取公钥
FileInputStream f3=new FileInputStream("Skey_RSA_pub.dat");
ObjectInputStream b=new ObjectInputStream(f3);
RSAPublicKey pbk=(RSAPublicKey)b.readObject( );
分析:这里使用2.7节生成的公钥文件Skey_RSA_pub.dat ,通过文件输入流读入公钥存放在RSAPublicKey类型的变量pbk中。
(4) 获取Signature对象
Signature s=Signature.getInstance("MD5WithRSA");
分析:和4.3.1小节一样使用静态方法getInstance( )方法获取Signature对象,算法使用和4.3.1小节相同的“MD5WithRSA”算法。
(5)用公钥初始化Signature对象
s.initVerify(pbk);
分析:使用Signature对象的initVerify( )方法初始化Signature对象,其参数为第3步得到的公钥。这样,以后可以用这个公钥解密消息摘要。
(6)传入要签名的数据
s.update(data);
分析:执行Signature对象的update( )方法,其参数是第1步获得的需要签名的数据。
(7)检验签名
s.verify(signeddata);
分析:使用Signature对象的verify( )方法,将自动使用前几步的设置进行计算。如果验证通过,则返回true,否则返回false。
★代码与分析:
完整程序如下:
import java.io.*;
import java.security.*;
import java.security.spec.*;
import java.security.interfaces.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.crypto.interfaces.*;
public class CheckSign{
public static void main(String args[ ]) throws Exception{
//获取数据,放在data数组
FileInputStream f=new FileInputStream("msg.dat");
int num=f.available();
byte[ ] data=new byte[num];
f.read(data);
//读签名
FileInputStream f2=new FileInputStream("Sign.dat");
int num2=f2.available();
byte[ ] signeddata=new byte[num2];
f2.read(signeddata);
//读公钥
FileInputStream f3=new FileInputStream("Skey_RSA_pub.dat");
ObjectInputStream b=new ObjectInputStream(f3);
RSAPublicKey pbk=(RSAPublicKey)b.readObject( );
//获取对象
Signature s=Signature.getInstance("MD5WithRSA");
//初始化
s.initVerify(pbk);
//传入原始数据
s.update(data);
boolean ok=false;
try{
//用签名验证原始数据
ok= s.verify(signeddata);
System.out.println(ok);
}
catch(SignatureException e){ System.out.println(e);}
System.out.println("Check Over");
}
}
★运行程序
在当前目录中事先有两个文件:本小节的程序:CheckSign.class和公开获得的发送者A的公钥Skey_RSA_pub.dat(2.7小节得到的公钥文件)。
然后某个人拿来两个文件:存放原始数据的待检验的文件msg.dat及其数字签名Sign.dat,说是A发来的文件。接收者开始检验,输入java CheckSign来运行程序,则得到如下结果:
true
Check Over
表明签名验证通过,该文件确实是A发来的。
假如文件msg.dat及其数字签名Sign.dat在传递给接收者时被做了手脚,如我们可以对msg.dat或Sign.dat作任意修改以模仿攻击者做的手脚,这时接收者再输入java CheckSign来运行程序,则得到如下结果:
false
Check Over
说明msg.dat已经不是发送者发来的原始内容了。
如果攻击者想修改msg.dat而让接收者检查不出来,则只有重新计算Sign.dat。而Sign.dat只有知道发送者A的私钥才能正确计算出,所以攻击者无计可施。这样,数据的身份可以唯一确定,无法仿冒。
下面我们再看一下数字签名如何实现不可否认性。若接收者拥有了msg.dat和相应的签名文件Sign.dat,以后发送者A不承认msg.dat中的内容,则接收者可以让仲裁者使用A对外公开的公钥文件Skey_RSA_pub.dat运行一下“java CheckSign”来检验msg.dat和Sign.dat,若显示“true”,则仲裁者可以确信发送者A确实承认过msg.dat中的内容。因为只有A才拥有公钥Skey_RSA_pub.dat对应的私钥,其他人都无法由msg.dat计算出能通过验证的签名文件Sign.dat。反过来,如果A已经没有文件msg.dat的原件了,A怀疑接收者出示的msg.dat是否做过手脚,也可以运行“java CheckSign”来检验一下,因为即使接收者对msg.dat做了手脚,接收者也无法计算出新的能通过验证的签名文件Sign.dat。
4.4 使用消息摘要保存口令
程序中经常需要验证用户输入的口令是否正确,如果将正确的用户口令直接存放在程序、文件或数据库中,则很容易被黑客窃取到,这时可以只保存口令的消息摘要。
4.4.1 使用消息摘要保存口令
★ 实例说明
在4.1节中介绍了消息摘要的计算,本节的实例将介绍如何在程序中将口令的消息摘要保存在文件中,以便以后验证用。
本实例中,运行“java SetPass 账号 口令”,将把账号以明文方式保存在文件passwd.txt中,而把口令的消息摘要保存在passwd.txt中。
★ 编程思路:
作为示例,为程序的简洁不妨通过命令行参数穿入账号和口令,然后按照4.1.1小节的方法计算消息摘要,最后将消息摘要
(1)读入帐号口令
, ; , String name=args[0];
String passwd=args[1];
分析:这里为了简便而通过命令行读入帐号和口令,实际程序中可以制作图形界面供用户输入。
(2)生成MessageDigest对象
MessageDigest m=MessageDigest.getInstance("MD5");
分析:执行MessageDigest类的静态方法getInstance( )生成MessageDigest对象。其中传入的参数指定计算消息摘要所使用的算法。
(3)传入需要计算的字节数组
m.update(passwd.getBytes("UTF8" ));
分析:passwd为需要计算的口令,使用getBytes( )方法生成字符串数组,传入MessageDigest对象的update( )方法。
(4)计算消息摘要
byte s[ ]=m.digest( );
分析:执行MessageDigest对象的digest( )方法完成计算,计算的结果通过字节类型的数组返回。
(5)在文件中或数据库中保存帐号和口令的消息摘要
PrintWriter out= new PrintWriter(new FileOutputStream("passwd.txt"));
out.println(name);
out.println(result);
out.close();
分析:这里将帐号和口令消息摘要报存在passwd.txt文件中,更好的做法是将其保存在数据库中。
★代码与分析:
本实例完整代码如下:
import java.io.*;
import java.security.*;
public class SetPass{
public static void main(String args[ ]) throws Exception{
String name=args[0];
String passwd=args[1];
MessageDigest m=MessageDigest.getInstance("MD5");
m.update(passwd.getBytes("UTF8"));
byte s[ ]=m.digest( );
String result="";
for (int i=0; i
result+=Integer.toHexString((0x000000ff & s[i]) |
0xffffff00).substring(6);
}
PrintWriter out= new PrintWriter(
new FileOutputStream("passwd.txt"));
out.println(name);
out.println(result);
out.close();
}
}
★运行程序
程序运行在C:/java/ch4/password目录,在命令行中输入
javac SetPass.java
编译程序,输入
java SetPass xyx akwi
运行程序,则以“xyx”为账号,“akwi”为口令,在文件passwd.txt中将保存如下信息:
xyx
4e4452f998059e3e574c696a489aac82
根据MD5消息摘要算法,只知道“4e4452f998059e3e574c696a489aac82”是无法推测出原有口令“akwi”的。因此,黑客即使得到了passwd.txt文件,仍旧无法知道原有口令是什么。
4.4.2 使用消息摘要验证口令
★ 实例说明
在4.4.1小节中将口令的消息摘要保存在文件中,本节的实例将介绍如何使用所保存的消息摘要验证用户输入的口令是否正确。
本实例中,运行“java CheckPass 账号 口令”,若账号和口令都和保存在passwd.txt中的相同,则提示“OK”,否则提示“Wrong password”。
★ 编程思路:
根据用户输入口令计算消息摘要,根据用户输入的账号在4.3.1小节保存口令的文件中找到预先保存的正确的口令的消息摘要。比较两个消息摘要是否相等。若不相等则说明输入的口令不正确。其编程步骤如下:
(1)根据用户输入的账号读取文件中对应的口令的消息摘要
String name="", passwd="";
BufferedReader in = new BufferedReader(new FileReader("passwd.txt"));
while ((name = in.readLine( )) != null) {
passwd=in.readLine( );
if (name.equals(args[0])){
break;
}
}
分析:不妨将第一个命令行参数args[0]的值作为用户输入的账号。在4.4.1小节中,第一行保存的是帐号,第二行保存的是账号对应的口令的消息摘要。如果有多个账号和口令,则可以如该程序的方法依次读取账号/口令摘要,直到所读取的帐号和命令行参数指定的账号相同,则退出读取。
(2)计算用户输入的口令的消息摘要
MessageDigest m=MessageDigest.getInstance("MD5");
m.update(args[1].getBytes("UTF8" ));
byte s[ ]=m.digest( );
分析:不妨将第二个命令行参数args[1]的值作为用户输入的口令。使用4.4.1小节中相同的步骤进行计算其消息摘要。
(3)比较用户输入的口令的消息摘要和文件中保存的口令摘要是否一致
if(name.equals(args[0])&&result.equals(passwd)){
System.out.println("OK");
}
else{
System.out.println("Wrong password");
}
分析:当账号和口令摘要都和文件中保存的一致,则验证通过。
★代码与分析:
本实例完整代码如下:
import java.io.*;
import java.security.*;
public class CheckPass{
public static void main(String args[ ]) throws Exception{
/* 读取保存的口令摘要 */
String name="";
String passwd="";
BufferedReader in = new BufferedReader(
new FileReader("passwd.txt"));
while ((name = in.readLine( )) != null) {
passwd=in.readLine( );
if (name.equals(args[0])){
break;
}
}
/* 生成用户输入的口令摘要 */
MessageDigest m=MessageDigest.getInstance("MD5");
m.update(args[1].getBytes("UTF8"));
byte s[ ]=m.digest( );
String result="";
for (int i=0; i
result+=Integer.toHexString((0x000000ff & s[i]) |
0xffffff00).substring(6);
}
/* 检验口令摘要是否匹配 */
if(name.equals(args[0])&&result.equals(passwd)){
System.out.println("OK");
}
else{
System.out.println("Wrong password");
}
}
}
★运行程序
程序运行在C:/java/ch4/password目录,在命令行中输入
javac CheckPass.java
编译程序,输入
java CheckPass xyx akwi
运行程序,程序输出“OK”,表明帐号和口令正确。输入
java CheckPass xyx qwert
提示“Wrong password”,可见可以正确进行验证。
4.4.3 攻击消息摘要保存的口令
★ 实例说明
4.4.1小节使用消息摘要保存口令的较为安全的机理是,攻击者即使通过攻击得到了口令文件,例如知道了xyx的口令摘要是4e4452f998059e3e574c696a489aac82,也难以通过该值反推出口令的值。因而无法登录系统。
但是当口令比较短时,攻击者很容易通过字典式攻击由口令的消息摘要反推出原有口令的值。本实例给出一个例子。
★ 编程思路:
使用字典式攻击的思路是:实现计算好各种长度的字符组合所得到的字符串的消息摘要的,将其保存在文件(称为字典)中。这虽然要花很多时间,但只需要做一次。以后如果攻击者得到了某个人的口令消息摘要,则不需要进行耗时的计算,直接和字典中的值相匹配,即可知道用户的口令。
其编程步骤可以如下
(1)生成字符串组合
for(int i1='a';i1<'z';i1++){
System.out.println("Now Processing"+(char)i1);
for(int i2='a';i2<'z';i2++)
for(int i3='a';i3<'z';i3++)
for(int i4='a';i4<'z';i4++){
char[ ] ch={(char)i1,(char)i2,(char)i3,(char)i4};
String passwd=new String(ch);
分析:这里不妨使用四重for循环生成四个字符的所有组合,为简化程序,不妨只考虑口令为小写字符a到z的情况。实际使用时,需考虑各种常用字符,并需计算从1个字符到多个字符的各种组合。
(2)计算消息摘要
m.update(passwd.getBytes("UTF8"));
byte s[ ]=m.digest( );
String result="";
for (int i=0; i
result+=Integer.toHexString((0x000000ff & s[i]) |
0xffffff00).substring(6);
}
分析:使用4.4.1小节中相同的步骤计算字符组合的消息摘要。
(3)保存字典
PrintWriter out= new PrintWriter(
new FileOutputStream("dict.txt"));
out.print(passwd+" ");
out.println(result);
分析:将字母组合和消息摘要的对应关系写入字典文件。
字典文件生成后,如果知道了一个消息摘要,只要编写程序查找包含该消息摘要的一行即可。可通过字符串的indexOf( )方法查看是否包含给定的消息摘要:
if (md.indexOf(args[0])!=-1){
System.out.println(md);
break;
}
★代码与分析:
本实例完整代码如下:
import java.io.*;
import java.security.*;
public class AttackPass{
public static void main(String args[ ]) throws Exception{
MessageDigest m=MessageDigest.getInstance("MD5");
PrintWriter out= new PrintWriter(
new FileOutputStream("dict.txt"));
for(int i1='a';i1<'z';i1++){
System.out.println("Now Processing"+(char)i1);
for(int i2='a';i2<'z';i2++)
for(int i3='a';i3<'z';i3++)
for(int i4='a';i4<'z';i4++){
char[ ] ch={(char)i1,(char)i2,(char)i3,(char)i4};
String passwd=new String(ch);
m.update(passwd.getBytes("UTF8"));
byte s[ ]=m.digest( );
String result="";
for (int i=0; i
result+=Integer.toHexString((0x000000ff & s[i]) |
0xffffff00).substring(6);
}
out.print(passwd+" ");
out.println(result);
}
}
out.close();
}
}
根据已知的消息摘要值查找字典的代码如下:
import java.io.*;
import java.security.*;
public class DoAttack{
public static void main(String args[ ]) throws Exception{
String md;
BufferedReader in = new BufferedReader(
new FileReader("dict.txt"));
while ((md = in.readLine( )) != null) {
if (md.indexOf(args[0])!=-1){
System.out.println(md);
break;
}
}
in.close();
}
}
★运行程序
程序运行在C:/java/ch4/password目录,在命令行中输入
javac AttackPass.java
javac DoAttack.java
编译程序,输入
java AttackPass
运行程序,则在不长的时间内就完成了所有四个字符的组合,生成的字典保存在dict.txt文件中。如果要生成5个字符、6个字符、…的组合,则所需时间将成指数级增长,但只要口令长度不长,机器速度足够快,哪怕需要耗时十几年,由于生成字典只需做一次,一旦足够长度的字符组合的字典生成好了,以后就可以一劳永逸地迅速破解所有使用4.4.1小节机制的系统。本实例生成的字典dict.txt只针对4个字符长度的小写字母,因而速度较快。其部分内容如下所示:
aafe 519704dcefcb42669c7afbf64a81c647
aaff b82bf3c70e89fd848b9e3f2785ebfecc
aafg ec02a3166c4d9e4dbd7a925e5a363cb4
aafh 9e1dc4b41cadd812256d7983eed97ac6
aafi 214eedbe6eec0d2aa91e487e82a4a939
aafj 0afff9a7e8a30e96c524407dfa6fe5f4
aafk ec8f1121bd879cc498d1c2fdc991a1e6
如果攻击者得到了4.4.1小节所生成的口令文件pass.txt,知道了xyx的口令消息摘要是“4e4452f998059e3e574c696a489aac82”,则可以运行如下程序来通过字典获取用户xyx的口令值:
java DoAttack 4e4452f998059e3e574c696a489aac82
程序输出
akwi 4e4452f998059e3e574c696a489aac82
这个查找过程瞬间就可以完成,可见4.4.1小节用户使用的口令已经被破解。
4.4.4 使用加盐技术防范字典式攻击
★ 实例说明
4.4.1小节的口令被轻松攻击的主要原因在于口令过短。如果口令很长,则计算所有组合的消息摘要可能要成百上千年,这将大大加大生成字典的难度。
不过口令很长也给用户带来不便,因此用户使用的口令长度总是有限的。加盐技术即可在有限的口令长度基础上增加攻击者生成字典的难度。
★ 编程思路:
加盐技术的基本原理是,在用户输入的口令前面加上一串随机数(称为盐),然后将随机数和口令组合在一起计算消息摘要。最后将随机数(盐)和消息摘要一起保存。
其基本步骤如下:
(1)读入帐号口令
String name=args[0];
String passwd=args[1];
分析:这里为了简便而通过命令行读入帐号和口令,实际程序中可以制作图形界面供用户输入。
(2)生成随机数(盐)
Random rand=new Random();
byte[ ] salt=new byte[12];
rand.nextBytes(salt);
分析:创建字节数组salt。使用Java中Random类生成随机数,执行Random类的nextBytes( )方法,方法的参数为salt,即可生成的随机数并将随机数赋值给salt。
(3)生成MessageDigest对象
MessageDigest m=MessageDigest.getInstance("MD5");
分析:执行MessageDigest类的静态方法getInstance( )生成MessageDigest对象。其中传入的参数指定计算消息摘要所使用的算法。
(4)传入盐和需要计算的字节数组
m.update(salt);
m.update(passwd.getBytes("UTF8" ));
分析:将第2步的盐和第1步的口令分别传递给MessageDigest对象的update( )方法。
(5)计算消息摘要
byte s[ ]=m.digest( );
分析:执行MessageDigest对象的digest( )方法完成计算,计算的结果通过字节类型的数组返回。
(6)在文件中或数据库中保存帐号和口令的消息摘要
PrintWriter out= new PrintWriter(new FileOutputStream("passwdsalt.txt"));
out.println(name);
for (int i=0; i
out.print(salt[i]+",");
}
out.println("");
out.println(result);
分析:这里将帐号、盐和口令消息摘要报存在passwd.txt文件中。对于盐,这里将数组中各个byte值以数字保存在文件中,各个数字之间以逗号隔开,这样比较直观,实际使用时可直接将字节数组以二进制保存。
如果攻击者得到了随机数(盐)和消息摘要,虽然他也可以将各种长度的字符组合和用户所使用的盐合并起来计算消息摘要,但是这个盐只在这一个口令中有效,其他口令使用的是其他随机数,因此攻击者对每个口令都要进行一次4.4.3小节中运行AttackPass计算字典的计算量,而不像4.4.3小节那样耗时计算一次,以后所有口令都就可以使用DoAttack快速进行匹配。因此如果用户口令在合理的长度内,攻击者的计算量将非常巨大。
如果攻击者对加盐的口令也想像4.4.3小节那样先生成字典然后进行攻击,则生成字典的计算量也将比4.4.3小节呈指数级增长直至可以认为不可能。如果口令有12位,在加盐之前,攻击者生成字典需要计算到长度为12个字符的组合。如果在口令前加了12位盐,则攻击者需要计算到长度为24个字符的组合,才能一劳永逸地通过简单匹配来获取消息摘要对应的口令。
★代码与分析:
本实例完整代码如下:
import java.util.*;
import java.io.*;
import java.security.*;
public class SetPassSalt{
public static void main(String args[ ]) throws Exception{
//读入账号口令
String name=args[0];
String passwd=args[1];
//生成盐
Random rand=new Random();
byte[ ] salt=new byte[12];
rand.nextBytes(salt);
//计算消息摘要
MessageDigest m=MessageDigest.getInstance("MD5");
m.update(salt);
m.update(passwd.getBytes("UTF8"));
byte s[ ]=m.digest( );
String result="";
for (int i=0; i
result+=Integer.toHexString((0x000000ff & s[i]) |
0xffffff00).substring(6);
}
//保存账号、盐和消息摘要
PrintWriter out= new PrintWriter(
new FileOutputStream("passwdsalt.txt"));
out.println(name);
for (int i=0; i
out.print(salt[i]+",");
}
out.println("");
out.println(result);
out.close();
}
}
★运行程序
程序运行在C:/java/ch4/password目录,在命令行中输入
javac SetPassSalt.java
编译程序,输入
java SetPassSalt xyx akwi
运行程序,则将账号xyx和盐及口令的消息摘要保存在passwdsalt.txt文件中,打开该文件可以发现其内容如下:
xyx
67,45,-101,90,69,-31,100,-7,-71,110,-88,-99,
ada08d0495ca044cf0919b695544b7f6
再次输入
java SetPassSalt xyx akwi
打开passwdsalt.txt文件可以发现其内容如下
xyx
-80,-18,-116,-43,-108,-109,-54,73,1,-109,74,-82,
0c9c9bf284663373f630e297a0328c95
可见每次使用的盐都不一样不同,这样,同一个口令的计算出的消息摘要也不一样。攻击者得到passwdsalt.txt文件后,如果像4.4.3小节那样先生成字典,即使计算出1到20个字符长度的所有字符组合,也只能攻击8个字符长度的口令,如果用户口令长度超过8个,则字典将无效。而如果不预先生成字典进行攻击,则攻击者每次都必须先取出passwdsalt.txt文件中口令的盐的值,然后重复进行4.4.3小节的计算,而不是只需要计算一次,这样,如果一次计算需要耗时半年,而用户不到半年如一个月就修改一次口令,则攻击者将无法得逞。
4.4.5 验证加盐的口令
★ 实例说明
本实例演示如何验证4.4.4小节中加盐的口令。
★ 编程思路:
为了验证加盐的口令,需根据用户输入的账号在4.4.4小节保存口令的文件passwdsalt.txt中找到预先保存的、与该账号对应的盐和消息摘要。然后使用口令文件passwdsalt.txt中的盐和用户输入口令组合在一起计算消息摘要。比较两个消息摘要是否相等。若不相等则说明输入的口令不正确。其编程步骤如下:
(1)根据用户输入的账号读取对应的盐和消息摘要
BufferedReader in = new BufferedReader(new FileReader("passwdsalt.txt"));
while ((name = in.readLine( )) != null) {
salts=in.readLine( );
passwd=in.readLine( );
if (name.equals(args[0])){
break;
}
}
分析:不妨将第一个命令行参数args[0]的值作为用户输入的账号。在4.4.4小节中,第一行保存的是帐号,第二行保存的是盐,第三行保存的是账号对应的口令的消息摘要。因此顺序读取账号、盐和口令摘要,直到所读取的帐号和命令行参数指定的账号相同。
(2)将盐值转换为byte数组
String salttmp[ ]=salts.split(",");
byte salt[ ]=new byte[salttmp.length];
for (int i=0; i
salt[i]=Byte.parseByte(salttmp[i]);
}
分析:为了直观,口令文件passsalt.txt中保存的盐的值是以数字保存的,各个数字之间以逗号隔开。上一步读取的盐的字符串即是以逗号隔开的一串数字。这里使用字符串的split( )方法以参数中的字符串(逗号)为分隔符,将字符串分解开来,存放在字符串数组中。然后使用Byte类的parseByte( )方法将数组中各个字符形式的数字串转换成byte类型。
(3)计算用户输入的口令的消息摘要
MessageDigest m=MessageDigest.getInstance("MD5");
m.update(salt);
m.update(args[1].getBytes("UTF8"));
byte s[ ]=m.digest( );
分析:不妨将第二个命令行参数args[1]的值作为用户输入的口令,加上上一步得到的盐,使用4.4.1小节中相同的步骤进行计算其消息摘要。
(4)比较用户输入的口令的消息摘要和文件中保存的口令摘要是否一致
if(name.equals(args[0])&&result.equals(passwd)){
System.out.println("OK");
}
else{
System.out.println("Wrong password");
}
分析:当账号和口令摘要都和文件中保存的一致,则验证通过。
★代码与分析:
本实例完整代码如下:
import java.io.*;
import java.security.*;
public class CheckPassSalt{
public static void main(String args[ ]) throws Exception{
/* 读取保存的盐和口令摘要 */
String name="";
String passwd="";
String salts="";
BufferedReader in = new BufferedReader(new
FileReader("passwdsalt.txt"));
while ((name = in.readLine( )) != null) {
salts=in.readLine( );
passwd=in.readLine( );
if (name.equals(args[0])){
break;
}
}
String salttmp[ ]=salts.split(",");
byte salt[ ]=new byte[salttmp.length];
for (int i=0; i
salt[i]=Byte.parseByte(salttmp[i]);
}
/* 生成用户输入的口令摘要 */
MessageDigest m=MessageDigest.getInstance("MD5");
m.update(salt);
m.update(args[1].getBytes("UTF8"));
byte s[ ]=m.digest( );
String result="";
for (int i=0; i
result+=Integer.toHexString((0x000000ff & s[i]) |
0xffffff00).substring(6);
}
/* 检验口令摘要是否匹配 */
if(name.equals(args[0])&&result.equals(passwd)){
System.out.println("OK");
}
else{
System.out.println("Wrong password");
}
}
}
★运行程序
程序运行在C:/java/ch4/password目录,在命令行中输入
javac CheckPassSalt.java
编译程序,输入
java CheckPassSalt xyx akwi
运行程序,程序输出“OK”,表明帐号和口令正确。输入
java CheckPassSalt xyx qwert
提示“Wrong password”,可见可以正确进行验证。
本章介绍了认证机制的几个基本技术:消息摘要、消息验证码和数字签名,并给出了消息摘要在口令验证中的应用。
数字签名验证的实际上是公钥对应的私钥持有者认可了某个数据。但这个私钥持有者到底是谁则不一定可靠。攻击者有可能自己生成一个私钥和公钥,对外宣称该公钥是属于A。这样,接收者将被误导。因此,验证数字签名所使用的公钥必须是从可靠的途径得到,如通过信誉很好的报纸等,此外下一章的数字证书也将从计算机的角度解决这一问题。