利用用户密码生成密钥 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);
}//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, secretKe
查看本文来源
在本文中,我将开始实现生成并处理这些消息的 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
}//else
return 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 ho
查看本文来源