得到一個服務票據
已經處理了 TGT 響應並提取了 TGT 和會話密鑰。現在可以使用這個 TGT 和會話密鑰向 KDC 服務器 請求一個服務票據。對服務票據的請求類似於對我在清單 1 中生成的對 TGT 的請求。我在 TGT 請求中 省略的可選 padata 字段在服務票據請求中不再是可選的了。因此,需要在服務票據請求中加上 padata 字段。
padata 字段是包含兩個字段 ―― padata-type 和 padata-value ―― 的 SEQUENCE。padata-value 字段帶有幾種類型的數據,因此相應的 padata-type 字段指定了 padata-value 字段所帶的數據的類型 。
在 本系列的第一篇文章的圖 5 中我介紹了服務票據中的 padata 字段的結構。在那裡說過服務票據 請求中的 padata 字段包裝了一個認證頭(一個 KRB_AP_REQ 結構),它又包裝了 TGT 以及其他數據。
所以,在可以開始生成票據請求之前,必須生成一個認證頭。下面是分析了生成認證頭的過程。
生成一個認證頭
我在 KerberosClient 類中加入了以下方法以生成一個認證頭:
getMD5DigestValue()
getChceksumBytes()
authorDigestAndEncrypt()
getAuthenticationHeader()
這四個方法都是 helper 方法。第五個方法( getAuthenticationHeader() )使用 helper 方法並生 成認證頭。
authorDigestAndEncrypt()
清單 15 顯示的 authorDigestAndEncrypt() 方法取一個純文本數據字節數組和一個加密密鑰。這個 方法對純文本數據計算一個摘要值、加密純文本數據、並返回一個 EncryptedData 結構,這個結構與我 作為輸入傳遞給 清單 12 的 decryptAndVerifyDigest() 方法的結構完全匹配。
可以說 清單 15 的 authorDigestAndEncrypt() 方法與前面討論的 decryptAndVerifyDigest() 方法 正好相反。authorDigestAndEncrypt() 方法取 decryptAndVerifyDigest() 方法返回的純文本數據作為 輸入。與此類似, authorDigestAndEncrypt() 方法返回的 EncryptedData 結構就是我作為輸入傳遞給 decryptAndVerifyDigest() 方法的結構。
authorDigestAndEncrypt() 方法實現了以下策略:
首先,生成八個隨機字節,它們構成了 confounder。
然後,聲明一個名為 zeroedChecksum 的字節數組,它有十六個字節並初始化為零。這個有十六個零 的數組作為一個全為零的摘要值。
第三,用其他的字節填入輸入數據字節數組,以使數組中的字節數成為八的倍感數。編寫了一個名為 getPaddedData() 的方法(如 清單 16所示),它取一個字節數組並在填充後返回這個數組。下面,鏈接 (第 1 步得到的)confounder、(第 2 步得到的)全為零的摘要以及填充後的純文本字節數組。
第四步是對第 3 步串接的字節數組計算 MD5 摘要值。
第五步是將摘要字節放到它們相應的位置上。第 5 的結果與第 3 步一樣,只不過全為零的摘要現在 換成了真正的摘要值。
現在調用 encrypt() 方法以加密第 5 步得到的字節數組。
然後,生成 etype 字段(特定於上下文的標簽號 0)。
然後,調用 getOctetStringBytes() 方法將第 6 步得到的加密字節數組包裝到 OCTET STRING 中。 然後將 OCTET STRING 包裝到 cipher 字段中(一個特定於上下文的標簽號 2)。
最後,鏈接 etype 和 cipher 字段,將這個字符串包裝到一個 SEQUENCE 中,並返回這個 SEQUENCE 。
清單 15. authorDigestAndEncrypt() 方法
public byte[] authorDigestAndEncrypt(byte[] key, byte[] data)
{
/****** Step 1: ******/
byte[] conFounder = concatenateBytes (getRandomNumber(), getRandomNumber ());
/****** Step 2: ******/
byte[] zeroedChecksum = new byte[16];
/****** Step 3: ******/
byte[] paddedDataBytes = concatenateBytes (conFounder,
concatenateBytes(zeroedChecksum,
getPaddedData(data)
)
);
/****** Step 4: ******/
byte[] checksumBytes = getMD5DigestValue(paddedDataBytes);
/****** Step 5: ******/
for (int i=8; i < 24; i++)
paddedDataBytes[i] = checksumBytes[i-8];
/****** Step 6: ******/
byte[] encryptedData = encrypt(key, paddedDataBytes, null);
/****** Step 7: ******/
byte[] etype = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(3)
);
/****** Step 8: ******/
byte[] cipher = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
2, getOctetStringBytes(encryptedData)
);
/****** Step 9: ******/
byte[] ASN1_encryptedData = getSequenceBytes (
concatenateBytes(etype,cipher)
);
return ASN1_encryptedData;
}//authorDigestAndEncrypt
清單 16. getPaddedData() 方法
public byte[] getPaddedData(byte[] data) {
int numberToPad = 8 - ( data.length % 8 );
if (numberToPad > 0 && numberToPad != 8)
{
byte[] bytesPad = new byte[numberToPad];
for (int x = 0; x < numberToPad; x++)
bytesPad [x] = (byte)numberToPad;
return concatenateBytes(data, bytesPad);
}
else
return data;
}//getPaddedData()
getChecksumBytes()
getChecksumBytes() 方法生成一個稱為 Checksum 的結構,如 清單 17 所示。Checksum 結構包含兩 個字段: cksumtype 和 checksum。
清單 17. Checksum 結構
Checksum ::= SEQUENCE {
cksumtype[0] INTEGER,
checksum[1] OCTET STRING
}+
有兩個地方需要 Checksum 結構 ―― 第一個是生成服務票據響應時,然後是生成安全上下文建立請 求時。Checksum 結構的作用在這兩種情況下是不同的,需要在生成服務票據和上下文建立請求時說明 (elaborate)。
清單 18 所示的 getChecksumBytes() 方法取兩個字節數組參數。第一個參數帶有 checksum 字段, 而第二個參數帶有 cksumtype 字段。
getChecksumBytes() 方法將 cksumtype 字段包裝到一個特定於上下文的標簽號 0(它表示 cksumtype 字段,如 清單 17 所示),而將 checksum 字段包裝到一個特定於上下文的標簽號 1(它表 示 checksum 字段,同樣如 清單 17 所示)。然後它鏈接這兩個字段,將這個數組包裝到一個 SEQUENCE 中,並返回這個 SEQUENCE。
清單 18. getChecksumBytes() 方法
public byte[] getChecksumBytes(byte[] cksumData, byte[] cksumType){
byte[] cksumBytes = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC, 3,
getSequenceBytes (
concatenateBytes (
getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0,
cksumType
),
getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC, 1,
getOctetStringBytes(cksumData)
)
)
)
);
return cksumBytes;
}//getChecksumBytes()
getAuthenticationHeader()
在 本系列的第一篇文章 中的“服務票據請求”一節中,介紹過 KRB-AP-REQ 結構(也稱為認證頭) 包裝了 Kerberos 票據。此外,認證頭還包裝了 authenticator 字段,它表明客戶機是否掌握了 會話 或者 子會話 密鑰。
如 第一篇文章的圖 5 所示,認證頭由五個字段組成,即 pvno、msg-type、ap-options、ticket 和 authenticator。
清單 19 的 getAuthenticationHeader() 方法逐一生成這五個字段,然後以正確的順序將各個字段串 接起來以形成一個完整的認證頭。
清單 19. getAuthenticationHeader() 方法
public byte[] getAuthenticationHeader( byte[] ticketContent,
String clientRealm,
String clientName,
byte[] checksumBytes,
byte[] encryptionKey,
int sequenceNumber
)
{
byte[] authenticator = null;
byte[] vno = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(5)
);
byte[] ap_req_msg_type = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
1, getIntegerBytes(14)
);
byte[] ap_options = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
2, getBitStringBytes(new byte[5])
);
byte[] ticket = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
3, ticketContent
);
byte[] realmName = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getGeneralStringBytes(clientRealm)
);
byte[] generalStringSequence = getSequenceBytes(
getGeneralStringBytes (clientName)
);
byte[] name_string = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, generalStringSequence
);
byte[] name_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_PRINCIPAL)
);
byte[] clientNameSequence = getSequenceBytes(
concatenateBytes (name_type, name_string)
);
byte[] cName = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
2, clientNameSequence);
byte[] cusec = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
4, getIntegerBytes(0)
);
byte[] ctime = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
5, getGeneralizedTimeBytes (
getUTCTimeString(System.currentTimeMillis()).getBytes()
)
);
if (sequenceNumber !=0 ) {
byte[] etype = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(3)
);
byte[] eKey = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
1, getOctetStringBytes(encryptionKey)
);
byte[] subKey_sequence = getSequenceBytes (concatenateBytes(etype, eKey));
byte[] subKey = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
6, subKey_sequence
);
byte[] sequenceNumberBytes = {
(byte)0xff,
(byte)0xff,
(byte)0xff,
(byte)0xff
};
sequenceNumberBytes[3] = (byte)sequenceNumber;
byte[] seqNumber = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
7, getIntegerBytes(sequenceNumberBytes)
);
authenticator = getTagAndLengthBytes(ASN1DataTypes.APPLICATION_TYPE,
2, getSequenceBytes(
concatenateBytes(vno,
concatenateBytes(realmName,
concatenateBytes(cName,
concatenateBytes(checksumBytes,
concatenateBytes(cusec,
concatenateBytes(ctime,
concatenateBytes(subKey,seqNumber)
)
)
)
)
)
)
)
);
} else {
authenticator = getTagAndLengthBytes(ASN1DataTypes.APPLICATION_TYPE,
2, getSequenceBytes(
concatenateBytes(vno,
concatenateBytes(realmName,
concatenateBytes(cName,
concatenateBytes(checksumBytes,
concatenateBytes(cusec,ctime)
)
)
)
)
)
);
}//if (sequenceNumber !=null)
byte[] enc_authenticator = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
4, authorDigestAndEncrypt(encryptionKey, authenticator)
);
byte[] ap_req = getTagAndLengthBytes (
ASN1DataTypes.APPLICATION_TYPE,
14, getSequenceBytes(
concatenateBytes (vno,
concatenateBytes(ap_req_msg_type,
concatenateBytes(ap_options,
concatenateBytes(ticket, enc_authenticator)
)
)
)
)
);
return ap_req;
}//getAuthenticationHeader
getAuthenticationHeader() 方法有幾個輸入參數:
名為 ticketContent 的字節數組,它包含由 getAuthenticationHeader() 方法包裝到認證頭的 Kerberos 票據( TGT )。
名為 clientRealm 的字符串類型參數,它指定(生成這個請求的)Kerberos 客戶機所注冊的域 (realm )的名字。
名為 clientName 的字符串類型參數指定生成這個請求的 Kerberos 客戶機的名字。
checksumBytes 字節數組攜帶一個 Checksum 結構以及 getChecksumBytes() 方法。
encryptionKey 字節數組攜帶用於生成認證頭的加密部分的加密密鑰。
名為 sequenceNumber 的參數是一個 integer 值,它標識發送者的請求號。
在 第一篇文章的圖 5 介紹過,認證頭包含以下字段:
pvno
msg-type
ap-options
ticket
authenticator
現在讓我們看看 清單 19 中的 getAuthenticationHeader() 方法實現是如何生成認證頭的各個字段 的( KRB-AP-REQ 結構):
首先要生成 pvno 字段,它有特定於上下文的標簽號 0,並包裝一個值為 5 的 ASN1 INTEGER 。調用 getTagAndLengthBytes() 方法執行這項任務。我將 pvno 字段存儲 在一個名為 vno 的字節數組中。
類似地,兩次調用 getTagAndLengthBytes() 方法生成 msg-type (特定於上下文的標簽號 1)和 ap-options 字段(特定於上下文的標簽號 2)。
下一行( byte[] ticket = getTagAndLengthBytes(ASN1DataTypes.Context_Specific, 3, ticketContent) )將票據結構包裝到特定於上下文的標簽號 3 中,這是認證頭的第四個字段。
然後,必須生成認證頭的第五個字段(名為 authenticator ,它有特定於上下文的標簽號 4)。 authenticator 字段是一個 EncryptedData 結構。authenticator 字段的純文本格式是一個 Authenticator 結構。因此,首先生成純文本格式的完整 Authenticator 結構,將這個純文本 Authenticator 傳遞給 authorDigestAndEncrypt() 方法,這個方法返回 Authenticator 的完整 EncryptedData 表示。
注意在 第一篇文章中的清單 3 和圖 5 中,純文本格式的 Authenticator 結構由以下字段組成(省 略最後一個字段,它是不需要的):
authenticator-vno
creal
cname
cksum
cusec
ctime
subkey
seq-number
在解釋 第一篇文章的圖 5時,我已經介紹了每一個字段的意義。
authenticator-vno 字段與 pvno 字段完全相同(本節前面討論了 vno 字節數組,它包含特定於上下 文的標簽號 0 且帶值為 5 的 INTEGER )。所以我重用了在 authenticator_vno 字段中使用的同一個字 節數組。
現在該生成 crealm 字段了,它類似於我在 第二篇 文章“生成請求正文”一節中介紹的 realm 字段 。同樣,在那一節也介紹了 PrincipalName 類型的 cname 字段。在這裡我就不介紹 crealm 和 cname 字段的生成細節了。
下一項任務是生成 cksum 字段,它是 Checksum 類型。服務票據請求中的 cksum 字段的作用是加密 結合 authenticator 與一些應用程序數據。注意以下三點:
authenticator 結構包含 cksum 字段。
cksum 字段包含一些應用程序數據的加密哈希值。
整個 authenticator 結構(包括 cksum 字段)是用一個密鑰加密的。
只要在 authenticator 中的 cksum 字段與對應用程序數據的加密 checksum 相匹配,就證明生成 authenticator 和應用程序數據的客戶機擁有密鑰。
調用 getAuthenticationHeader() 方法的應用程序(通過調用 getChecksumBytes() 方法)生成 Checksum 結構,並將 Checksum 字節數組作為 checksumBytes 參數的值傳遞給 getAuthenticationHeader() 方法。
結果, checksumBytes 參數中就有了 Checksum 結構。只需要將 checksumBytes 包裝到特定於上下 文的標簽號 3 中(這是 authenticator 結構中 cksum 字段的標簽號)。
現在生成 cusec 字段,它表示客戶機時間的微秒部分。這個字段的取值范圍為 0 到 999999。這意味 著可以在這個字段提供的最大值為 999999 微秒。不過,MIDP 不包含任何可以提供比一毫秒更精確的時 間值的方法。因此,不能指定客戶機的微秒部分。只是對這個字段傳遞一個零值。
在 Authenticator 結構中,還要生成兩個字段 ―― subkey 和 seq-number 。在為票據請求而生成 的 Authenticator 中不一定要包含這兩個字段,但是後面在用同一個 getAuthenticationHeader() 方法 生成上下文建立請求時需要它們。
現在,只需知道只要檢查 sequenceNumber 參數是否為零。對於服務票據請求它為零,對於上下文建 立請求它為非零。
如果 sequenceNumber 參數為非零,那麼就生成 subkey 和 seq-number 字段,然後鏈接 authenticator-vno、 realm、cname、cksum、cusec、ctime、subkey 和 seq-number 字段以構成一個字 節數組,將這個字節數組包裝到一個 SEQUENCE 中,然後將 SEQUENCE 包裝到 Authenticator 中(應用 程序級別標簽號 2)。
如果 sequenceNumber 參數為零,那麼可以忽略 subkey 和 seq-number 字段,鏈接 authenticator -vno、crealm、cname、cksum、cusec 和 ctime 字段以構成串接的字節數組,將這個字節數組包裝到一 個 SEQUENCE 中,然後將這個 SEQUENCE 包裝到 Authenticator 中(應用程序級別標簽號 2)。
下面,需要取完整的 Authenticator 結構並將它傳遞 authorDigestAndEncrypt() 方法,這個方法返 回純文本 Authenticator 的完整 EncryptedData 表示。
下一個任務是串接認證頭或者 KRB-AP-REQ 結構的五個字段( pvno、msg-type、ap-options、ticket 、authenticator )為一個字節數組,將這個字節數組包裝為一個 SEQUECNE ,最後將這個 SEQUENCE 包 裝到應用程序級別的標簽號 14。
現在已經完成認證頭,可以將它返回給給調用應用程序了。
生成服務票據請求
我討論了生成服務票據請求需要的所有低層方法。將使用 清單 1 中請求 TGT 時所使用的同一個 getTicketResponse() 方法生成服務票據請求,只需要對 清單 1 稍加修改以使它可以同時用於 TGT 和 服務票據請求。讓我們看一下這個過程。
看一下 清單 20,其中可以看到修改過的清單 1 中的 getTicketRespone() 方法。與 清單 1相比, 修改過的版本增加了一些代碼:
清單 20. getTicketResponse() 方法
public byte[] getTicketResponse( String userName,
String serverName,
String realmName,
byte[] kerberosTicket,
byte[] key
)
{
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 (serverName),
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)
)
)
)
)
)
)
);
if (kerberosTicket != null) {
msg_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
2, getIntegerBytes(12));
sname_string = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
1, getSequenceBytes(getGeneralStringBytes(serverName)));
sname_type = getTagAndLengthBytes(ASN1DataTypes.CONTEXT_SPECIFIC,
0, getIntegerBytes(ASN1DataTypes.NT_UNKNOWN));
sprincipalNameSequence = getSequenceBytes(
concatenateBytes (sname_type, sname_string)
);
sname = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
3, sprincipalNameSequence
);
byte[] req_body_sequence = getSequenceBytes(
concatenateBytes(kdc_options,
concatenateBytes(realm,
concatenateBytes(sname,
concatenateBytes(till,
concatenateBytes(nonce, etype)
)
)
)
)
);
req_body = getTagAndLengthBytes (
ASN1DataTypes.CONTEXT_SPECIFIC,
4, req_body_sequence
);
byte[] cksum = getChecksumBytes(
getMD5DigestValue(req_body_sequence),
getIntegerBytes(7)
);
byte[] authenticationHeader = getAuthenticationHeader(
kerberosTicket,
realmName,
userName,
cksum,
key,
0
);
byte[] padata_sequence = getSequenceBytes(concatenateBytes(
getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
1,getIntegerBytes(1)),
getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
2, getOctetStringBytes (authenticationHeader)
)
)
);
byte[] padata_sequences = getSequenceBytes(padata_sequence);
byte[] padata = getTagAndLengthBytes(
ASN1DataTypes.CONTEXT_SPECIFIC,
3, padata_sequences
);
ticketRequest = getTagAndLengthBytes(
ASN1DataTypes.APPLICATION_TYPE,
12, getSequenceBytes(
concatenateBytes(pvno,
concatenateBytes(msg_type,
concatenateBytes(padata, req_body)
)
)
)
);
} else {
ticketRequest = getTagAndLengthBytes(
ASN1DataTypes.APPLICATION_TYPE,
10, getSequenceBytes(
concatenateBytes(pvno,
concatenateBytes(msg_type, req_body)
)
)
);
}
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();
}
return ticketResponse;
}//getTicketResponse
清單 20 中顯示的新的 getTicketResponse() 方法有五個參數: userName、serverName、realmName 、kerberosTicket 和 key 。要請求一個服務票據,需要傳遞 kerberosTicket 字節數組的 TGT 。另一 方面,在請求 TGT 時,不必傳遞一個票據,因此對於 kerberosTicket 字節數組傳遞“null”。
TGT 請求與服務票據請求的主要區別是 padata 字段。在本系列 第一篇文章 中的“請求服務票據” 一節中討論服務票據請求的 padata 字段時已經介紹過。
在 getTicketResponse() 的最後,我加入了一個 if (kerberosTicket!=null) 塊。只有在 kerberosTicket 參數不為 null 時才進入這個塊(在所有 TGT 請求中它都是 null)。
在 if (kerberosTicket!=null) 塊中,我生成了 padata 字段。正如 第一篇文章的圖 5 中描述的, 這個 padata 字段包裝一個可由 getAuthenticationHeader() 方法生成的認證頭。
在 getAuthenticationHeader() 方法中還可了解到,為了生成一個認證頭,需要一個可由 getChecksumBytes() 方法生成的 Checksum 結構。
現在,回想一下在討論 getChecksumBytes() 方法時說過,為了生成一個 Checksum 結構,需要用於 cksumtype 和 checksum 字段的數據。
因此,生成一個認證頭需要三步:
生成 cksumtype 和 checksum 字段的數據。如果是服務票據請求,那麼 checksum 字段的數據只是對 包含服務票據請求的 req-body 字段的所有子字段的 SEQUENCE 計算的 MD5 摘要值(注意在 第一篇文章 的圖 5, req-body 是服務票據請求的第四個字段,就在服務票據請求的第三個字段 padata 字段後面) 。cksumtype 字段的數據是 integer 7 的 ASN1 表示。這個值指定 checksum 的類型。
調用 getChecksumBytes() 方法並傳遞 cksumtype 和 checksum 字段的數據。getChecksumBytes() 方法生成完整的 Checksum 結構。
調用 getAuthenticationHeader() 方法,同時傳遞 Checksum 結構。getAuthenticationHeader() 返 回認證頭。
生成了認證頭後,必須將它包裝到一個 padata 字段中。為此,有幾件事要做:
調用我在 第二篇文章的清單 5 中描述的 getOctetStringBytes() 方法將認證頭包裝到一個 OCTET STRING 中。
將這個 OCTET STRING 包裝到 padata-value 字段中(特定於上下文的標簽號 2),調用 getTagAndLengthBytes() 方法以完成這項任務。
再次調用 getTagAndLengthBytes() 方法生成對應於第 2 步生成的 padata-value 的 padata-type 字段。
現在,鏈接 padata-type 和 padata-value 字段。
將第 4 步鏈接的字節數組放入一個 SEQUENCE 中。這個 SEQUENCE 表示一個 PADATA 結構(如 第一 篇文章的圖 5 和清單 3所示)。
第一篇文章的圖 5 和清單 3 中顯示的 padata 字段是 PADATA 結構的一個 SEQUENCE 。這意味著一 個 padata 字段可以包含幾個 PADATA 結構。不過,只有一個 PADATA 結構要包裝到 padata 字段中,這 意味著只要將第 5 步得到的 SEQUENCE 包裝到另一個外部或者更高層的 SEQUENCE 中。
第 6 步的更高層 SEQUENCE 表示 PADATA 結構的 SEQUENCE ,現在可以將它包裝到 padata 字段中( 一個特定於上下文的標簽號 3)。
在 清單 20 的結尾處的 if (kerberosTicket!=null) 塊中可以找到 getTicketResponse() 方法中增 加的所有新代碼。
到此就結束了對於修改現有的 getTicketResponse() 方法以使它可同時用於 TGT 和服務票據請求的 討論。getTicketResponse() 方法生成一個服務票據請求、將這個請求發送給 KDC 、接收服務票據響應 、並將響應返回給調用應用程序。
從服務票據響應中提取服務票據和子會話密鑰
服務票據響應類似於 TGT 響應。在 清單 13 中的 getTicketAndKey() 方法解析一個 TGT 響應以提 取 TGT 和會話密鑰。同一個方法也解析服務票據響應以從服務票據響應中提取服務票據和子會話密鑰。 所以,不必編寫任何提取服務票據和子會話密鑰的代碼。
創建一個安全通信上下文
現在有了與電子銀行的業務邏輯服務器建立安全通信上下文所需要的兩項內容:子會話密鑰和服務票 據。這時 Kerberos 客戶機必須生成針對電子銀行的業務邏輯服務器的上下文建立請求。
參見 第一篇文章的圖 7 和清單 5,它們描述了客戶機為建立安全上下文而發送給電子銀行服務器的 消息。清單 21 中顯示的 createKerberosSession() 方法處理與電子銀行的業務邏輯服務器建立安全通 信上下文的所有方面(包括生成上下文建立請求、向服務器發送請求、從服務器中獲得響應、解析響應以 檢查遠程服務器是否同意上下文建立請求,並將這些工作的結果返回給調用應用程序)。
看一下 清單 21 中的 createKerberosSession() 方法,它有以下參數:
ticketContent 字節數組帶有准備用於建立安全上下文的服務票據。
clientRealm 字符串包裝了請求客戶機所屬的域 realm 的名字。
clientName 字符串指定了請求客戶機的名字。
sequenceNumber 參數是一個表示這個消息序號(sequence number)的 integer。
encryptionKey:子會話密鑰。
inStream 和 outStream 是 createKerberosSession() 方法用來與電子銀行的服務器通信的輸入輸出 流。
正如在第一篇文章中介紹的,要使用 Java-GSS 實現電子銀行的服務器端邏輯。GSS-Kerberos 機制規 定服務票據包裝在一個認證頭中,而這個認證頭本身又包裝在 第一篇文章的圖 7 和清單 5 中顯示的 InitialContextToken 包裝器中。
可以使用 清單 19 的 getAuthenticationHeader() 方法包裝服務票據。回想一下在 清單 20 的 getTicketResponse() 方法中我使用了 getAuthenticationHeader() 方法包裝一個 TGT。
為了生成認證頭,需要一個 Checksum 。回想在討論 清單 19 的 getAuthenticationHeader() 方法 時說過, Checksum 的目的是加密綁定認證頭與一些應用程序數據。但是,與票據請求認證頭不一樣,上 下文建立認證頭不帶有應用程序數據。
GSS-Kerberos 機制出於不同的目的使用 Checksum 結構。除了將認證頭綁定到一些應用程序數據外, GSS-Kerberos 機制使用一個 Checksum 結構用物理網絡地址(即客戶機可以用來與服務器進行安全通信 的網絡地址)綁定安全上下文。如果使用這種功能,那麼只能從它所綁定的網絡地址上使用安全上下文。
不過,我不作准備在這個示例移動銀行應用程序中使用這種功能。這就是為什麼我在 Checksum 結構 中指定安全上下文沒有任何綁定的緣故。為此,我編寫了一個名為 getNoNetworkBindings() 的方法,如 清單 22 所示。getNoNetworkBindings() 方法非常簡單。它只是生成一個硬編碼的字節數組,表明不需 要任何網絡綁定。然後它調用 getChecksumBytes() 方法以將硬編碼的數組包裝到 cksum 字段中。
得到了無網絡綁定的 Checksum 的字節數組後,可以將這個數組傳遞給 getAuthenticationHeader() 方法,這個方法返回完整的認證頭。
生成了認證頭後, 清單 21 的 createKerberosSession() 方法將認證頭字節數組與一個名為 gssHeaderComponents 的硬編碼的字節數組相鏈接。gssHeaderComponents 字節數組包含一個 GSS 頭的 字節表示,這個 GSS 頭在上下文建立請求中將伴隨一個認證頭。
最後,將串接的 GSS 頭和認證頭包裝到一個應用程序級別的標簽號 0 中。GSS 要求所有上下文建立 請求都包裝到應用程序級別的標簽號 0 中。
現在完成了上下文建立請求。下一項任務就是通過一個輸出流( outStream 對象)發送這個請求。發 送了請求後,監聽並接收 inStream 對象上的響應。
當 createKerberosSession() 方法收到響應後,它就檢查響應是確認創建一個新的上下文還是顯示一 個錯誤消息。要進行這種檢查,必須知道消息開始標簽字節後面的長度字節的字節數。GSS 頭字節(緊接 著長度字節)提供了答案。
不用解析響應以進行任何進一步的處理。只是要知道電子銀行的服務器是創建了一個新會話還是拒絕 會話。如果電子銀行的服務器確認創建新會話,那麼 createKerberosSession() 方法就返回 true ,如 果不是,它就返回 false。
清單 21. createKerberosSession() 方法
public boolean createKerberosSession (
byte[] ticketContent,
String clientRealm,
String clientName,
int sequenceNumber,
byte[] encryptionKey,
DataInputStream inStream,
DataOutputStream outStream
)
{
byte[] cksum = getNoNetworkBindings();
if (sequenceNumber == 0)
sequenceNumber++;
byte[] authenticationHeader = getAuthenticationHeader(
ticketContent,
clientRealm,
clientName,
cksum,
encryptionKey,
sequenceNumber
);
byte[] gssHeaderComponents = {
(byte)0x6,
(byte)0x9,
(byte)0x2a,
(byte)0xffffff86,
(byte)0x48,
(byte)0xffffff86,
(byte)0xfffffff7,
(byte)0x12,
(byte)0x1,
(byte)0x2,
(byte)0x2,
(byte)0x1,
(byte)0x0
};
byte[] contextRequest = getTagAndLengthBytes(
ASN1DataTypes.APPLICATION_TYPE,
0, concatenateBytes (
gssHeaderComponents, authenticationHeader
)
);
try {
outStream.writeInt(contextRequest.length);
outStream.write(contextRequest );
outStream.flush();
byte[] ebankMessage = new byte[inStream.readInt()];
inStream.readFully(ebankMessage);
int respTokenNumber = getNumberOfLengthBytes (ebankMessage[1]);
respTokenNumber += 12;
byte KRB_AP_REP = (byte)0x02;
if (ebankMessage[respTokenNumber] == KRB_AP_REP){
return true;
} else
return false;
} catch (Exception io) {
io.printStackTrace();
}
return false;
}//createKerberosSession
清單 22. getNoNetworkBindings() 方法
public byte[] getNoNetworkBindings() {
byte[] bindingLength = { (byte)0x10, (byte)0x0, (byte)0x0, (byte) 0x0};
byte[] bindingContent = new byte[16];
byte[] contextFlags_bytes = {
(byte)0x3e,
(byte)0x00,
(byte)0x00,
(byte)0x00
};
byte[] cksumBytes = concatenateBytes (
concatenateBytes(bindingLength,bindingContent),
contextFlags_bytes);
byte[] cksumType = {
(byte)0x2,
(byte)0x3,
(byte)0x0,
(byte)0x80,
(byte)0x3
};
byte[] cksum = getChecksumBytes(cksumBytes, cksumType);
return cksum;
}//getNoNetWorkBindings()
向電子銀行的業務邏輯服務器發送安全消息
如果 createKerberosSession() 方法返回 true ,就知道成功地與遠程 Kerberos 服務器建立了一個 安全會話。就可以開始與 Kerveros 服務器交換消息了。
看一下 清單 23 的 sendSecureMessage() 方法。這個方法取一個純文本消息、一個加密密鑰、一個 序號(它惟一地標識了發送的消息)和與服務器交換數據所用的輸入輸出流對象作為參數。 endSecureMessage() 方法生成一個安全消息、通過輸出流將這個消息發送給服務器、監聽服務器的響應 ,並返回服務器的響應。
發送給服務器的消息是用子會話密鑰保護的。這意味著只有特定的接收者(擁有子會話密鑰的電子銀 行業務邏輯服務器)可以解密並理解這個消息。而且,安全消息包含消息完整性數據,所以電子銀行的服 務器可以驗證來自客戶機的消息的完整性。
讓我們看一下 sendSecureMessage() 方法是如何用一個純文本消息生成一個安全 GSS 消息的。
一個 GSS 安全消息采用 token(token 格式的字節數組)的形式。token 格式由以下部分組成:
一個 GSS 頭,類似於我在討論 createKerberosSession() 方法時介紹的頭。
一個八字節 token 頭。在 GSS-Kerveros 規范中有幾個不同類型的 token,每一種 token 類型都由 一個惟一的頭所標識。其中只有要在 sendSecureMessage() 方法中生成的安全消息頭是我們感興趣的。 一個安全消息 token 是由具有值 0x02、0x01、0x00、0x00、0x00、0x00、0xff 和 0xff 的頭所標識的 。
一個加密的序號,它有助於檢測回復攻擊。例如,如果有惡意的黑客想要重現(即重復)一個轉賬指 令,他是無法生成加密形式的正確序號的(當然,除非他知道 子會話 密鑰)。
消息的加密摘要值。
加密後的消息。
將上面列出的五個字段以正確的順序鏈接在一起,然後包裝到一個 ASN.1 應用程序級別的標簽號 0 中。這就構成了完整的 GSS-Kerberos 安全消息 token,如 圖 1所示。
圖 1.
為了生成如 圖 1所示的完整安全 token,必須生成所有五個字段。
前兩個字段沒有動態內容,它們在所有安全消息中都是相同的,所以我在 清單 23中硬編碼了它們的 值。另外三個字段必須根據以下算法動態計算:
1. 在純文本消息中添加額外的字節以使消息中的字節數是八的倍數。
2. 生成一個名為 confounder 的八字節隨機數。鏈接 confounder 與第 1 步中填充的消息。
3. 串接 token 頭( 圖 1中的第二個字段)和第 2 步的結果。然後對鏈接的結果計算 16 字節 MD5 摘要值。
4. 用 子會話 密鑰加密第 3 步得到的 16 字節摘要值。加密算法是使用零 IV 的 DES-CBC。加密的 數據的最後八個字節(放棄前八個字節)構成了 圖 1第四個字段(加密的摘要值)。
5. 現在必須生成一個加密的 8 字節序號( 圖 1 的第三個字段)。這個序號是用 子會話 密鑰和第 4 步使用 IV 的加密摘要值的後八個字節加密的。
6. 現在取第 2 步的結果(鏈接在一起的 confounder 和填充的消息)並用 DES-CBC 加密它。要進行 這種加密,使用一個用 0xF0 對 子會話 密鑰的所有字節執行 OR 操作生成的密鑰。這種加密得到的結果 構成了 圖 1的第五個字段,也就是加密的消息。
生成了各個字段後,將它們鏈接為一個字節數組,最後,調用 getTagAndLengthBytes() 方法以在鏈 接的字節數組前面附加一個應用程序級別的標簽號 0。
可以觀察 清單 23 的 sendSecureMessage() 方法中的這些步驟。生成了安全消息後,通過輸出流將 消息發送給服務器、監聽服務器的響應並將響應返回給接收者。
清單 23. sendSecureMessage() 方法
public byte[] sendSecureMessage( String message, byte[] sub_sessionKey,
int seqNumber,
DataInputStream inStream,
DataOutputStream outStream
)
{
byte[] gssHeaderComponents = {
(byte)0x6,
(byte)0x9,
(byte)0x2a,
(byte)0x86,
(byte)0x48,
(byte)0x86,
(byte)0xf7,
(byte)0x12,
(byte)0x01,
(byte)0x02,
(byte)0x02
};
byte[] tokenHeader = {
(byte)0x02,
(byte)0x01,
(byte)0x00,
(byte)0x00,
(byte)0x00,
(byte)0x00,
(byte)0xff,
(byte)0xff
};
try {
/***** Step 1: *****/
byte[] paddedDataBytes = getPaddedData (message.getBytes());
/***** Step 2: *****/
byte[] confounder = concatenateBytes (getRandomNumber(), getRandomNumber ());
/***** Step 3: *****/
byte[] messageBytes = concatenateBytes(confounder, paddedDataBytes);
byte[] digestBytes = getMD5DigestValue(
concatenateBytes (tokenHeader,messageBytes));
CBCBlockCipher cipher = new CBCBlockCipher(new DESEngine());
KeyParameter kp = new KeyParameter(sub_sessionKey);
ParametersWithIV iv = new ParametersWithIV (kp, new byte[8]);
cipher.init(true, iv);
byte processedBlock[] = new byte[digestBytes.length];
byte message_cksum[] = new byte[8];
for(int x = 0; x < digestBytes.length/8; x ++) {
cipher.processBlock(digestBytes, x*8, processedBlock, x*8);
System.arraycopy(processedBlock, x*8, message_cksum, 0, 8);
iv = new ParametersWithIV (kp, message_cksum);
cipher.init (true, iv);
}
/***** Step 4: *****/
byte[] sequenceNumber = {
(byte)0xff,
(byte)0xff,
(byte)0xff,
(byte)0xff,
(byte)0x00,
(byte)0x00,
(byte)0x00,
(byte)0x00
};
sequenceNumber[0] = (byte)seqNumber;
/***** Step 5: *****/
byte[] encryptedSeqNumber = encrypt(sub_sessionKey, sequenceNumber, message_cksum);
/***** Step 6: *****/
byte[] encryptedMessage = encrypt(getContextKey(sub_sessionKey),
messageBytes, new byte[8]);
byte[] messageToken = getTagAndLengthBytes (
ASN1DataTypes.APPLICATION_TYPE,
0,
concatenateBytes (
gssHeaderComponents, concatenateBytes (
tokenHeader, concatenateBytes (
encryptedSeqNumber, concatenateBytes (
message_cksum, encryptedMessage
)
)
)
)
);
/***** Step 7: *****/
outStream.writeInt(messageToken.length);
outStream.write(messageToken);
outStream.flush();
/***** Step 8: *****/
byte[] responseToken = new byte[inStream.readInt()];
inStream.readFully(responseToken);
return responseToken;
} catch(IOException ie){
ie.printStackTrace();
} catch(Exception e){
e.printStackTrace();
}
return null;
}//sendSecureMessage
public byte[] getContextKey(byte keyValue[])
{
for (int i =0; i < keyValue.length; i++)
keyValue[i] ^= 0xf0;
return keyValue;
}//getContextKey
解碼服務器消息
就像生成並發送給服務器的消息一樣, sendSecureMessage() 方法返回的服務器消息是安全的。它遵 循 圖 1 所示的同樣的 token 格式,這意味著只有擁有 子會話 密鑰的客戶機才能解密這個消息。
我編寫了一個名為 decodeSecureMessage() 的方法(如 清單 24 所示),它以一個安全消息和解密 密鑰為參數,解密這個消息並返回純文本格式的消息。解碼算法如下:
第一步是將消息的加密部分(圖 24 所示的第五個字段)與 token 頭分離。token 頭的長度是固定的 ,所以只有長度字節的數目是隨消息的總長度而變化的。因此,只要讀取長度字節數並相應地將消息的加 密部分拷貝到一個單獨的字節數組中。
第二步是讀取消息 checksum( 圖 1的第四個字段)。
現在用解密密鑰解密加密的消息。
然後,取 token 頭( 圖 1 的第二個字段),將它與解密的消息鏈接,然後取鏈接的字節數組的 MD5 摘要值。
現在加密 MD5 摘要值。
需要比較第 2 步的八字節消息 checksum 與第 5 步的 MD5 摘要值的後八個字節。如果它們相匹配, 那麼完整性檢查就得到驗證。
經過驗證後,刪除 cofounder(解密的消息的前八個字節)並返回消息的其余部分(它就是所需要的 純文本消息)。
清單 24. decodeSecureMessage() 方法
public String decodeSecureMessage (byte[] message, byte[] decryptionKey){
int msg_tagAndHeaderLength = 36;
int msg_lengthBytes = getNumberOfLengthBytes (message[1]);
int encryptedMsg_offset = msg_tagAndHeaderLength + msg_lengthBytes;
byte[] encryptedMessage = new byte[message.length - encryptedMsg_offset];
System.arraycopy(message, encryptedMsg_offset,
encryptedMessage, 0,
encryptedMessage.length);
byte[] msg_checksum = new byte[8];
System.arraycopy(message, (encryptedMsg_offset-8),
msg_checksum, 0,
msg_checksum.length);
byte[] decryptedMsg = decrypt (decryptionKey, encryptedMessage, new byte [8]);
byte[] tokenHeader = {
(byte)0x2,
(byte)0x1,
(byte)0x0,
(byte)0x0,
(byte)0x0,
(byte)0x0,
(byte)0xff,
(byte)0xff
};
byte[] msg_digest = getMD5DigestValue (concatenateBytes (tokenHeader,decryptedMsg));
byte[] decMsg_checksum = new byte[8];
try {
CBCBlockCipher cipher = new CBCBlockCipher(new DESEngine());
KeyParameter kp = new KeyParameter(getContextKey(decryptionKey));
ParametersWithIV iv = new ParametersWithIV (kp, decMsg_checksum);
cipher.init(true, iv);
byte[] processedBlock = new byte[msg_digest.length];
for(int x = 0; x < msg_digest.length/8; x ++) {
cipher.processBlock(msg_digest, x*8, processedBlock, x*8);
System.arraycopy(processedBlock, x*8, decMsg_checksum, 0, 8);
iv = new ParametersWithIV (kp, decMsg_checksum);
cipher.init (true, iv);
}
} catch(java.lang.IllegalArgumentException il){
il.printStackTrace();
}
for (int x = 0; x < msg_checksum.length; x++) {
if (!(msg_checksum[x] == decMsg_checksum[x]))
return null;
}
return new String (decryptedMsg,
msg_checksum.length,
decryptedMsg.length - msg_checksum.length);
}//decodeSecureMessage()
public byte[] getContextKey(byte keyValue[])
{
for (int i =0; i < keyValue.length; i++)
keyValue[i] ^= 0xf0;
return keyValue;
}//getContextKey
示例移動銀行應用程序
已經完成了示例移動銀行應用程序所需要的安全 Kerberos 通信的所有階段。現在可以討論移動銀行 MIDlet 如何使用 Kerberos 客戶機功能並與電子銀行的服務器通信了。
清單 25顯示了一個簡單的 MIDlet,它模擬了示例移動銀行應用程序。
清單 25. 一個示例移動銀行 MIDlet
import java.io.*;
import java.util.*;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.io.*;
public class J2MEClientMIDlet extends MIDlet implements CommandListener, Runnable {
private Command OKCommand = null;
private Command exitCommand = null;
private Command sendMoneyCommand = null;
private Display display = null;
private Form transForm;
private Form transResForm;
private Form progressForm;
private TextField txt_userName;
private TextField txt_password;
private TextField txt_amount;
private TextField txt_sendTo;
private StringItem si_message;
private TextField txt_label;
private SocketConnection sc;
private DataInputStream is;
private DataOutputStream os;
private DatagramConnection dc;
private KerberosClient kc;
private TicketAndKey tk;
private String realmName = "EBANK.LOCAL";
private String kdcServerName = "krbtgt";
private String kdcAddress = "localhost";
private int kdcPort = 8080;
private String e_bankName = "ebankserver";
private String e_bankAddress = "localhost";
private int e_bankPort = 8000;
private int i =0;
private byte[] response;
public J2MEClientMIDlet() {
exitCommand = new Command("Exit", Command.EXIT, 0);
sendMoneyCommand = new Command("Pay", Command.SCREEN, 1);
OKCommand = new Command("Back", Command.EXIT, 2);
display = Display.getDisplay(this);
transactionForm();
}
public void startApp() {
Thread t = new Thread(this);
t.start();
}//startApp()
public void pauseApp() {
}//pauseApp()
public void destroyApp(boolean unconditional) {
}//destroyApp
public void commandAction(Command c, Displayable s) {
if (c == exitCommand) {
destroyApp(false);
notifyDestroyed();
} else if(c == sendMoneyCommand) {
sendMoney();
} else if (c == OKCommand) {
transactionForm();
} else if (c == exitCommand) {
destroyApp(true);
}
}//commandaction
public void sendMoney() {
System.out.println("MIDlet... SendMoney() Starts");
String userName = txt_userName.getString();
String password = txt_password.getString();
kc.setParameters(userName, password, realmName);
System.out.println("MIDlet... Getting TGT Ticket");
response = kc.getTicketResponse (
userName,
kdcServerName,
realmName,
null,
null
);
System.out.println ("MIDLet...Getting Session Key from TGT Response");
tk = new TicketAndKey();
tk = kc.getTicketAndKey(response, kc.getSecretKey());
System.out.println ("MIDLet...Getting Service Ticket (TGS)");
response = kc.getTicketResponse (
userName,
e_bankName,
realmName,
tk.getTicket(),
tk.getKey()
);
System.out.println ("MIDLet...Getting Sub-Session Key from TGS Response");
tk = kc.getTicketAndKey( response, tk.getKey());
i++;
System.out.println ("MIDLet...Establishing Secure context with E-Bank");
boolean isEstablished = kc.createKerberosSession (
tk.getTicket(),
realmName,
userName,
i,
tk.getKey(),
is,
os
);
if (isEstablished) {
System.out.println ("MIDLet...Sending transactoin message over secure context");
byte[] rspMessage = kc.sendSecureMessage(
"Transaction of Amount:"+txt_amount.getString()
+ " From: "+userName
+" To: "+txt_sendTo.getString(),
tk.getKey(),
i,
is,
os
);
String decodedMessage = kc.decodeSecureMessage(rspMessage, tk.getKey ());
if (decodedMessage!=null)
showTransResult(" OK", decodedMessage);
else
showTransResult(" Error!", "Transaction failed..");
} else
System.out.println ("MIDlet...Context establishment failed..");
}//sendMoney()
public synchronized void run() {
try {
dc = (DatagramConnection)Connector.open ("datagram://"+kdcAddress+":"+kdcPort);
kc = new KerberosClient(dc);
sc = (SocketConnection)Connector.open ("socket://"+e_bankAddress+":"+e_bankPort);
sc.setSocketOption(SocketConnection.KEEPALIVE, 1);
is = sc.openDataInputStream();
os = sc.openDataOutputStream();
} catch (ConnectionNotFoundException ce) {
System.out.println("Socket connection to server not found....");
} catch (IOException ie) {
ie.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}//run
public void transactionForm(){
transForm = new Form("EBANK Transaction Form");
txt_userName = new TextField("Username", "", 10, TextField.ANY);
txt_password = new TextField("Password", "", 10, TextField.PASSWORD);
txt_amount = new TextField("Amount", "", 4, TextField.NUMERIC);
txt_sendTo = new TextField("Pay to", "", 10, TextField.ANY);
transForm.append(txt_userName);
transForm.append(txt_password);
transForm.append(txt_amount);
transForm.append(txt_sendTo);
transForm.addCommand(sendMoneyCommand);
transForm.addCommand(exitCommand);
transForm.setCommandListener(this);
display.setCurrent(transForm);
}
public void showTransResult(String info, String message) {
transResForm = new Form("Transaction Result");
si_message = new StringItem("Status:" , info);
txt_label = new TextField("Result:", message, 150, TextField.ANY);
transResForm.append(si_message);
transResForm.append(txt_label);
transResForm.addCommand(exitCommand);
transResForm.addCommand(OKCommand);
transResForm.setCommandListener(this);
display.setCurrent(transResForm);
}
}//J2MEClientMIDlet
運行這個 MIDlet 會得到如 圖 2所示的屏幕。
圖 2.
圖 2顯示為這個示例移動銀行應用程序開發了一個非常簡單的 GUI。圖 2 還顯示了四個數據輸入字段 :
“ Username ”字段取要使用移動銀行 MIDlet 的金融服務的人的用戶名。
“ Password ”字段取用戶的密碼。
“ Amount ”字段允許輸入要支付給一個收款人的金額。
“ Pay to ”字段包含收款人的用戶名。
輸入完數據後,按 Pay 按鈕。Pay 按鈕的事件處理器( 清單 25 中的 sendMoney() 方法)執行 Kerveros 通信的所有七個階段:
生成一個 TGT 請求、將請求發送給出服務器、並接收 TGT 響應。
從 TGT 響應中提取 TGT 和會話密鑰。
生成一個服務票據請求、將請求發送給 KDC 、並接收服務票據響應。
從服務票據響應中提取服務票據和子會話密鑰。
生成上下文建立請求並發送給電子銀行的業務邏輯服務器、接收響應、並解析它以確定服務器同意建 立一個新的安全上下文。
生成一個安全消息、將這個消息發送給服務器、並接收服務器的響應。
解碼來自服務器的消息。
清單 25 的 MIDlet 代碼相當簡單,不需要很多解釋。只要注意以下幾點:
我使用了不同的線程( 清單 25 中的 run() 方法)創建 Datagram 連接 ( dc ) 和 Socket 連接上 的數據輸入和輸出流。這是因為 MIDP 2.0 不允許在 J2ME MIDlet 的主執行線程中創建 Datagram 和 Socket 連接。
在 清單 25 的 J2ME MIDlet 中,我硬編碼了 KDC 服務器的域、服務器名、地址和端口號以及電子銀 行服務器的名字和地址。注意 MIDlet 中的硬編碼只是出於展示目的。另一方面, KerberosClient 是完 全可重用的。
為了試驗這個應用程序,需要一個作為電子銀行服務器運行的 GSS 服務器。本文的 源代碼下載 包含 一個服務器端應用程序和一個 readme.txt 文件,它描述了如何運行這個服務器。
最後,注意我沒有設計電子銀行通信框架,我只是設計了基於 Kerberos 的安全框架。可以設計自己 的通信框架,並用 KerberosClient 提供安全支持。例如,可以使用 XML 格式定義不同的消息作為轉賬 指令。
結束語
在這個三部分的系列文章中,我展示了 J2ME 應用程序中的安全 Kerberos 通信。介紹了進行一系列 加密密鑰交換的 Kerveros 通信。還介紹了 J2ME 應用程序是如何使用密鑰與遠程電子銀行服務器建立通 信上下文並安全地交換消息的。我還提供了展示文章中討論的所有概念的 J2ME 代碼。