計算消息摘要實例、各種加密實例

當數據(可能是一個文件,一個程序,一段文字等)從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。這樣,接收者將被誤導。因此,驗證數字簽名所使用的公鑰必須是從可靠的途徑得到,如通過信譽很好的報紙等,此外下一章的數字證書也將從計算機的角度解決這一問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章