转:用 Kerberos 为 J2ME 应用程序上锁,第 2 部分: 生成一个 Kerberos 票据请求

原创 2005年04月23日 10:00:00
用 Kerberos 为 J2ME 应用程序上锁,第 2 部分: 生成一个 Kerberos 票据请求
内容:
基于 J2ME 的 Kerveros 客户机中的类
生成基本 ASN.1 数据类型
利用用户密码生成密钥
生成 TGT 请求
结束语
参考资料
关于作者
对本文的评价
相关内容:
Simplify enterprise Java authentication with single sign-on
订阅:
developerWorks 时事通讯
J2ME 有足够的能力进行复杂的加密

Faheem Khan
自由顾问
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 功能,如发布像 INTEGERSTRING 这样的通用数据类型。 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
        }//else
        
        return lengthBytes;

    }//getLengthBytes

回想一下在本系列的 第一篇文章 中对表 2 的讨论,有两种表示字节长度的方法:单字节表示法和多字节表示法。单字节长度表示法用于表示小于或者等于 127 的长度值,而当长度值大于 127 时使用多字节长度表示法。

getLengthBytes() 方法首先检查长度值是否为负。如果为负,则只是返回 null,因为我不能处理负值。

然后这个方法检查长度值是否小于或者等于 127。如果是,就需要使用单字节长度表示法。

注意在 J2ME 中一个整数是 4 字节数据,而单字节长度表示法只需要 1 个字节。如果长度参数是 0 到 127 之间(包括这个两数)的一个值,那么其字节表达就在 0x000000000x0000007f 之间(意味着只有最低有效位字节包含有用的数据)。将这个整数造型为一个单字节时,只有最低有效位字节( 0x000x7f )会作为十六进制值拷贝到单字节数组。因此,如果长度值在 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 这一行代码。这一行代码对 firstLengthByte0x801000 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() 方法的讨论中见过的同样实现逻辑:

  1. 声明一个名为 finalBytes 的字节数组,它将包含 ASN.1 字节数组表达的所有字节。
  2. 计算容纳 ASN.1 字节数组表达的内容所需要的字节数。
  3. 用清单 3 中的 getLengthBytes() 方法生成长度字节。将长度字节储存到一个名为 lengthBytes 的数组中。
  4. 得到 lengthBytes 数组中的字节数。
  5. 计算容纳完整的 ASN.1 字节数组表达所需要的字节数(标签字节、长度字节和内容字节的总和)。将这个字节数储存到一个名为 totalBytesCount 的变量中。
  6. 实例化一个具有 totalBytesCount 的值大小的 finalBytes 数组。
  7. 将标签字节拷贝到 finalBytes 数组的开始处。
  8. lengthBytes 数组中的长度字节拷贝到 finalBytes 数组中紧随标签字节的位置。
  9. 将内容字节拷贝到 finalBytes 数组中紧随长度字节的位置。
  10. 返回 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 所使用的密钥的生成过程,如下所示:

  1. 将用户密码、KDC 域名和用户的用户名串接到一起以构成一个字符串。Kerberos 利用这个串接的字符串而不仅仅是密码生成密钥。为什么要在密钥生成中加入域名和用户名呢?许多用户会在不同的服务器上使用同样的密码。如果我只使用密码生成密钥,那么一个给定的密码在所有 Kerberos 服务器上总是会生成同样的密钥。因而,如果一个黑客可以取得用户在一台 Kerberos 服务器上的密钥,那么,他就可以在所有 Kerberos 服务器上使用同一个密钥。另一方面,如果我加入了域名和用户名,那么一个受到这种攻击的密钥将只会侵害特定的域。
  2. 得到第 1 步中串接的字符串的字节数组表达。
  3. 统计第 2 步中字节数组中的字节数。在这个字节串的后面附加适当数量的零字节以使它成为 8 的整数倍。例如,如果这个字节数组包含 53 个字节,那么就在这个字节数组的最后附加三个字节使它具有 56 个字节。
  4. 将第 3 步中附加了字节后的字节数组分为大小相同的块,每一块有 8 个字节。
  5. 每隔一个块倒转块的位顺序。换句话说,第一块保持不变,第二块的位顺序应该倒转,第三块应保持不变,第中块的位顺序应倒转,以此类推。
  6. 取第一个(未改变的)块并与第二个(倒转的)块进行每一位的 exclusive OR 。然后将第一次 exclusive OR 操作得到的结果与第三个(未改变的)块进行另一次 exclusive OR 操作。继续 exclusive OR 操作直到完成了所有块。所有 exclusive OR 操作的最后结果是一个 8 字节长的块。
  7. 修正在第 6 步中得到的 8 字节块的奇偶性。每一块的最低有效位保留为奇偶位。统计 8 字节块中每字节中的 1 的个数,如果 1 的个数为偶数,那么就设置最低位为 1 使它成为奇数。例如,如果一个字节的值为 00000000 ,那么就要将它改为 00000001 。如果一个字节中 1 的个数已经为奇数,那么就将它的最低位设置为零。例如,如果一个字节为 00000010 ,那么就不需要为修正其奇偶性做任何改变。
  8. DES 定义了一些弱的、因而不适合用于加密的密钥。我们的密钥生成过程的第八步是要检查奇偶修正后的字节数组是否是一个弱的密钥。如果是的话,就要用 0xf011110000 )与奇偶修正过的 8 字节块进行 exclusive OR 。如果奇偶修正得到的不是弱密钥,那么就不需要进行这种 exclusive OR 操作。经过这种弱密钥处理的字节数组是一个临时密钥。
  9. 现在我要使用这个临时密钥以 DES-CBC 算法加密第 3 步中得到的附加后的字节数组。这个临时密钥同时作为密钥的值和 DES-CBC 加密的初始矢量的值。回想在前面的讨论中说过,CBC 要求密文块链接。第 9 步的结果是最后 8 字节块的加密结果(放弃所以以前的密文块)。因此,这一步的结果是另一个 8 字节块。
  10. 现在我修正第 9 步产生的 8 字节块中的每一个字节的奇偶性。在上面第 7 步中我解释了奇偶性修正。
  11. 现在再次检查第 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);
    }//KerberosKey

    public 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 for

                for (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;

    }//generateKey


    public 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;
    }//getFinalKey


    public byte[] setParity (byte byteValue[])
    {
        for(int x=0; x<8; x++)
            byteValue[x] = parityValues[byteValue[x] & 0xff];

        return byteValue;
    }


    // Checks weak key
    public 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 encodings
    public 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;
         else
             x = 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 entries
    private 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 步的结果得到的临时密钥。

DESEngineCBCBlockCipher 类进行实际的加密操作。这些类是 Bouncy Castle 组的 J2ME 平台开放源代码加密实现的一部分。Bouncy Castle 的实现可以免费得到,并可用于任何目的,只要您在发布时加入许可证信息。您将需要下载 Bouncy Castle 类(链接请参阅 参考资料)并遵照它所附带的设置指示才能使用本文的示例代码。清单 11 中的 KerberosKey 类包含在 Kerberos 类中使用 Bouncy Castle 类时需要的所有 import 语句。

现在看一下在清单 11 中的 getFinalKey() 方法中发生了什么事情。我首先实例化了 DESEngine 类,这个类实现了 DES 加密算法。然后,我将这个 DESEngine 对象传递给构造函数 CBCBlockCipher 以创建一个名为 cipherCBCBlockCipher 对象。这个 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;
    }//KerberosClient


    public 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 请求包含四个数据字段: pvnomsg-typepadatareq-body 。生成 pvnomsg-type 字段非常简单,因为这两个字段分别只包含一个整数(如在 第一篇文章 中“请求 TGT”一节中提到的, pvno 为 5, msg-type 为 10)。

您只需要调用 getIntegerBytes() 方法,向这个方法传递这个整数值。 getIntegerBytes() 方法返回以 ASN.1 字节数组表达的 INTEGER 结构,您将它传递给 getTagAndLengthBytes() 方法。这个方法将返回 pvno 或者 msg-type 字段的完整 ASN.1 表达。这就是我在清单 12 中的 getTicketResponse() 方法的开始时生成 pvnomsg-type 字段的方法。

在生成 pvnomsg-type 字段后,下一步就是生成 padata 字段。这个字段是可选的。大多数 KDC 服务器有一个设置选项,可以对单独的客户机进行配置。系统管理员可以将 Kerberos 服务器设置为特定客户可以发送不包括 padata 字段的 TGT 请求。

为了减轻在资源有限的 J2ME 设备上的处理负担,我假定电子银行有一个允许无线移动用户发送不带 padata 字段的 TGT 请求的 Kerberos 服务器(并且我将在本系列的下一篇文章中展示如何设置 Keberos 服务器使它具有这种行为)。因此我将在要生成的 TGT 请求中略去 padata 字段。所以,在生成 pvnomsg-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)) )执行两项任务:

  1. 它首先向 getBitStringBytes() 方法传递 noOptions 字节数组,它返回用 ASN.1 的位字符串表达的 5 个零。
  2. 然后它将位字符串传递给 getTagAndLengthBytes() 方法,这个方法返回 kdc-options 字段的完整 ASN.1 字节数组表达。

下一步是生成 cname 结构。在 第一篇文章 清单 1 的讨论中说过, cname 字段的类型为 type cname 。这种数据类型是两个字段 - 即 name-typename-string ―― 的 SEQUENCEname-type 字段是用一个 INTEGER 构造的。 name-string 字段是 GeneralString s 的一个 SEQUENCE

因此,为了生成 cname 结构,我必须遵循清单 12 的 getTicketResponse() 方法中的几个步骤:

  1. 调用 getGeneralStringBytes() 方法,同时传递客户的用户名。 getGeneralStringBytes() 方法将返回客户的用户名的 GeneralString 表达。

  2. getSequenceBytes() 方法传递 GeneralString ,这个方法会在 GeneralString 前面附加 SEQUENCE 字节并返回包含客户的用户名字符串的 SEQUENCE 的 ASN.1 表达。

    byte generalStringSequence[] = getSequenceBytes (getGeneralStringBytes (userName)); 这一行执行这前两步。

  3. 调用 getTagAndLengthBytes() 方法,传递 SEQUENCE 字节作为其内容。 getTagAndLengthBytes() 方法会在 SEQUENCE 前面附加 name-string 标签字节(上下文特定的标签数字 0)以及长度字节,并返回完整的 name-string 结构。

    byte name_string[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 1, generalStringSequence); 这一行执行这一步。

  4. 生成 PrincipalNamename-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)); 这一行执行这项任务。

  5. PrincipalNamename-typename-string 部分串接到一起,然后在串接字节数组前面附加 SEQUENCE 字节。 byte principalNameSequence [] = getSequenceBytes (concatenateBytes (name_type, name_string)); 一行执行这项任务。

  6. 在上面第 5 步的 SEQUENCE 前面附加 cname 标签字节(上下文特定的标签数 1)和长度字节。这样就得到了完整的 cname 结构。 byte cname[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 1, principalNameSequence); 一行执行这项任务。

上述 6 步策略就可以生成完整的 cname 结构。

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

  1. getGeneralStringBytes() 方法调用生成 GeneralString
  2. 连同 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 请求的最后一步是将在本节前面生成的 pvnomsg-fieldsreq-body 字节串接在一起。然后将这些字段放入一个 SEQUENCE ,最后调用 getTagAndLengthBytes() 方法,得到一个完整的、可以发送给 Kerberos 服务器的票据请求。

结束语
在本文中我讨论了几个基本概念。我开发了一个 J2ME 类,它包含几个用于生成 ASN.1 数据结构的方法,我还展示了如何利用用户的密码生成一个 Kerberos 密钥。最后,我演示了 Kerberos 客户机如何生成 TGT 请求。

下一次,我将搭建一个 KDC 服务器、从该服务器中获取 Kerberos 票据、并用这些票据与电子银行的业务逻辑服务器交换密钥。

参考资料

关于作者
Faheem Khan 是一个独立软件顾问,专长是企业应用集成 (EAI) 和 B2B 解决方案。读者可以通过 fkhan872@yahoo.com与 Faheem 联系。

使用NetBeans开发J2ME应用程序

  • 2007年10月09日 08:36
  • 3MB
  • 下载

使用eclipse开发J2ME应用程序(上)

  • 2007年10月05日 01:44
  • 4.98MB
  • 下载

J2ME 101,第 3 部分: 深入记录管理系统

J2ME 101,第 3 部分: 深入记录管理系统 发布者:IBM   日期:2004-01-22 00:00:00 浏览次数:0 (共有_条评论...

J2ME图片半透明处理(使用图片像素)(仅部分手机支持)

其实就是把一张图的argb值都取出来存在一个整型数组里(用Image对象的getRGB方法),然后用循环逐一修改argb值得最高位(最高位 的8个2进制值代表了阿尔法通道也不什么的,反正就是透明度),...

使用Eclipse调试J2ME应用程序

  • 2006年06月06日 11:53
  • 700KB
  • 下载

在 Android 上使用 XML 和 JSON,第 1 部分: 在 Android 应用程序上研究 JSON 和 XML 益处

(源自:http://www.ibm.com/developerworks/cn/xml/x-andbene1/) 考虑 Android 平台上使用的 XML 和 JSON 数据交换格式 ...
  • njcit
  • njcit
  • 2011年08月22日 13:10
  • 631

一个小的插件 应用J2ME的

  • 2011年10月07日 17:15
  • 331KB
  • 下载
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:转:用 Kerberos 为 J2ME 应用程序上锁,第 2 部分: 生成一个 Kerberos 票据请求
举报原因:
原因补充:

(最多只允许输入30个字)