簡介:如果您已經學習了本系列的前兩部分,那麼現在可以開始第三部分,也就是最後一部分,您將 設置一個 KDC 服務器,向它發送 Kerberos 票據請求並取得其響應。然後,您將學習處理 KDC 服務器的 響應所需的低層 ASN1 處理方法,以便取得票據和會話密鑰。取得了服務票據後,將向電子銀行的業務邏 輯服務器發送一個建立安全上下文的請求。最後,您將學會與電子銀行業務邏輯服務器進行實際的安全通 信。
回顧本系列的 第一篇文章,它介紹了移動銀行 MIDlet 應用程序,並解釋了 Kerberos 是如何滿足這 種應用程序的安全要求的。文章還描述了 Kerberos 用來提供安全性的數據格式。
本系列的 第二篇 文章展示了如何在 J2ME 中生成 ASN.1 數據類型。介紹了如何用 Bouncy Castle 加密庫進行 DES 加密,並用用戶的密碼生成 Kerberos 密鑰。最後將這些內容放到一起並生成一個 Kerberos 票據請求。
在本系列文章中開發的 Kerberos 客戶不要求某個特定的 Kerberos 服務器,它可以使用所有 KDC 實 現。
不管所選的是什麼 KDC 服務器,必須告訴服務器移動銀行 MIDlet 的用戶在對 TGT 的請求中不需要 發送預認證數據( padata ,本系列第一篇文章的圖 2 中顯示的 KDC-REQ 結構的第三個字段)。
根據 Kerberos 規范,發送 padata 字段是可以選擇的。因此,KDC 服務器通常允許配置特定的用戶 ,使得對於所配置的用戶不需要 padata 字段就可以接受 TGT 請求。為了盡量減少 Kerberos 客戶機上 的負荷,必須告訴 KDC 服務器接受電子銀行移動用戶的不帶 padata 的 TGT 請求。
在這個例子中,我使用了 Microsoft 的 KDC 服務器以試驗基於 J2ME 的移動銀行應用程序。在本文 源代碼下載 中的 readme.txt 文件包含了如何設置 KDC 服務器、以及如何告訴它接受不帶 padata 字段 的 TGT 請求的指導。(在我的“用單點登錄簡化企業 Java 認證”一文中,我使用了同一個 KDC 服務器 展示單點登錄。有關鏈接請參閱 參考資料。)
向 KDC 服務器發送 TGT 請求
設置了 KDC 服務器後,就向它發送 TGT 請求。看一下 清單 1 中的 getTicketResponse() 方法。它 與 本系列第二篇文章中的清單 12 中的 getTicketResponse() 方法是相同的,只有一處不同:這個方法 現在包括向 KDC 服務器發送 TGT 請求的 J2ME 代碼。在 清單 1中標出了新的代碼,所以您可以觀察在 清單 12中沒有的新增代碼。
在 清單 1 的 NEW CODE 部分中,我以一個現有的 DatagramConnection 對象( dc )為基礎創建了 一個新的 Datagram 對象( dg )。注意在本文的最後一節中,移動銀行 MIDlet 創建了我在這裡用來創 建 Datagram 對象的 dc 對象。
創建了 dg 對象後,getTicketResponse() 方法調用了它的 send() 方法,向 KDC 服務器發送票據請 求。
在向服務器發送了 TGT 請求之後,清單 1 的 getTicketResponse() 方法接收服務器的 TGT 響應。 收到響應後,它將響應返回給調用應用程序。
清單 1. getTicketResponse() 方法
public byte[] getTicketResponse( )
{
byte ticketRequest[];
byte msg_type[];
byte pvno[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getIntegerBytes(5));
msg_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
2, getIntegerBytes(10));
byte kdc_options[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getBitStringBytes(new byte[5]));
byte generalStringSequence[] = getSequenceBytes (
getGeneralStringBytes (userName));
byte name_string[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, generalStringSequence);
byte name_type[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_PRINCIPAL));
byte principalNameSequence [] = getSequenceBytes(
concatenateBytes (name_type, name_string));
byte cname[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
1, principalNameSequence);
byte realm[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
2, getGeneralStringBytes (realmName));
byte sgeneralStringSequence[] =
concatenateBytes(getGeneralStringBytes(kdcServiceName),
getGeneralStringBytes (realmName));
byte sname_string[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getSequenceBytes(sgeneralStringSequence));
byte sname_type[] = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_UNKNOWN));
byte sprincipalNameSequence [] = getSequenceBytes
(concatenateBytes (sname_type, sname_string));
byte sname[] = getTagAndLengthBytes (ASN1DataTypes.CONTEXT_SPECIFIC,
3, sprincipalNameSequence);
byte till[] = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
5,
getGeneralizedTimeBytes (
new String("19700101000000Z").getBytes()));
byte nonce[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
7,
getIntegerBytes (getRandomNumber()));
byte etype[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
8,
getSequenceBytes(getIntegerBytes(3)));
byte req_body[] = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
4,
getSequenceBytes(
concatenateBytes(
kdc_options,
concatenateBytes(
cname,
concatenateBytes(
realm,
concatenateBytes(
sname,
concatenateBytes(
till,
concatenateBytes
(nonce, etype)
)
)
)
)
)
)
);
ticketRequest = getTagAndLengthBytes(
ASN1DataTypes.APPLICATION_TYPE,
10,
getSequenceBytes(
concatenateBytes(
pvno,
concatenateBytes
(msg_type, req_body)
)
)
);
/****** NEW CODE BEGINS ******/
try {
Datagram dg = dc.newDatagram(ticketRequest, ticketRequest.length);
dc.send(dg);
} catch (IllegalArgumentException il) {
il.printStackTrace();
}
catch (Exception io) {
io.printStackTrace();
}
byte ticketResponse[] = null;
try
{
Datagram dg = dc.newDatagram(700);
dc.receive(dg);
if (dg.getLength() > 0) {
ticketResponse = new byte[dg.getLength()];
System.arraycopy(dg.getData(), 0, ticketResponse, 0, dg.getLength ());
} else
return null;
} catch (IOException ie){
ie.printStackTrace();
}
/****** NEW CODE ENDS ******/
return ticketResponse;
}//getTicketResponse
處理 TGT 響應
既然已經收到了來自 KDC 的 TGT 響應,現在該對響應進行處理以便從響應中提取 票據 和 會話密鑰 。
自然,響應處理包括一些低層 ASN.1 處理(就像在本系列第二篇文章中生成票據請求時遇到的低層 ASN.1 生成方法一樣)。所以在展示如何使用低層處理方法從票據響應中提取 票據 和 會話密鑰 之前, 我將實現並解釋一些低層 ASN.1 處理方法以及一些低層加密支持方法。
像以前一樣,低層 ASN1 處理方法放在 ASN1DataTypes 類中。下面的方法在本文的 源代碼下載 中的 ASN1DataTypes.java 文件中:
isSequence()
getIntegerValue()
isASN1Structure()
getNumberOfLengthBytes()
getLength()
getASN1Structure()
getContents()
下面是上面列出的每一個低層 ASN.1 處理方法的說明。
isSequence()
清單 2 中顯示的 isSequence() 方法取單個 字節 作為參數,並檢查這個 字節 是否是一個 ASN.1 SEQUENCE 字節。如果 字節 值表示一個 SEQUENCE ,那麼它就返回 true,否則它返回 false。
清單 2. isSequence() 方法
public boolean isSequence(byte tagByte)
{
if (tagByte == (byte)0x30)
return true;
else
return false;
}//isSequence
getIntegerValue()
清單 3 中顯示的 getIntegerValue() 方法只取一個輸入參數,它是表示一個 ASN.1 INTEGER 數據類 型的內容的 字節 數組。它將輸入 字節 數組轉換為 J2ME int 數據類型,並返回 J2ME int 。在從 ASN.1 INTEGER 中提取了內容字節,並且希望知道它所表示的是什麼 integer 值時就需要這個方法。還 要用這個方法將長度字節轉換為 J2ME int 。
注意,getIntegerValue() 方法設計為只處理正的 integer 值。
ASN.1 以最高有效位優先(most-significant-byte-first)的序列存儲一個正的 INTEGER 。例如, 用 ASN.1 表示的十進制 511 就是 0x01 0xFF 。可以寫出十進制值的完整位表示(對於 511 ,它是 1 11111111 ),然後對每一個字節寫出 十六進制 值(對於 511,它是 0x01, 0xFF ),最後以最高有效位 優先的順序寫出 十六進制 值。
另一方面,在 J2ME 中一個 int 總是四字節長,並且最低有效 字節 占據了最右邊的位置。在正 integer 值中空出的位置上填入零。例如,對於 511 ,J2ME int 的寫法是 0x00 0x00 0x01 0xFF 。
這意味著在將 ASN.1 INTEGER 轉換為一個 J2ME int 時,必須將輸入數組的每一個 字節 正確地放到 輸出 J2ME int 中的相應位置上。
例如,如果輸入字節數組包含兩個字節的數據 (0x01, 0xFF) ,那麼必須像下面這樣將這些字節放到 輸出 int 中:
必須在輸出 int 的最左邊或者最高有效位置寫入 0x00 。
類似地,必須在與輸出 int 的最高有效 字節 相鄰的位置上寫入 0x00 。
輸入數組的第一個字節 (0x01) 放入輸出 int 中與最低有效位置相鄰的位置。
輸出數組的第二個字節 (0xFF) 放到輸出 int 的最低有效或者最右邊的位置。
getIntegerValue() 方法中的 for 循環計算每一個 字節 的正確位置,再將這個 字節 拷貝到其相應 的位置上。
還要注意因為 J2ME int 總是有四個字節,getIntegerValue() 方法只能處理最多四 字節 integer 值。能力有限的、基於 J2ME 的 Kerberos 客戶不需要處理更大的值。
清單 3. getIntegerValue() 方法
public int getIntegerValue(byte[] intValueAsBytes)
{
int intValue = 0;
int i = intValueAsBytes.length;
for (int y = 0; y < i; y++)
intValue |= ((int)intValueAsBytes[y] & 0xff) << ((i-(y+1)) * 8);
return intValue;
}//getIntegerValue()
isASN1Structure()
清單 4 中顯示的 isASN1Structure() 方法分析一個輸入字節是否表示具有特定標簽號的特定類型的 ASN.1 結構(即,特定於上下文的 (context specific)、應用程序級 (application level) 或者通用類 型 (universal type ))的標簽字節(第一個字節)。
這個方法取三個參數。第一個參數( tagByte )是要分析的輸入 字節 。第二和第三個參數( tagType 和 tagNumber )分別表示所要查找的標簽類型和標簽號。
為了檢查 tagByte 是否具有所需要的標簽號的標簽類型,isASN1Structure() 方法首先用 tagType 和 tagNumber 參數構建一個新的臨時標簽字節( tempTagByte )。然後比較 tempTagByte 與 tagByte 。如果它們是相同的,那麼方法就返回 true,如果不相同它就返回 false。
清單 4. isASN1Structure() 方法
public boolean isASN1Structure (byte tagByte, int tagType, int tagNumber)
{
byte tempTagByte = (byte) (tagType + tagNumber);
if (tagByte == tempTagByte)
return true;
else
return false;
}//isASN1Structure
getNumberOfLengthBytes()
清單 5 顯示的 getNumberOfLengthBytes() 方法取一個參數( firstLengthByte )。 firstLengthByte 參數是 ASN.1 結構的第一個長度字節。getNumberOfLengthBytes() 方法處理第一個長 度字節,以計算 ASN.1 結構中長度字節的字節數。這是一個工具方法,ASN1DataTypes 類中的其他方法 在需要知道一個 ASN.1 結構的長度字節的字節數時就使用它。
清單 5 中的 getNumberOfLengthBytes() 方法的實現策略如下:
檢查 firstLengthByte 的最高有效位(第 8 位)是否為零。清單 5 中的 if ( (firstLengthByte) & (1<<8)==0) 這一行完成這一任務。
如果最高有效位為零,那麼長度字節就遵循 單字節 長度表示法。在 本系列的第 1 部分 我們說過有 兩種長度表示法 ―― 單字節 和 多字節 。在 單字節 長度表示法中總是有一個長度字節。因此,如果 最高有效位為零,那麼只需返回 1 作為長度字節的字節數。
如果 firstLengthByte 的最高有效位是 1,這意味著長度字節遵循 多字節 長度表示法。在這時,清 單 5 中的 else 塊取得控制。
在 多字節 長度格式中,firstLengthByte 的最高有效位指定後面有多少長度字節。例如,如果 firstLengthByte 的值是 1000 0010 ,那麼最左邊的 1(最高有效位)說明後面的長度字節使用 多字節 長度表示法。其他 7 位( 000 0010 )說明還有兩個長度字節。因此,在這裡 getNumberOfLengthBytes() 方法應當返回 3( firstLengthBytes 加上另外兩個長度字節)。
清單 5 中 else 塊的第一行( firstLengthByte &= (byte)0x7f; )刪除 firstLengthByte 的 最高有效位。
else 塊中的第二行( return (int)firstLengthByte + 1; )將 firstLengthByte 強制轉換為 integer ,在得到的 integer 值中加 1,並返回這個 integer 。
清單 5. getNumberOfLengthBytes() 方法
public int getNumberOfLengthBytes (byte firstLengthByte) {
if ( (firstLengthByte & 1<<8) == 0 )
return 1;
else {
firstLengthByte &= (byte)0x7f;
return (int)firstLengthByte + 1;
}
}//getNumberOfLengthBytes
getLength()
這個方法的目的是檢查一個特定的 AS1 結構有多少個字節。處理應用程序通常有一個由多個 ASN.1 結構構成的嵌入層次所組成的字節數組。getLength() 方法計算特定結構中的字節數。
這個方法取兩個參數。第一個參數( ASN1Structure )是一個字節數組,它應當包含至少一個完整的 ASN.1 結構,這個結構本身包含標簽字節、長度字節和內容字節。第二個參數( offset )是一個在 ASN1Structure 字節數組中的偏移值。這個參數指定在 ASN1Structure 字節數組中包含的 ASN.1 結構的 開始位置。
getLength() 方法返回一個等於從 offset 字節處開始的 ASN.1 結構中的字節總數。
看一下 清單 6,它顯示了 getLength() 方法的一個實現:
第一步是向 getNumberOfLengthBytes() 方法傳 ASN.1 結構的第二個字節。這個 ASN.1 結構從 offset 字節開始,所以可以預計 offset 字節實際上就是標簽字節。因為所有 Kerberos 結構只包含一 個標簽字節,所以第二個字節(在 offset 字節後面的那個字節)是第一個長度字節。第一個長度字節說 明長度字節的總字節數,getNumberOfLengthBytes() 方法返回長度字節數。int numberOfLengthBytes = getNumberOfLengthBytes(ASN1Structure [offset+1]); 這一行執行這項任務 。
如果 getNumberOfLengthBytes() 方法返回一個大於 1 的值,那麼必須處理 多字節 長度表示法。在 這種情況下,將從 offset + 2 (讓過標簽字節和第一個長度字節) 開始的長度字節讀到一個名為 lengthValueAsBytes 的變量中。然後用 getIntegerValue() 方法將長度值從 ASN.1 字節轉換為 J2ME int 。最後,將結果加 1(以補償不包含在長度值中的標簽字節),再將長度值返回給調用應用程序。
如果 getNumberOfLengthBytes() 方法返回 1,則要處理 單字節 長度表示法。在這種情況下,只要 將第一個(也是惟一的一個)長度字節轉換為 J2ME int ,對它加 1(以補償不包含在長度值中的標簽字 節),並將得到的值返回給調用應用程序。
清單 6 getLength() 方法
public int getLength (byte[] ASN1Structure, int offset) {
int structureLength;
int numberOfLengthBytes = getNumberOfLengthBytes(ASN1Structure[offset + 1]);
byte[] lengthValueAsBytes = new byte[numberOfLengthBytes - 1];
if (numberOfLengthBytes > 1)
{
for (int i=0; i < numberOfLengthBytes-1 ; i++)
lengthValueAsBytes[i]= ASN1Structure [offset + i + 2];
structureLength = getIntegerValue(lengthValueAsBytes);
}
else
structureLength = (int) (ASN1Structure[offset+1]);
structureLength += numberOfLengthBytes + 1;
return structureLength;
}//getLength()
getASN1Structure
清單 7 中的 getASN1Structure() 方法從一個包含一系列 ASN.1 結構的字節數組中找出並提取特定 ASN.1 結構。這個方法有三個參數。第一個參數( inputByteArray )是輸入字節數組,需要從這個字節 數組中找到所需要的 ASN.1 結構。第二個參數是一個 int ,它指定要查找的標簽的類型。第三個參數指 定標簽號。
看一下 清單 7 中的 getASN1Strucute() 方法實現。它將 offset 值初始化為零並進入 do-while 循 環。
在 do-while 循環中,將字節數組中第一個字節讀入名為 tagByte 的字節中。然後用 isASN1Structure() 方法檢查輸入數組的第一個字節是否是所需要的 ASN.1 結構。
如果第一個字節代表所需要的結構,那麼就用 getLength() 方法找到要返回的所需數量的字節。然後 將所需要的字節拷貝到名為 outputBytes 的字節數組中、並將這些字節返回到調用應用程序。
如果第一個字節不代表所需要的結構,那麼就要跳到下一個結構。為此,將 offset 值設置為下一個 結構的開始位置。
do-while 循環在下一個循環中檢查下一個結構,並以此方式檢查整個輸入數組。如果沒有找到所需要 的結構,那麼 do-while 循環就會退出並返回 null。
清單 7. getASN1Structure() 方法
public byte[] getASN1Structure (byte[] inputByteArray, int tagType, int tagNumber)
{
byte tagByte;
int offset = 0;
do {
tagByte = inputByteArray[offset];
if (isASN1Structure(tagByte, tagType, tagNumber)) {
int lengthOfStructure = getLength(inputByteArray, offset);
byte[] outputBytes = new byte[lengthOfStructure];
for (int x =0; x < lengthOfStructure; x++)
outputBytes[x]= inputByteArray [x + offset];
return outputBytes;
}
else
offset += getLength(inputByteArray, offset);
} while (offset < inputByteArray.length);
return null;
}//getASN1Structure
getContents()
清單 8 中顯示的 getContents() 方法取 ASN1Structure 字節數組並返回一個包含 ASN1Structure 內容的字節數組。
getContents() 方法假定所提供的字節數組是一個有效的 ASN1 結構,所以它忽略結構中表示標簽字 節的第一個字節。它將第二個字節(即第一個長度字節)傳遞給 getNumberOfLengthBytes() 方法,這個 方法返回 ASN1Structure 輸入字節數組中的長度字節數。
然後它構建一個名為 contentBytes 的新字節數組,並將 ASN1Structure 的內容拷貝到 contentBytes 數組中(去掉標簽和長度字節)。
清單 8. getContents() 方法
public byte[] getContents (byte[] ASN1Structure)
{
int numberOfLengthBytes = getNumberOfLengthBytes(ASN1Structure [1]);
byte[] contentBytes = new byte[ASN1Structure.length - (numberOfLengthBytes + 1)];
for (int x =0; x < contentBytes.length; x++)
contentBytes[x]= ASN1Structure [x + numberOfLengthBytes + 1];
return contentBytes;
}//getContents
一些低層加密支持方法
除了前面描述的低層處理方法,還需要一些低層加密支持方法以處理一個票據響應。這就是為什麼在 解釋票據響應的處理之前,我要討論以下這些為 Kerberos 客戶機提供加密支持的方法:
encrypt()
decrypt()
getMD5DigestValue()
decryptAndVerifyDigest()
這些方法是 KerberosClient 類的組成部分,可以在 KerberosClient.java 文件中找到它們,本文的 源代碼下載中可以找到這個文件。下面是對這幾個方法的說明:
encrypt()
清單 9 中顯示的 encrypt() 方法處理低層加密並加密一個輸入字節數組。
這個方法取三個字節數組參數,即一個用於加密的密碼( keyBytes )、要加密的純文本數據( plainData )和一個初始向量或者 IV( ivBytes )。它用密鑰和 IV 加密純文本數據,並返回加密後的 純文本數據。
注意在 清單 9 中的 encrypt() 方法中,我使用了 DESEngine 、 CBCBlockCipher 、 KeyParameter 和 ParametersWithIV 類以加密這個純文本數據。這些類屬於在討論 第二篇文章中的清單 11 中的 getFinalKey() 方法時介紹的 Bouncy Castle 加密庫。回頭看一下並比較 清單 9 中的 encrypt() 方法 與第二篇文章中 清單 11 中的 getFinalKey() 方法。注意以下幾點:
getFinalKey() 方法使用一個包裝了初始向量的 ParametersWithIV 類。Kerberos 規范要求在生成加 密密鑰時,用加密密鑰作為 IV。因此,方法中的加密算法用加密密鑰作為 IV。因此,getFinalKey() 方 法中的算法使用這個加密密鑰作為一個 IV。
另一方面,encrypt() 方法設計為可以使用或者不使用 IV 值。更高級別的應用程序邏輯使用 encrypt() 方法時可以提供一個 IV 值或者忽略它。如果應用程序要求一個沒有 IV 值的數據加密,那麼 它將傳遞 null 作為第三個參數。
如果有 IV,那麼 encrypt() 方法用一個 ParametersWithIV 實例初始化 CBCBlockCipher。注意在 清 單 9 的 if (ivBytes != null) 塊中,我傳遞了一個 ParametersWithIV 實例作為給 cbcCipher.init() 方法調用的第二個參數。
如果第三個參數為 null,那麼 encrypt() 方法就用一個 KeyParameter 對象實始化 CBCBlockCipher 對象。注意在 清單 9 中的 else 塊中,我傳遞了一個 KeyParameter 實例作為 cbcCipher.init() 方法 調用的第二個參數。
第二篇文章的清單 11 中的 getFinalKey() 方法返回輸入數據最後一塊的處理結果。另一方面, encrypt() 方法將純文本處理的每一步的結果串接在一起、並返回串接在一起的所有處理過的(加密的) 字節。
清單 9. encrypt() 方法
public byte[] encrypt(byte[] keyBytes, byte[] plainData, byte[] ivBytes)
{
byte[] encryptedData = new byte[plainData.length];
CBCBlockCipher cbcCipher = new CBCBlockCipher(new DESEngine());
KeyParameter keyParameter = new KeyParameter(keyBytes);
if (ivBytes != null) {
ParametersWithIV kpWithIV = new ParametersWithIV (keyParameter, ivBytes);
cbcCipher.init(true, kpWithIV);
} else
cbcCipher.init(true, keyParameter);
int offset = 0;
int processedBytesLength = 0;
while (offset < encryptedData.length) {
try {
processedBytesLength = cbcCipher.processBlock( plainData,
offset,
encryptedData,
offset
);
offset += processedBytesLength;
} catch (Exception e) {
e.printStackTrace();
}//catch
}
return encryptedData;
}
decrypt()
( 清單 10 顯示的) decrypt() 方法與 encrypt() 方法的工作方式完全相同,只不過解密時, cbcCipher.init() 方法的第一個參數是 false (加密時它是 true )。
清單 10. decrypt() 方法
public byte[] decrypt(byte[] keyBytes, byte[] encryptedData, byte[] ivBytes)
{
byte[] plainData = new byte[encryptedData.length];
CBCBlockCipher cbcCipher = new CBCBlockCipher(new DESEngine());
KeyParameter keyParameter = new KeyParameter(keyBytes);
if (ivBytes != null) {
ParametersWithIV kpWithIV = new ParametersWithIV (keyParameter, ivBytes);
cbcCipher.init(false, kpWithIV);
} else
cbcCipher.init(false, keyParameter);
int offset = 0;
int processedBytesLength = 0;
while (offset < encryptedData.length) {
try {
processedBytesLength = cbcCipher.processBlock( encryptedData,
offset,
plainData,
offset
);
offset += processedBytesLength;
} catch (Exception e) {
e.printStackTrace();
}//catch
}
return plainData;
}//decrypt()
getMD5DigestValue()
清單 11 中顯示的 getMD5DigestValue() 方法取一個輸入數據字節數組,並返回一個用輸入數據計算 的 MD5 摘要值。
Bouncy Castle 加密庫在一個名為 MD5Digest 的類中包含 MD5 摘要支持。使用 MD5Digest 類進行摘 要計算需要四步:
首先,實例化一個 MD5Digest 對象。
然後,調用 MD5Digest 對象的 update() 方法,在調用同時傳遞要摘要的數據。
然後,實例化一個用來包含 MD5 摘要值輸出字節數組。
最後,調用 MD5Digest 對象的 doFinal() 方法,同時傳遞輸出字節數組。doFinal() 方法計算摘要 值並將它放到輸出字節數組中。
清單 11. getMD5DigestValue() 方法
public byte[] getMD5DigestValue (byte[] data)
{
MD5Digest digest = new MD5Digest();
digest.update (data, 0, data.length);
byte digestValue[] = new byte[digest.getDigestSize()];
digest.doFinal(digestValue, 0);
return digestValue;
}
decryptAndVerifyDigest()
回想一下在 第一篇文章圖 3 和清單 2 中,KDC 服務器的票據響應包含一個名為 enc-part 的字段, 它包裝了一個名為 EncryptedData 的加密的數據結構。就像在第一篇文章的 圖 3 的說明中描述的那樣 ,EncryptedData 結構由三個字段組成。
清單 12 中顯示的 decryptAndVerifyDigest() 方法取一個 EncryptedData 結構(實質上就是 enc- part 字段的內容)和一個解密密鑰作為參數,並返回 EncryptedData 結構的純文本表示。加密過程步驟 如下:
第 1 步:注意在 第一篇文章的清單 2 中,EncryptedData 結構實際上是 etype、kvno 和 cipher 字段的一個 SEQUENCE 。因此,第一步是檢查輸入字節數組是否是一個 SEQUENCE 。為此調用 isSequence() 方法。
第 2 步:如果輸入字節數組是一個 SEQUENCE ,那麼需要解析這個 SEQUENCE 並提取出其內容。調用 getContents() 方法以提取出 SEQUENCE 內容。
在 SEQUENCE 內容中,感興趣的是第一個字段( etype ,特定於上下文的標簽號 0),它表明了加密 類型。使用了 getASN1Structure() 方法調用以從 SEQUENCE 內容中提取 etype 字段。
第 3 步:調用 getContents() 方法以提取 etype 字段的內容,這是一個 ASN.1 INTEGER 。再次調 用 getContents() 方法以提取 INTEGER 的內容。然後將 INTEGER 內容傳遞給 getIntegerValue() 方法 ,這個方法返回 J2ME int 格式的 INETGER 內容。將 J2ME int 值存儲為一個名為 eTypeValue 的變量 。eTypeValue int 指定在生成 EncryptedData 結構時使用的加密類型。
第 4 步:回想一下 Kerberos 客戶機只支持一種加密類型 ―― DES-CBC ―― 它的標識號為 3。因 此,我檢查 eTypeValue 是否為 3。如果它不是 3(即服務器使用了非 DES-CBC 的加密算法), 那麼 Kerberos 客戶機就不能處理這一過程。
第 5 步:下一步是從 EncryptedDataSEQUENCE 內容中提取第三個字段( cipher ,特定於上下文的 標簽號 2)。調用 getASN1Structure() 方法以完成這項任務。
第 6 步:下一步,調用 getContents() 方法提取 cipher 字段的內容。cipher 字段的內容是一個 ASN.1 OCTET STRING 。還需要再調用 getContents() 方法,以提取 OCTET STRING 的內容 。
第 7 步: OCTET STRING 內容是加密的,因此需要用前面討論的 decrypt() 方法解密。
第 8 步:解密的數據字節數組由三部分組成。第一部分由前八位組成,它包含一個稱為 confounder 的隨機數。confounder 字節沒有意義,它們只是幫助增加黑客的攻擊的難度。
解密的數據的第 9 到第 24 個字節構成了第二部分,它包含一個 16 字節的 MD5 摘要值。這個摘要 值是對整個解密的數據 ―― 其中16 個摘要字節(第二部分)是用零填充的 ―― 計算的。
第三部分是要得到實際純文本數據。
因為第八步進行完整性檢查,所以必須將解密的數據的第 9 到第 24 個字節用零填充,對整個數據計 算一個 MD5 摘要值,並將摘要值與第二部分(第 9 到第 24 個字節)進行匹配。如果兩個摘要值匹配, 那麼消息的完整性就得到驗證。
第 9 步:如果通過了完整性檢查,那麼就返回解密的數據的第三部分(第 25 個字節到結束)。
清單 12. decryptAndVerifyDigest() 方法
public byte[] decryptAndVerifyDigest (byte[] encryptedData, byte[] decryptionKey)
{
/****** Step 1: ******/
if (isSequence(encryptedData[0])) {
/****** Step 2: ******/
byte[] eType = getASN1Structure(getContents(encryptedData),
CONTEXT_SPECIFIC, 0);
if (eType != null) {
/****** Step 3: ******/
int eTypeValue = getIntegerValue(getContents(getContents(eType)));
/****** Step 4: ******/
if ( eTypeValue == 3) {
/****** Step 5: ******/
byte[] cipher = getASN1Structure(getContents(encryptedData),
CONTEXT_SPECIFIC, 2);
/****** Step 6: ******/
byte[] cipherText = getContents(getContents(cipher));
if (cipherText != null) {
/****** Step 7: ******/
byte[] plainData = decrypt(decryptionKey,
cipherText, null);
/****** Step 8: ******/
int data_offset = 24;
byte[] cipherCksum = new byte [16];
for (int i=8; i < data_offset; i++)
cipherCksum[i-8] = plainData[i];
for (int j=8; j < data_offset; j++)
plainData[j] = (byte) 0x00;
byte[] digestBytes = getMD5DigestValue(plainData);
for (int x =0; x < cipherCksum.length; x++) {
if (!(cipherCksum[x] == digestBytes[x]))
return null;
}
byte[] decryptedAndVerifiedData = new byte[plainData.length - data_offset];
/****** Step 9: ******/
for (int i=0; i < decryptedAndVerifiedData.length; i++)
decryptedAndVerifiedData[i] = plainData[i+data_offset];
return decryptedAndVerifiedData;
} else
return null;
} else
return null;
} else
return null;
} else
return null;
}//decryptAndVerifyDigest
從票據響應中提取票據和密鑰
我們已經討論了低層 ASN.1 處理以及低層加密支持方法,現在可以討論如何用這些方法處理在前面用 清單 1 中的 getTicketResponse() 方法提取的票據響應了。
看一下 清單 13 中顯示的 getTicketAndKey() 方法(它屬於 KerberosClient 類)。這個方法取票 據響應字節數組和一個解密密鑰字節數組作為參數。這個方法從票據響應中提取票據和密鑰。
getTicketAndKey() 方法返回一個名為 TicketAndKey 的類的實例(這是一個要從票據響應中提取的 密鑰和票據的包裝器)。我在 清單 14 中已經展示了 TicketAndKey 類。這個類只有四個方法:兩個子 setter 方法和兩個 getter 方法。setKey() 和 getKey() 方法分別設置和獲得密鑰字節。setTicket() 和 getTicket() 方法分別設置和獲得票據字節。
現在看一看在 清單 13 的 getTicketAndKey() 方法中所發生的過程。回想在對 第一篇文章的圖 4 和清單 2的討論中,介紹了 Kerberos 密鑰和票據是如何存儲在票據響應中的。從票據響應中提取密鑰是 一個漫長的過程,包括以下步驟:
1. 首先,檢查 ticketResponse 字節數組是否真的包含了票據響應。為此,我使用了 isASN1Structure() 方法。如果 isASN1Structure() 方法返回 false,那麼它表明輸入 ticketResponse 字節數組不是有效的票據響應。在這種情況下,不進行任何進行一步的處理並返回 null。
注意在 清單 13 中,我調用了兩次 isASN1Structure() 方法。第一調用 isASN1Structure() 方法時 用“11”作為第三個參數的值,而第二次調用 isASN1Structure() 方法時,用“13”作為第三個參數的 值。這是因為“11”是 TGT 響應的特定於應用程序的標簽號(本系列的 第一篇文章的清單 2),而“13 ”是服務票據響應的特定於應用程序的標簽號(本系列的 第一篇文章的清單 4)。如果 ticketResponse 字節數組是一個 TGT 響應或者服務票據響應,那麼這兩次方法調用之一會返回 true,就可以進行進一步 的處理。如果這兩個方法調用都不返回 true,那麼表明 ticketResponse 字節數組不是一個票據響應, 就要返回 null 並且不做任何進一步的處理。
2. 第二步是提取票據響應結構的內容。為此,我使用了 getContents() 方法調用。
3. 票據響應的內容應當是一個 ASN.1 SEQUENCE ,可以調用 isSequence() 方法對此進行檢查。
4. 接下來,我調用 getContents() 方法提取 SEQUENCE 的內容。
5. SEQUENCE 的內容是票據響應的七個結構(如圖 3 和 第一篇文章的清單 2所示)。在這七個結構 之外,只需要兩個:ticket 和 enc-part。
因此,第五步是從 SEQUENCE 內容中提取 ticket 字段(調用 getASN1Structure() 方法),提取 ticket 字段(調用 getContents() 方法)的內容,並將內容存儲到在前面創建的 TicketAndKey 對象中 。注意 ticket 字段是特定於上下文的標簽號 5,而這個字段的內容是實際的票據,它以一個應用程序級 別的標簽號 1 開始,如 第一篇文章的清單 3 和圖 9所示。
6. 下面,必須從在第 4 步中得到的 SEQUENCE 內容中提取密鑰。這個鍵在在 SEQUENCE 內容的 enc -part 字段中。因此,在第 6 步,我調用 getASN1Structure() 方法從 SEQUENCE 內容中捕捉 enc-part 字段。
7. 得到了 enc-part 字段後,就要調用 getContents() 方法得到其內容。enc-part 字段的內容構成 了一個 EncryptedData 結構。
8. 可以向 decryptAndVerifyDigest() 方法傳遞 EncryptedData 結構,這個方法解密 EncryptedData 結構並對 EncryptedData 進行一個摘要驗證檢查。
9. 如果成功進行了解密和摘要驗證過程,那麼 decryptAndVerifyDigest() 方法就從已解密的密文數 據中提取了 ASN.1 數據。ASN.1 數據應當符合我在 第一篇文章的圖 4中展示的結構。注意所需要的密鑰 是 第一篇文章的圖 4中顯示的結構中的第一個字段。一個應用程序級別的標簽號“25”或者“26”包裝 純文本數據。這個結構稱為 EncKDCRepPart (加密的 KDC 回復部分)。
這樣,下一步就是檢查由 decryptAndVerifyDigest() 方法返回的數據是否是一個應用程序級別的標 簽號 25 或者 26。
10. 下一步是提取 EncKDCRepPart 結構的內容 。調用 getContents() 方法提取所需要的內容。
EncKDCRepPart 內容是一個 SEQUENCE ,所以還必須提取 SEQUENCE 內容 。再一次調用 getContents() 方法以提取 SEQUENCE 內容。
11. SEQUENCE 內容的第一個字段(稱為 key,具有上下文特定的標簽號 0)包含 key 字段。可以調 用 getASN1Structure() 方法以從 SEQUENCE 內容中提取第一個字段。
12. 下面,提取 key 字段的內容。調用 getConents() 方法可以返回這些內容。
key 字段的內容構成另一個名為 EncryptionKey 的 ASN.1 結構,它是一個兩字段 ―― 即 keytype 和 keyvalue ―― 的 SEQUENCE 。再一次調用 getContents() 方法提取 SEQUENCE 的內容。
13. 所需要的會話密鑰在 SEQUENCE 內容的第二個字段中( keyvalue )。因此,必須調用 getASN1Structure() 方法以從 SEQUENCE 內容中提取 keyvalue 字段(特定於上下文的標簽號 1)。
14. 現在已經有了 keyvalue 字段。必須調用 getContents() 方法提取它的內容。keyvalue 內容是 一個 OCTET STRING ,所以必須再次調用 getContents() 方法以提取 OCTET STRING 的內容,它就是所 要找的那個密鑰。
所以只要將這個密鑰字節包裝在 KeyAndTicket 對象中(通過調用其 setKey() 方法)並返回 KeyAndTicket 對象。
清單 13. getTicketAndKey() 方法
public TicketAndKey getTicketAndKey( byte[] ticketResponse, byte[] decryptionKey)
{
TicketAndKey ticketAndKey = new TicketAndKey();
int offset = 0;
/***** Step 1:*****/
if ((isASN1Structure(ticketResponse[0], APPLICATION_TYPE, 11)) ||
(isASN1Structure(ticketResponse[0], APPLICATION_TYPE, 13))) {
try {
/***** Step 2:*****/
byte[] kdc_rep_sequence = getContents(ticketResponse);
/***** Step 3:*****/
if (isSequence(kdc_rep_sequence[0])) {
/***** Step 4:*****/
byte[] kdc_rep_sequenceContent = getContents(kdc_rep_sequence);
/***** Step 5:*****/
byte[] ticket = getContents(getASN1Structure(kdc_rep_sequenceContent,
CONTEXT_SPECIFIC, 5));
ticketAndKey.setTicket(ticket);
/***** Step 6:*****/
byte[] enc_part = getASN1Structure(kdc_rep_sequenceContent,
CONTEXT_SPECIFIC, 6);
if (enc_part!=null) {
/***** Step 7:*****/
byte[] enc_data_sequence = getContents(enc_part);
/***** Step 8:*****/
byte[] plainText = decryptAndVerifyDigest(enc_data_sequence,
decryptionKey);
if (plainText != null){
/***** Step 9:*****/
if ((isASN1Structure(plainText[0],APPLICATION_TYPE, 25)) ||
(isASN1Structure(plainText[0], APPLICATION_TYPE, 26))) {
/***** Step 10:*****/
byte[] enc_rep_part_content = getContents(getContents (plainText));
/***** Step 11:*****/
byte[] enc_key_structure = getASN1Structure (enc_rep_part_content,
CONTEXT_SPECIFIC, 0);
/***** Step 12:*****/
byte[] enc_key_sequence = getContents(getContents (enc_key_structure));
/***** Step 13:*****/
byte[] enc_key_val = getASN1Structure(enc_key_sequence,
CONTEXT_SPECIFIC, 1);
/***** Step 14:*****/
byte[] enc_key = getContents(getContents(enc_key_val));
ticketAndKey.setKey(enc_key);
return ticketAndKey;
} else
return null;
} else
return null;
} else
return null;
} else
return null;
} catch (Exception e) {
e.printStackTrace();
}
return null;
} else
return null;
}//getTicketAndKey()
清單 14. TicketAndKey 類
public class TicketAndKey
{
private byte[] key;
private byte[] ticket;
public void setKey(byte[] key)
{
this.key = key;
}//setKey()
public byte[] getKey()
{
return key;
}//getKey
public void setTicket(byte[] ticket)
{
this.ticket = ticket;
}//setTicket
public byte[] getTicket()
{
return ticket;
}//getTicket
}