3atv精品不卡视频,97人人超碰国产精品最新,中文字幕av一区二区三区人妻少妇,久久久精品波多野结衣,日韩一区二区三区精品

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

用 Kerberos 为 J2ME 应用程序上锁

發布時間:2023/12/20 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 用 Kerberos 为 J2ME 应用程序上锁 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

(源自:http://www.ibm.com/developerworks/cn/java/wi-kerberos/)

用 Kerberos 為 J2ME 應用程序上鎖,第 1 部分:?Kerberos 數據格式介紹

理解 Kerberos 如何保證無線安全性

文檔選項

將此頁作為電子郵件發送

未顯示需要 JavaScript 的文檔選項


級別: 初級

Faheem Khan?(fkhan872@yahoo.com), 自由顧問

2003 年 12 月 18 日

用戶需要確保所使用的無線應用程序不會損害他們的敏感信息。其中一種方法就是使用行業標準協議如 Kerberos 來提供安全性。在本系列中,Faheem Khan 將創建一個示例 J2ME MIDlet,它使用 Kerberos 來保護財務數據。本文是該系列的第一篇文章,他通過解釋為他的應用程序的安全性提供骨架的 Kerberos 數據格式,介紹了一些基本知識。

許多用戶不愿意使用通過無線連接發送敏感數據的應用程序,因為他們不信任無線安全性。但是使傳統有線網絡上的電子商務的安全成為可能的這些協議,同樣也可以使無線交易成為安全的。在這個由三篇文章組成的系列中,我將展示 J2ME 客戶機和服務器端 Java 應用程序之間使用 Kerberos 協議(參閱?參考資料中的鏈接)進行的安全消息傳遞。我將開發一個移動銀行 MIDlet 應用程序,它可以通過 Internet 安全地發送和接收付款信息。MIDlet 應用程序將使用一個基于 J2ME 的 Kerberos 客戶機來進行實際的安全消息傳遞。在本文中,我首先解釋移動銀行應用程序的使用模型,然后我將解釋 Kerberos 消息交換的順序,從而為后續的 J2ME 客戶機和服務器端 Java 應用程序之間進行的安全消息傳遞建立安全的上下文。緊接著描述了 Kerberos 消息傳遞中使用的數據格式。本文的最后一部分簡單介紹了 Kerberos 客戶機的體系結構,它最終創建并處理 Kerberos 消息和數據格式。

一個用于移動銀行的安全 MIDlet

我將首先考慮這樣一個用例場景:其中有兩個擁有電子銀行帳戶的移動電話用戶 Alice 和 Bob。

如果 Alice 想要付款給 Bob,那么她就向 MIDlet 應用程序提供 Bob 的手機號碼(或者他的銀行帳戶號碼)并讓 MIDlet 向他付款。MIDlet 安全地與電子銀行聯系并請求電子銀行向 Bob 付款。電子銀行完成這筆交易并將確認發送回 Alice。Bob 在他的手機上也安裝了 MIDlet,可以用于查看 Alice 的付款是否到達他的帳戶。MIDlet 安全地與電子銀行進行通信以驗證付款狀態。

我在本系列文章中將設計并實現的 MIDlet 處理所有與電子銀行進行的特定于應用程序的消息傳遞。當然,MIDlet 必須保證通信是安全的。下面的功能構成了 MIDlet 的安全性需求:

  • 電子銀行應該可以確認請求付款和更新帳戶狀態的用戶的身份。這種安全性需求通常稱為?身份驗證,其中服務器需要驗證發出請求的客戶的身份。
  • 像 Alice 和 Bob 這樣的用戶應該可以確保他們是真的與電子銀行而不是一些惡意的黑客進行通信。這也是一種身份驗證的情況,在這里客戶希望對服務器進行身份驗證。客戶和服務器彼此驗證的情況稱為?雙向身份驗證。
  • 所有通信都應該是加密的以維護消息的機密性。即使黑客可以得到在網絡上傳送的消息字節,他也不能理解這些加密的數據字節的意義。
  • 通信的雙方(用戶和電子銀行)都應該能夠驗證收到的消息的完整性。如果惡意黑客改變了傳輸中的消息,那么接收一方應該可以發現這種改變。

我將使用 Kerberos 協議來滿足這些安全性需求(有關 Kerveros 的更多信息的鏈接請參閱?參考資料)。我將所有與 Kerberos 相關的功能封裝到一個小型的、適合于 J2ME 設備的客戶端 Kerberos 實現中。我們的移動銀行 MIDlet 只需要負責特定于應用程序的消息傳遞功能,而 Kerberos 客戶機將處理所有安全問題。

這種技術對于讀者來說有一個好處。Kerberos 客戶機包裝了安全功能(身份驗證、機密性和消息完整性)。因此,如果要開發自己的 MIDlet,以用于發出和接收付款以外的目的,您也可以使用這里描述的 Kerberos 客戶機來使之具有安全功能。

本文的大部分用于討論 Kerberos 是如何與應用程序一同工作的,以及描述為了建立安全通信而在客戶機與服務器之間交換的不同消息。我將在本系列的后續文章中詳細描述客戶端應用程序本身。




回頁首


Kerberos 消息交換

本節我將描述以下三個參與者之間的 Kerberos 消息交換:

  • 支持 J2ME 的無線設備
  • 無線設備用戶
  • 電子銀行

圖 1. 為建立安全通信上下文而進行的 Kerberos 消息交換?
?

圖 1 給出了三個參與者之間出現的消息交換的概括視圖。本節我將討論這一視圖并解釋圖中顯示的消息交換的最終結果。下一節解釋每一消息的具體細節(即結構和格式)。

注意,圖 1 中支持 J2ME 的無線設備包含兩個參與者:MIDlet 和 Kerberos 客戶機。與此類似,電子銀行系統也包含兩個參與者:業務邏輯服務器和 Kerberos 分發中心 (Kerberos Distribution Center,KDC)。

業務邏輯服務器是實現了電子銀行業務邏輯的服務器端 Java 應用程序。KDC 是一個管理和分發 Kerberos 票據的 Kerberos 服務器。KDC 又由兩個服務器組成:一個?身份驗證服務器(AS) 和一個?票據授予服務器(TGS)。AS 和 TGS 都接收客戶機請求并發出 Kerberos 票據以響應這些請求。當 AS 接收客戶機的票據請求時,它發出一個初始票據。然后客戶機向 TGS 展示這個初始票據。TGS 根據這個初始票據發出服務票據。

獲得初始票據的主要目的是在以后用它得到一個或者多個服務票據。這就是為什么初始票據也稱為?票據授予票據(TGT)。

注意,服務票據是用于客戶機與特定服務器之間的安全通信的一種手段。另一方面, TGT 并不是針對任何特定的服務器的。因此,TGT 邏輯上等于一個開放連接,它的一端是客戶機,而另一端是未知的。當對一個 TGT 生成一個服務票據時,另一端也就確定了。同一個 TGT 可以用于獲得任意數量的服務票據。

下面描述的消息交換步驟揭開了 MIDlet 如何以及為什么獲得并使用 Kerberos 票據的神秘面紗。圖 1 中的每一個數字都對應于下面討論的一個步驟。

  • 手機用戶向 MIDlet 應用程序提供他或者她的用戶名和密碼(只由用戶和電子銀行共享的一個秘密)。這個密碼只用于在 J2ME 應用程序內部的處理,它永遠也不會進入網絡。通過網絡傳輸的只有用戶名。
  • MIDlet 將用戶名和密碼交給 Kerberos 客戶機。由 Kerberos 客戶機負責建立與電子銀行進行安全通信的上下文。
  • Kerberos 客戶機請求 AS 發出一個 TGT。一個 TGT 請求表示一個安全會話。一個客戶機可以在一個安全會話中建立多個子會話。 TGT 請求包含了發出請求的客戶的用戶名,但是不包括密碼(共享的秘密)。
  • Kerberos 客戶機向 AS 發送請求。
  • 當 AS 收到 TGT 請求時,它從請求中提出用戶名并從內部數據庫中取出相應的密碼(共享的秘密)。然后 AS 發布一個 TGT 并將 TGT 包裝在回復消息中。這個 TGT 包含一個純文本部分和一個密碼文本(加密的)部分。為了加密 TGT 的密碼部分,AS 使用了由用戶的密碼生成的加密密鑰。因此,只有知道密碼的用戶才能解開加密的部分。TGT 的加密部分還包含一個加密密鑰,稱為?會話密鑰。
  • AS 向發出請求的 Kerberos 客戶機發送回復消息(包括 TGT)。
  • 收到 AS 的回復后,Kerberos 客戶機從回復中取出 TGT 并解密 TGT 中加密的部分。 然后 Kerberos 客戶機發出一個服務票據請求。這個請求包含了 TGT 和一個稱為?鑒別碼 (authenticator)的加密結構。客戶機用從 TGT 提取出的會話密鑰加密鑒別碼。鑒別碼證明客戶機掌握會話密鑰。服務票據請求還指定了電子銀行業務邏輯服務器的名字。
  • 客戶機向 TGS(它是電子銀行的 KDC 的一部分)發送服務票據請求。
  • 收到服務票據請求后,TGS 提取客戶機向其請求服務票據的服務器的名稱,然后授予一個服務票據。服務票據與 TGT 沒有很大差別。與 TGT 一樣,服務票據包含一個純文本部分和一個密碼文本(加密的)部分。TGS 用服務器的密鑰(一個用服務器的共享秘密生成的密鑰)加密服務票據的密碼文本部分,這樣只有那個服務器可以解密這個服務票據的密碼文本部分。TGS 在服務票據的密碼文本部分中還加入了一個新的加密密鑰。這個密鑰稱為?子會話密鑰。注意現在有兩個密鑰:會話密鑰和子會話密鑰。
  • 授予了服務票據之后,TGS 將服務票據包裝在一個響應消息中。這個響應消息也包含一個密碼文本部分,它是用會話密鑰加密的。響應消息的密碼文本部分包含子會話密鑰。
  • TGS 向客戶機發送響應消息。
  • 收到 TGS 響應后,客戶機用會話密鑰解密密碼文本部分,從而提取出子會話密鑰。客戶機還提取服務票據。
  • 然后客戶機向電子銀行業務邏輯服務器發出消息,并在消息中包裝了服務票據。這個消息請求服務器與客戶機建立新的安全會話。
  • 客戶機向電子銀行的業務邏輯服務器發送消息。
  • 電子銀行的業務邏輯服務器從請求中提取服務票據,解密它的密碼文本部分,并提取子會話密鑰。這樣客戶機和服務器就都掌握這個密鑰了。
  • 電子銀行的服務器向客戶機發送一個肯定應答。
  • 客戶機和電子銀行服務器現在可以用子會話密鑰進行安全通信了。




    回頁首


    Kerberos 消息

    那么這些加密是如何工作的呢?在本文的其余部分,我將詳細探討圖 1 中步驟 3 到步驟 16 中交換的 Kerberos 消息的結構。

    TGT 請求

    圖 2 圖示了在圖 1 的步驟 3 中討論的 TGT 請求消息的表示。
    圖2. TGT 請求消息的結構?
    ?

    Kerberos 協議定義了在 Kerberos 消息傳遞中使用的所有數據結構和消息的標題 (title)。注意,圖 2 中消息的標題是AS-REQ?--這是一個 AS 請求。

    圖 2 顯示了一種嵌套框的結構。每一個框表示一個數據字段。一些字段又包含了不同的字段,從而構成了一種嵌套的層次結構。

    最外面的框標記為?AS-REQ?,包含一個標記為?KDC-REQ?的更小的框。這個?KDC-REQ?框包含四個字段:

    • pvno?:這個數據字段表示 Kerberos 協議版本號。本系列文章的討論是針對 Kerveros 版本 5 的,它相當穩定,并且在目前是最新的。
    • msg-type?:可以通過消息類型號來識別不同的 Kerberos 消息。TGS 請求消息的類型號是 10。
    • padata?:這是一個可選的字段,在大多數 TGT 請求消息中都沒有它。這個字段的目的是加入身份驗證信息。在本節后面描述服務票據請求時我將描述?padata?字段的結構。
    • 第四個字段標記為?req-body?,它是 TGT 請求的正文。它又進一步分為幾個字段:
      • kdc-options?: 這個字段表示 KDC 選項。Kerveros 定義了客戶機可能希望請求的幾個選項。例如,客戶機可能需要一個?forwardable?票據(可以轉發給不同 KDC 服務器的票據)。與此類似,客戶機可能會請求一個?renewable?票據(可以在失效后更新的票據)。在本系列文章的后面,我將討論一些可用的選項,特別是那些與我的移動銀行應用程序有關的選項。
      • cname?:客戶的用戶名。
      • realm?:KDC 領域(realm)。注意,使用 Kerberos 的每一個機構都可以建立自己的領域。領域就像信任域,比如我們的電子銀行。一個領域可能跨越或者不跨越企業邊界。這意味著一個領域可能需要或者不需要與屬于其他企業的用戶通信。在移動銀行應用程序中,我將盡量保持簡單,假定所有用戶(如 Alice 和 Bob)是在電子銀行自己的企業中注冊的。
      • sname?:這是一個標識客戶機將向其出示所請求的票據的服務器的名稱。對于 TGT 請求,?sname?字段指定了 TGS 的名稱(因為客戶機最終要向 TGS 展示 TGT)。另一方面,對于服務票據請求(您將在本節的后面看到它),?sname?字段將指定電子銀行服務器的名稱(因為客戶機最終是要向電子銀行的業務邏輯服務器出示服務票據)。
      • from?:這是一個可選的字段,它表明客戶機需要一個填遲日期的票據,即其有效性將在將來的某一時刻開始。在移動銀行應用程序中我不需要這個功能。
      • till?:這個字段表明 TGT 失效的時間。客戶機指定 TGT 在什么時候失效。所有 Kerberos 時間字段都遵循YearMonthDateHourMinSecZ?格式。例如,1947 年 8 月 14 日早上 3:30 將表示為?19470814033000Z?。
      • rtime?:這個字段指定在什么時刻后票據就不能更新了。這是一個可選的字段,只有當客戶機在?kdc-options?字段中選擇了?renewable?選項后才會使用。
      • nonce?:這是一個隨機生成的整數。盡管這個整數本身沒有意義,但是它有助于檢測回復攻擊。
      • etype?:這個字段指定客戶機要使用的加密算法。Kerberos 為常用的加密算法定義了不同的整數,客戶機將使用對應的整數值。
      • addresses?:這是一個可選的字段,它包含一組地址,只有從這些地址來的票據才是有效的。客戶可以在這里指定從什么網絡地址上使用所請求的票據。
      • enc-authorization-data?:這是一個可選字段,它包裝了身份驗證數據,服務器可以根據這些數據來實施其身份驗證策略。我不準備在移動銀行應用程序中展示這種功能的使用。
      • additional-tickets?:這是一個可選字段,它使 Kerberos 客戶機可以根據客戶機已經獲得的多個票據請求一個安全會話。我不準備在移動銀行應用程序中使用這個功能。
    用 ASN.1 定義數據結構

    Kerberos 用 Abstract Syntax Notation One (ASN.1) 定義在 Kerberos 通信中使用的各種數據結構和字節格式。ASN.1 是一個 ITU-T (International Telecommunication Union-Telecommunication standardization sector) 標準。它由參考號為 X.680 到 X.699 的不同文檔組成(參閱?參考資料中的鏈接)。

    ASN.1 語法和編碼細節不是本文的重點。不過,我需要對 ASN.1 的概念做一些討論以解釋 Kerberos 結構和字節格式。我將只討論那些解釋 Kerberos 消息格式所需要的 ASN.1 概念。

    Kerberos 消息的字節編碼?在討論 Kerberos 消息的其他內容之前,我將首先描述如何將圖 2 中的 TGT 請求消息編碼為字節值序列。

    清單 1 是圖 2 中 TGT 請求消息的 ASN.1 類定義(有關 ASN.1 的更多內容見側欄)。稍后的表 2 顯示了 TGT 請求消息編碼的每一字節的格式。為了理解 TGT 請求消息的字節編碼,必須將圖 2、清單 1 和表 2 聯系在一起。

    注意,清單 1 中的主要結構標記為 AS-REQ。相同的 AS-REQ 標記出現在圖 2 中的外圍框中。

    現在看一下清單 1 中?AS-REQ?的后面是什么。?AS-REQ?后面的兩個冒號和等號 (?::=?) 表明這一行定義了?AS-REQ?結構。接下來是方括號中的一個字符串?APPLICATION 10?。A?APPLICATION 10?表明這個?AS-REQ?結構在這個?APPLICATION?的各種結構中是用編號 10 識別的。我們可以說編號 10 是一個?應用程序級標簽號。這個編號在應用程序中是惟一的── 換句話說,其他 Kerberos 結構將不會使用這個編號。

    現在注意在?[APPLICATION 10]?字符串后的?KDC-REQ?字符串。這表明在?AS-REQ?結構的格式后面是另一個名為?KDC-REQ?的結構的定義。這是 ASN.1 中的一種重用機制。?KDC-REQ?結構用在兩個地方。因此,Kerberos 定義?KDC-REQ?一次并使用它兩次。

    總而言之,?AS-REQ ::= [APPLICATION 10] KDC-REQ?表明在該應用程序中的不同結構中?AS-REQ?標記為 10,后面是另一個名為?KDC-REQ?的結構的定義。?
    清單 1. TGT 請求消息的 ASN.1 類定義

    AS-REQ ::= [APPLICATION 10] KDC-REQ KDC-REQ ::= SEQUENCE {pvno[1] INTEGER,msg-type[2] INTEGER,padata[3] SEQUENCE OF PA-DATA OPTIONAL,req-body[4] KDC-REQ-BODY } PA-DATA ::= SEQUENCE {padata-type[1] INTEGER,padata-value[2] OCTET STRING,-- might be encoded AP-REQ } KDC-REQ-BODY ::= SEQUENCE {kdc-options[0] KDCOptions,cname[1] PrincipalName OPTIONAL,-- Used only in AS-REQrealm[2] Realm, -- Server's realm-- Also client's in AS-REQsname[3] PrincipalName OPTIONAL,from[4] KerberosTime OPTIONAL,till[5] KerberosTime,rtime[6] KerberosTime OPTIONAL,nonce[7] INTEGER,etype[8] SEQUENCE OF INTEGER, -- EncryptionType,-- in preference orderaddresses[9] HostAddresses OPTIONAL,enc-authorization-data[10] EncryptedData OPTIONAL,-- Encrypted AuthorizationData encodingadditional-tickets[11] SEQUENCE OF Ticket OPTIONAL } EncryptedData ::= SEQUENCE {etype[0] INTEGER, -- EncryptionTypekvno[1] INTEGER OPTIONAL,cipher[2] OCTET STRING -- ciphertext } KDCOptions ::= BIT STRING {reserved(0),forwardable(1),forwarded(2),proxiable(3),proxy(4),allow-postdate(5),postdated(6),unused7(7),renewable(8),unused9(9),unused10(10),unused11(11),renewable-ok(27),enc-tkt-in-skey(28),renew(30),validate(31) } PrincipalName ::= SEQUENCE {name-type[0] INTEGER,name-string[1] SEQUENCE OF GeneralString } KerberosTime ::= GeneralizedTime-- Specifying UTC time zone (Z) HostAddresses ::= SEQUENCE OF SEQUENCE {addr-type[0] INTEGER,address[1] OCTET STRING }

    現在讓我們來看一下?KDC-REQ?結構。

    清單 1 中的?KDC-REQ ::= SEQUENCE?這一行表明?KDC-REQ?結構是不同數據結構的序列。在?SEQUENCE?關鍵詞后的一組花括號描述了共同構成?KDC-REQ?結構的數據結構。

    花括號中有四行代碼。第一行 (?pvno[1] INTEGER?) 定義了?KDC-REQ?結構的第一個元素,它是圖 2 中的?pvno?字段。

    第二行 (?msg-type[2] INTEGER?) 對應于圖 2 中的?msg-type?字段。注意?pvno?和?msg-type?字段的類型為?INTEGER,這意味著它們是用?INTEGER?數據類型構建的。

    還要注意在清單 1 中?pvno?和?msg-type?后面的方括號中的數字 (?pvno[1]?and?msg-type[2]?)。與在前面?AS-REQ ::= [APPLICATION 10] KDC-REQ?一行中見到的應用程序級標簽號相反,它們是?上下文特定的標簽號。

    應用程序級和上下文特定的標簽號有什么區別呢?應用程序級標簽號在整個應用程序中是惟一的和有效的。例如,在整個 Kerberos 應用程序中編號?10?都是指?AS-REQ?結構。而上下文特定的標簽號只在定義它們的上下文中有意義。例如,在?KDC-REQ?的結構內部,上下文特定的標簽號 1 表示?pvno?。但是在查看其他結構的內部時,同樣的上下文特定的標簽號 1 表示的則是一些其他的字段。

    在后面討論將清單 1 編碼為表 2 中的字節序列時,我將解釋這些應用程序級和上下文特定的標簽號的使用。

    現在看一看清單 1 中?KDC-REQ?結構中第三行 (?padata[3] SEQUENCE OF PA-DATA?) 和第四行 (?req-body[4] KDC-REQ-BODY?)。第三行定義了圖 2 中的?padata?字段,它是?PA-DATA?結構的一個?SEQUENCE?。第四行表示圖 2 中的?req-body框。

    padata?和?req-body?字段又是由不同字段組成的 Kerberos 結構。例如,?req-body?的數據類型是?KDC-REQ-BODY?,而該數據類型自己又是一個帶有幾個字段(像前面討論的那樣帶有?kdc-options、?cname?、?realm?、?sname?、?till?、?nonce?和?etype?等字段)的 Kerberos 結構。

    回想一下?pvno?字段是用一個?INTEGER?構建的。另一方面,?req-body?字段的數據類型為?KDC-REQ-BODY?,該數據類型本身又是由幾個字段構建的一種結構。

    還要注意?INTEGER?是基本 ASN.1 數據類型的一個例子,而?KDC-REQ-BODY?是由其他字段構建的派生數據類型。

    ASN.1 定義了一些可以被應用程序使用的數據類型,稱為?通用數據類型。大多數通用數據類型是基本類型,只有少數是構建的。ASN.1 為通用數據類型定義了?標簽號,如表 1 所示。

    表 1. 一些通用數據類型和標簽號

    ?

    通用數據類型通用標簽號構建類型還是基本類型?
    BOOLEAN1基本類型
    INTEGER2基本類型
    BIT STRING3基本類型
    OCTET STRING4基本類型
    SEQUENCE16構建類型
    GeneralizedTime24基本類型
    GeneralString27基本類型

    表 1 顯示?INTEGER?數據類型的通用標簽號是 2,并且?INTEGER?數據類型是基本類型。?SEQUENCE?是表 1 中惟一的構建類型的通用標簽。這是因為?SEQUENCE?數據類型是使用其他字段普遍定義的,?SEQUENCE?總是用其他字段構建的。

    在 ASN.1 定義中沒有提到通用數據類型的標簽號,這是因為這些標簽號已經得到普遍定義和理解。不過,在試圖將 Kerberos 結構編碼為字節序列時需要用到通用標簽號。在稍后您會看到這一點。

    現在,讓我解釋圖 2 與清單 1 中的?AS-REQ?消息是如何編碼為字節序列的。為了展示這個編碼過程,我提供了表 2,它顯示了客戶機發送給 KDC 服務器以請求 TGT 的 TGT 請求(一個?AS-REQ?結構)中實際的字節序列。表 2 中顯示的字節序列是我們要分析的?AS-REQ?編碼過程的最終結果。

    因為它比較長,所以我用一個?單獨的文件來提供表 2。在閱讀進行下面的討論時您應該在一個單獨的瀏覽器窗口中打開它。

    看一下表 2 中的第一個字節 (?01101010?)。這是消息的第一個字節,表示清單 1 的?AS-REQ?結構的開始(圖 2 中的外圍框)。位?8?和位?7?(?01?) 表明這是一個應用程序級標簽。位?6?(?1?) 表明這是一個構建的結構。位?5?到位?1?(?01010?) 表示?AS-REQ?的標簽號(我在前面的討論中說過?AS-REQ?結構的應用級標簽號為 10,其二進制表示為?01010?)。

    可以將標簽字節分為三部分:

    • 位?8?和位?7?指定標簽的?類型或者類。對于應用程序級標簽,位?8?和位?7?應該是?01?,對于上下文特定的標簽,它們應該是?10?,對于通用標簽,它們是?00?。
    • 位?6?指定標簽是基本的 (?0?) 還是構建的 (?1?)。
    • 位?5?到位?1?編碼標簽號。

    因為用于表示標簽號的位數限制了可以您可以擁有的標簽的個數,所以您可能奇怪為什么只有 5 位用于標簽號。ASN.1 定義了編碼標簽號的一個完整機制,與標簽號的大小無關。不過,Kerberos 沒有定義任何不能用 5 位表達的標簽號。因此,為了將討論集中于 Kerberos,我在這里將不討論如何處理大的標簽號。

    現在,看一下表 2 中的第二個字節 (?10000001?)。在標簽字節后,有一個或者多個?長度字節。這些長度字節指定了構成完整標簽的總字節數。

    有兩種定義長度字節的方法。第一種是?單字節長度表示,第二種是?多字節長度表示。對于單字節長度表示,要使第一個長度字節的位?8?為?0?,并用其余的位指定長度字節的個數。例如,如果您想說這個結構中有 65 個字節,可以將長度值編碼為?01000001?(位?8?設置為?0?,位?7?到位?1?──1000001?──表示 65)。在這種方法中,總是有一個長度字節,下一個字節標記結構內容的開始。用這種方法可編碼的最大值是 127。

    對于多字節表示法,設置第一個長度字節的位?8?為?1?,并用位?7?到位?1?指定隨后的長度字節的個數。例如,如果要編碼值 210,第一個長度字節將是?10000001?(位?8?設置為?1?,位?7?到位?1?設置為?1?,即?0000001?表明還有一個長度字節),后面再跟一個字節,其值為?11010010?(表示十進制的?210?)。

    看一下表 2 中的字節?2?和字節?3?,它們分別是?10000001?和?11010010?。這意味著我用多字節長度表示法來指定?AS-REQ?結構的長度,即?210?。

    注意在表 2 中共有 213 字節,其中 210 是在第?3?個字節后。在字節?3?后的所有 210 個字節都屬于?AS-REQ?結構。因此,第?4?個字節和后面的所有字節構成了?AS-REQ?結構的內容。

    注意,清單 1 中?AS-REQ?結構后面是?KDC-REQ?結構的定義,這個結構是一個?SEQUENCE?。回想一下,在表 1 中?SEQUENCE?是?通用標簽,它是?構建的數據類型,并且其標簽號為 16。這就是為什么表 2 中字節?4?的值為?00110000?。位?8和位?7?(?00?) 指定這是一個通用標簽。位?6?(?1?) 表明這個結構是構建的。位?5?到位?1?指定標簽號,對于?SEQUENCE來說這是 16(二進制為?10000?)。

    字節?5?和字節?6?指定?KDC-REQ?結構中的字節數。字節?5?(?10000001?) 指定有一個長度字節。字節?6?(?11001111?) 指定?KDC-REQ?序列的實際長度 (207)。我已經解釋過長度字節的工作方式。

    清單 1 中?KDC-REQ?序列的第一個字段是?pvno?,它是一個?上下文特定的,構建?的字段,其標簽號為?1?。表 2 中的字節?7?(?10100001?) 表示這個字段。位?8?和位?7?(?10?) 表示這是一個上下文特定的標簽。位?6?(?1?) 表示這是一個構建的字段,位?5?到位?1?(?00001?) 指定?pvno?字段的標簽號。

    字節?8?(?00000011?) 指定?pvno?字段的長度。這個長度字節的位?8?是?0?,這表明我使用單字節長度表示法。位?7?到位?1?(?0000011?) 指定長度為 3。這里要注意,正如前面所解釋的,當結構的長度小于 127 時,我就使用單字節長度表示法。

    pvno?字段的內容從表 2 中的第?9?個字節開始。?pvno?字段是用?INTEGER?基本數據類型構建的,所以我可以期望?pvno?的內容為一個整數值。

    字節?9?(?00000010?) 表示?INTEGER?標簽。位?8?和位?7?(?00?) 標識這是一個通用標簽,位?6?(0) 說明它是基本類型,位?5?到位?1?(?00010?) 表明標簽號為 2(見表 1)。

    位?10?(?00000001?) 提供了這個?INTEGER?數據類型在單字節長度表示法中的長度。?INTEGER?值只由一個字節組成,即下一個字節(第?11?個字節)。

    您將注意到字節編碼過程遵循一個清晰的模式。我以對應于清單 1 中一個字段的標簽值開始。標簽值后面是長度字節,長度字節后面是標簽的內容。我不斷使用嵌套的、分層次的構建結構,直到達到一個基本數據類型。

    您可以根據這種模式完成表 2。表 2 的說明欄可以幫助您理解每一字節的作用。對于本文中的要討論的其他消息,我不會提供逐字節的分析,不過對這個消息的分析可以使您了解它們的工作方式。

    包裝 TGT 的響應

    圖 3 是一個 AS 發出的、包裝了 TGT 的響應消息的圖形表示。圖 3 同樣使用了圖 2 中展示的嵌套框結構。
    圖 3. AS 的 TGT 響應的結構?
    ?

    圖 3 中的主框(外圍框)標記為?AS-REP?,它包含一個標記為?KDC-REP?的更小的框。?KDC-REP?是由幾個字段組成的一個序列。

    • pvno?:在討論圖 2 時我解釋了這個字段。
    • msg-type?:消息的類型。它是一個整數,對于 TGT 響應消息它的值應該為 11。
    • padata?:在討論圖 2 時我對這個字段做過說明。這是一個可選字段,在大多數 TGT 響應消息中沒使用它。
    • crealm?和?cname?:在討論圖 2 時對這些字段做過說明。
    • ticket?:實際的 TGT。我將在本文的后面一節中討論 Kerberos 票據自身的格式。
    • enc-part?:這是一個加密數據的包裝器。Kerveros 消息中所有加密的部分都包含三個字段:
      • etype?:指定用于進行密碼加密的加密算法的標識符。
      • kvno?:用于加密的加密密鑰的版本。AS 使用客戶機的密鑰進行加密。這個字段指定用于進行加密的密鑰的版本。
      • cipher?:一系列字節。這是實際的加密數據。數據解密后,我就有了另一個結構,如圖 4 所示。

    圖 4. TGT 響應消息的加密部分的結構?
    ?

    圖 4 顯示了對 TGT 響應消息中加密部分進行解密后得到的結構。它包含以下字段:

    • key?:這是會話密鑰。當前會話的后續通信將使用這個密鑰而不是密碼密鑰。
    • last-req?:這個字段指定客戶機的最后一次票據請求的時間。這個字段有助于通知客戶機,它的請求已經收到了。
    • nonce?:在討論圖 2 時我對這個字段做過說明。AS 將包含它在請求中收到的隨機數的一個副本。這有助于檢測回復攻擊。如果黑客取得了 TGT 響應并想反復回復它,那么客戶機就可以將響應的?nonce?字段與其請求進行比較以檢測回復。
    • key-expiration?:這是一個可選字段,它指定客戶機的密鑰失效的時間。
    • flags?:這個字段對應于圖 2 的 TGT 請求的?kdc-options?字段。這些?flags?表示 Kerberos 客戶機可能請求的不同可選功能。AS 將標志發送回客戶機,從而允許客戶機比較 AS 是否可以提供所請求的可選功能。
    • authtime?:AS 發出票據的時間。
    • starttime?:票據生效的時間。
    • endtime?:票據失效的時間。
    • renew-till?:可更新的票據的最終失效時間。
    • srealm?:服務器的領域。
    • sname?:服務器名,其所帶票據是有效的。
    • caddr?:這個字段指定一個地址列表,這些地址給出的相應票據是可以使用的。這個字段的目的是使黑客使用偷來的票據更困難。

    清單 2 提供了 TGT 響應消息的 ASN.1 類定義。讀者可以將清單 2 中的類定義與圖 3 和圖 4 中顯示的不同字段相對照。
    清單 2. TGT 響應消息的 ASN.1 類定義

    AS-REP ::= [APPLICATION 11] KDC-REPKDC-REP ::= SEQUENCE {pvno[0] INTEGER,msg-type[1] INTEGER,padata[2] SEQUENCE OF PA-DATA OPTIONAL,crealm[3] Realm,cname[4] PrincipalName,ticket[5] Ticket,enc-part[6] EncryptedData}EncryptedData ::= SEQUENCE {etype[0] INTEGER, -- EncryptionTypekvno[1] INTEGER OPTIONAL,cipher[2] OCTET STRING -- ciphertext}EncASRepPart ::= [APPLICATION 25] EncKDCRepPartEncKDCRepPart ::= SEQUENCE {key[0] EncryptionKey,last-req[1] LastReq,nonce[2] INTEGER,key-expiration[3] KerberosTime OPTIONAL,flags[4] TicketFlags,authtime[5] KerberosTime,starttime[6] KerberosTime OPTIONAL,endtime[7] KerberosTime,renew-till[8] KerberosTime OPTIONAL,srealm[9] Realm,sname[10] PrincipalName,caddr[11] HostAddresses OPTIONAL}

    服務票據請求

    收到 TGT 后,客戶機發出服務票據請求,如圖 5 所示。服務票據請求與我在圖 2 中討論的 TGT 請求非常相似。可以將圖 5 中的所有字段與圖 2 中的相應字段進行對照。我只需要解釋專門針對服務票據請求的?padata?字段。?
    圖 5. 服務票據請求消息的結構
    ?

    padata?字段是?PA-DATA?結構的序列,它包含?身份驗證數據。服務票據請求需要向票據授予服務器發送 TGT。?padata?字段將 TGT 包裝到它的一個?PA-DATA?結構中。

    Kerberos 用?padata?字段實現不同的目的,包裝 TGT 只是其中一項。因此,Kerberos 定義了不同的整數值以指定?PA-DATA?結構包裝的是什么類型的數據。

    圖 5 顯示?PA-DATA?序列只包含一個?PA-DATA?結構,它由兩個子字段組成,即?padata-type?和?padata-value?。?padata?序列中的每一個?PA-DATA?結構都包含這兩個字段。

    padata-type?是一個整數值,用于指定所帶?padata-value?字段中的數據類型。當在服務票據請求中?padata-value?字段包裝了一個 TGT 時,?padata-type?字段的值就是 1。

    padata-value?字段是一串字節,其中包含 TGT。?padata-value?字段中的字節串實際上是另一個名為?KRB_AP_REQ?(或者簡稱為?AP-REQ?)的 Kerberos 結構,也稱為?身份驗證頭。

    身份驗證頭包含 TGT 以及一些其他字段,如下所示:

    • pvno?:在對圖 2 的討論中我已經解釋過這個字段。
    • message-type?:這個字段包含一個整數值 (12),用于識別?KRB_TGS_REQ?消息。
    • ap-options?:這是一組選項。第一個選項保留為以后使用。第二個選項指定相應 TGT 是用包裝在相應 TGT 中的會話密鑰加密的。因為 TGS 已經知道會話密鑰,所以它可以使用這把密鑰進行解密。如果選擇了第三個選項,那么它就指定客戶機請求雙向身份驗證。
    • ticket?:TGT 本身。
    • authenticator?:這是一個加密的結構,其中包含幾個字段,允許客戶機證明它擁有會話密鑰并幫助 TGS 檢測回復攻擊。在身份驗證頭中 鑒別碼是以加密的(密文)形式出現的。客戶機使用會話密鑰加密鑒別碼。鑒別碼結構的字段如下:
      • authenticator-vno?:鑒別碼格式的版本號。對于 Kerberos 版本 5,這個字段應該指定 5。
      • crealm?和?cname:?我在對圖 2 的討論中解釋了這些字段。
      • cksum?:一個校驗和或者散列值,由圖 5 中顯示的?req-body?字段的字節編碼計算而來。這個字段讓 TGS 可以檢查請求消息的完整性。因為該校驗和位于一個用會話密鑰加密的結構中,所以這個字段也證明客戶機擁有會話密鑰。
      • cusec?和?ctime?:這兩個字段共同指定發出?KRB_AP_REQ?消息的客戶機時間。?cusec?字段指定時間的微秒部分,而?ctime?字段指定日期和以毫秒計的時間。
      • subkey?:這是一個可選的字段,客戶機可以用它指定隨后與服務器之間的通信所要使用的密鑰。對于移動銀行應用程序,我將盡量減少在 J2ME 客戶機上的處理負荷,因此,讓服務器決定會話密鑰和子會話密鑰。
      • seq-number?:這是一個可選字段,可以包含消息的一個序列號以檢測回復攻擊。
      • authorization-data?:這是一個可選字段,帶有特定于應用程序的身份驗證數據。在移動銀行應用程序中我沒有使用這個字段。

    清單 3 提供了服務票據請求消息的 ASN.1 類定義。讀者可以將圖 5 中顯示的不同字段與清單 3 中的類定義相對照。
    清單 3. 服務票據請求消息的 ASN.1 類定義

    TGS-REQ ::= [APPLICATION 12] KDC-REQ KDC-REQ ::= SEQUENCE {pvno[1] INTEGER,msg-type[2] INTEGER,padata[3] SEQUENCE OF PA-DATA OPTIONAL,req-body[4] KDC-REQ-BODY } PA-DATA ::= SEQUENCE {padata-type[1] INTEGER,padata-value[2] OCTET STRING,-- might be encoded AP-REQ } KDC-REQ-BODY ::= SEQUENCE {kdc-options[0] KDCOptions,realm[2] Realm, -- Server's realm-- Also client's in AS-REQsname[3] PrincipalName OPTIONAL,from[4] KerberosTime OPTIONAL,till[5] KerberosTime,rtime[6] KerberosTime OPTIONAL,nonce[7] INTEGER,etype[8] SEQUENCE OF INTEGER, -- EncryptionType,-- in preference orderaddresses[9] HostAddresses OPTIONAL,enc-authorization-data[10] EncryptedData OPTIONAL,-- Encrypted AuthorizationData encodingadditional-tickets[11] SEQUENCE OF Ticket OPTIONAL } AP-REQ ::= [APPLICATION 14] SEQUENCE {pvno [0] INTEGER, -- indicates Version 5msg-type [1] INTEGER, -- indicates KRB_AP_REQap-options[2] APOptions,ticket[3] Ticket,authenticator[4] EncryptedData } APOptions ::= BIT STRING {reserved (0),use-session-key (1),mutual-required (2) } Ticket ::= [APPLICATION 1] SEQUENCE {tkt-vno [0] INTEGER, -- indicates Version 5realm [1] Realm,sname [2] PrincipalName,enc-part [3] EncryptedData } -- Encrypted part of ticket EncTicketPart ::= [APPLICATION 3] SEQUENCE {flags[0] TicketFlags,key[1] EncryptionKey,crealm[2] Realm,cname[3] PrincipalName,transited[4] TransitedEncoding,authtime[5] KerberosTime,starttime[6] KerberosTime OPTIONAL,endtime[7] KerberosTime,renew-till[8] KerberosTime OPTIONAL,caddr[9] HostAddresses OPTIONAL,authorization-data[10] AuthorizationData OPTIONAL } -- Unencrypted authenticator Authenticator ::= [APPLICATION 2] SEQUENCE {authenticator-vno[0] INTEGER,crealm[1] Realm,cname[2] PrincipalName,cksum[3] Checksum OPTIONAL,cusec[4] INTEGER,ctime[5] KerberosTime,subkey[6] EncryptionKey OPTIONAL,seq-number[7] INTEGER OPTIONAL,authorization-data[8] AuthorizationData OPTIONAL }

    響應包含服務票據

    當 TGS 收到服務票據的請求時,它就在響應中發出一個服務票據。圖 6 顯示了包裝有服務票據的 TGS 響應。您可以對照圖 6 與圖 3。您會發現圖 3 中顯示的字段與在圖 6 中顯示的一樣,只不過圖 3 中的 ticket 字段是 TGT,而圖 6 的 ticket 字段是服務票據。

    還要注意,在產生圖 6 的加密部分時,KDC 使用了在以前的消息中與客戶機交換的會話密鑰。服務票據包裝了客戶機與電子銀行服務器進行安全通信時將會使用的子會話密鑰。
    圖 6. TGS 的服務票據響應的結構?
    ?

    清單 4 提供了服務票據響應消息的 ASN.1 類定義。讀者可以將圖 6 中不同字段與清單 4 中的類定義進行對照。
    清單 4. 服務票據響應消息的 ASN.1 類定義

    TGS-REP ::= [APPLICATION 13] KDC-REPKDC-REP ::= SEQUENCE {pvno[0] INTEGER,msg-type[1] INTEGER,padata[2] SEQUENCE OF PA-DATA OPTIONAL,crealm[3] Realm,cname[4] PrincipalName,ticket[5] Ticket,enc-part[6] EncryptedData}EncryptedData ::= SEQUENCE {etype[0] INTEGER, -- EncryptionTypekvno[1] INTEGER OPTIONAL,cipher[2] OCTET STRING -- ciphertext}EncTGSRepPart ::= [APPLICATION 26] EncKDCRepPartEncKDCRepPart ::= SEQUENCE {key[0] EncryptionKey,last-req[1] LastReq,nonce[2] INTEGER,key-expiration[3] KerberosTime OPTIONAL,flags[4] TicketFlags,authtime[5] KerberosTime,starttime[6] KerberosTime OPTIONAL,endtime[7] KerberosTime,renew-till[8] KerberosTime OPTIONAL,srealm[9] Realm,sname[10] PrincipalName,caddr[11] HostAddresses OPTIONAL}

    從客戶機到電子銀行業務邏輯服務器的消息

    現在客戶機有了服務票據,可以將它發送到電子銀行業務邏輯服務器。客戶機發送圖 7 中的消息到電子銀行服務器。這個消息的目的是請求服務器建立與客戶機的新的安全上下文。
    圖 7. 從 Kerberos 客戶機到電子銀行服務器的消息的結構?
    ?

    我說過本文的目的是展示基于 J2ME 的安全移動銀行應用程序。我準備在服務器端使用 Generic Security Services API(GSS API,或者簡稱為 GSS)來為電子銀行業務邏輯服務器提供安全功能。GSS 是一種一般性的高層安全 API ,它可以在像 Kerberos 這樣的不同安全技術上面工作(有關 GSS 的更多內容請參閱?參考資料)。

    我將在本系列的第三篇文章中討論 GSS API 在電子銀行業務邏輯服務器中的使用。現在,只要知道與 Kerberos 一樣,GSS 也是一種 IETF 標準。IETF 為客戶機與服務器之間相互傳遞的 Kerveros 消息定義了 GSS 包裝器。為了在服務器端使用 GSS,我必須保證 GSS 客戶機可以發出并處理 GSS 包裝器。

    圖 7 中的外圍框標記為?InitialContextToken?,它實際上是包裝了從 GSS 客戶機到 GSS 服務器的消息的 GSS 包裝器的名字。在?InitialContextToken?包裝器中,第一個字段名為?thisMech?,它定義了 GSS 作為低層安全技術使用的安全機制(在這里是 Kerveros)。

    InitialContextToken?框中的第二個字段標記為?KRB_AP_REQ?,我在討論圖 5 時分析過它。回想一下前面的討論中說過?KRB_AP_REQ?結構包裝了票據。這就是為什么我可以使用這個結構包裝一個服務票據并發送給電子銀行服務器。我說過服務票據已經包含了子會話密鑰。

    清單 5 提供了 Kerberos 客戶機發給電子銀行業務邏輯服務器的消息的 ASN.1 類定義。您可以將圖 7 中的不同字段與清單 5 中的類定義進行對照。
    清單 5. 從客戶機到服務器的安全上下文請求消息的 ASN.1 類定義

    InitialContextToken ::=[APPLICATION 0] IMPLICIT SEQUENCE {thisMech MechType-- MechType is OBJECT IDENTIFIER-- representing "Kerberos V5"innerContextToken ANY DEFINED BY thisMech-- contents mechanism-specific;-- ASN.1 usage within innerContextToken-- is not required}AP-REQ ::= [APPLICATION 14] SEQUENCE {pvno [0] INTEGER, -- indicates Version 5msg-type [1] INTEGER, -- indicates KRB_AP_REQap-options[2] APOptions,ticket[3] Ticket,authenticator[4] EncryptedData}APOptions ::= BIT STRING {reserved (0),use-session-key (1),mutual-required (2)}Ticket ::= [APPLICATION 1] SEQUENCE {tkt-vno [0] INTEGER, -- indicates Version 5realm [1] Realm,sname [2] PrincipalName,enc-part [3] EncryptedData}-- Encrypted part of ticketEncTicketPart ::= [APPLICATION 3] SEQUENCE {flags[0] TicketFlags,key[1] EncryptionKey,crealm[2] Realm,cname[3] PrincipalName,transited[4] TransitedEncoding,authtime[5] KerberosTime,starttime[6] KerberosTime OPTIONAL,endtime[7] KerberosTime,renew-till[8] KerberosTime OPTIONAL,caddr[9] HostAddresses OPTIONAL,authorization-data[10] AuthorizationData OPTIONAL}-- Unencrypted authenticatorAuthenticator ::= [APPLICATION 2] SEQUENCE {authenticator-vno[0] INTEGER,crealm[1] Realm,cname[2] PrincipalName,cksum[3] Checksum OPTIONAL,cusec[4] INTEGER,ctime[5] KerberosTime,subkey[6] EncryptionKey OPTIONAL,seq-number[7] INTEGER OPTIONAL,authorization-data[8] AuthorizationData OPTIONAL}

    電子銀行的響應

    當電子銀行的業務邏輯服務器收到圖 7 中的消息時,它提取出服務票據并解密票據中的加密部分以得到子會話密鑰。客戶機已經有了同樣的子會話密鑰。因此,服務器和客戶機可以使用這個子會話密鑰彼此進行安全通信。
    圖 8. 電子銀行對 Kerberos 客戶機的響應的結構?
    ?

    電子銀行業務邏輯服務器向客戶機發回一個確認消息,如圖 8 所示。下面是構成圖 8 的消息的字段:

    • pvno?:我在對圖 2 的討論中解釋了這個字段。
    • msg-type?:這是具有整數值 14 的消息類型標識符。
    • enc-part?:消息的加密部分。客戶機將用子會話密鑰解密這個加密部分。在解密時,它將展現另一個帶有以下字段的結構:
      • ctime?和?cusec?:我在對圖 6 的討論中解釋了這些字段。
      • subkey?:這是一個服務器可能發送給客戶機的可選密鑰。如果服務器將這個密鑰發送給客戶機,那么這個密鑰將被用于隨后客戶機與服務器之間的安全通信(取代子會話密鑰)。
      • seq-number?:我在對圖 5 的討論中解釋了這個字段。

    清單 6 提供了電子銀行對 Kerveros 客戶機的響應的 ASN.1 類定義。讀者可以將圖 8 中顯示的不同字段與清單 6 中的類定義相對照。
    清單 6. 從服務器到客戶機的安全上下文響應的 ASN.1 類定義

    AP-REP ::= [APPLICATION 15] SEQUENCE {pvno [0] INTEGER, -- represents Kerberos V5msg-type [1] INTEGER, -- represents KRB_AP_REPenc-part [2] EncryptedData}EncAPRepPart ::= [APPLICATION 27] SEQUENCE {ctime [0] KerberosTime,cusec [1] INTEGER,subkey [2] EncryptionKey OPTIONAL,seq-number [3] INTEGER OPTIONAL}





    回頁首


    Kerberos 票據

    在結束這篇文章之前,我想要展示 Kerberos 票據本身的結構。圖 9 顯示了 Kerberos 票據的結構。
    圖 9. Kerberos 票據的結構?
    ?

    它包含 11 個字段:

    • tkt-vno?:票據格式的版本。當前它是 5。
    • realm?和?sname?:我在對圖 2 的討論中解釋了這些字段。這兩個字段共同指定可以給出有效票據的服務器的完整標識符。對于 TGT,這兩個字段標識了 TGS。另一方面,對于服務票據,它們指定電子銀行業務邏輯服務器。
    • enc-part?:這是票據的機密部分。這個加密的部分解密后是另一個 Kerberos 結構,它包含如下所描述的一些字段:
      • flags?:這是我在討論圖 2 中的?kdc-options?字段時提到的一組標志。其中一個標志用于說明這是 TGT 還是服務票據。
      • key?:這是會話密鑰(對于 TGT)或者子會話密鑰(對于服務票據)。
      • creal?和?cname?:我在對圖 2 的討論中解釋了這些字段。
      • transited?:正如前面提到的,在不同領域中工作的不同 Kerberos 服務器可以將票據從一個領域轉發到另一個領域。這個字段指定在發布這種票據時所涉及的不同領域的名字。在移動銀行應用程序中我不需要這種功能。
      • authtime?:這是 KDC 驗證請求客戶身份的時間。
      • starttime?和?endtime?:票據從?starttime?到?endtime?是有效的。
      • renew-till?:正如前面提到的,Kerberos 票據是可以更新的。這種票據可以包含這個字段,它指定票據的最終失效時間。在這個時間之后,票據將不再是?renewable?的。
      • caddr?:我在對圖 4 的討論中解釋了這個字段。




    回頁首


    展望:設計 Kerberos 客戶機

    在本系列的其余部分,我將構建一個 Kerberos 客戶機,它為移動銀行應用程序提供安全功能。Kerberos 客戶機的主要目的是發布并處理這里詳細說明的 Kerberos 消息。客戶機將可以從客戶機向票據或者電子銀行服務器發布所有消息(圖 2、5 和 7 所示的消息)并處理從服務器發回的消息(圖 2、4、6、8 和 9 所示的消息)。

    我所開發的 Kerberos 客戶機將在資源有限的無線設備上運行。因此,客戶機只有很少的資源。我的重點放在高效地使用可用的設備資源上。

    安全應用程序通常使設備資源承擔繁重的處理負荷。為了提高程序的效率,我必須對良好的面向對象的設計做法做出一些妥協。這對于 重大的 J2ME 應用程序來說是很常見的。本系列的后兩篇文章中將展示在移動銀行應用程序中是如何做的。




    回頁首


    結束語

    在本文中,我解釋了移動銀行應用程序的使用模型和安全性需求。我還描述了在 Kerberos 客戶機和一個電子銀行服務器之間交換加密密鑰以進行安全通信的 Kerberos 消息的序列(以及 Kerberos 數據格式)。然后我簡要展望了要在本系列后兩篇文章中構建的 J2ME Kerberos 客戶機。

    我希望本文的內容對于所有希望了解 Kerberos 消息的工作細節的讀者可以提供有用的信息。在寫作本系列的其余部分時我需要用到所有這些信息。

    參考資料

    • 您可以參閱本文在 developerWorks 全球站點上的?英文原文.?
    • 下載本文中討論的全部?清單以及?表 2。?

    • 閱讀 IETF.org 上的 Kerberos(版本 5)的官方?RFC 1510以及 GSS 的?RFC 1964。?

    • 訪問 IETF 網站上的?Kerberos working group頁。?

    • 在“?Simplify enterprise Java authentication with single sign-on”一文中(?developerWorks,2003 年 9 月),Faheem Khan 使用 Kerberos 和 Java GSS API 論證了單點登錄。?

    • MIT 上的這一頁包含關于 Kerberos 的一組很好的鏈接。?

    • 下載完整的 ASN.1 文檔和編碼規則。?

    • 閱讀 Jason Garman 的?Kerberos: The Definitive Guide?一書(O'Reilly & Associates,2003 年),以學習 Kerberos 的使用。?

    • 看看?IBM 產品是如何使用 Kerberos 的。?

    • IBM alphaWorks 提供了?Web Services Toolkit for Mobile Devices以將您的 J2ME 設備與 Web 服務世界相連接。?


    關于作者

    ?

    Faheem Khan 是一個獨立軟件顧問,專長是企業應用集成 (EAI) 和 B2B 解決方案。讀者可以通過?fkhan872@yahoo.com與 Faheem 聯系。


    用 Kerberos 為 J2ME 應用程序上鎖,第 2 部分:?生成一個 Kerberos 票據請求

    J2ME 有足夠的能力進行復雜的加密

    文檔選項

    將此頁作為電子郵件發送

    未顯示需要 JavaScript 的文檔選項


    級別: 初級

    Faheem Khan?(fkhan872@yahoo.com), 自由顧問

    2004 年 1 月 16 日

    在本系列的上一篇文章中,您看到了對可以安全地連接到支持 Kerveros 的服務器的 J2ME 應用程序的描述,還可了解在字節水平上 Kerberos 加密的細節問題。本文則深入到應用程序自身內部。您將看到如何使用 J2ME 的工具程序以及一些開放源代碼庫完成異常強大的加密任務。

    在本系列的?上一篇文章?中,我介紹了一個使用 Kerberos 與電子銀行服務器進行安全通信的移動銀行 MIDlet 應用程序。我還解釋了基于 J2ME 的 Kerveros 客戶機應用程序與遠程服務器交換 Kerberos 票據和密鑰時所交換的數據格式和消息序列。

    在本文中,我將開始實現生成并處理這些消息的 J2ME 類。我將首先簡單描述構成這個基于 J2ME 的 Kerveros 客戶機的主要類的作用,然后我將解釋并展示這些類如何生成在第一篇文章中討論過的基本 ASN.1 數據類型。在第三節中,我將展示如何生成一個用于在 Kerveros 通信中進行加密和解密的密鑰。最后一節將展示 J2ME 客戶機如何生成對 Kerveros 票據的請求。

    基于 J2ME 的 Kerveros 客戶機中的類

    在本文中,將要討論三個 J2ME 類的操作:

    • ASN1DataTypes
    • KerberosClient
    • KerberosKey

    ASN1DataTypes?類將包裝所有一般性的 ASN.1 功能,如發布像?INTEGER?和?STRING?這樣的通用數據類型。?KerberosClient?類擴展?ASN1DataTypes?類,使用它的底層功能,并提供所有特定于 Kerveros 的功能。因此,可以說我將所需要的功能簡單地分為兩組:所有一般性的 ASN.1 功能都在?ASN1DataTypes?類中,而所有特定于 Kerveros 的功能都在?KerberosClient?類中。這提高了代碼的重用性。如果您希望構建自己的、使用 ASN.1 功能的非 Kerveros 應用程序,那么您可以使用?ASN1DataTypes?類。

    Kerberos 定義了一種利用用戶的密碼生成密鑰的算法。?KerberosKey?類實現了這種算法 。在 Kerveros 通信中您將需要這個密鑰。

    我將在本文分別展示這些類中的每個方法。我還在一個單獨的?源代碼下載中加入了這些類。這個包將所有東西放到一組類中,可以將它們編譯為一個 J2ME 項目。這個下載包含以下文件:

    • ReadMe.txt?,它包含描述如何根據本文的需要練習這些代碼的指導。
    • ASN1DataTypes.java?,它實現了?ASN1DataTypes?類。
    • KerberosClient.java?,它實現了?KerberosClient?類。
    • KerberosKey.java?,它實現了?KerberosKey?類。
    • J2MEClientMIDlet.java?,它提供了可以用來測試這些代碼的一個非常簡單的 MIDlet 包裝器。

    現在,我將進一步探討這些類的細節。




    回頁首


    生成基本 ASN.1 數據類型

    清單 1 中顯示的?ASN1DataTypes?類處理生成和處理 ASN.1 數據結構所需要的所有底層功能。這個類包含兩種方法:?生成(authoring)?方法負責生成 ASN.1 數據結構,而?處理(processing)?方法負責處理已生成的或者從遠程應用程序收到的消息。我將在本文中解釋并實現生成方法,在本系列的下一篇文章中討論處理方法。

    清單 1 只包含 ASN.1 類中不同方法的聲明。我將在后面的幾節中用單獨的清單展示每一個方法的實現。
    清單 1. ASN1DataTypes 類

    public class ASN1DataTypes {public byte[] getLengthBytes(int length){}public byte[] getIntegerBytes (int integerContents){}public byte[] getGeneralStringBytes (String generalStringContent){}public byte[] getOctetStringBytes (byte[] octetStringContents){}public byte[] getBitStringBytes (byte[] content){}public byte[] getGeneralizedTimeBytes (byte[] generalizedTimeContent){}public byte[] concatenateBytes (byte[] array1, byte[] array2){}public byte[] getSequenceBytes (byte[] sequenceContents){}public byte[] getTagAndLengthBytes (int tagType, int tagNumber, byte[] tagContents){} }//ASN1DataTypes

    getLengthBytes()

    (在清單 2 中顯示的)這個方法將一個整數值(?length?)作為參數。它生成一個該長度的 ASN.1 表示,并返回一個符合 ASN.1 長度格式的字節數組。?
    清單 2. getLengthBytes() 方法

    public byte[] getLengthBytes(int length){if (length < 0)return null;byte lengthBytes[];if (length <= 127){lengthBytes = new byte[1];lengthBytes[0] = (byte)(length & 0xff);}else{int tempLength = length;int bytesRequired = 2;do {tempLength = tempLength / 256;if (tempLength > 0)bytesRequired ++;}while (tempLength > 0); lengthBytes = new byte[bytesRequired];byte firstLengthByte = (byte) (bytesRequired -1);firstLengthByte |= 0x80;lengthBytes[0] = firstLengthByte;int j = bytesRequired - 1;for (int i=1; i < bytesRequired; i++) {j--;lengthBytes[i] = (byte)(length >>> (j*8) & 0xff);}//for}//elsereturn lengthBytes;}//getLengthBytes

    回想一下在本系列的?第一篇文章?中對表 2 的討論,有兩種表示字節長度的方法:單字節表示法和多字節表示法。單字節長度表示法用于表示小于或者等于 127 的長度值,而當長度值大于 127 時使用多字節長度表示法。

    getLengthBytes()?方法首先檢查長度值是否為負。如果為負,則只是返回 null,因為我不能處理負值。

    然后這個方法檢查長度值是否小于或者等于 127。如果是,就需要使用單字節長度表示法。

    注意在 J2ME 中一個整數是 4 字節數據,而單字節長度表示法只需要 1 個字節。如果長度參數是 0 到 127 之間(包括這個兩數)的一個值,那么其字節表達就在?0x00000000?與?0x0000007f?之間(意味著只有最低有效位字節包含有用的數據)。將這個整數造型為一個單字節時,只有最低有效位字節(?0x00?到?0x7f?)會作為十六進制值拷貝到單字節數組。因此,如果長度值在 0 到 127 之間,那么我可以只執行該長度與?0xff?之間的一個按位?AND?操作。這個操作會得到一個整數,它有效的最高 3 個字節都將填入零。因此,我可以將按位操作的結果造型為一個字節,將這個字節放入一個單字節數組,并將這個數組返回給調用應用程序。

    如果長度值大于 127,那么我必須使用多字節長度表示法,它至少使用 2 字節數據。第一個字節表明長度字節的字節數,后面是實際的長度字節(有關這種格式的詳細解釋請參閱?第一篇文章)。

    如果長度值小于 256,那么就需要總共 2 個長度字節 ── 1 個字節表明還有一個長度字節,1 個字節包含實際的長度值。如果長度值至少為 256 并小于 65536(256 乘 256),那么就需要總共 3 個長度字節 ── 1 個字節表明還有 2 個長度字節,兩個字節包含實際的長度值。

    因此,在多字節格式中所需要的字節數取決于長度值。這就是為什么在?getLengthBytes()?的?else?塊的?do?-?while?循環中要計算長度字節所需要的字節數。

    確定所需要字節數的方法很簡單。我聲明了一個名為?bytesRequired?的字節計數器,從 2 開始計數(所需要的最少字節數),將長度值除以 256,并檢查商是否大于或者等于 1。如果是,那么就表明原始長度值大于 256,因而需要至少 3 個字節,所以我增加計數器(?bytesRequired?)。

    我繼續將長度值除以 256 并增加字節計數器,直到除得的值小于 1。這時,我就知道找到了在多字節整數格式中需要的字節數。

    知道了所需要的字節數后,我就實例化一個具有適當大小的字節數組。自然,長度字節中的第一個字節將表明還有多少個長度字節。因此,我只是將所需要的字節數減 1(?bytesRequired-1?),并拷貝到一個名為?firstLengthByte?的字節中。

    看一下清單 2 中?getLengthBytes()?方法中的?firstLengthByte |= 0x80?這一行代碼。這一行代碼對?firstLengthByte和?0x80?(?1000 0000?)進行按拉?OR?操作,并將結果儲存到?firstLengthByte?中。這種邏輯?OR?操作會將?firstLengthByte?的最左邊(最高有效)位設置為 1。回想在本系列?第一篇文章?中的討論,在希望使用多字節整數格式的時候,必須將第一個長度字節的最左邊一位設置為 1。

    下一行(?lengthBytes[0]=firstLengthByte?)只是拷貝在包含長度字節的數組的開始位置上的?firstLengthByte?。然后,有一個?for?循環,它將長度字節從長度參數中拷貝到在?lengthBytes?數組中它們的正確位置上。當?for?循環退出時,就得到了符合 ASN.1 格式的這個?lengthBytes?數組。清單 2 中?getLengthBytes()?方法的最后一行返回這個數組。

    getIntegerBytes()

    這個方法取一個整數(?value?)作為參數并返回以 ASN.1?INTEGER?表達的這個整數值。回想一下在本系列?第一篇文章的表 1 中曾提到,在ASN.1 中?INTEGER?是一種通用數據類型。

    清單 3 中顯示了?getIntegerBytes()?方法的實現。?
    清單 3. getIntegerBytes() 方法

    public byte[] getIntegerBytes (int integerContents){//1. Declare a byte array named finalBytes, which will // hold all the bytes of the ASN.1 byte array representation.byte finalBytes[];//2. Calculate the number of bytes required to hold the // contents part of the ASN.1 byte array representation.int tempValue = integerContents;int contentBytesCount = 1;do {tempValue = tempValue / 256;if (tempValue >0)contentBytesCount ++;} while (tempValue > 0);//3. Use the getLengthBytes() method of Listing 3 to author // the length bytes. Store the length bytes in an array named lengthBytes.byte lengthBytes[] = getLengthBytes(contentBytesCount );//4. Get the number of bytes in the lengthBytes array.int lengthBytesCount = lengthBytes.length;//5. Calculate the number of bytes required to hold the // complete ASN.1 byte array representation // (the sum total of the number of tag bytes, length bytes, and content bytes).// Store the number of bytes in a variable named totalBytesCount.int totalBytesCount = 1 + lengthBytesCount + contentBytesCount ;//6. Instantiate the finalBytes array to totalBytesCount size.finalBytes = new byte[totalBytesCount];//7. Copy the tag byte at the start of the finalBytes array.finalBytes[0] = (byte)0x02;//8. Copy the length bytes from the lengthBytes array // to the finalBytes array just after the tag byte.for (int i=0; i < lengthBytes.length; i++)finalBytes[i+1] = lengthBytes[i];//9. Copy the content bytes to the finalBytes array // just after the length bytes.int k = totalBytesCount - lengthBytesCount - 1;for (int j=lengthBytesCount+1; j<totalBytesCount; j++){k--; finalBytes[j] = (byte) (integerContents >>> (k*8) & 255); }//for//10. Return the finalBytes array.return finalBytes;}//getIntegerBytes

    這個方法首先聲明一個名為?finalBytes?的字節數組。這個字節數組包含?INTEGER?數據類型結構的所有字節。不過,我還不知道?finalBytes?數組的大小。我首先需要計算?INTEGER?結構中的字節數,這種計算由幾步組成:

    第一步是計算容納這個整數值(?INTEGER?結構的內容部分)所需要的字節數。為此,我使用了一個?do?-?while?循環,它不斷地將?value?整數除以 256,直到得到的值小于1。當這個循環退出時,容納內容部分所需要的字節數就儲存在一個名為?contentBytesCount?的變量中。

    這個方法再將所需要的長度作為一個整數傳遞給?getLengthBytes()?方法,這個方法返回以 ASN.1 表達的長度字節。我將長度字節數儲存到一個名為?lengthBytesCount?的變量中。

    回想一下在?本系列第一篇文章中討論過,所有 ASN.1 數據類型表達的字節數組都包含三個部分:標簽字節、長度字節和內容字節。因此,ASN.1 字節數組表達需要包含所有這三部分的足夠空間。

    下一步是計算將要包含?INTEGER?結構的所有字節的數組的大小。我是通過將標簽字節長度(對于?INTEGER?和所有其他在 Kerberos 中使用的標簽來說是 1)、長度字節數和內容字節數相加進行這種計算的。?int totalBytesCount = 1 + lengthBytesCount + contentBytesCount;?一行進行的就是這種計算,并將所需要的字節數儲存到一個名為?totalBytesCount的變量中。

    下面,我實例化一個大小為?totalBytesCount?的字節數組?finalBytes?。過程的其余部分很簡單,我將標簽字節(對于?INTEGER?來說是?0x02?)儲存到?finalBytes?數組的開始處。然后,將長度字節拷貝到?finalBytes?數組中標簽字節后面。最后,我將內容字節拷貝到長度字節后并返回?finalBytes?數組。

    getGeneralStringBytes()、getOctetStringBytes()、getBitStringBytes() 和 getGeneralizedTimeBytes()

    像?getIntegerBytes()?一樣,每一個方法返回一種 ASN.1 通用數據類型結構。

    清單 4 中的?getGeneralStringBytes()?方法生成一個 ASN.1?GeneralString?的字節數組表達。類似地,清單 5 中的?getOctetStringBytes()?方法返回 ASN.1?OctetString?的字節數組表達。清單 6 中的?getBitStringBytes()?方法返回?BitString?的 ASN.1 表達。最后,清單 7 中的?getGeneralizedTimeBytes()?方法返回 ASN.1?GeneralizedTime?值的字節數組表達。

    所有這些方法遵循在前面對?getIntegerBytes()?方法的討論中見過的同樣實現邏輯:

  • 聲明一個名為?finalBytes?的字節數組,它將包含 ASN.1 字節數組表達的所有字節。
  • 計算容納 ASN.1 字節數組表達的內容所需要的字節數。
  • 用清單 3 中的?getLengthBytes()?方法生成長度字節。將長度字節儲存到一個名為?lengthBytes?的數組中。
  • 得到?lengthBytes?數組中的字節數。
  • 計算容納完整的 ASN.1 字節數組表達所需要的字節數(標簽字節、長度字節和內容字節的總和)。將這個字節數儲存到一個名為?totalBytesCount?的變量中。
  • 實例化一個具有?totalBytesCount?的值大小的?finalBytes?數組。
  • 將標簽字節拷貝到?finalBytes?數組的開始處。
  • 將?lengthBytes?數組中的長度字節拷貝到?finalBytes?數組中緊隨標簽字節的位置。
  • 將內容字節拷貝到?finalBytes?數組中緊隨長度字節的位置。
  • 返回?finalBytes?數組。
  • 清單 4、清單 5、清單 6 和清單 7 帶有幫助您跟蹤和對照上述 10 步中每一步與 J2ME 代碼中相應行的注釋。
    清單 4. getGeneralStringBytes() 方法

    public byte[] getGeneralStringBytes (String generalStringContent){//1. Declare a byte array named finalBytes, which will // hold all the bytes of the ASN.1 byte array representation.byte finalBytes[];//2. Calculate the number of bytes required to hold the // contents part of the ASN.1 byte array representation.int contentBytesCount = generalStringContent.length();//3. Use the getLengthBytes() method of Listing 3 to author // the length bytes. Store the length bytes in // an array named lengthBytes.byte lengthBytes[] = getLengthBytes(contentBytesCount );//4. Get the number of bytes in the lengthBytes array.int lengthBytesCount = lengthBytes.length;//5. Calculate the number of bytes required to hold the complete // ASN.1 byte array representation (the sum total of the number // of tag bytes, length bytes, and content bytes). // Store the number of bytes in a variable named totalBytesCount.int totalBytesCount = 1 + lengthBytesCount + contentBytesCount ;//6. Instantiate the finalBytes array to totalBytesCount size.finalBytes = new byte[totalBytesCount];//7.Copy the tag byte at the start of the finalBytes array.finalBytes[0] = (byte)0x1B;//8. Copy the length bytes from the lengthBytes array// to the finalBytes array just after the tag byte.for (int i=0; i < lengthBytes.length; i++)finalBytes[i+1] = lengthBytes[i];//9. Copy the content bytes to the finalBytes array just after the length bytes.byte tempString[] = generalStringContent.getBytes();for (int j=lengthBytesCount+1; j<totalBytesCount; j++)finalBytes[j] = tempString[j-(lengthBytesCount+1)]; //10. Return the finalBytes array.return finalBytes;}//getGeneralStringBytes



    清單 5. getOctetStringBytes() 方法
    public byte[] getOctetStringBytes (byte[] octetStringContents){//1. Declare a byte array named finalBytes, which will // hold all the bytes of the ASN.1 byte array representation.byte finalBytes[];//2. Calculate the number of bytes required to hold the // contents part of the ASN.1 byte array representation.int contentBytesCount = octetStringContents.length;//3. Use the getLengthBytes() method of Listing 3 to author // the length bytes. Store the length bytes in // an array named lengthBytes.byte lengthBytes[] = getLengthBytes(contentBytesCount );//4. Get the number of bytes in the lengthBytes array.int lengthBytesCount = lengthBytes.length;//5. Calculate the number of bytes required to hold the complete // ASN.1 byte array representation (the sum total of the number // of tag bytes, length bytes, and content bytes). // Store the number of bytes in a variable named totalBytesCount.int totalBytesCount = 1 + lengthBytesCount + contentBytesCount ;//6. Instantiate the finalBytes array to totalBytesCount size.finalBytes = new byte[totalBytesCount];//7. Copy the tag byte at the start of the finalBytes array.finalBytes[0] = (byte)0x04;//8. Copy the length bytes from the lengthBytes array to the // finalBytes array just after the tag byte.for (int i=0; i < lengthBytes.length; i++)finalBytes[i+1] = lengthBytes[i];//9. Copy the content bytes to the finalBytes array // just after the length bytes.for (int j=lengthBytesCount+1; j<totalBytesCount; j++)finalBytes[j] = octetStringContents[j-(lengthBytesCount+1)]; //10. Return the finalBytes array.return finalBytes;}//getOctetStringBytes



    清單 6. getBitStringBytes() 方法
    public byte[] getBitStringBytes (byte[] content){//1. Declare a byte array named finalBytes, which will // hold all the bytes of the ASN.1 byte array representation.byte finalBytes[];//2. Calculate the number of bytes required to hold the // contents part of the ASN.1 byte array representation.int contentBytesCount = content.length;//3. Use the getLengthBytes() method of Listing 3 to author // the length bytes. Store the length bytes in // an array named lengthBytes.byte lengthBytes[] = getLengthBytes(contentBytesCount );//4. Get the number of bytes in the lengthBytes array.int lengthBytesCount = lengthBytes.length;//5. Calculate the number of bytes required to hold the complete // ASN.1 byte array representation (the sum total of the number // of tag bytes, length bytes, and content bytes). // Store the number of bytes in a variable named totalBytesCount.int totalBytesCount = 1 + lengthBytesCount + contentBytesCount ;//6. Instantiate the finalBytes array to totalBytesCount size.finalBytes = new byte[totalBytesCount];//7. Copy the tag byte at the start of the finalBytes array.finalBytes[0] = (byte)0x03;//8. Copy the length bytes from the lengthBytes array to the // finalBytes array just after the tag byte.for (int i=0; i < lengthBytes.length; i++)finalBytes[i+1] = lengthBytes[i];//9. Copy the content bytes to the finalBytes array // just after the length bytes.for (int j=lengthBytesCount+1; j<totalBytesCount; j++)finalBytes[j] = content[j-(lengthBytesCount+1)]; //10. Return the finalBytes array.return finalBytes;}//getBitStringBytes



    清單 7. getGeneralizedTimeBytes() 方法
    public byte[] getGeneralizedTimeBytes (byte[] generalizedTimeContent){//1. Declare a byte array named finalBytes, which will // hold all the bytes of the ASN.1 byte array representation.byte finalBytes[];//2. Calculate the number of bytes required to hold the // contents part of the ASN.1 byte array representation.int contentBytesCount = generalizedTimeContent.length;//3. Use the getLengthBytes() method of Listing 3 to author // the length bytes. Store the length bytes in // an array named lengthBytes.byte lengthBytes[] = getLengthBytes(contentBytesCount );//4. Get the number of bytes in the lengthBytes array.int lengthBytesCount = lengthBytes.length;//5. Calculate the number of bytes required to hold the complete // ASN.1 byte array representation (the sum total of the number // of tag bytes, length bytes, and content bytes). // Store the number of bytes in a variable named totalBytesCount.int totalBytesCount = 1 + lengthBytesCount + contentBytesCount ;//6. Instantiate the finalBytes array to totalBytesCount size.finalBytes = new byte[totalBytesCount];//7. Copy the tag byte at the start of the finalBytes array.finalBytes[0] = (byte)0x18;//8. Copy the length bytes from the lengthBytes array to the // finalBytes array just after the tag byte.for (int i=0; i < lengthBytes.length; i++)finalBytes[i+1] = lengthBytes[i];//9. Copy the content bytes to the finalBytes array // just after the length bytes.for (int j=lengthBytesCount+1; j<totalBytesCount; j++)finalBytes[j] = generalizedTimeContent[j-(lengthBytesCount+1)]; //10. Return the finalBytes array.return finalBytes;}//getGeneralizedTimeBytes

    concatenateBytes()

    這個方法(見清單 8)取兩個字節數組,將第二個數組串接到第一個之后,并返回串接的數組。

    因為這個方法取兩個字節數組并返回另一個字節數組,所以它可以自身串聯任意次以串接任意數量的字節數組。例如,?concatenateBytes(byteArray1, concatenateBytes(byteArray2, byteArray3))?會將?byteArray3?加在?byteArray2?后,再將結果加到?byteArray1?后。?
    清單 8. concatenateBytes() 方法

    public byte[] concatenateBytes (byte[] array1, byte[] array2){byte concatenatedBytes[] = new byte[array1.length + array2.length];for (int i=0; i<array1.length; i++) concatenatedBytes[i] = array1[i];for (int j=array1.length; j<concatenatedBytes.length; j++) concatenatedBytes[j] = array2[j-array1.length];return concatenatedBytes;}//concatenateBytes

    getSequenceBytes()

    這個方法(見清單 9)生成一個 ASN.1?SEQUENCE?的字節數組表達。它取一個字節數組作為輸入參數,將這個字節數組作為?SEQUENCE?的內容,在內容前面加上?SEQUENCE?標簽字節(?0x30?)和長度字節,并返回完整的?SEQUENCE?結構。

    通常,?getSequenceBytes()?方法會與?concatenateBytes()?配合使用。一個應用程序將生成?SEQUENCE?中單獨的結構,將各個結構的字節數組表達串接在一起以構成一個數組,并將串接后的數組傳遞給?getSequenceBytes()?方法,這個方法將返回?SEQUENCE?的完整字節數組表達。?
    清單 9. getSequenceBytes() 方法

    public byte[] getSequenceBytes (byte[] sequenceContents){//1. Declare a byte array named finalBytes, which will // hold all the bytes of the ASN.1 byte array representation.byte finalBytes[];//2. Calculate the number of bytes required to hold the // contents part of the ASN.1 byte array representation.int contentBytesCount = sequenceContents.length;//3. Use the getLengthBytes() method of Listing 3 to author // the length bytes. Store the length bytes in // an array named lengthBytes.byte lengthBytes[] = getLengthBytes(contentBytesCount );//4. Get the number of bytes in the lengthBytes array.int lengthBytesCount = lengthBytes.length;//5. Calculate the number of bytes required to hold the complete // ASN.1 byte array representation (the sum total of the number // of tag bytes, length bytes, and content bytes). // Store the number of bytes in a variable named totalBytesCount.int totalBytesCount = lengthBytesCount + 1;//6. Instantiate the finalBytes array to totalBytesCount size.finalBytes = new byte[totalBytesCount];//7. Copy the tag byte at the start of the finalBytes array.finalBytes[0] = (byte)0x30;//8. Copy the length bytes from the lengthBytes array to the // finalBytes array just after the tag byte.for (int i=0; i < lengthBytes.length; i++)finalBytes[i+1] = lengthBytes[i];//9. Copy the content bytes to the finalBytes array // just after the length bytes.finalBytes = concatenateBytes (finalBytes, sequenceContents);//10. Return the finalBytes array.return finalBytes;}//getsequenceBytes

    getTagAndLengthBytes()

    這個方法與所討論過的各種?getXXXBytes()?方法非常相象。不過,雖然其中每一個方法生成一個特定的 ASN.1 通用數據類型,但是?getTagAndLengthBytes()?方法(見清單 10)生成應用程序級和上下文特定的數據類型。

    這個方法取三個參數。第一個參數(?tagType?)指定標簽類型。如果它的值等于靜態整數?ASN1DataTypes.Context_Specific?,那么它指定的是一個上下文特定標簽,如果它的值等于?ASN1DataTypes.Application_Type?,那么它指定的是一個應用程序級標簽。

    第二個參數(?tagNumber?)指定標簽數,而第三個(?tagContents?)包含了內容字節數組。

    getTagAndLengthBytes()?根據輸入參數計算標簽和長度字節的值,將標簽和長度字節加到內容字節前面,并返回應用程序級或者上下文特定的 ASN.1 結構的完整字節數組表達。?
    清單 10. getTagAndLengthBytes() 方法

    public byte[] getTagAndLengthBytes (int tagType, int tagNumber, byte[] tagContents){//1. Declare a byte array named finalBytes, // which will hold all the bytes of the ASN.1 byte array representation.byte finalBytes[];//2. Declare a byte array named tagAndLengthBytes,// which will hold the tag and length bytes.byte tagAndLengthBytes[];//3. Now calculate the value of the tag byte.int tag = tagType + tagNumber;//4. Calculate the number of bytes required to hold// the contents part of the ASN.1 byte array representation.int contentBytesCount = tagContents.length;//5. Use the getLengthBytes() method of Listing 3 // to author the length bytes.// Store the length bytes in an array named lengthBytes.byte lengthBytes[] = getLengthBytes (contentBytesCount);//6. Get the number of bytes in the lengthBytes array.int lengthBytesCount = lengthBytes.length;//7. Calculate the number of bytes required to hold // the tag byte and length bytes // (the sum total of the number of tag bytes and length bytes).// Store the number of bytes in a variable named tagBytesCount.int tagAndLengthBytesCount = 1 + lengthBytesCount;//8. Instantiate the finalBytes array to tagAndLengthBytesCount size.tagAndLengthBytes = new byte[tagAndLengthBytesCount];//9. Copy the tag byte at the start of the tagAndLengthBytes array.tagAndLengthBytes[0] = (byte)tag;//10. Copy the length bytes from the lengthBytes array // to the tagAndLengthBytes array just after the tag byte.for (int i=0; i < lengthBytes.length; i++)tagAndLengthBytes[i+1] = lengthBytes[i];//11. Now instansiate the finalBytes array of size equal to // the sum total of the number of tag bytes, // length bytes and content bytes.finalBytes = new byte [1 + tagAndLengthBytesCount + contentBytesCount ];//12. Copy the content bytes to the finalBytes array // just after the length bytes.finalBytes = concatenateBytes(tagAndLengthBytes, tagContents); //13. Return the finalBytes array.return finalBytes;}//getTagAndLengthBytes

    至此就完成了對?ASN1DataTypes?類的生成方法的討論。不過,在開始討論?KerberosClient?如何使用?ASN1DataTypes?方法生成一個 TGT 請求之前,我需要討論如何利用用戶的密碼生成密鑰。在與 Kerberos 服務器進行通信時,會在幾個地方需要這個密鑰。?




    回頁首


    利用用戶密碼生成密鑰

    Kerberos 定義了一種對用戶密碼進行處理以生成一個?密鑰的算法。在獲得 TGT 的過程中 Kerberos 客戶機將用這個密鑰進行解密

    對這個基于 J2ME 的 Kerberos 客戶機,我將只支持一種加密算法,即 CBC(密碼分組鏈接 cipher block chaining)模式下的 DES(數據加密標準)。DES 是一個 FIPS(聯邦信息處理標準 Federal Information Processing Standards)發表,它描述了一種將要加密的數據(純文本)和密鑰作為輸入傳遞給加密過程的加密算法。根據 DES 算法對密鑰和純文本統一處理以生成一個加密的(密文)形式的純文本數據。(有關 DES 的更多信息請參閱?參考資料)。

    CBC 是一種加密操作模式,其中純文本數據分為同樣大小的數據塊。例如,在 64 位 DES-CBC 加密中,數據會分為 8 字節的塊。如果純文數據中的字節數不是您希望每一個塊所具有的字節數的整數倍,就要在最后一塊中加上適當的數量的字節以使它的大小與其他的塊相同。

    然后創建一個與您的塊具有同樣大小的字節數組。這個字節數組稱為?初始矢量(IV)。Kerveros 規范定義了所有基于 Kerberos 的應用程序的初始矢量(類似地,其他使用 DES-CBC 的規范定義了它們使用的 IV 值)。之后,取這個 IV、純文數據的第一塊以及密鑰并根據 DES 算法對它們共同進行處理,以構成對應于純文本數據第一個數據塊的密文。然后取第一個數據塊的密文形式作為第二個塊的初始矢量并進行同樣的 DES 加密過程以生成第二個純文本數據塊的密文形式。以這種方式繼續一塊接一塊地生成每一個塊的密文形式。最后,串接所有密文塊以得到全部純文本數據的密文形式。

    因為我只打算在這個 Kerberos 客戶機中支持 DES-CBC,所以我將只討論 DES-CBC 所使用的密鑰的生成過程,如下所示:

  • 將用戶密碼、KDC 域名和用戶的用戶名串接到一起以構成一個字符串。Kerberos 利用這個串接的字符串而不僅僅是密碼生成密鑰。為什么要在密鑰生成中加入域名和用戶名呢?許多用戶會在不同的服務器上使用同樣的密碼。如果我只使用密碼生成密鑰,那么一個給定的密碼在所有 Kerberos 服務器上總是會生成同樣的密鑰。因而,如果一個黑客可以取得用戶在一臺 Kerberos 服務器上的密鑰,那么,他就可以在所有 Kerberos 服務器上使用同一個密鑰。另一方面,如果我加入了域名和用戶名,那么一個受到這種攻擊的密鑰將只會侵害特定的域。
  • 得到第 1 步中串接的字符串的字節數組表達。
  • 統計第 2 步中字節數組中的字節數。在這個字節串的后面附加適當數量的零字節以使它成為 8 的整數倍。例如,如果這個字節數組包含 53 個字節,那么就在這個字節數組的最后附加三個字節使它具有 56 個字節。
  • 將第 3 步中附加了字節后的字節數組分為大小相同的塊,每一塊有 8 個字節。
  • 每隔一個塊倒轉塊的位順序。換句話說,第一塊保持不變,第二塊的位順序應該倒轉,第三塊應保持不變,第中塊的位順序應倒轉,以此類推。
  • 取第一個(未改變的)塊并與第二個(倒轉的)塊進行每一位的 exclusive?OR?。然后將第一次 exclusive?OR?操作得到的結果與第三個(未改變的)塊進行另一次 exclusive?OR?操作。繼續 exclusive?OR?操作直到完成了所有塊。所有 exclusive?OR?操作的最后結果是一個 8 字節長的塊。?
  • 修正在第 6 步中得到的 8 字節塊的奇偶性。每一塊的最低有效位保留為奇偶位。統計 8 字節塊中每字節中的 1 的個數,如果 1 的個數為偶數,那么就設置最低位為 1 使它成為奇數。例如,如果一個字節的值為?00000000?,那么就要將它改為?00000001?。如果一個字節中 1 的個數已經為奇數,那么就將它的最低位設置為零。例如,如果一個字節為?00000010?,那么就不需要為修正其奇偶性做任何改變。?
  • DES 定義了一些弱的、因而不適合用于加密的密鑰。我們的密鑰生成過程的第八步是要檢查奇偶修正后的字節數組是否是一個弱的密鑰。如果是的話,就要用?0xf0?(?11110000?)與奇偶修正過的 8 字節塊進行 exclusive?OR。如果奇偶修正得到的不是弱密鑰,那么就不需要進行這種 exclusive?OR?操作。經過這種弱密鑰處理的字節數組是一個臨時密鑰。?
  • 現在我要使用這個臨時密鑰以 DES-CBC 算法加密第 3 步中得到的附加后的字節數組。這個臨時密鑰同時作為密鑰的值和 DES-CBC 加密的初始矢量的值。回想在前面的討論中說過,CBC 要求密文塊鏈接。第 9 步的結果是最后 8 字節塊的加密結果(放棄所以以前的密文塊)。因此,這一步的結果是另一個 8 字節塊。
  • 現在我修正第 9 步產生的 8 字節塊中的每一個字節的奇偶性。在上面第 7 步中我解釋了奇偶性修正。
  • 現在再次檢查第 10 步得到的經過奇偶修正的 8 字節塊是不是弱密鑰(就像在第 8 步中所做的那樣)。
  • 第 11 步的結果是一個 Kerveros 客戶機可以用來與 Kerberos 服務器進行通信的密鑰。

    現在看一下清單 11 中的?KerberosKey?類。這個類的?generateKey()?方法實現了上面描述的 11 步密鑰生成算法。?
    清單 11. KerberosKey 類

    import org.bouncycastle.crypto.params.ParametersWithRandom; import org.bouncycastle.crypto.modes.CBCBlockCipher; import org.bouncycastle.crypto.generators.DESKeyGenerator; import org.bouncycastle.crypto.params.DESParameters; import org.bouncycastle.crypto.engines.DESEngine; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.ParametersWithIV; public class KerberosKey {private CBCBlockCipher cipher;private KeyParameter kp;private ParametersWithIV iv;private byte kerberosKey[];private ASN1DataTypes asn1;private String principalID;public KerberosKey(String userName, String password, String realmName){kerberosKey = new byte[8];kerberosKey = generateKey (password, realmName, userName);}//KerberosKeypublic byte[] generateKey (String password, String realmName, String userName){//Step 1:String str = new String (password + realmName + userName);byte secretKey [] = new byte[8];//Step 2:byte encodedByteArray[] = encodeString(str);//Step 3:byte paddedByteArray[] = padString(encodedByteArray);//Step 4:int i = paddedByteArray.length / 8;//Step 5:for(int x=0; x<i; x++){byte blockValue1[] = new byte [8];System.arraycopy (paddedByteArray, x*8, blockValue1, 0, 8);if(x % 2 == 1){byte tempbyte1 = 0; byte tempbyte2 = 0;byte blockValue2[] = new byte [8];for (int y=0; y<8; y++){tempbyte2 = 0;for (int z=0; z<4; z++){tempbyte2 = (byte) ((1<<(7-z)) & 0xff);tempbyte1 |= (blockValue1[y] & tempbyte2) >>> (7-2*z);tempbyte2 = 0;}for (int z=4; z<8; z++){tempbyte2 = (byte) ((1<<(7-z)) & 0xff);tempbyte1 |= (blockValue1[y] & tempbyte2) << (2*z-7);tempbyte2 = 0;}blockValue2 [7-y] = tempbyte1;tempbyte1 = 0;}//outer forfor (int a = 0; a <8; a ++)blockValue2[a] = (byte) ((((byte)blockValue2[a] & 0xff) >>> 1) & 0xff);System.arraycopy(blockValue2, 0, blockValue1, 0, blockValue2.length);}//if(x % 2 == 1) for (int a = 0; a <8; a ++)blockValue1[a] = (byte) ((((byte)blockValue1[a] & 0xff) << 1) & 0xff);//Step 6:for (int b = 0; b <8; b ++)secretKey[b] ^= blockValue1[b];}// for//Step 7:secretKey= setParity(secretKey);//Step 8: if (isWeakKey(secretKey))secretKey = getStrongKey(secretKey); //Step 9:secretKey = getFinalKey(paddedByteArray, secretKey);//Step 10:secretKey = setParity(secretKey);if (isWeakKey(secretKey))secretKey = getStrongKey(secretKey); return secretKey;}//generateKeypublic byte[] getFinalKey (byte data[], byte key[]){//The cipher instance with DES algo and CBC mode.cipher = new CBCBlockCipher( new DESEngine());kp = new KeyParameter(key);iv = new ParametersWithIV (kp, key);cipher.init(true, iv);byte encKey[] = new byte[data.length];byte ivBytes[] = new byte[8];for(int x = 0; x < data.length / 8; x ++){cipher.processBlock(data, x*8, encKey, x*8);System.arraycopy(encKey, x*8, ivBytes, 0, 8);iv = new ParametersWithIV (kp, ivBytes);cipher.init (true, iv);}return ivBytes;}//getFinalKeypublic byte[] setParity (byte byteValue[]){for(int x=0; x<8; x++)byteValue[x] = parityValues[byteValue[x] & 0xff];return byteValue;}// Checks weak keypublic boolean isWeakKey (byte keyValue[]){byte weakKeyValue[];for(int x = 0; x < weakKeyByteValues.length; x++){weakKeyValue = weakKeyByteValues[x];if(weakKeyValue.equals(keyValue))return true;} return false;}//isWeakKey// Corrects the weak key by exclusive OR with 0xf0 constant.public byte[] getStrongKey(byte keyValue[]){keyValue[7] ^= 0xf0;return keyValue;}//checkWeakKey// Encodes string with ISO-Lation encodingspublic byte[] encodeString (String str){byte encodedByteArray[] = new byte[str.length()];try{encodedByteArray = str.getBytes("8859_1");}catch(java.io.UnsupportedEncodingException ue){ }return encodedByteArray;}//encodeString//This method pads the byte[] with ASCII nulls to an 8 byte boundary.public byte[] padString (byte encodedString[]){int x;if(encodedString.length < 8)x = encodedString.length;elsex = encodedString.length % 8;if(x == 0)return encodedString;byte paddedByteArray[] = new byte[(8 - x) + encodedString.length];for(int y = paddedByteArray.length - 1; y > encodedString.length - 1; y--)paddedByteArray[y] = 0;System.arraycopy(encodedString, 0, paddedByteArray, 0, encodedString.length);return paddedByteArray;}//padString//returns the secret key bytes.public byte[] getKey(){return this.kerberosKey; }//getKey()private byte weakKeyByteValues[][] = {{(byte)0x10, (byte)0x10, (byte)0x10, (byte)0x10,(byte)0x10, (byte)0x10, (byte)0x10, (byte)0x1},{(byte)0xfe, (byte)0xfe, (byte)0xfe, (byte)0xfe,(byte)0xfe, (byte)0xfe, (byte)0xfe, (byte)0xfe},{(byte)0x1f, (byte)0x1f, (byte)0x1f, (byte)0x1f,(byte)0x1f, (byte)0x1f, (byte)0x1f, (byte)0x1f},{(byte)0xe0, (byte)0xe0, (byte)0xe0, (byte)0xe0,(byte)0xe0, (byte)0xe0, (byte)0xe0, (byte)0xe0},{(byte)0x1f, (byte)0xe0, (byte)0x1f, (byte)0xe0,(byte)0x1f, (byte)0xe, (byte)0x01, (byte)0xfe},{(byte)0xfe, (byte)0x01, (byte)0xfe, (byte)0x01,(byte)0xfe, (byte)0x01, (byte)0xfe, (byte)0x01},{(byte)0x1f, (byte)0xe0, (byte)0x1f, (byte)0xe0,(byte)0x0e, (byte)0xf1, (byte)0x0e, (byte)0xf1},{(byte)0xe0, (byte)0x1f, (byte)0xe0, (byte)0x1f,(byte)0xf1, (byte)0x0e, (byte)0xf1, (byte)0x0e},{(byte)0x1e, (byte)0x00, (byte)0x1e, (byte)0x00,(byte)0x1f, (byte)0x10, (byte)0x1f, (byte)0x1},{(byte)0xe0, (byte)0x01, (byte)0xe0, (byte)0x01,(byte)0xf1, (byte)0x01, (byte)0xf1, (byte)0x01}, {(byte)0x1f, (byte)0xfe, (byte)0x1f, (byte)0xfe,(byte)0x0e, (byte)0xfe, (byte)0x0e, (byte)0xfe},{(byte)0xfe, (byte)0x1f, (byte)0xfe, (byte)0x1f,(byte)0xfe, (byte)0x0e, (byte)0xfe, (byte)0x0e},{(byte)0x11, (byte)0xf0, (byte)0x11, (byte)0xf0,(byte)0x10, (byte)0xe0, (byte)0x10, (byte)0xe},{(byte)0x1f, (byte)0x01, (byte)0x1f, (byte)0x01,(byte)0x0e, (byte)0x01, (byte)0x0e, (byte)0x01},{(byte)0xe0, (byte)0xfe, (byte)0xe0, (byte)0xfe,(byte)0xf1, (byte)0xfe, (byte)0xf1, (byte)0xfe},{(byte)0xfe, (byte)0xe0, (byte)0xfe, (byte)0xe0,(byte)0xfe, (byte)0xf1, (byte)0xfe, (byte)0xf1}};//Parity values for all possible combinations//256 entriesprivate byte parityValues[] = {1, 1, 2, 2, 4, 4, 7, 7, 8, 8, 11, 11, 13, 13, 14, 14, 16, 16, 19, 19, 21, 21, 22, 22, 25, 25, 26, 26, 28, 28, 31, 31, 32, 32, 35, 35, 37, 37, 38, 38, 41, 41, 42, 42, 44, 44, 47, 47, 49, 49, 50, 50, 52, 52, 55, 55, 56, 56, 59, 59, 61, 61, 62, 62, 64, 64, 67, 67, 69, 69, 70, 70, 73, 73, 74, 74, 76, 76, 79, 79, 81, 81, 82, 82, 84, 84, 87, 87, 88, 88, 91, 91, 93, 93, 94, 94, 97, 97, 98, 98, 100, 100, 103, 103, 104, 104, 107, 107, 109, 109, 110, 110, 112, 112, 115, 115, 117, 117, 118, 118, 121, 121, 122, 122, 124, 124, 127, 127, -128, -128, -125, -125, -123, -123, -122, -122, -119, -119, -118, -118, -116, -116, -113, -113, -111, -111, -110, -110, -108, -108, -105, -105, -104, -104, -101, -101, -99, -99, -98, -98, -95, -95, -94, -94, -92, -92, -89, -89, -88, -88, -85, -85, -83, -83, -82, -82, -80, -80, -77, -77, -75, -75, -74, -74, -71, -71, -70, -70, -68, -68, -65, -65, -63, -63, -62, -62, -60, -60, -57, -57, -56, -56, -53, -53, -51, -51, -50, -50, -48, -48, -45, -45, -43, -43, -42, -42, -39, -39, -38, -38, -36, -36, -33, -33, -32, -32, -29, -29, -27, -27, -26, -26, -23, -23, -22, -22, -20, -20, -17, -17, -15, -15, -14, -14, -12, -12, -9, -9, -8, -8, -5, -5, -3, -3, -2, -2}; }//KerberosKey class

    我已經用注釋標記了清單 11 中?generateKey()?方法中那些代碼行,以幫助您將算法的各個步驟與 J2ME 代碼中的相應行對應起來。編碼細節中真正需要解釋的一點是第 9 步,在這里我實際執行了 DES-CBC 加密。

    看一下清單 11 中?generateKey()?方法中的那些行代碼,它們用注釋標記為第 9 步。它是一個對名為?getFinalKey()的方法的調用,這個方法實現了第九步并取兩個參數。第一個參數(?data?)是第 3 步的附加操作得到的字節數組,而第二個參數(?key?)是作為第 8 步的結果得到的臨時密鑰。

    DESEngine?和?CBCBlockCipher?類進行實際的加密操作。這些類是 Bouncy Castle 組的 J2ME 平臺開放源代碼加密實現的一部分。Bouncy Castle 的實現可以免費得到,并可用于任何目的,只要您在發布時加入許可證信息。您將需要下載 Bouncy Castle 類(鏈接請參閱?參考資料)并遵照它所附帶的設置指示才能使用本文的示例代碼。清單 11 中的?KerberosKey?類包含在 Kerberos 類中使用 Bouncy Castle 類時需要的所有 import 語句。

    現在看一下在清單 11 中的?getFinalKey()?方法中發生了什么事情。我首先實例化了?DESEngine?類,這個類實現了 DES 加密算法。然后,我將這個?DESEngine?對象傳遞給構造函數?CBCBlockCipher?以創建一個名為?cipher?的?CBCBlockCipher?對象。這個?cipher?對象將執行實際的 DES-CBC 操作。

    然后我通過向名為?KeyParameter?的類的構造函數傳遞一個?key?參數創建一個名為?kp?的對象。這個?KeyParameter?類也是 Bouncy Castle 的加密庫的一部分。?kp?對象現在包裝了密鑰,所以在需要指定密鑰時我將傳遞這個對象。

    下一步是創建另一個名為?iv?的對象。這個對象是另一個名為?ParameterWithIV?的 Bouncy Castle 類的實例。?ParameterWithIV?構造函數取兩個參數。第一個是包裝了密鑰的?kp?對象。第二個是初始矢量字節數組。因為我必須用密鑰作為初始矢量,所以將密鑰作為初始矢量字節數組傳遞。

    iv?對象現在包裝了密鑰以及初始矢量,所以我在需要指定密鑰和初始矢量時傳遞這個對象。

    下一步是調用?cipher?對象的?init()?方法初始化這個對象。這個方法取兩個參數。第一個是布爾類型,在需要初始化一個密碼進行加密時傳遞?true?,在希望進行解碼時傳遞?false?。第二個是包裝了密鑰和初始矢量的?iv?對象,

    現在可以進行密文塊鏈接了。我聲明了一個名為?ivBytes?的字節數組,它將包含密碼塊鏈接每一步的初始矢量字節。一個?for?循環將連續調用?cipher?對象的?processBlock()?方法。?processBlock()?方法一次處理一個數據塊。

    processBlock()?方法取四個參數。第一個是輸入數組(?data?),第二個是字節數組中的偏移。?processBlock()?方法從這個偏移值開始處理塊輸入。第三個參數是輸出數組的名字,第四個是輸出數組中的偏移。

    for?循環調用?processBlock()?方法一次處理一個塊。這個方法一次處理一塊并將輸出(加密的結果)儲存在?ivBytes?數組中。之后,我通過向?ParametersWithIV?構造函數傳遞?ivBytes?數組創建一個新的?iv?對象(?ParametersWithIV?類的一個實例)。然后我用新的?iv?對象重新初始化這個密碼。于是循環可以用與第一塊的結果相等的初始矢量處理下一塊。

    循環退出時,我只是返回最后一個數據塊的加密結果,這就是密鑰生成過程第 9 步的結果。




    回頁首


    生成 TGT 請求

    到目前為止,我討論了?ASN1DataTypes?類的底層方法并實現了利用用戶的密碼生成密鑰的算法。現在可以展示KerberosClient?類如何利用這些底層細節了。

    看一下清單 12,它顯示了?getTicketResponse()?方法的實現。這個方法屬于?KerberosClient?類。

    getTicketResponse()?方法的基本目的是生成一個對 Kerberos 票據(一個 TGT 或者服務票據)的請求、向 Kerberos 服務器發送票據請求、從服務器得到響應、并將響應返回給調用應用程序。在本文中,我將只描述生成 TGT 請求的過程。本系列的下一篇文章將展示設置 KDC 服務器、向 KDC 發送請求、得到響應并對它進行處理的步驟。?
    清單 12. getTicketResponse() 方法

    import org.bouncycastle.crypto.digests.MD5Digest; public class KerberosClient extends ASN1DataTypes {static long seed = System.currentTimeMillis();private String kdcServiceName = "krbtgt";private KerberosKey krbKey;private String userName;private String password;private String realmName;public KerberosClient(String userName, String password, String realmName){krbKey = new KerberosKey(userName, password, realmName); this.userName = userName;this.password = password;this.realmName = realmName;}//KerberosClientpublic byte[] getTicketResponse (){byte pvno[] = getTagAndLengthBytes(ASN1DataTypes.Context_Specific,1, getIntegerBytes(5));byte msg_type[] = getTagAndLengthBytes(ASN1DataTypes.Context_Specific,2, getIntegerBytes(10)); byte noOptions[] = new byte [5];byte kdc_options[] = getTagAndLengthBytes(ASN1DataTypes.Context_Specific,0, getBitStringBytes(noOptions));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))))))));byte ticketRequest[] = getTagAndLengthBytes(ASN1DataTypes.Application_Type,10,getSequenceBytes(concatenateBytes(pvno,concatenateBytes(msg_type,req_body))));return ticketRequest;}public byte[] getRandomNumber (){String userData = userName + password;byte secretKey[] = getByteArray(System.currentTimeMillis() * 6 + seed);seed = seed / 5;int userDataHash = userData.hashCode() * 5;byte numData[] = new String(String.valueOf(userDataHash)).getBytes();byte numBytes[]= krbKey.getFinalKey(numData, secretKey);byte randomNum []= new byte[4];int j=1;for (int i=0; i<4; i++){randomNum[i]= numBytes[i+j];j++;}return randomNum; }//getRandomNumber//It is a helper method used to generate the random number bytes structure.public byte[] getIntegerBytes (byte[] byteContent){byte finalBytes[];int contentBytesCount = byteContent.length;byte lengthBytes[] = getLengthBytes(contentBytesCount );int lengthBytesCount = lengthBytes.length;int integerBytesCount = lengthBytesCount + contentBytesCount + 1;finalBytes = new byte[integerBytesCount];finalBytes[0] = (byte)0x02;for (int i=0; i < lengthBytes.length; i++)finalBytes[i+1] = lengthBytes[i];for (int j=lengthBytesCount+1; j<integerBytesCount; j++)finalBytes[j] = byteContent[j-(lengthBytesCount+1)]; return finalBytes;}//getIntegerBytes// Converts a long into a byte array.public byte[] getByteArray (long l){byte byteValue[] = new byte[8];for(int x=0; x<8; x++)byteValue[x] = (byte)(int)(l >>> (7 - x) * 8 & 255L);return byteValue;} }//KerberosClient class

    在本系列的?第一篇文章?對圖 2、清單 1 和表 2 的討論中我討論過 TGT 請求的結構。回想在那里的討論中,TGT 請求包含四個數據字段:?pvno?、?msg-type?、?padata?和?req-body?。生成?pvno?和?msg-type?字段非常簡單,因為這兩個字段分別只包含一個整數(如在?第一篇文章?中“請求 TGT”一節中提到的,?pvno?為 5,?msg-type?為 10)。

    您只需要調用?getIntegerBytes()?方法,向這個方法傳遞這個整數值。?getIntegerBytes()?方法返回以 ASN.1 字節數組表達的?INTEGER?結構,您將它傳遞給?getTagAndLengthBytes()?方法。這個方法將返回?pvno?或者?msg-type?字段的完整 ASN.1 表達。這就是我在清單 12 中的?getTicketResponse()?方法的開始時生成?pvno?和?msg-type?字段的方法。

    在生成?pvno?和?msg-type?字段后,下一步就是生成?padata?字段。這個字段是可選的。大多數 KDC 服務器有一個設置選項,可以對單獨的客戶機進行配置。系統管理員可以將 Kerberos 服務器設置為特定客戶可以發送不包括?padata?字段的 TGT 請求。

    為了減輕在資源有限的 J2ME 設備上的處理負擔,我假定電子銀行有一個允許無線移動用戶發送不帶?padata?字段的 TGT 請求的 Kerberos 服務器(并且我將在本系列的下一篇文章中展示如何設置 Keberos 服務器使它具有這種行為)。因此我將在要生成的 TGT 請求中略去?padata?字段。所以,在生成?pvno?和?msg-type?字段后,我就直接開始生成?req-body結構,這需要幾步。

    生成請求正文

    在清單 12 的?getTicketResponse()?方法中,我的請求正文(?req-body?結構)生成策略是生成結構的所有單獨的子字段,然后將它們串接到一起并包裝到一個?SEQUENCE?中以構成請求正文。

    回想在?第一篇文章?圖 2 的討論中,?req-body?的子字段有(去掉了一些可選字段):

    • kdc-options
    • cname
    • realm
    • sname
    • till
    • nonce
    • etype

    我將按它們在上面列表中的順序生成這些字段。因此,第一項任務是生成?kdc-options?字段。

    因為我不想使用任何 KDC 選項,所以我不需要對生成?kdc-options?字段進行任何邏輯處理。我只是使用一個全為零的 5 字節數組作為其內容。看一下清單 12 的?getTicketResponse()?方法中?byte noOptions[] = new byte [5];?這一行。這個方法實例化一個名為?noOptions?的 5 字節數組,它初始化為五個零。

    下一行(?byte kdc_options[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 0, getBitStringBytes(noOptions))?)執行兩項任務:

  • 它首先向?getBitStringBytes()?方法傳遞?noOptions?字節數組,它返回用 ASN.1 的位字符串表達的 5 個零。
  • 然后它將位字符串傳遞給?getTagAndLengthBytes()?方法,這個方法返回?kdc-options?字段的完整 ASN.1 字節數組表達。
  • 下一步是生成?cname?結構。在?第一篇文章?清單 1 的討論中說過,?cname?字段的類型為 type?cname?。這種數據類型是兩個字段 - 即?name-type?和?name-string?── 的?SEQUENCE?。?name-type?字段是用一個?INTEGER?構造的。?name-string?字段是?GeneralString?s 的一個?SEQUENCE?。

    因此,為了生成?cname?結構,我必須遵循清單 12 的?getTicketResponse()?方法中的幾個步驟:

  • 調用?getGeneralStringBytes()?方法,同時傳遞客戶的用戶名。?getGeneralStringBytes()?方法將返回客戶的用戶名的?GeneralString?表達。

  • 向?getSequenceBytes()?方法傳遞?GeneralString?,這個方法會在?GeneralString?前面附加?SEQUENCE?字節并返回包含客戶的用戶名字符串的?SEQUENCE?的 ASN.1 表達。

    byte generalStringSequence[] = getSequenceBytes (getGeneralStringBytes (userName));?這一行執行這前兩步。

  • 調用?getTagAndLengthBytes()?方法,傳遞?SEQUENCE?字節作為其內容。?getTagAndLengthBytes()?方法會在?SEQUENCE?前面附加?name-string?標簽字節(上下文特定的標簽數字 0)以及長度字節,并返回完整的?name-string?結構。

    byte name_string[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 1, generalStringSequence);?這一行執行這一步。

  • 生成?PrincipalName?的?name-type?部分。?name-type?部分只包含一個?INTEGER?,它標識了用戶名的類型。Kerbros 允許幾種類型的名字(用戶名、惟一標識等等)。對于這個基于 J2ME 的 Kerberos 客戶機,我感興趣的惟一名稱類型是用戶名,它的名稱類型標識是 1。因此,我將首先構造一個?INTEGER?,然后向?getTagAndLengthBytes()?方法傳遞這個?INTEGER?字節。這個方法生成?PrincipalName?的完整?name-type?部分。清單 12 中?byte name_type[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 0, getIntegerBytes (ASN1DataTypes.NT_PRINCIPAL));?這一行執行這項任務。

  • 將?PrincipalName?的?name-type?和?name-string?部分串接到一起,然后在串接字節數組前面附加?SEQUENCE?字節。?byte principalNameSequence [] = getSequenceBytes (concatenateBytes (name_type, name_string));?一行執行這項任務。

  • 在上面第 5 步的?SEQUENCE?前面附加?cname?標簽字節(上下文特定的標簽數 1)和長度字節。這樣就得到了完整的?cname?結構。?byte cname[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 1, principalNameSequence);?一行執行這項任務。

  • 上述 6 步策略就可以生成完整的?cname?結構。

    我的下一步是生成?realm?字段,它的類型為?GeneralString?。生成?realm?字段的策略如下:

  • 用?getGeneralStringBytes()?方法調用生成?GeneralString?。
  • 連同?getTagAndLengthBytes()?方法一起傳遞?GeneralString?字節,它會返回?realm?字段的完整字節字符串表達。
  • 清單 12 中?byte realm[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 2, getGeneralStringBytes (realmName));?這一行進行這兩個方法調用。

    下一項任務是生成?sname?字段,它是?PrincipalName?數據類型。我已經在上面討論?cname?字段時描述過了生成?PrincipalName?數據結構的策略。

    在?sname?字段后,我需要生成?till?字段,它指定我所請求的票據的失效時間。對于這個基于 J2ME 的 Kerberos 客戶機,我不想指定票據的任何特定失效時間,我只希望由 KDC 服務器根據服務器的策略發布具有標準失效時間的票據。因此,我總是發送硬編碼的日期(1970 年 1 月 1 日)作為?till?字段的值。我所選擇的日期是過去日期,這表明我不希望為請求的票據指定一個失效時間。

    till?字段為?KerberosTime?類型,它遵循?GeneralizedTime?通用數據類型。生成?KerberosTime?結構的過程是首先調用getGeneralizedTimeBytes()?方法并與方法調用同時傳遞時間字符串。例如,?etGeneralizedTimeBytes(new String("19700101000000Z")?方法調用會返回 1970 年 1 月 1 日的?GeneralizedTime?結構。

    有了?GeneralizedTime?字節數組后,我可以將它傳遞給?getTagAndLengthbytes()?方法調用,它會生成?till?參數的完整字節數組。清單 12 中?getTicketResponse()?方法的?byte till[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 5, getGeneralizedTimeBytes (new String("19700101000000Z").getBytes()));?這一行生成完整的?till?結構。

    下面,需要生成?nonce?字段,它包裝了一個隨機數作為一個整數。我首先生成一個隨機數,然后生成這個隨機數的字節數組表達,最后調用?getTagAndLengthBytes()?方法,它生成?nonce?字段的完整結構。

    在?req-body?字段中,還必須生成的最后一個結構是?etype?字段,這是一個?INTEGER?序列。?SEQUENCE?中的每個?INTEGER?指定客戶機支持的一種加密算法。我只希望支持一種加密算法(CBC 模式下的 DES),根據客戶機所選擇的消息摘要算法,它的?INTEGER?標識號是 1、2 或者 3。我將在本系列的下一篇文章中解釋消息摘要算法的使用,但是現在只要知道我要在 Kerberos 客戶機中使用 MD5 消息摘要算法。

    DES-CBC-MD5 組合的標識號是 3。因此,我將首先生成 3 的?INTEGER?字節,然后在?INTEGER?字節前附加?SEQUENCE?字節,最后調用?getTagAndLengthBytes()?方法,獲得?etype?字段的完整字節數組表達。

    現在我已經生成了?req-body?字段的所有字段。因此,我可以多次調用?concatenateBytes()?方法以將所有單獨的字段串接為一個字節數組。下一步是調用?getSequenceBytes()?方法以將串接的字節數組放到一個?SEQUENCE?中。一個?getTagAndLengthBytes()?方法將取?SEQUENCE?字節并生成完整的?req-body?結構。

    生成 TGT 請求的最后一步是將在本節前面生成的?pvno?和?msg-fields?與?req-body?字節串接在一起。然后將這些字段放入一個?SEQUENCE?,最后調用?getTagAndLengthBytes()?方法,得到一個完整的、可以發送給 Kerberos 服務器的票據請求。?




    回頁首


    結束語

    在本文中我討論了幾個基本概念。我開發了一個 J2ME 類,它包含幾個用于生成 ASN.1 數據結構的方法,我還展示了如何利用用戶的密碼生成一個 Kerberos 密鑰。最后,我演示了 Kerberos 客戶機如何生成 TGT 請求。

    下一次,我將搭建一個 KDC 服務器、從該服務器中獲取 Kerberos 票據、并用這些票據與電子銀行的業務邏輯服務器交換密鑰。

    參考資料

    • 您可以參閱本文在 developerWorks 全球站點上的?英文原文.?
    • 下載本文附帶的?源代碼。?

    • 閱讀本系列的?第一篇文章。?

    • 閱讀 IETF.org 上的 Kerberos(第 5 版)官方?RFC 1510。?

    • 下載?Bouncy Castle 的加密庫。我用 Bouncy Castle 的 1.19 版測試了本文的代碼。如果您要發布包括這些庫的代碼,一定要閱讀 Bouncy Castle 的?許可條款。?

    • 閱讀官方?DES?和?DES Modes of Operation(包括 CBC 模式)規范。?

    • 訪問 IETF 網站上的?Kerberos working group頁。?

    • 在“?Simplify enterprise Java authentication with single sign-on”一文中(?developerWorks,2003 年 9 月),Faheem Khan 使用 Kerberos 和 Java GSS API 論證了單點登錄。?

    • MIT 的這一頁包含有關 Kerberos 的很好的一組鏈接。?

    • 下載?完整的 ASN.1 文檔和編碼規則。?

    • 閱讀 Jason Garman 的?Kerberos: The Definitive Guide?(O'Reilly & Associates,2003),以學習 Kerberos 的使用。?


      • 看看?IBM 產品是如何使用 Kerberos的。?

      • IBM alphaWorks 提供了?Web Services Toolkit for Mobile Devices,可以將您的 J2ME 設備與 Web 服務世界相連接。?


    關于作者

    ?

    Faheem Khan 是一個獨立軟件顧問,專長是企業應用集成 (EAI) 和 B2B 解決方案。讀者可以通過?fkhan872@yahoo.com與 Faheem 聯系。


    用 Kerberos 為 J2ME 應用程序上鎖,第 3 部分:?建立與電子銀行的安全通信

    設置服務器、請求票據、獲取響應

    文檔選項

    將此頁作為電子郵件發送

    未顯示需要 JavaScript 的文檔選項


    級別: 初級

    Faheem Khan?(fkhan872@yahoo.com), 自由顧問

    2004 年 3 月 27 日

    如果您已經學習了本系列的前兩部分,那么現在可以開始第三部分,也就是最后一部分,您將設置一個 KDC 服務器,向它發送 Kerberos 票據請求并取得其響應。然后,您將學習處理 KDC 服務器的響應所需的低層 ASN1 處理方法,以便取得票據和會話密鑰。取得了服務票據后,將向電子銀行的業務邏輯服務器發送一個建立安全上下文的請求。最后,您將學會與電子銀行業務邏輯服務器進行實際的安全通信。

    回顧本系列的?第一篇文章,它介紹了移動銀行 MIDlet 應用程序,并解釋了 Kerberos 是如何滿足這種應用程序的安全要求的。文章還描述了 Kerberos 用來提供安全性的數據格式。

    本系列的?第二篇?文章展示了如何在 J2ME 中生成 ASN.1 數據類型。介紹了如何用 Bouncy Castle 加密庫進行 DES 加密,并用用戶的密碼生成 Kerberos 密鑰。最后將這些內容放到一起并生成一個 Kerberos 票據請求。

    在本系列文章中開發的 Kerberos 客戶不要求某個特定的 Kerberos 服務器,它可以使用所有 KDC 實現。參考資料部分包含了一些可以被 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;elsereturn 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;elsereturn 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;}elseoffset += 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);} elsecbcCipher.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);} elsecbcCipher.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 步:下一步是從?EncryptedData?SEQUENCE?內容中提取第三個字段(?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;} elsereturn null;} elsereturn null;} elsereturn null;} elsereturn 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;} elsereturn null;} else return null;} else return null;} else return null; } catch (Exception e) {e.printStackTrace();}return null;} elsereturn 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;}//getKeypublic void setTicket(byte[] ticket) {this.ticket = ticket;}//setTicketpublic byte[] getTicket(){return ticket;}//getTicket }





    回頁首


    得到一個服務票據

    已經處理了?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);}elsereturn 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;} elsereturn 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;}//sendSecureMessagepublic 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) { }//destroyApppublic 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);}}//commandactionpublic 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);elseshowTransResult(" Error!", "Transaction failed..");} elseSystem.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();}}//runpublic 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 代碼。

    參考資料

    • 您可以參閱本文在 developerWorks 全球站點上的?英文原文.?
    • 閱讀本系列的?第一篇?和?第二篇文章。?

    • 下載下載本文附帶的源代碼。?

    • 閱讀 Kerberos(第 5 版)的官方?RFC 1510。?

    • 下載?Bouncy Castle 的加密庫。本文的代碼是用 Bouncy Castle 的 1.19 版測試的。?

    • 閱讀官方?DES?和?DES Modes of Operation(包括 CBC 模式)規范。?

    • 訪問 IETF 網站上的?Kerberos 工作組頁面。?

    • 在“?Simplify enterprise Java authentication with single sign-on”一文中(?developerWorks,2003 年 9 月),展示了使用 Kerberos 和 Java GSS API 的單點登錄(SSO)。?

    • 在這里可以找到?有關 Kerberos 的很好的一組鏈接。?

    • 下載?完整的 ASN.1 文檔和編碼規則。?

    • 閱讀 Jason Garman 的?Kerberos: The Definitive Guide?(O'Reilly Associates,2003年),以學習 Kerberos 的使用。?

    • 訪問?GSS 頁。?

    • 看看 MIT 的這個?KDC 實現。?

    • CSG 組和?Heimdal?都提供了免費的 Kerberos 實現。?

    • 在?開發者書店?上尋找有關 Kerberos 所有內容的更多信息。?


    關于作者

    ?

    Faheem Khan 是一個獨立軟件顧問,專長是企業應用集成 (EAI) 和 B2B 解決方案。讀者可以通過?fkhan872@yahoo.com與 Faheem 聯系

    總結

    以上是生活随笔為你收集整理的用 Kerberos 为 J2ME 应用程序上锁的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    日日躁夜夜躁狠狠躁 | 久久久久免费看成人影片 | 亚洲欧美综合区丁香五月小说 | 精品亚洲成av人在线观看 | 日本大乳高潮视频在线观看 | 日韩人妻无码一区二区三区久久99 | 日日夜夜撸啊撸 | 久久亚洲中文字幕精品一区 | √8天堂资源地址中文在线 | 日日麻批免费40分钟无码 | 娇妻被黑人粗大高潮白浆 | 久久久国产精品无码免费专区 | 亚洲热妇无码av在线播放 | 婷婷五月综合缴情在线视频 | 国产免费久久精品国产传媒 | 欧美人与禽zoz0性伦交 | 真人与拘做受免费视频 | 人人爽人人澡人人人妻 | 无遮无挡爽爽免费视频 | 黑人巨大精品欧美一区二区 | 麻豆国产丝袜白领秘书在线观看 | 国产精品久久久久久无码 | 亚洲精品一区二区三区婷婷月 | 少妇一晚三次一区二区三区 | ass日本丰满熟妇pics | 九九综合va免费看 | 激情内射日本一区二区三区 | 亚洲国产综合无码一区 | 国产无套内射久久久国产 | 久久久久久a亚洲欧洲av冫 | 综合人妻久久一区二区精品 | 伊人色综合久久天天小片 | 亚洲一区二区三区含羞草 | 我要看www免费看插插视频 | 亚洲色欲色欲天天天www | 国内丰满熟女出轨videos | 丰满少妇人妻久久久久久 | 伊人久久大香线蕉午夜 | 国产成人综合美国十次 | 国产超碰人人爽人人做人人添 | 国产三级久久久精品麻豆三级 | 国产成人无码区免费内射一片色欲 | 久久综合给合久久狠狠狠97色 | 人人爽人人澡人人高潮 | 日日摸夜夜摸狠狠摸婷婷 | 国产精品二区一区二区aⅴ污介绍 | 亚洲成a人一区二区三区 | 99精品无人区乱码1区2区3区 | 55夜色66夜色国产精品视频 | 爽爽影院免费观看 | 色妞www精品免费视频 | 色诱久久久久综合网ywww | 日韩欧美群交p片內射中文 | 久久无码人妻影院 | 成人片黄网站色大片免费观看 | 色五月五月丁香亚洲综合网 | 亚洲高清偷拍一区二区三区 | 欧洲精品码一区二区三区免费看 | 大肉大捧一进一出视频出来呀 | 日本精品少妇一区二区三区 | 精品无码一区二区三区的天堂 | 国产精品久久久久久亚洲影视内衣 | 国产国产精品人在线视 | 欧美性生交活xxxxxdddd | 东京热男人av天堂 | 人人爽人人澡人人高潮 | 国产成人精品久久亚洲高清不卡 | 天天做天天爱天天爽综合网 | 国产办公室秘书无码精品99 | 免费观看又污又黄的网站 | 国产成人无码a区在线观看视频app | 久久99国产综合精品 | 久久精品99久久香蕉国产色戒 | 无套内射视频囯产 | 又湿又紧又大又爽a视频国产 | 日韩精品乱码av一区二区 | 国产又爽又黄又刺激的视频 | √天堂资源地址中文在线 | 1000部啪啪未满十八勿入下载 | 日韩精品成人一区二区三区 | 少妇高潮喷潮久久久影院 | 亚洲精品欧美二区三区中文字幕 | 一本久久伊人热热精品中文字幕 | 成人综合网亚洲伊人 | 一个人看的www免费视频在线观看 | 国产成人无码午夜视频在线观看 | 久久综合香蕉国产蜜臀av | 狠狠躁日日躁夜夜躁2020 | 欧美日韩精品 | 久久亚洲日韩精品一区二区三区 | 国产亚洲人成在线播放 | 无码播放一区二区三区 | 未满小14洗澡无码视频网站 | 中文字幕色婷婷在线视频 | 一个人免费观看的www视频 | 亚洲aⅴ无码成人网站国产app | 国产精品爱久久久久久久 | 亚洲经典千人经典日产 | 丁香花在线影院观看在线播放 | 国产女主播喷水视频在线观看 | 久久精品人人做人人综合试看 | 美女极度色诱视频国产 | 帮老师解开蕾丝奶罩吸乳网站 | 精品久久久久久人妻无码中文字幕 | 日日天干夜夜狠狠爱 | 亚洲 高清 成人 动漫 | 久久伊人色av天堂九九小黄鸭 | 疯狂三人交性欧美 | av无码电影一区二区三区 | 人妻天天爽夜夜爽一区二区 | 久久zyz资源站无码中文动漫 | 亚洲精品国产精品乱码视色 | 亚洲成色在线综合网站 | 亚洲欧美综合区丁香五月小说 | 国产真实夫妇视频 | 欧美黑人性暴力猛交喷水 | 精品久久久久久人妻无码中文字幕 | 性欧美疯狂xxxxbbbb | 久久午夜夜伦鲁鲁片无码免费 | ass日本丰满熟妇pics | 中文字幕人妻无码一区二区三区 | 亚洲国产成人a精品不卡在线 | 亚洲精品国产第一综合99久久 | 日韩少妇内射免费播放 | 荡女精品导航 | 天天躁夜夜躁狠狠是什么心态 | 国产亚洲精品久久久久久 | 精品无码国产一区二区三区av | 在线天堂新版最新版在线8 | √天堂中文官网8在线 | 十八禁视频网站在线观看 | 熟女体下毛毛黑森林 | 国产午夜福利亚洲第一 | 日日鲁鲁鲁夜夜爽爽狠狠 | 精品一区二区三区无码免费视频 | 国内丰满熟女出轨videos | 久久国产劲爆∧v内射 | 乌克兰少妇xxxx做受 | 久久精品无码一区二区三区 | 久久国产精品偷任你爽任你 | 中文无码精品a∨在线观看不卡 | 高清国产亚洲精品自在久久 | 国产午夜无码视频在线观看 | 日本欧美一区二区三区乱码 | 亚洲中文字幕在线无码一区二区 | 国产又爽又猛又粗的视频a片 | 我要看www免费看插插视频 | 99麻豆久久久国产精品免费 | 久久久久成人片免费观看蜜芽 | 亚洲日韩精品欧美一区二区 | 任你躁国产自任一区二区三区 | 一本一道久久综合久久 | aa片在线观看视频在线播放 | 久久成人a毛片免费观看网站 | 无遮无挡爽爽免费视频 | 全球成人中文在线 | 日日鲁鲁鲁夜夜爽爽狠狠 | 日本熟妇人妻xxxxx人hd | 在线看片无码永久免费视频 | 国产午夜亚洲精品不卡下载 | 性欧美牲交xxxxx视频 | 男女爱爱好爽视频免费看 | 国产精品久久久久久亚洲毛片 | 天干天干啦夜天干天2017 | 国产乱人伦av在线无码 | 亚洲а∨天堂久久精品2021 | aa片在线观看视频在线播放 | 国产亚洲精品久久久久久国模美 | 国产成人一区二区三区别 | 精品少妇爆乳无码av无码专区 | 亚洲码国产精品高潮在线 | 狠狠色噜噜狠狠狠狠7777米奇 | 最近中文2019字幕第二页 | 亚洲人成网站在线播放942 | 国产精品二区一区二区aⅴ污介绍 | 国产亚洲精品久久久久久久 | 天干天干啦夜天干天2017 | 亚洲一区二区三区含羞草 | 蜜桃视频插满18在线观看 | 久久www免费人成人片 | 一本一道久久综合久久 | 亚洲日本va中文字幕 | 亚洲国产欧美国产综合一区 | 两性色午夜视频免费播放 | 无码乱肉视频免费大全合集 | 扒开双腿疯狂进出爽爽爽视频 | 秋霞特色aa大片 | 欧美高清在线精品一区 | 久久久久国色av免费观看性色 | 中文字幕亚洲情99在线 | 丁香啪啪综合成人亚洲 | 国产欧美精品一区二区三区 | 精品国产青草久久久久福利 | 少妇性荡欲午夜性开放视频剧场 | 国产 浪潮av性色四虎 | 色狠狠av一区二区三区 | 成人无码精品1区2区3区免费看 | 国产精品va在线观看无码 | 内射白嫩少妇超碰 | 国产偷自视频区视频 | 亚洲国产欧美国产综合一区 | 乌克兰少妇xxxx做受 | 狠狠色丁香久久婷婷综合五月 | 亚洲精品无码国产 | 久热国产vs视频在线观看 | 狂野欧美性猛xxxx乱大交 | 久久99国产综合精品 | 人妻少妇精品无码专区二区 | 丰满少妇高潮惨叫视频 | 欧美阿v高清资源不卡在线播放 | 欧洲熟妇色 欧美 | 女人被男人躁得好爽免费视频 | 国产特级毛片aaaaaaa高清 | 免费观看黄网站 | 日本精品少妇一区二区三区 | 久久久久免费精品国产 | 日韩精品无码一区二区中文字幕 | 国产成人综合色在线观看网站 | 7777奇米四色成人眼影 | 久久精品国产一区二区三区 | 99久久婷婷国产综合精品青草免费 | 国产成人无码一二三区视频 | 日韩亚洲欧美中文高清在线 | 久久精品中文闷骚内射 | 国产极品美女高潮无套在线观看 | 双乳奶水饱满少妇呻吟 | 麻豆国产人妻欲求不满 | 帮老师解开蕾丝奶罩吸乳网站 | 男女下面进入的视频免费午夜 | 精品久久8x国产免费观看 | 欧洲熟妇色 欧美 | 欧美性生交xxxxx久久久 | 国产成人精品视频ⅴa片软件竹菊 | 国产福利视频一区二区 | 精品日本一区二区三区在线观看 | 亚洲精品一区二区三区在线 | 无码帝国www无码专区色综合 | 国产精品无码永久免费888 | 无码人妻出轨黑人中文字幕 | 亚洲精品无码人妻无码 | 成人无码影片精品久久久 | 夜夜影院未满十八勿进 | 东京一本一道一二三区 | 天天av天天av天天透 | 夜精品a片一区二区三区无码白浆 | 国产无av码在线观看 | 动漫av网站免费观看 | 人人爽人人澡人人人妻 | 色婷婷综合中文久久一本 | 中文字幕中文有码在线 | 无码一区二区三区在线观看 | 性欧美疯狂xxxxbbbb | 婷婷色婷婷开心五月四房播播 | 中文字幕日韩精品一区二区三区 | 国产精品久久久久影院嫩草 | 欧美自拍另类欧美综合图片区 | 久久午夜无码鲁丝片午夜精品 | 狂野欧美激情性xxxx | 99精品国产综合久久久久五月天 | 偷窥日本少妇撒尿chinese | 一本大道久久东京热无码av | 日韩无套无码精品 | 国产97色在线 | 免 | 欧美老熟妇乱xxxxx | 中文字幕无线码免费人妻 | 国产精品久久久午夜夜伦鲁鲁 | 2020久久香蕉国产线看观看 | 天堂久久天堂av色综合 | 国产猛烈高潮尖叫视频免费 | 76少妇精品导航 | 亚洲精品国产a久久久久久 | 久久亚洲精品成人无码 | 色欲久久久天天天综合网精品 | 学生妹亚洲一区二区 | 精品国精品国产自在久国产87 | www一区二区www免费 | 精品久久久久久人妻无码中文字幕 | 老司机亚洲精品影院无码 | 天天做天天爱天天爽综合网 | 内射爽无广熟女亚洲 | 国产人妻人伦精品1国产丝袜 | 内射欧美老妇wbb | 国产精品亚洲lv粉色 | 黑森林福利视频导航 | 成 人 网 站国产免费观看 | 国产av久久久久精东av | 亚洲精品成人av在线 | 亚洲毛片av日韩av无码 | 思思久久99热只有频精品66 | 亚拍精品一区二区三区探花 | 伊人久久婷婷五月综合97色 | √天堂资源地址中文在线 | 精品偷拍一区二区三区在线看 | 真人与拘做受免费视频一 | 一本色道婷婷久久欧美 | 午夜精品一区二区三区的区别 | 国产偷国产偷精品高清尤物 | www国产亚洲精品久久久日本 | 狠狠亚洲超碰狼人久久 | 精品人妻av区 | 国产极品美女高潮无套在线观看 | 99久久精品午夜一区二区 | 免费无码的av片在线观看 | 久久国产精品萌白酱免费 | 久久综合九色综合欧美狠狠 | 小泽玛莉亚一区二区视频在线 | 亚洲精品久久久久avwww潮水 | 精品欧美一区二区三区久久久 | 人人妻人人澡人人爽欧美一区 | 中文亚洲成a人片在线观看 | 国产国语老龄妇女a片 | 麻花豆传媒剧国产免费mv在线 | 强开小婷嫩苞又嫩又紧视频 | 欧美三级不卡在线观看 | 理论片87福利理论电影 | 午夜精品久久久内射近拍高清 | 免费看少妇作爱视频 | 人人澡人人透人人爽 | 精品偷自拍另类在线观看 | 国产av无码专区亚洲awww | 一区二区传媒有限公司 | 少妇激情av一区二区 | 麻豆果冻传媒2021精品传媒一区下载 | 无码乱肉视频免费大全合集 | 国产精品鲁鲁鲁 | 国产在线精品一区二区三区直播 | 久久国语露脸国产精品电影 | 国产又爽又猛又粗的视频a片 | 久久久无码中文字幕久... | 久久五月精品中文字幕 | 网友自拍区视频精品 | 动漫av网站免费观看 | 天干天干啦夜天干天2017 | 国产又爽又猛又粗的视频a片 | 东京热无码av男人的天堂 | 少妇久久久久久人妻无码 | 亚洲国产欧美在线成人 | 男女猛烈xx00免费视频试看 | 97久久国产亚洲精品超碰热 | а天堂中文在线官网 | 久久精品女人的天堂av | 日韩精品成人一区二区三区 | 国产亚洲精品久久久久久国模美 | 粗大的内捧猛烈进出视频 | 国产熟妇另类久久久久 | 精品国偷自产在线 | 日本乱人伦片中文三区 | 中文字幕乱码人妻二区三区 | 色婷婷综合激情综在线播放 | 男女作爱免费网站 | 久久人人爽人人爽人人片av高清 | 麻花豆传媒剧国产免费mv在线 | 久久99精品国产麻豆蜜芽 | 国产成人精品一区二区在线小狼 | 香蕉久久久久久av成人 | 少女韩国电视剧在线观看完整 | 国产成人无码a区在线观看视频app | 在线a亚洲视频播放在线观看 | 无码av岛国片在线播放 | 精品人妻中文字幕有码在线 | 性欧美牲交在线视频 | 亚洲精品一区二区三区婷婷月 | 无码人中文字幕 | 亚洲乱亚洲乱妇50p | 在线视频网站www色 | 久久亚洲精品成人无码 | 影音先锋中文字幕无码 | 一个人看的视频www在线 | 欧美真人作爱免费视频 | 色综合久久久无码网中文 | 久久伊人色av天堂九九小黄鸭 | 欧美性色19p | 国产精品二区一区二区aⅴ污介绍 | 亚洲第一网站男人都懂 | 久久久久亚洲精品中文字幕 | 国产色在线 | 国产 | 国产亚洲精品精品国产亚洲综合 | 伊在人天堂亚洲香蕉精品区 | av人摸人人人澡人人超碰下载 | 夫妻免费无码v看片 | 无码人妻丰满熟妇区五十路百度 | 精品一区二区三区波多野结衣 | 亚洲成a人片在线观看日本 | 丰满妇女强制高潮18xxxx | 偷窥日本少妇撒尿chinese | 亚洲欧洲中文日韩av乱码 | 久久国产精品精品国产色婷婷 | 夜夜高潮次次欢爽av女 | 亚洲狠狠色丁香婷婷综合 | 在线观看欧美一区二区三区 | 亚洲自偷自偷在线制服 | 国产精华av午夜在线观看 | 麻豆国产人妻欲求不满谁演的 | 亚洲色在线无码国产精品不卡 | 国产明星裸体无码xxxx视频 | 亚洲 激情 小说 另类 欧美 | 在线观看国产午夜福利片 | 久久久久99精品国产片 | 色五月丁香五月综合五月 | 久久久久av无码免费网 | 曰本女人与公拘交酡免费视频 | 狠狠色噜噜狠狠狠狠7777米奇 | 曰韩无码二三区中文字幕 | 久久午夜无码鲁丝片午夜精品 | 波多野结衣一区二区三区av免费 | 成人片黄网站色大片免费观看 | 欧美乱妇无乱码大黄a片 | 水蜜桃av无码 | 7777奇米四色成人眼影 | 国产精品人人妻人人爽 | 亚洲中文字幕无码一久久区 | 一本加勒比波多野结衣 | 亚洲一区二区三区 | 亚洲热妇无码av在线播放 | 无遮挡啪啪摇乳动态图 | 国产亚洲视频中文字幕97精品 | 欧美黑人性暴力猛交喷水 | 久久久中文字幕日本无吗 | 精品无码一区二区三区爱欲 | 九月婷婷人人澡人人添人人爽 | 久久精品人人做人人综合试看 | 日日躁夜夜躁狠狠躁 | 影音先锋中文字幕无码 | 久久天天躁狠狠躁夜夜免费观看 | 久久精品国产精品国产精品污 | 撕开奶罩揉吮奶头视频 | 美女极度色诱视频国产 | 强开小婷嫩苞又嫩又紧视频 | 久久国内精品自在自线 | 无码福利日韩神码福利片 | 人人妻人人澡人人爽欧美精品 | 无码吃奶揉捏奶头高潮视频 | 久久综合狠狠综合久久综合88 | 极品尤物被啪到呻吟喷水 | 强伦人妻一区二区三区视频18 | 成人欧美一区二区三区黑人 | 日韩人妻无码一区二区三区久久99 | 中文无码成人免费视频在线观看 | 亚洲精品国产品国语在线观看 | 日本熟妇乱子伦xxxx | 极品尤物被啪到呻吟喷水 | 久久精品人人做人人综合试看 | 国产99久久精品一区二区 | 国产亚洲精品久久久ai换 | 午夜福利不卡在线视频 | 国产午夜亚洲精品不卡 | 中文字幕av无码一区二区三区电影 | 色综合久久久久综合一本到桃花网 | 日韩人妻无码中文字幕视频 | 日韩少妇内射免费播放 | 国产97色在线 | 免 | 99久久精品无码一区二区毛片 | 理论片87福利理论电影 | 国产av人人夜夜澡人人爽麻豆 | 国产成人无码一二三区视频 | 欧洲熟妇精品视频 | 少妇性l交大片 | 日韩视频 中文字幕 视频一区 | 亚洲欧美日韩综合久久久 | 性欧美牲交在线视频 | 激情五月综合色婷婷一区二区 | 欧美 丝袜 自拍 制服 另类 | 丁香啪啪综合成人亚洲 | 少妇人妻大乳在线视频 | 久久综合给久久狠狠97色 | 7777奇米四色成人眼影 | 亲嘴扒胸摸屁股激烈网站 | 亚洲一区二区观看播放 | 黑人巨大精品欧美黑寡妇 | 国产手机在线αⅴ片无码观看 | 国产农村妇女高潮大叫 | 成人女人看片免费视频放人 | 小鲜肉自慰网站xnxx | 久久久久久久久蜜桃 | 国产精品va在线观看无码 | 日本免费一区二区三区最新 | 国产成人一区二区三区在线观看 | 国产精品亚洲专区无码不卡 | 2020久久香蕉国产线看观看 | 亚洲国产精品久久久久久 | 丝袜人妻一区二区三区 | 欧美乱妇无乱码大黄a片 | 又黄又爽又色的视频 | 2020久久超碰国产精品最新 | 亚洲一区二区三区含羞草 | 国产欧美亚洲精品a | 国产无遮挡吃胸膜奶免费看 | 欧美黑人巨大xxxxx | 色五月五月丁香亚洲综合网 | 日韩人妻少妇一区二区三区 | 一二三四在线观看免费视频 | 久久国产精品_国产精品 | 国产熟女一区二区三区四区五区 | 国产精品二区一区二区aⅴ污介绍 | 日日碰狠狠丁香久燥 | 精品国产一区av天美传媒 | 国产精品鲁鲁鲁 | 国产亲子乱弄免费视频 | 久久久久成人片免费观看蜜芽 | 亚洲伊人久久精品影院 | 亚洲精品久久久久avwww潮水 | 东京热男人av天堂 | 亚洲精品国产精品乱码不卡 | 亚洲色偷偷男人的天堂 | 青青青手机频在线观看 | 国产九九九九九九九a片 | 黑人巨大精品欧美一区二区 | 97无码免费人妻超级碰碰夜夜 | 精品国产精品久久一区免费式 | 偷窥村妇洗澡毛毛多 | 欧美成人免费全部网站 | 国产三级精品三级男人的天堂 | 色婷婷欧美在线播放内射 | 亚洲 欧美 激情 小说 另类 | 亚洲熟悉妇女xxx妇女av | www国产亚洲精品久久久日本 | 学生妹亚洲一区二区 | 精品一区二区三区波多野结衣 | 国产乱人偷精品人妻a片 | 妺妺窝人体色www在线小说 | 色欲av亚洲一区无码少妇 | 亚洲综合另类小说色区 | 思思久久99热只有频精品66 | 麻豆国产丝袜白领秘书在线观看 | 99久久99久久免费精品蜜桃 | 午夜精品一区二区三区在线观看 | 国产人妻精品一区二区三区 | 波多野结衣av一区二区全免费观看 | 亚洲精品成a人在线观看 | 成人av无码一区二区三区 | 国产免费无码一区二区视频 | 国产农村乱对白刺激视频 | 色欲人妻aaaaaaa无码 | 男女性色大片免费网站 | 99国产精品白浆在线观看免费 | 国产精品久久久久久亚洲影视内衣 | 欧美一区二区三区 | 成在人线av无码免观看麻豆 | 国産精品久久久久久久 | 精品少妇爆乳无码av无码专区 | 久久久久久亚洲精品a片成人 | 亚洲人成人无码网www国产 | 亚洲人成网站色7799 | 久久精品女人天堂av免费观看 | 亚洲男人av香蕉爽爽爽爽 | 男人扒开女人内裤强吻桶进去 | 国产成人一区二区三区在线观看 | 99riav国产精品视频 | 精品乱码久久久久久久 | 中文字幕人妻无码一夲道 | 中文字幕无码av激情不卡 | 欧美老妇交乱视频在线观看 | 成人无码视频免费播放 | 免费播放一区二区三区 | 永久免费观看美女裸体的网站 | 天天拍夜夜添久久精品大 | 乱人伦人妻中文字幕无码久久网 | 亚洲区小说区激情区图片区 | 夜夜躁日日躁狠狠久久av | 97夜夜澡人人爽人人喊中国片 | 国产深夜福利视频在线 | 精品水蜜桃久久久久久久 | 国产亚洲精品久久久ai换 | 亚洲国精产品一二二线 | 日本一区二区三区免费播放 | 十八禁视频网站在线观看 | 日韩精品a片一区二区三区妖精 | 亚洲午夜福利在线观看 | 亚洲综合另类小说色区 | 亚洲欧洲日本综合aⅴ在线 | 国产性猛交╳xxx乱大交 国产精品久久久久久无码 欧洲欧美人成视频在线 | 久久婷婷五月综合色国产香蕉 | 国产免费久久精品国产传媒 | 国产成人亚洲综合无码 | 久久婷婷五月综合色国产香蕉 | 国产色在线 | 国产 | 奇米综合四色77777久久 东京无码熟妇人妻av在线网址 | 无码成人精品区在线观看 | 中文字幕无线码免费人妻 | 99久久无码一区人妻 | 在线成人www免费观看视频 | 国产亚洲美女精品久久久2020 | 国产精品99爱免费视频 | 国产高潮视频在线观看 | 国产极品视觉盛宴 | 小泽玛莉亚一区二区视频在线 | 亚洲の无码国产の无码影院 | 中文无码伦av中文字幕 | 正在播放老肥熟妇露脸 | 99er热精品视频 | 亚洲日本一区二区三区在线 | 精品国偷自产在线 | 国产人妻精品一区二区三区不卡 | 欧美国产日产一区二区 | 国产精品香蕉在线观看 | 久久视频在线观看精品 | 成在人线av无码免观看麻豆 | 国产午夜精品一区二区三区嫩草 | 中文无码伦av中文字幕 | 欧美黑人性暴力猛交喷水 | 国产乱人偷精品人妻a片 | 一区二区三区乱码在线 | 欧洲 | 亚洲成av人片在线观看无码不卡 | 久久久久久a亚洲欧洲av冫 | 在线天堂新版最新版在线8 | 日韩人妻无码一区二区三区久久99 | 成人欧美一区二区三区黑人 | 国产亚洲美女精品久久久2020 | 久久aⅴ免费观看 | 免费看男女做好爽好硬视频 | 少妇无码一区二区二三区 | 久久综合九色综合97网 | 国产av一区二区三区最新精品 | 国产真实夫妇视频 | 国产电影无码午夜在线播放 | 正在播放老肥熟妇露脸 | 中文字幕久久久久人妻 | 欧洲vodafone精品性 | 国产av剧情md精品麻豆 | 综合人妻久久一区二区精品 | 蜜桃av抽搐高潮一区二区 | 男女下面进入的视频免费午夜 | 亚洲欧美日韩成人高清在线一区 | 欧美刺激性大交 | 黑人粗大猛烈进出高潮视频 | 亚洲欧美精品aaaaaa片 | 激情内射亚州一区二区三区爱妻 | 国产三级精品三级男人的天堂 | 亚洲男女内射在线播放 | 免费人成在线视频无码 | 国产成人无码av一区二区 | 99久久99久久免费精品蜜桃 | 97久久国产亚洲精品超碰热 | 国产精品多人p群无码 | 小sao货水好多真紧h无码视频 | 97色伦图片97综合影院 | 在线观看免费人成视频 | 国产av人人夜夜澡人人爽麻豆 | 国产深夜福利视频在线 | 久久99精品国产麻豆 | 夜夜夜高潮夜夜爽夜夜爰爰 | 国内少妇偷人精品视频免费 | 丁香花在线影院观看在线播放 | 永久免费精品精品永久-夜色 | 97久久精品无码一区二区 | 亚洲毛片av日韩av无码 | 国产福利视频一区二区 | 欧美兽交xxxx×视频 | 18精品久久久无码午夜福利 | 亚洲色大成网站www | 成 人影片 免费观看 | 撕开奶罩揉吮奶头视频 | 久久人人爽人人爽人人片av高清 | 国产一区二区三区影院 | 久久午夜无码鲁丝片 | 亚洲精品国偷拍自产在线观看蜜桃 | 亚洲欧美日韩综合久久久 | 中文字幕av日韩精品一区二区 | 牲欲强的熟妇农村老妇女视频 | 精品亚洲韩国一区二区三区 | 欧美三级a做爰在线观看 | 国语精品一区二区三区 | 一区二区传媒有限公司 | 色婷婷香蕉在线一区二区 | 天干天干啦夜天干天2017 | 四虎影视成人永久免费观看视频 | 最近中文2019字幕第二页 | 国产 浪潮av性色四虎 | 亚洲精品一区二区三区婷婷月 | 18禁黄网站男男禁片免费观看 | 夜夜夜高潮夜夜爽夜夜爰爰 | 精品偷拍一区二区三区在线看 | 成人无码视频在线观看网站 | 亚洲精品鲁一鲁一区二区三区 | 亚拍精品一区二区三区探花 | 国产凸凹视频一区二区 | 久久久久久九九精品久 | 欧美人与动性行为视频 | 成年女人永久免费看片 | 欧美黑人乱大交 | 国产精品美女久久久网av | 国产成人亚洲综合无码 | 色婷婷综合中文久久一本 | 丰满妇女强制高潮18xxxx | 丁香啪啪综合成人亚洲 | 国产精品毛片一区二区 | 亚洲精品国偷拍自产在线麻豆 | 久久精品无码一区二区三区 | 亚洲国产高清在线观看视频 | 国内丰满熟女出轨videos | 国产办公室秘书无码精品99 | 日本成熟视频免费视频 | 狠狠色噜噜狠狠狠7777奇米 | 亚洲理论电影在线观看 | 精品一区二区三区波多野结衣 | 国产手机在线αⅴ片无码观看 | 成人亚洲精品久久久久软件 | 久久国产劲爆∧v内射 | 欧美性生交xxxxx久久久 | 国产av无码专区亚洲awww | 天堂无码人妻精品一区二区三区 | 亚洲精品美女久久久久久久 | 永久黄网站色视频免费直播 | 久久久精品人妻久久影视 | 久久久成人毛片无码 | 亚洲gv猛男gv无码男同 | 丰满人妻翻云覆雨呻吟视频 | 成人免费视频一区二区 | 精品夜夜澡人妻无码av蜜桃 | 国产亚洲tv在线观看 | 色五月丁香五月综合五月 | 性色欲情网站iwww九文堂 | 四虎4hu永久免费 | 永久黄网站色视频免费直播 | 久久久久久久久蜜桃 | 亚无码乱人伦一区二区 | 图片区 小说区 区 亚洲五月 | 欧美激情内射喷水高潮 | 欧美黑人巨大xxxxx | 国産精品久久久久久久 | 色综合视频一区二区三区 | 97久久国产亚洲精品超碰热 | 免费人成在线视频无码 | 午夜无码区在线观看 | 精品熟女少妇av免费观看 | 日本精品高清一区二区 | 老太婆性杂交欧美肥老太 | 国产精品亚洲综合色区韩国 | 国产乱子伦视频在线播放 | 女人被爽到呻吟gif动态图视看 | 无码av免费一区二区三区试看 | 亚洲 欧美 激情 小说 另类 | 在线播放无码字幕亚洲 | 亚洲热妇无码av在线播放 | 青青草原综合久久大伊人精品 | 无码吃奶揉捏奶头高潮视频 | 成人片黄网站色大片免费观看 | 午夜福利一区二区三区在线观看 | 黑人巨大精品欧美黑寡妇 | 亚洲成a人一区二区三区 | 四虎永久在线精品免费网址 | 亚洲乱亚洲乱妇50p | 亚洲第一无码av无码专区 | 精品久久久久久人妻无码中文字幕 | 帮老师解开蕾丝奶罩吸乳网站 | 日韩 欧美 动漫 国产 制服 | 成人影院yy111111在线观看 | 大地资源中文第3页 | 在线观看欧美一区二区三区 | 无码成人精品区在线观看 | 久久久中文字幕日本无吗 | 国产精品久久久久久无码 | 国内精品久久久久久中文字幕 | 男人的天堂2018无码 | 亚洲阿v天堂在线 | 国产综合久久久久鬼色 | 国产精品怡红院永久免费 | 亚洲一区二区三区在线观看网站 | 国产乱人伦app精品久久 国产在线无码精品电影网 国产国产精品人在线视 | 亚洲阿v天堂在线 | 亚洲国产精品久久人人爱 | 久久久精品人妻久久影视 | 日韩 欧美 动漫 国产 制服 | 一本色道久久综合狠狠躁 | 中文精品久久久久人妻不卡 | a国产一区二区免费入口 | 中文无码成人免费视频在线观看 | 少妇性l交大片欧洲热妇乱xxx | 国产成人无码午夜视频在线观看 | 麻豆国产人妻欲求不满 | 蜜臀aⅴ国产精品久久久国产老师 | 对白脏话肉麻粗话av | 又大又紧又粉嫩18p少妇 | 狠狠综合久久久久综合网 | 一本色道久久综合狠狠躁 | 亚洲欧美日韩综合久久久 | 亚洲精品成人福利网站 | 久久久久久亚洲精品a片成人 | 人妻与老人中文字幕 | 小sao货水好多真紧h无码视频 | 内射老妇bbwx0c0ck | 国产精品美女久久久久av爽李琼 | 国产猛烈高潮尖叫视频免费 | 无码人妻久久一区二区三区不卡 | 日本乱偷人妻中文字幕 | 欧美激情综合亚洲一二区 | 香港三级日本三级妇三级 | 好男人www社区 | 国产乱子伦视频在线播放 | 国产av一区二区三区最新精品 | 无人区乱码一区二区三区 | 成熟女人特级毛片www免费 | 日本大乳高潮视频在线观看 | 人妻少妇精品视频专区 | 欧美日韩综合一区二区三区 | 97无码免费人妻超级碰碰夜夜 | 国内老熟妇对白xxxxhd | 久精品国产欧美亚洲色aⅴ大片 | 日本又色又爽又黄的a片18禁 | 国产熟妇高潮叫床视频播放 | 无码午夜成人1000部免费视频 | 蜜桃视频插满18在线观看 | 亚洲小说春色综合另类 | 亚洲国产精品无码久久久久高潮 | 国产精品美女久久久久av爽李琼 | 人妻中文无码久热丝袜 | 国产网红无码精品视频 | 免费乱码人妻系列无码专区 | 88国产精品欧美一区二区三区 | 久久亚洲精品中文字幕无男同 | 欧美国产日韩久久mv | 欧美熟妇另类久久久久久不卡 | 成人亚洲精品久久久久软件 | 日本va欧美va欧美va精品 | 最近免费中文字幕中文高清百度 | 无码吃奶揉捏奶头高潮视频 | 色婷婷久久一区二区三区麻豆 | 妺妺窝人体色www婷婷 | 狠狠色丁香久久婷婷综合五月 | 无码精品人妻一区二区三区av | 天天拍夜夜添久久精品 | 日本护士毛茸茸高潮 | 日韩人妻无码中文字幕视频 | 国产做国产爱免费视频 | 久久久久久a亚洲欧洲av冫 | 色情久久久av熟女人妻网站 | 精品国产aⅴ无码一区二区 | 久久国产精品二国产精品 | 久久国内精品自在自线 | 国产成人精品必看 | 精品国产aⅴ无码一区二区 | 亚洲国产av美女网站 | 国产激情综合五月久久 | 中文精品久久久久人妻不卡 | 鲁大师影院在线观看 | 国产成人精品三级麻豆 | 国产精品视频免费播放 | 欧美xxxx黑人又粗又长 | 国产午夜亚洲精品不卡 | 中文无码成人免费视频在线观看 | 国内少妇偷人精品视频免费 | 国产精品亚洲专区无码不卡 | 国产精品久久久午夜夜伦鲁鲁 | 欧美一区二区三区 | 帮老师解开蕾丝奶罩吸乳网站 | 精品人妻人人做人人爽夜夜爽 | 色综合久久久久综合一本到桃花网 | 色诱久久久久综合网ywww | 国产女主播喷水视频在线观看 | 人妻人人添人妻人人爱 | 人妻少妇精品无码专区二区 | 国产在线无码精品电影网 | 妺妺窝人体色www婷婷 | 98国产精品综合一区二区三区 | 人妻体内射精一区二区三四 | 午夜精品久久久久久久 | 亚洲日韩中文字幕在线播放 | 婷婷五月综合激情中文字幕 | 亚洲一区av无码专区在线观看 | 特黄特色大片免费播放器图片 | 久久亚洲精品成人无码 | 少妇人妻偷人精品无码视频 | 久久国产自偷自偷免费一区调 | 久久天天躁狠狠躁夜夜免费观看 | 欧美日韩色另类综合 | 国产97人人超碰caoprom | 蜜桃臀无码内射一区二区三区 | 亚洲中文字幕乱码av波多ji | 人妻互换免费中文字幕 | 久久精品中文字幕一区 | 国产偷国产偷精品高清尤物 | 97久久精品无码一区二区 | 亚洲国产精品无码一区二区三区 | 九月婷婷人人澡人人添人人爽 | 亚洲另类伦春色综合小说 | 青青青手机频在线观看 | 狂野欧美性猛xxxx乱大交 | 精品夜夜澡人妻无码av蜜桃 | 中文字幕无码日韩专区 | 亚洲成av人影院在线观看 | 日本丰满护士爆乳xxxx | 色综合久久久久综合一本到桃花网 | 欧美 日韩 人妻 高清 中文 | 久久精品成人欧美大片 | 激情国产av做激情国产爱 | 欧美xxxxx精品 | 青草青草久热国产精品 | 欧美人与物videos另类 | av无码电影一区二区三区 | 亚洲中文字幕久久无码 | 国产精品久久福利网站 | 亚洲精品综合一区二区三区在线 | 亚洲成在人网站无码天堂 | 自拍偷自拍亚洲精品被多人伦好爽 | 日韩欧美中文字幕在线三区 | 日日天日日夜日日摸 | 色噜噜亚洲男人的天堂 | 天堂а√在线地址中文在线 | 亚洲国产精品久久人人爱 | 亚洲春色在线视频 | 久久无码中文字幕免费影院蜜桃 | 乱码av麻豆丝袜熟女系列 | 一本久道久久综合婷婷五月 | 国产成人午夜福利在线播放 | 久久国产精品二国产精品 | 色综合久久久无码中文字幕 | 久久人人97超碰a片精品 | 免费男性肉肉影院 | 日韩精品乱码av一区二区 | 国产办公室秘书无码精品99 | 亚洲爆乳无码专区 | 国产精品无码mv在线观看 | 日本乱人伦片中文三区 | 久久久久久av无码免费看大片 | 国产另类ts人妖一区二区 | 巨爆乳无码视频在线观看 | 欧美日韩精品 | 国产另类ts人妖一区二区 | 老太婆性杂交欧美肥老太 | 伊人色综合久久天天小片 | 天天做天天爱天天爽综合网 | 免费国产成人高清在线观看网站 | 色欲av亚洲一区无码少妇 | 久久久中文久久久无码 | 亚洲国产成人av在线观看 | 国产九九九九九九九a片 | 国产成人人人97超碰超爽8 | 撕开奶罩揉吮奶头视频 | 成人无码视频免费播放 | 成熟妇人a片免费看网站 | 国产精品人妻一区二区三区四 | 好爽又高潮了毛片免费下载 | 捆绑白丝粉色jk震动捧喷白浆 | 粉嫩少妇内射浓精videos | 美女极度色诱视频国产 | 偷窥村妇洗澡毛毛多 | 内射爽无广熟女亚洲 | 老头边吃奶边弄进去呻吟 | 亚洲性无码av中文字幕 | 乱人伦人妻中文字幕无码久久网 | 人人妻人人澡人人爽人人精品浪潮 | 国产精品毛多多水多 | 国产成人精品视频ⅴa片软件竹菊 | 成人精品视频一区二区 | 中文字幕乱妇无码av在线 | 欧美乱妇无乱码大黄a片 | 国产熟女一区二区三区四区五区 | 人人妻人人澡人人爽人人精品浪潮 | 国产精品人人爽人人做我的可爱 | 久激情内射婷内射蜜桃人妖 | 性生交大片免费看l | 四虎影视成人永久免费观看视频 | 免费看男女做好爽好硬视频 | 全黄性性激高免费视频 | 色综合久久中文娱乐网 | 国产人妻精品午夜福利免费 | 亚洲大尺度无码无码专区 | 国产一精品一av一免费 | 真人与拘做受免费视频一 | 免费人成网站视频在线观看 | 国产乱人伦偷精品视频 | 国产精品a成v人在线播放 | 国产精品无码成人午夜电影 | 国产熟女一区二区三区四区五区 | 欧美野外疯狂做受xxxx高潮 | 久久久亚洲欧洲日产国码αv | 夜夜高潮次次欢爽av女 | 青青草原综合久久大伊人精品 | 日本www一道久久久免费榴莲 | 精品无码国产自产拍在线观看蜜 | av无码电影一区二区三区 | 亚洲日韩乱码中文无码蜜桃臀网站 | 久久亚洲精品中文字幕无男同 | 精品一区二区三区波多野结衣 | 大地资源中文第3页 | 国产精品久久久久久久9999 | 国产区女主播在线观看 | 六月丁香婷婷色狠狠久久 | 18精品久久久无码午夜福利 | 亚洲综合另类小说色区 | 国色天香社区在线视频 | 99久久人妻精品免费一区 | 欧美日韩一区二区免费视频 | 日本护士xxxxhd少妇 | 久久精品国产99精品亚洲 | 伊人久久婷婷五月综合97色 | 又粗又大又硬又长又爽 | 亚洲一区二区三区在线观看网站 | 日日橹狠狠爱欧美视频 | 日本熟妇大屁股人妻 | а√资源新版在线天堂 | 亚洲人成网站免费播放 | 无套内谢的新婚少妇国语播放 | 十八禁视频网站在线观看 | 成人精品视频一区二区三区尤物 | 秋霞成人午夜鲁丝一区二区三区 | 久久国产精品二国产精品 | 爆乳一区二区三区无码 | 动漫av网站免费观看 | 熟妇人妻无乱码中文字幕 | 亚洲精品一区二区三区在线观看 | 激情爆乳一区二区三区 | 天天摸天天透天天添 | 色一情一乱一伦 | av无码久久久久不卡免费网站 | 国产成人无码av片在线观看不卡 | 男女爱爱好爽视频免费看 | 天堂无码人妻精品一区二区三区 | 国产极品美女高潮无套在线观看 | 国产精品亚洲а∨无码播放麻豆 | 免费人成在线视频无码 | 亚洲а∨天堂久久精品2021 | 少妇人妻偷人精品无码视频 | 久久精品国产99精品亚洲 | 欧洲美熟女乱又伦 | 老子影院午夜精品无码 | 久久久久国色av免费观看性色 | 欧美大屁股xxxxhd黑色 | 婷婷丁香五月天综合东京热 | 欧美国产日韩亚洲中文 | 国产一区二区三区四区五区加勒比 | 日日鲁鲁鲁夜夜爽爽狠狠 | 激情国产av做激情国产爱 | 99久久精品国产一区二区蜜芽 | 亚洲色无码一区二区三区 | 亚洲综合无码久久精品综合 | 久久精品国产99精品亚洲 | 色五月丁香五月综合五月 | 无码中文字幕色专区 | 永久免费观看美女裸体的网站 | 中文字幕人成乱码熟女app | 波多野结衣 黑人 | 国产成人综合色在线观看网站 | 日本精品高清一区二区 | 欧美日本精品一区二区三区 | 亚洲va欧美va天堂v国产综合 | 欧美黑人巨大xxxxx | 夜先锋av资源网站 | 国产亚洲精品久久久ai换 | 国产午夜无码视频在线观看 | 国产成人精品无码播放 | 国产精品亚洲一区二区三区喷水 | 又紧又大又爽精品一区二区 | 中文精品无码中文字幕无码专区 | 色婷婷综合中文久久一本 | 精品国产精品久久一区免费式 | 无套内谢的新婚少妇国语播放 | 久久综合九色综合97网 | 麻豆人妻少妇精品无码专区 | 亚洲精品一区国产 | 2020最新国产自产精品 | 天天摸天天透天天添 | 国产亚洲tv在线观看 | 免费男性肉肉影院 | 国产 浪潮av性色四虎 | 色综合久久久久综合一本到桃花网 | 亚洲日韩av一区二区三区中文 | 婷婷五月综合激情中文字幕 | 国产另类ts人妖一区二区 | 国产内射爽爽大片视频社区在线 | 亚洲 a v无 码免 费 成 人 a v | 鲁鲁鲁爽爽爽在线视频观看 | 2020久久超碰国产精品最新 | 久久国产精品偷任你爽任你 | 国产av一区二区精品久久凹凸 | 亚洲乱亚洲乱妇50p | 2020久久超碰国产精品最新 | 亚洲成在人网站无码天堂 | 99久久精品午夜一区二区 | 亚洲色在线无码国产精品不卡 | 色婷婷久久一区二区三区麻豆 | 天天综合网天天综合色 | 性生交大片免费看l | 全黄性性激高免费视频 | 偷窥村妇洗澡毛毛多 | 永久免费精品精品永久-夜色 | 国产内射老熟女aaaa | 亚洲成a人片在线观看日本 | 四虎4hu永久免费 | 久久这里只有精品视频9 | 久久久久99精品国产片 | 久久97精品久久久久久久不卡 | 伊人久久婷婷五月综合97色 | 精品人妻中文字幕有码在线 | 亚洲伊人久久精品影院 | 麻豆精品国产精华精华液好用吗 | 日本一本二本三区免费 | 国产成人一区二区三区在线观看 | 骚片av蜜桃精品一区 | 麻豆国产97在线 | 欧洲 | 久久97精品久久久久久久不卡 | 精品国精品国产自在久国产87 | 国产日产欧产精品精品app | 欧洲精品码一区二区三区免费看 | 国产97在线 | 亚洲 | 无码午夜成人1000部免费视频 | 最近中文2019字幕第二页 | 丰满少妇熟乱xxxxx视频 | 老子影院午夜精品无码 | 精品久久久久久人妻无码中文字幕 | 性啪啪chinese东北女人 | 一本无码人妻在中文字幕免费 | 人人妻人人澡人人爽欧美精品 | 日本一卡2卡3卡四卡精品网站 | 国产特级毛片aaaaaa高潮流水 | 欧美国产日产一区二区 | 国产精品第一国产精品 | 中文字幕无码av激情不卡 | 乱中年女人伦av三区 | 国产色视频一区二区三区 | 日本一本二本三区免费 | 欧美老人巨大xxxx做受 | 人人澡人摸人人添 | 国产精品无码一区二区桃花视频 | 欧洲极品少妇 | 亚洲色欲色欲欲www在线 | 亚洲精品综合一区二区三区在线 | 青青草原综合久久大伊人精品 | 理论片87福利理论电影 | 无码av岛国片在线播放 | 久久精品国产一区二区三区 | 亚洲大尺度无码无码专区 | 中文字幕亚洲情99在线 | 99久久人妻精品免费二区 | 精品亚洲韩国一区二区三区 | 99久久精品午夜一区二区 | 精品亚洲成av人在线观看 | 精品久久久无码人妻字幂 | 国产成人一区二区三区在线观看 | 久久久久久a亚洲欧洲av冫 | 婷婷综合久久中文字幕蜜桃三电影 | 成人精品视频一区二区三区尤物 | 国产真人无遮挡作爱免费视频 | 国产舌乚八伦偷品w中 | 天堂无码人妻精品一区二区三区 | 亚洲色大成网站www | 欧美午夜特黄aaaaaa片 | 狂野欧美性猛xxxx乱大交 | 久久亚洲精品中文字幕无男同 | 国产亚洲精品久久久久久久 | 一区二区三区高清视频一 | 中文字幕 人妻熟女 | 国产午夜亚洲精品不卡下载 | 荫蒂被男人添的好舒服爽免费视频 | 亚洲欧美日韩国产精品一区二区 | 成人毛片一区二区 | 久久综合激激的五月天 | 国产另类ts人妖一区二区 | 丰满少妇女裸体bbw | 麻豆国产人妻欲求不满 | 国产精品二区一区二区aⅴ污介绍 | 国产精品久久久久久亚洲影视内衣 | 国产av剧情md精品麻豆 | 牲交欧美兽交欧美 | 亚洲s色大片在线观看 | 亚洲成a人片在线观看无码 | 内射欧美老妇wbb | 精品久久久无码人妻字幂 | 国产精品久久久av久久久 | 香港三级日本三级妇三级 | 精品无码av一区二区三区 | 99久久亚洲精品无码毛片 | 牲交欧美兽交欧美 | 99久久精品午夜一区二区 | 男女下面进入的视频免费午夜 | 中文字幕人成乱码熟女app | 国产莉萝无码av在线播放 | 97资源共享在线视频 | 日韩成人一区二区三区在线观看 | 成人无码视频在线观看网站 | 婷婷五月综合激情中文字幕 | 亚洲一区av无码专区在线观看 | 激情内射日本一区二区三区 | 4hu四虎永久在线观看 | 亚洲aⅴ无码成人网站国产app | 欧美日韩精品 | 亚洲精品中文字幕乱码 | 老子影院午夜精品无码 | 99国产精品白浆在线观看免费 | 高潮喷水的毛片 | 99久久久无码国产精品免费 | 精品国产一区av天美传媒 | 亚洲欧洲无卡二区视頻 | 99久久人妻精品免费一区 | 亚洲国产一区二区三区在线观看 | 日本大乳高潮视频在线观看 | 国产精品亚洲一区二区三区喷水 | 中文字幕乱妇无码av在线 | 亚洲精品国产精品乱码视色 | 一区二区传媒有限公司 | 欧美熟妇另类久久久久久多毛 | 99久久精品国产一区二区蜜芽 | 国产福利视频一区二区 | 一本久久伊人热热精品中文字幕 | 色 综合 欧美 亚洲 国产 | 久久精品国产日本波多野结衣 | 欧美精品免费观看二区 | 国产超碰人人爽人人做人人添 | 男人和女人高潮免费网站 | 国产av人人夜夜澡人人爽麻豆 | 国产精品理论片在线观看 | 欧美三级a做爰在线观看 | 97无码免费人妻超级碰碰夜夜 | 1000部夫妻午夜免费 | 玩弄人妻少妇500系列视频 | 亚洲s色大片在线观看 | 国产精品毛片一区二区 | 国产精品无码久久av | 精品成人av一区二区三区 | 国产精品久免费的黄网站 | 国产热a欧美热a在线视频 | 色情久久久av熟女人妻网站 | 国产精品高潮呻吟av久久 | 日韩在线不卡免费视频一区 | 76少妇精品导航 | 久久亚洲国产成人精品性色 | 亚洲人成网站免费播放 | 久久精品国产一区二区三区 | 天天综合网天天综合色 | 久青草影院在线观看国产 | 欧美人与物videos另类 | 动漫av网站免费观看 | av香港经典三级级 在线 | 少妇无码一区二区二三区 | 无遮无挡爽爽免费视频 | 亚洲精品成a人在线观看 | 国产亚洲欧美日韩亚洲中文色 | yw尤物av无码国产在线观看 | 5858s亚洲色大成网站www | 在线视频网站www色 | 无码一区二区三区在线 | 成在人线av无码免观看麻豆 | 亚洲人成网站在线播放942 | 亚洲中文字幕av在天堂 | 一个人免费观看的www视频 | 午夜精品久久久久久久久 | 中文字幕无码热在线视频 | 色综合视频一区二区三区 | 亚洲国产av精品一区二区蜜芽 | 熟妇人妻激情偷爽文 | 狠狠色欧美亚洲狠狠色www | 久久99精品国产麻豆蜜芽 | 狠狠亚洲超碰狼人久久 | 午夜丰满少妇性开放视频 | 国产精品久久久午夜夜伦鲁鲁 | 99久久婷婷国产综合精品青草免费 | 成人影院yy111111在线观看 | 少妇人妻偷人精品无码视频 | 亚洲欧洲日本综合aⅴ在线 | a片在线免费观看 | 99久久精品无码一区二区毛片 | 日本护士毛茸茸高潮 | 中文字幕无码人妻少妇免费 | 国产亚洲美女精品久久久2020 | 精品午夜福利在线观看 | 2019nv天堂香蕉在线观看 | 一本色道久久综合亚洲精品不卡 | 亚洲精品综合五月久久小说 | 欧美变态另类xxxx | 成人一在线视频日韩国产 | 中文字幕日韩精品一区二区三区 | 奇米影视7777久久精品 | 中国大陆精品视频xxxx | 亚洲自偷自拍另类第1页 | 亚洲一区二区三区偷拍女厕 | 少妇性俱乐部纵欲狂欢电影 | 日本护士xxxxhd少妇 | 成人三级无码视频在线观看 | 国产成人综合在线女婷五月99播放 | 免费男性肉肉影院 | 人人妻人人澡人人爽精品欧美 | 蜜桃视频韩日免费播放 | 精品无码一区二区三区爱欲 | 国产成人综合在线女婷五月99播放 | 亚洲国产精品毛片av不卡在线 | 亚洲va欧美va天堂v国产综合 | 亚洲精品一区二区三区在线 | 一本一道久久综合久久 | 男女性色大片免费网站 | 精品一区二区三区无码免费视频 | 小鲜肉自慰网站xnxx | 沈阳熟女露脸对白视频 | 国产熟女一区二区三区四区五区 | 黑人巨大精品欧美一区二区 | 久久zyz资源站无码中文动漫 | 亚洲色偷偷男人的天堂 | 亚洲国产精品久久人人爱 | 一区二区传媒有限公司 | 丝袜美腿亚洲一区二区 | 六月丁香婷婷色狠狠久久 | 精品国产精品久久一区免费式 | 动漫av一区二区在线观看 | 欧美性色19p | 欧美国产日产一区二区 | 国产成人一区二区三区在线观看 | 中国大陆精品视频xxxx | 97无码免费人妻超级碰碰夜夜 | 中文字幕av日韩精品一区二区 | 中文字幕无码免费久久9一区9 | 少妇无码一区二区二三区 | 日本一区二区三区免费播放 | 精品欧洲av无码一区二区三区 | 超碰97人人做人人爱少妇 | 国产精品内射视频免费 | 欧美兽交xxxx×视频 | 娇妻被黑人粗大高潮白浆 | 久久午夜无码鲁丝片 | 九月婷婷人人澡人人添人人爽 | 少妇邻居内射在线 | 国产特级毛片aaaaaaa高清 | 中文字幕+乱码+中文字幕一区 | 成人无码精品1区2区3区免费看 | 亚洲欧美综合区丁香五月小说 | 秋霞成人午夜鲁丝一区二区三区 | 亚洲国产av美女网站 | 亚洲一区二区三区含羞草 | 亚洲va中文字幕无码久久不卡 | 中文字幕人妻丝袜二区 | 任你躁国产自任一区二区三区 | 国产xxx69麻豆国语对白 | 日日摸夜夜摸狠狠摸婷婷 | 亚洲中文字幕无码中字 | 成人女人看片免费视频放人 | 亚洲第一网站男人都懂 | 中文字幕av伊人av无码av | 日韩精品一区二区av在线 | 爆乳一区二区三区无码 | 天堂а√在线地址中文在线 | 曰韩无码二三区中文字幕 | 亚洲成色在线综合网站 | 久久午夜夜伦鲁鲁片无码免费 | 亚洲精品中文字幕久久久久 | 日日摸日日碰夜夜爽av | 2019nv天堂香蕉在线观看 | 中文字幕无码免费久久99 | 99久久久无码国产精品免费 | 欧美野外疯狂做受xxxx高潮 | 国产精品高潮呻吟av久久 | 亚洲日韩精品欧美一区二区 | 无人区乱码一区二区三区 | 日韩精品无码免费一区二区三区 | 亚洲啪av永久无码精品放毛片 | 欧美黑人性暴力猛交喷水 | 在线视频网站www色 | 亚洲天堂2017无码中文 | 国产内射爽爽大片视频社区在线 | 少妇性l交大片欧洲热妇乱xxx | 久久精品一区二区三区四区 | 2020久久香蕉国产线看观看 | 丁香花在线影院观看在线播放 | 国产色视频一区二区三区 | 亚洲成熟女人毛毛耸耸多 | 男人的天堂2018无码 | 荫蒂被男人添的好舒服爽免费视频 | www国产亚洲精品久久久日本 | 亚洲色欲久久久综合网东京热 | 澳门永久av免费网站 | 国产一区二区三区日韩精品 | 亚洲人成影院在线观看 | 荫蒂添的好舒服视频囗交 | 无码av中文字幕免费放 | 丝袜美腿亚洲一区二区 | 人人澡人摸人人添 | 黑人巨大精品欧美一区二区 | 扒开双腿疯狂进出爽爽爽视频 | 又紧又大又爽精品一区二区 | 一本加勒比波多野结衣 | 欧美乱妇无乱码大黄a片 | 性生交大片免费看女人按摩摩 | 免费观看的无遮挡av | 中文字幕无线码免费人妻 | 在线观看免费人成视频 | 性做久久久久久久免费看 | 麻豆蜜桃av蜜臀av色欲av | 亚洲精品无码人妻无码 | 成熟妇人a片免费看网站 | 婷婷综合久久中文字幕蜜桃三电影 | 中文字幕精品av一区二区五区 | 亚洲欧洲日本综合aⅴ在线 | 欧美日韩综合一区二区三区 | 色欲人妻aaaaaaa无码 | 亚洲国产av精品一区二区蜜芽 | 荫蒂被男人添的好舒服爽免费视频 | 免费观看激色视频网站 | 亚洲欧洲日本综合aⅴ在线 | 精品国产一区二区三区四区在线看 | 亚洲中文无码av永久不收费 | 377p欧洲日本亚洲大胆 | 强开小婷嫩苞又嫩又紧视频 | 少妇厨房愉情理9仑片视频 | 亚洲欧美综合区丁香五月小说 | 鲁一鲁av2019在线 | 乱人伦人妻中文字幕无码 | 婷婷丁香六月激情综合啪 | 夜精品a片一区二区三区无码白浆 | 色婷婷av一区二区三区之红樱桃 | 小sao货水好多真紧h无码视频 | 成人无码影片精品久久久 | 青青青手机频在线观看 | 日本乱偷人妻中文字幕 | 天堂а√在线地址中文在线 | 丰满少妇熟乱xxxxx视频 | 麻豆国产人妻欲求不满谁演的 | 亚洲精品国偷拍自产在线观看蜜桃 | 久久久精品国产sm最大网站 | 动漫av一区二区在线观看 | 国产无遮挡吃胸膜奶免费看 | 国产一区二区三区日韩精品 | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 精品国产av色一区二区深夜久久 | 国产精品久免费的黄网站 | 精品成人av一区二区三区 | 伊人久久大香线蕉午夜 | 最新国产麻豆aⅴ精品无码 | 国产亚洲精品精品国产亚洲综合 | 国产在线精品一区二区三区直播 | 丰满人妻翻云覆雨呻吟视频 | 成人精品一区二区三区中文字幕 | 天天爽夜夜爽夜夜爽 | 亚洲精品国偷拍自产在线观看蜜桃 | 男人扒开女人内裤强吻桶进去 | 亚洲精品久久久久avwww潮水 | 欧美一区二区三区视频在线观看 | 玩弄人妻少妇500系列视频 | 久久久久亚洲精品中文字幕 | 无码乱肉视频免费大全合集 | 久久久国产一区二区三区 | 亚洲国产精品一区二区第一页 | 精品国产麻豆免费人成网站 | 日韩人妻少妇一区二区三区 | 未满成年国产在线观看 | 精品夜夜澡人妻无码av蜜桃 | 亚洲欧美日韩国产精品一区二区 | 十八禁视频网站在线观看 | 成人免费视频在线观看 | 天堂а√在线地址中文在线 | 性欧美videos高清精品 | 亚洲の无码国产の无码影院 | 无码中文字幕色专区 | 无码帝国www无码专区色综合 | 乱中年女人伦av三区 | 久久精品人人做人人综合试看 | √8天堂资源地址中文在线 | 欧美精品一区二区精品久久 | www国产精品内射老师 | 久久精品国产一区二区三区 | 无码人妻少妇伦在线电影 | 国产人妻人伦精品 | 成人欧美一区二区三区黑人 | 久久人人爽人人爽人人片ⅴ | 纯爱无遮挡h肉动漫在线播放 | 99久久久国产精品无码免费 | 久久久久久九九精品久 | 精品国产精品久久一区免费式 | 国产亲子乱弄免费视频 | aa片在线观看视频在线播放 | 亚洲乱码日产精品bd | 欧美黑人巨大xxxxx | 青青草原综合久久大伊人精品 | 乌克兰少妇xxxx做受 | 大地资源中文第3页 | 国产亚洲人成在线播放 | 久久亚洲日韩精品一区二区三区 | 狠狠亚洲超碰狼人久久 | 国产美女极度色诱视频www | 亚洲男人av香蕉爽爽爽爽 | 亚洲天堂2017无码中文 | 双乳奶水饱满少妇呻吟 | 无遮挡国产高潮视频免费观看 | 日本丰满熟妇videos | 亚洲欧美日韩成人高清在线一区 | 粗大的内捧猛烈进出视频 | 国产成人av免费观看 | 男女猛烈xx00免费视频试看 | 5858s亚洲色大成网站www | 精品国精品国产自在久国产87 | 国产亚洲美女精品久久久2020 | 99国产欧美久久久精品 | 亚洲成在人网站无码天堂 | 丰满人妻被黑人猛烈进入 | 久久久久亚洲精品中文字幕 | 久久视频在线观看精品 | 精品国产国产综合精品 | 日日麻批免费40分钟无码 | 色综合久久88色综合天天 | 综合激情五月综合激情五月激情1 | 久久久无码中文字幕久... | 亚洲国产高清在线观看视频 | 亚洲综合无码一区二区三区 | 荡女精品导航 | 女人被男人躁得好爽免费视频 | 国内少妇偷人精品视频 | 亚洲欧美国产精品专区久久 | 久久99久久99精品中文字幕 | 国产精品久久福利网站 | 水蜜桃亚洲一二三四在线 | 久久99精品久久久久久动态图 | 日韩精品无码一区二区中文字幕 | 国产精品丝袜黑色高跟鞋 | 亚洲一区二区三区播放 | 亚洲国产成人a精品不卡在线 | 亚洲无人区一区二区三区 | 国产亚洲人成a在线v网站 | 色老头在线一区二区三区 | 亚无码乱人伦一区二区 | 麻豆果冻传媒2021精品传媒一区下载 | 永久免费精品精品永久-夜色 | 白嫩日本少妇做爰 | 久久精品国产一区二区三区 | 中文字幕无线码 | 午夜理论片yy44880影院 | 乱人伦人妻中文字幕无码久久网 | 国产乱人偷精品人妻a片 | 日本精品高清一区二区 | 亚洲欧洲中文日韩av乱码 | 亚洲 激情 小说 另类 欧美 | 天天摸天天碰天天添 | 3d动漫精品啪啪一区二区中 | 亚洲狠狠婷婷综合久久 | 免费观看又污又黄的网站 | 久久精品国产99精品亚洲 | 亚洲精品一区二区三区婷婷月 | 无码国模国产在线观看 | 亚洲va中文字幕无码久久不卡 | 少妇激情av一区二区 | 亚洲精品一区二区三区在线观看 | 国产精品无套呻吟在线 | 少妇被粗大的猛进出69影院 | 人妻插b视频一区二区三区 | 久久精品人人做人人综合 | 日韩人妻无码一区二区三区久久99 | 亚洲国产精品久久人人爱 | 国产精品香蕉在线观看 | 精品久久综合1区2区3区激情 | 国产激情无码一区二区 | 久久亚洲a片com人成 | 国产精品无套呻吟在线 | 丰满人妻翻云覆雨呻吟视频 | 女人被男人躁得好爽免费视频 | 亚洲中文字幕久久无码 | 特黄特色大片免费播放器图片 | 十八禁视频网站在线观看 | 最新版天堂资源中文官网 | 亚洲熟妇自偷自拍另类 | 99久久人妻精品免费二区 | 国产成人综合色在线观看网站 | 亚洲狠狠色丁香婷婷综合 | 午夜理论片yy44880影院 | 最近免费中文字幕中文高清百度 | 国产人妖乱国产精品人妖 | 双乳奶水饱满少妇呻吟 | 久久久久久九九精品久 | 伊人色综合久久天天小片 | 一本一道久久综合久久 | 亚洲码国产精品高潮在线 | 精品 日韩 国产 欧美 视频 | 国产精品怡红院永久免费 | 欧美午夜特黄aaaaaa片 | 国产精品二区一区二区aⅴ污介绍 | 欧美熟妇另类久久久久久多毛 | 久久久久成人精品免费播放动漫 | 国产午夜无码视频在线观看 | 波多野结衣aⅴ在线 | 国产激情艳情在线看视频 | 成人毛片一区二区 | 国产人妻人伦精品 | 日产精品99久久久久久 | 99久久人妻精品免费一区 | 国产乱人偷精品人妻a片 | 欧美老人巨大xxxx做受 | 色五月丁香五月综合五月 | 日本精品久久久久中文字幕 | 亚洲精品一区二区三区四区五区 | 中文字幕人妻丝袜二区 | 久久综合色之久久综合 | 国产精品久久久久久久影院 | 久久久中文久久久无码 | 中文字幕人妻无码一夲道 | 男人和女人高潮免费网站 | 亚洲人成网站在线播放942 | 欧美亚洲日韩国产人成在线播放 | 国产suv精品一区二区五 | 精品久久久久久人妻无码中文字幕 | 小鲜肉自慰网站xnxx | 无码精品人妻一区二区三区av | 亚洲一区av无码专区在线观看 | 久久久久久a亚洲欧洲av冫 | 国产无套内射久久久国产 | 人妻天天爽夜夜爽一区二区 | 国产亚洲精品久久久久久 | 国产免费无码一区二区视频 | 免费无码一区二区三区蜜桃大 | 又大又硬又黄的免费视频 | 蜜桃视频韩日免费播放 | 四虎4hu永久免费 | 领导边摸边吃奶边做爽在线观看 | 55夜色66夜色国产精品视频 | 无人区乱码一区二区三区 | 成人精品天堂一区二区三区 | 性色欲网站人妻丰满中文久久不卡 | av无码不卡在线观看免费 | 18无码粉嫩小泬无套在线观看 | 欧美成人午夜精品久久久 | 亚洲精品国产品国语在线观看 | 狂野欧美性猛xxxx乱大交 | 国产亚洲欧美在线专区 | 国产精品无码久久av | 大屁股大乳丰满人妻 | 国产精品久久久久无码av色戒 | 男女性色大片免费网站 | 未满小14洗澡无码视频网站 | 中文久久乱码一区二区 | 久久久久国色av免费观看性色 | 国产色在线 | 国产 | 欧美日韩综合一区二区三区 | 国产精品美女久久久网av | 国产一区二区三区日韩精品 | 国产精品a成v人在线播放 | 中文字幕日韩精品一区二区三区 | 国产莉萝无码av在线播放 | 亚洲日韩中文字幕在线播放 | 少妇被粗大的猛进出69影院 | 久久aⅴ免费观看 | 国产成人综合在线女婷五月99播放 | 欧洲熟妇色 欧美 | 国产成人av免费观看 | 麻豆精品国产精华精华液好用吗 | 日本护士毛茸茸高潮 | 天天做天天爱天天爽综合网 | 天海翼激烈高潮到腰振不止 | 亚洲a无码综合a国产av中文 | 久久精品成人欧美大片 | 亚洲日韩av片在线观看 | 强开小婷嫩苞又嫩又紧视频 | 亚洲欧洲无卡二区视頻 | 人人妻人人澡人人爽欧美一区九九 | 男女性色大片免费网站 | 最近的中文字幕在线看视频 | 对白脏话肉麻粗话av | 波多野结衣高清一区二区三区 | 国产香蕉97碰碰久久人人 | 午夜福利试看120秒体验区 | 亚洲无人区午夜福利码高清完整版 | 伦伦影院午夜理论片 | 97资源共享在线视频 | 天天拍夜夜添久久精品大 | 国产免费无码一区二区视频 | 大地资源中文第3页 | 少妇一晚三次一区二区三区 | 色狠狠av一区二区三区 | 亚洲精品国偷拍自产在线观看蜜桃 | 麻豆av传媒蜜桃天美传媒 | 天天综合网天天综合色 | 特级做a爰片毛片免费69 | 国产成人一区二区三区在线观看 | 亚洲码国产精品高潮在线 | 亚洲一区av无码专区在线观看 | 性做久久久久久久免费看 | 亚洲精品午夜国产va久久成人 | 性欧美大战久久久久久久 | 一本久久a久久精品亚洲 | 免费播放一区二区三区 | 在线成人www免费观看视频 | 亲嘴扒胸摸屁股激烈网站 | 日韩无码专区 | 亲嘴扒胸摸屁股激烈网站 | 999久久久国产精品消防器材 | 日本丰满护士爆乳xxxx | a在线观看免费网站大全 | 久久精品人妻少妇一区二区三区 | 国产亚洲视频中文字幕97精品 | 日日天日日夜日日摸 | 国产成人精品无码播放 | 国产一区二区三区日韩精品 | 少妇激情av一区二区 | 夜夜影院未满十八勿进 |