Skip to main content

Lock down J2ME applications with Kerberos, Part 2: Authoring a request for a Kerberos ticket

J2ME is powerful enough to perform advanced encryption

Faheem Khan (fkhan872@yahoo.com), Freelance Consultant
Faheem Khan is an independent software consultant specializing in enterprise application integration (EAI) and B2B solutions. Readers can reach Faheem at fkhan872@yahoo.com.

Summary:  In the last article in this series, you saw the outlines of a J2ME application that can connect securely to a Kerberos-enabled server; you also learned the details of how Kerberos encryption works at the byte level. This article gets into the meat of the application itself. You'll see how to use the J2ME's facilities and some open source libraries to perform surprisingly powerful encryption routines.

Date:  25 Nov 2003
Level:  Intermediate
Activity:  885 views

In the previous article of this series, I introduced a mobile banking MIDlet application that uses Kerberos for secure communication with an e-bank server. I also explained the data formats and sequence of messages that a J2ME-based Kerberos client application exchanges with remote servers to exchange Kerberos tickets and cryptographic keys.

In this article, I'll start implementing the J2ME classes that author and process those messages. I'll first briefly describe the purpose of the major classes that form our J2ME-based Kerberos client; then, I'll explain and demonstrate how the classes author the basic ASN.1 data types discussed in the first article. In the third section, I'll show how you can generate a cryptographic key used for encryption and decryption in Kerberos messaging. The last section will demonstrate how the J2ME client will author the request for a Kerberos ticket.

The classes in the J2ME-based Kerberos client

In this article, I'm going to discuss the operation of three J2ME classes:

  • ASN1DataTypes
  • KerberosClient
  • KerberosKey

The ASN1DataTypes class will wrap all generic ASN.1 functionality, like the authoring of such universal data types as INTEGER and STRING. The KerberosClient class extends the ASN1DataTypes class, uses its low-level functionality, and provides all Kerberos-specific features. Therefore, you can say that I have simply divided the required functionality into two groups: all the generic ASN.1 features go into the ASN1DataTypes class, and all Kerberos-specific features reside in the KerberosClient class. This increases the reusability of the code. If you want to build your own non-Kerberos application that uses ASN.1 features, you can use the ASN1DataTypes class.

Kerberos defines an algorithm to generate a secret key from the user's password. The KerberosKey class implements this algorithm. You will need this key during Kerberos messaging.

I will present the individual methods of these classes as separate listings in this article. I have also included the classes in a separate source code download. This package puts together everything in a set of classes that you can compile as a J2ME project. The download contains the following files:

  • ReadMe.txt, which contains instructions outlining how you can try out the code for this article
  • ASN1DataTypes.java, which implements the ASN1DataTypes class
  • KerberosClient.java, which implements the KerberosClient class
  • KerberosKey.java, which implements the KerberosKey class
  • J2MEClientMIDlet.java, which provides a very simple MIDlet wrapper that you can use to test the code

Now, I'll delve into the details of these classes.


Authoring the basic ASN.1 data types

The ASN1DataTypes class shown in Listing 1 handles all the low-level functionality required to author and process ASN.1 data structures. This class contains two types of methods: authoring methods are responsible for the authoring of ASN.1 data structures, while processing methods are responsible for the processing of messages that have already been authored or received from a remote application. I'll explain and implement the authoring methods in this article and cover the processing methods in the next article of this series.

Listing 1 only contains the declarations of the different methods of the ASN.1 class. I will illustrate the implementation of each of these methods in separate listings in the ensuing sections.


Listing 1. The ASN1DataTypes class
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()

This method (shown in Listing 2) takes an integer value (length) as a parameter. It authors the ASN.1 representation of the length and returns a byte array that conforms to ASN.1's length format.


Listing 2. The getLengthBytes() method
    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

Recall from the discussion of Table 2 in the first article of this series that there are two ways of representing length bytes: single-byte length notation and multi-byte length notation. Single-byte length notation is used to represent a length value less than or equal to 127, while multi-byte length notation is used when the length value is greater than 127.

The getLengthBytes() method first checks to see if the length value is negative. If it is, I simply return null because I cannot handle negative values.

The method then checks to see if the length value is less than or equal to 127. If it is, I need to use single-byte length notation.

Note that an integer in J2ME is 4 bytes of data, while single-byte length notation needs only 1 byte. If the length parameter has a value between 0 and 127 (both inclusive), its byte representation will be between 0x00000000 and 0x0000007f (meaning that only the least significant byte contains useful data). When you cast the integer into a single byte, only the least significant byte (0x00 to 0x7f) will be copied as a hex value to the single-byte array. Therefore, if the length value is between 0 and 127, I can simply perform a bitwise AND operation between the length and 0xff. This operation will result in an integer whose most significant 3 bytes will be zeroed out. Thus, I can cast the result of the bitwise operation as a byte, put the byte in a single-byte array, and return the array to the calling application.

If the length value is greater than 127, I have to use multi-byte length notation, which uses at least 2 bytes of data. The first byte will indicate the number of length bytes, and the actual length bytes will follow (refer to the first article for a more detailed explanation of this format).

If the length value is less than 256, I will need a total of 2 length bytes -- 1 byte to indicate that there is 1 more length byte and 1 byte to hold the actual length value. If the length value is at least 256 and less than 65,536 (256 times 256), I will need a total of 3 length bytes -- 1 byte to indicate that there are 2 more length bytes and 2 bytes to hold the actual length value.

Therefore, the number of bytes required in multi-byte format depends upon the length value. That's why the do-while loop in getLengthBytes()'s else block calculates the number of bytes required in the length bytes.

The method for determining the required number of bytes is simple. I declare a byte counter named bytesRequired and start counting from 2 (the minimum number of required bytes), divide the length value by 256, and check to see if the quotient is larger than or equal to 1. If it is, that means that the original length value was larger than 256 and therefore requires at least 3 bytes, so I increment the counter (bytesRequired).

I continue dividing the length value by 256 and incrementing the byte counter until the result of division yields a value less than 1. When this happens, I know that I have found the required number of bytes in the multi-byte integer format.

When I know the required number of bytes, I instantiate an array of bytes of an appropriate size. Naturally, the first among the length bytes will indicate how many more length bytes are coming. Thus, I have simply copied 1 less than the number of required bytes (bytesRequired-1) into a byte called firstLengthByte.

Look at the firstLengthByte |= 0x80 line of code in the getLengthBytes() method in Listing 2. This line of code executes a bitwise OR operation between firstLengthByte and 0x80 (1000 0000) and stores the result in firstLengthByte. This logical OR operation will set the leftmost (most significant) bit of firstLengthByte to 1. Recall from the discussion in the first article of this series that whenever I want to use multi-byte integer format, I must set the leftmost bit of the first length byte to 1.

The next line (lengthBytes[0]=firstLengthByte) simply copies the firstLengthByte at the starting position of the array that contains the length bytes. Next, I have a for loop, which copies the length bytes from the length parameter into their correct positions in the lengthBytes array. When the for loop exits, I have the lengthBytes array, which conforms to the ASN.1 format. The last line in the getLengthBytes() method in Listing 2 returns this array.

getIntegerBytes()

This method takes an integer (value) as a parameter and returns the ASN.1 INTEGER representation of the integer value. Recall from Table 1 of the first article in this series that INTEGER is a universal data type in ASN.1.

The getIntegerBytes() method implementation is shown in Listing 3.


Listing 3. The getIntegerBytes() method
    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

The method first declares a byte array named finalBytes. This byte array will hold all the bytes of the INTEGER data type structure. However, I don't yet know the size of the finalBytes array. I first need to calculate the number of bytes in the INTEGER structure, and this calculation consists of several steps.

The first step is to calculate the number of bytes required to hold the integer value (the content part of the INTEGER structure). For this purpose, I have used a do-while loop that continues dividing the value integer by 256 until the value drops below 1. When the loop exits, the number of bytes required to hold the content part is stored in a variable named contentBytesCount.

The method then passes the required length as an integer to the getLengthBytes() method, which returns the ASN.1 representation of the length bytes. I have stored the number of length bytes in a variable named lengthBytesCount.

Recall from the discussion in the first article in this series that the byte array representations of all ASN.1 data types consist of three sections: the tag byte, the length bytes, and the content bytes. Therefore, the ASN.1 byte array representations need enough bytes to hold all three portions.

The next step is to calculate the size of the array that will hold all the bytes of the INTEGER structure. I do this calculation by summing the tag byte length (which is 1 for INTEGER and all other tags used in Kerberos), the number of length bytes, and the number of content bytes. The int totalBytesCount = 1 + lengthBytesCount + contentBytesCount; line performs this calculation and stores the number of bytes required in a variable named totalBytesCount.

Next, I instantiate the finalBytes byte array of size totalBytesCount. The rest of the procedure is simple. I store the tag byte (0x02 for an INTEGER) at the start of the finalBytes array. Then, I copy the length bytes in the finalBytes array after the tag byte. Finally, I copy the content bytes after the length bytes and return the finalBytes array.

getGeneralStringBytes(), getOctetStringBytes(), getBitStringBytes(), and getGeneralizedTimeBytes()

Like getIntegerBytes(), each of these methods returns one of the ASN.1 universal data type structures.

The getGeneralStringBytes() method in Listing 4 authors the byte array representation of an ASN.1 GeneralString. Similarly, the getOctetStringBytes() method in Listing 5 returns the byte array representation of an ASN.1 OctetString. The getBitStringBytes() method in Listing 6 returns the ASN.1 representation of a BitString. Finally, the getGeneralizedTimeBytes() method in Listing 7 returns the byte array representation of an ASN.1 GeneralizedTime value.

All these methods follow the same implementation logic that you saw earlier while discussing the getIntegerBytes() method:

  1. Declare a byte array named finalBytes, which will hold all the bytes of the ASN.1 byte array representation.
  2. Calculate the number of bytes required to hold the contents of the ASN.1 byte array representation.
  3. Use the getLengthBytes() method in Listing 3 to author the length bytes. Store the length bytes in an array named lengthBytes.
  4. Get the number of bytes in the lengthBytes array.
  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.
  6. Instantiate the finalBytes array to a size equal to the value of totalBytesCount.
  7. Copy the tag byte to the start of the finalBytes array.
  8. Copy the length bytes from the lengthBytes array to the finalBytes array just after the tag byte.
  9. Copy the content bytes to the finalBytes array just after the length bytes.
  10. Return the finalBytes array.

Listing 4, Listing 5, Listing 6, and Listing 7 contain comments that will help you trace and map each of the ten steps described above with corresponding lines of J2ME code.


Listing 4. The getGeneralStringBytes() method
    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


Listing 5. The getOctetStringBytes() method
    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



Listing 6. The getBitStringBytes() method
    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


Listing 7. The getGeneralizedTimeBytes() method
    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()

This method (shown in Listing 8) takes two byte arrays, concatenates the second array after the first, and returns the concatenated array.

As this method takes two byte arrays and returns another byte array, it can be used in series with itself any number of times to concatenate any number of byte arrays. For example, concatenateBytes(byteArray1, concatenateBytes(byteArray2, byteArray3)) will append byteArray3 after byteArray2 and then append the result after byteArray1.


Listing 8. The concatenateBytes() method
    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()

This method (shown in Listing 9) authors the byte array representation of an ASN.1 SEQUENCE. It takes a byte array as an input parameter, treats the byte array as the contents of a SEQUENCE, prepends the SEQUENCE tag byte (0x30) and length bytes before the contents, and returns the complete SEQUENCE structure.

Normally, the getSequenceBytes() method will be used in conjunction with concatenateBytes(). An application will author the individual structures in a SEQUENCE, concatenate the byte array representation of individual structures together to form one array, and pass on the concatenated array to the getSequenceBytes() method, which will return the complete byte array representation of the SEQUENCE.


Listing 9. The getSequenceBytes() method
    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()

This method works very much like the various getXXXBytes() methods discussed so far. However, whereas each of those methods authors one particular ASN.1 universal data type, the getTagAndLengthBytes() method (shown in Listing 10) authors application-level and context-specific data types.

This method takes three parameters. The first parameter (tagType) specifies the type of tag. If its value is equal to the static integer ASN1DataTypes.Context_Specific, it specifies a context-specific tag; if its value is equal to ASN1DataTypes.Application_Type, it specifies an application-level tag.

The second parameter (tagNumber) specifies the tag number, while the third (tagContents) carries the contents byte array.

getTagAndLengthBytes() calculates the values of tag and length bytes from the input parameters, prepends the tag and length bytes before the content bytes, and returns the complete byte array representation of the application-level or context-specific ASN.1 structure.


Listing 10. The getTagAndLengthBytes() method
    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


This completes the discussion of the authoring methods of the ASN1DataTypes class. However, before I can start discussing how the KerberosClient uses the ASN1DataTypes methods to author a TGT request, I need to discuss the generation of a secret key from the user's password. I will need the secret key at several points while communicating with the Kerberos server.


Generating a secret key from the user's password

Kerberos defines an algorithm for processing a user's password to produce a secret key. The Kerberos client will use this key for decryption during the process of getting the TGT.

For the J2ME-based Kerberos client, I'll only support one encryption algorithm, which is DES (data encryption standard) in CBC (cipher block chaining) mode. DES is a FIPS (Federal Information Processing Standards) publication that describes an encryption algorithm in which the data to be encrypted (plain text) and the secret key are passed as inputs to the encryption process. The key and the plain text are processed together according to the DES algorithm to produce the encrypted (cipher text) form of the plain text data. (See Resources for more information on DES.)

CBC is a mode of cryptographic operation in which the plain text data is divided into chunks of equally sized data blocks. For instance, in 64-bit DES-CBC encryption, the data would be divided into 8-byte blocks. If the number of bytes in the plain text data is not an integral multiple of the number of bytes you want every block to have, you pad the last block with a suitable number of bytes to make its size equal to the rest of the blocks.

You then create a byte array that is the same size as one of your blocks. This byte array is called an initial vector (IV). The Kerberos specification defines the value of the initial vector for any Kerberos-based application. (Similarly, other specifications that use DES-CBC will define the IV value that they use.) Next, you take the IV, the first block of plain text data, and the key and process them together according to the DES algorithm to form cipher data corresponding to the first block of plain text data. You then take the cipher text form of the first data block as the initial vector for the second block and perform the same DES encryption process to produce the cipher text form of the second plain text data block. You continue chaining blocks in this manner, one after the other, to produce a cipher text form of each of the blocks. At the end, you concatenate all of the cipher text blocks to arrive at the cipher text form of the complete plain text data.

Because I'm only going to support DES-CBC in the Kerberos client, I will only discuss the secret key generation process used by DES-CBC, which is as follows:

  1. Concatenate the user's password, the KDC realm name, and the user's username together to form a string. Kerberos uses this concatenated string to generate the secret key instead of using the password alone. Why include the realm name and username for key generation? Many users tend to use the same password on different servers. If I were using only the password for key generation, a given password would always generate the same key on all Kerberos servers. Therefore, if a hacker were able to fetch the secret key for a user on one Kerberos server, he could use the same key on all Kerberos servers. On the other hand, if I include the realm name and username, a key that falls victim to such an attack will only be compromised in one particular realm.

  2. Get the byte array representation of the concatenated string from Step 1.

  3. Count the number of bytes in the byte array of Step 2. Pad an appropriate number of zero bytes at the end of the string to make it an exact multiple of 8. For example, if the byte array consists of 53 bytes, pad 3 more bytes at the end of the byte array to make it a total of 56 bytes.

  4. Divide the padded byte array from Step 3 into equal blocks of 8 bytes each.

  5. Reverse the bit order of alternate blocks. In other words, the first block should remain unchanged, the bit order of the second block should be reversed, the third block should remain unchanged, the bit order of the fourth block should be reversed, and so on.

  6. Take the first (unchanged) block and exclusive OR each of its bits with the corresponding bits of the second (reversed) block. Then perform another exclusive OR operation between the result of the first exclusive OR operation and the third (unchanged) block. Continue exclusive ORing until you have consumed all the blocks. You will end up with a single 8-byte block as the final end result of all exclusive OR operations.

  7. Fix the parity of the 8-byte block that you got in Step 6. The least significant bit of each block is reserved as the parity bit. Count the number of ones in each byte of the 8-byte block; if the number of ones is even, make it odd by setting the least significant bit to one. For example, if a byte has a value of 00000000, you will change it to 00000001. If the number of ones in a byte is already odd, you will set its least significant bit to a zero. For example, if a byte is 00000010, you will not change any of its bits while fixing its parity.

  8. DES defines some keys to be weak and therefore not suitable for encryption. The eighth step during our secret key generation process is to check whether the byte array after parity fixing turns out to be a weak key. If it is, you will exclusive OR the last byte of the 8-byte parity-fixed block with 0xf0 (11110000). If the parity fixing does not result in a weak key, you don't need this exclusive OR operation. The byte array after this weak key processing is a temporary secret key.

  9. I will now use the temporary secret key to encrypt the original padded byte array from Step 3 using the DES-CBC algorithm. The temporary secret key serves both as the value of the key and as the value of the initial vector for DES-CBC encryption. Recall from my earlier discussion that CBC asks for cipher-block chaining. The result of the encryption of the last 8-byte block (discarding all previous cipher blocks) is the outcome of Step 9. Therefore, the result of this step is another 8-byte block.

  10. Now I fix the parity of each of the bytes in the 8-byte block produced in Step 9. I explained parity fixing in Step 7 above.

  11. Now I again check to see if the 8-byte parity-fixed block of Step 10 is a weak key (just like I did in Step 8).

The result of Step 11 is a secret key that the Kerberos client can use to communicate with the Kerberos server.

Now have a look at the KerberosKey class in Listing 11. This class's generateKey() method implements the 11-step secret key generation algorithm described above.


Listing 11. The KerberosKey class
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


I have marked the lines of code in the generateKey() method of Listing 11 with comments to help you map the different steps of the algorithm with the corresponding lines of J2ME code. The only point where the coding details really require explanation is Step 9, where I actually performed the DES-CBC encryption.

Look at the line of code in the generateKey() method in Listing 11, which is marked in the comments as Step 9. It is a call to a method named getFinalKey(), which implements the ninth step and takes two parameters. The first parameter (data) is the byte array from the padding operation of Step 3, while the second parameter (key) is the temporary secret key, which I got as the result of Step 8.

The DESEngine and CBCBlockCipher classes perform the actual cryptographic operations. These classes are part of the Bouncy Castle group's open source cryptographic implementation for the J2ME platform. Bouncy Castle's implementation is available free of charge and you can use it for any purpose provided you include the license information in your distributions. You will need to download the Bouncy Castle classes (see Resources for a link) in order to use this article's sample code in accordance with its accompanying setup instructions. The KerberosKey class in Listing 11 contains all the necessary import statements that you need to use Bouncy Castle classes in the Kerberos client.

Now look at what's happening inside the getFinalKey() method in Listing 11. I first instantiate the DESEngine class, which implements the DES cryptographic algorithm. Next, I pass the DESEngine object to the CBCBlockCipher constructor to create a CBCBlockCipher object named cipher. The cipher object will perform the actual DES-CBC operations.

I then create an object named kp by passing the key parameter to the constructor of a class named KeyParameter. The KeyParameter class is also part of Bouncy Castle's cryptographic library. The kp object now wraps the key, and therefore I will pass this object whenever I need to specify the key.

The next step is to create another object, named iv. This object is an instance of another Bouncy Castle class, named ParameterWithIV. The ParameterWithIV constructor takes two parameters. The first is the kp object that wraps the key. The second is the initial vector byte array. Because I have to use the key as the initial vector, I have passed the key as the initial vector byte array.

The iv object now wraps the key as well as the initial vector, so I will pass this object whenever I need to specify the key and the initial vector.

The next step is to initiate the cipher object by calling its init() method. This method takes two parameters. The first is of Boolean type, for which you pass true when you need to initialize a cipher for encryption and false when you want to do decryption. The second is the iv object that wraps both the key and the initial vector.

I am now ready for cipher block chaining. I have declared a byte array named ivBytes, which will hold the initial vector bytes for each step of the cipher block chaining. A for loop will continue calling the processBlock() method of the cipher object. The processBlock() method processes one block of data at a time.

The processBlock() method takes four parameters. The first is the input array (data), and the second is an offset into the byte array. The processBlock() method starts processing the block input from this offset value. The third parameter is the name of the output array, and the fourth is an offset into the output array.

The for loop processes one block at a time by calling the processBlock() method. This method processes one block at a time and stores the output (the encrypted result) in the ivBytes array. After that, I create a new iv object (an instance of the ParametersWithIV class) by passing the ivBytes array to the ParametersWithIV constructor. I then re-initialize the cipher with the new iv object. This prepares the loop to process the next block with an initial vector equal to the result of the first block.

When the loop exits, I simply return the encryption result of the last data block, which is the result of Step 9 of the secret key generation process.


Authoring a TGT request

So far, I have discussed the low-level methods of the ASN1DataTypes class and implemented the algorithm for generating a secret key from the user's password. Now it's time to demonstrate how the KerberosClient class will use these low-level details.

Look at Listing 12, which shows the implementation of the getTicketResponse() method. This method belongs to the KerberosClient class.

The basic purpose of the getTicketResponse() method is to author a request for a Kerberos ticket (a TGT or a service ticket), send the ticket request to the Kerberos server, get a response from the server, and return the response to the calling application. In this article, I will only describe the process of authoring a TGT request. The next article in this series will demonstrate the steps for setting up a KDC server, sending the request to the KDC, getting the response, and processing it.


Listing 12. The getTicketResponse() method
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


I discussed the structure of a TGT request while discussing Figure 2, Listing 1, and Table 2 in the first article of this series. Recall from that discussion that a TGT request contains four data fields: pvno, msg-type, padata, and req-body. The authoring of the pvno and msg-type fields is very simple, as these two fields consist of just an integer each (5 for pvno and 10 for msg-type, as mentioned in the "Request for a TGT" section of the first article).

You only need to call the getIntegerBytes() method, passing the integer value to the method. The getIntegerBytes() method returns the ASN.1 byte array representation of the INTEGER structure, which you will pass on to the getTagAndLengthBytes() method. This method will return the complete ASN.1 representation of the pvno or msg-type fields. This is how I have authored the pvno and msg-type fields at the start of the getTicketResponse() method in Listing 12.

After authoring the pvno and msg-type fields, the next step is to author the padata field. This field is optional. Most KDC servers present a setup option to allow the configuration of individual clients. A system administrator can set a Kerberos server up in such a way that specific clients can send TGT requests without including the padata field.

In order to reduce the processing burden on a resource-constrained J2ME device, I'm assuming that the e-bank has a Kerberos server that allows a wireless mobile user to send a TGT request without a padata field (and I'll demonstrate how to set up a Kerberos server to behave in this way in the next article of this series). That's why I'm going to omit the padata field from the TGT request that I'm going to generate. Thus, after authoring the pvno and msg-type fields, I go straight into authoring the req-body structure, which requires a number of steps.

Authoring the request body

My authoring strategy for the request body (the req-body structure) in the getTicketResponse() method of Listing 12 is to author all the individual sub-fields of the structure and then concatenate them together and wrap them in a SEQUENCE to form the request body.

Recall from the discussion of Figure 2 in the first article that the sub-fields of req-body are (omitting some of the optional fields):

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

I'm going to author these fields in the order in which they appear in the list above. Therefore, my first task is to author the kdc-options field.

As I am not interested in using any of the KDC options, I don't need to do any logical processing to author the kdc-options field. I can simply use a 5-byte array of zeros as its content. Look at the byte noOptions[] = new byte [5]; line in the getTicketResponse() method of Listing 12. This method instantiates a 5-byte array named noOptions initialized to five zeros.

The next line (byte kdc_options[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 0, getBitStringBytes(noOptions))) performs two tasks:

  1. It first passes the noOptions byte array to the getBitStringBytes() method, which returns the ASN.1 bit string representation of 5 zeros.
  2. It then passes on the bit string to the getTagAndLengthBytes() method, which will return the complete ASN.1 byte array representation of the kdc-options field.

The next step is to author the cname structure. Recall from Listing 1 of the first article that the cname field is of type PrincipalName. This data type is a SEQUENCE of two fields, namely name-type and name-string. The name-type field is constructed using an INTEGER. The name-string field is a SEQUENCE of GeneralStrings.

Therefore, in order to author a cname structure, I must follow a number of steps in the getTicketResponse() method of Listing 12:

  1. Call the getGeneralStringBytes() method, passing the client's username along with the method call. The getGeneralStringBytes() method will return the GeneralString representation of the client's username.

  2. Pass on the GeneralString to the getSequenceBytes() method, which will prepend the SEQUENCE bytes before the GeneralString and return the ASN.1 representation of the SEQUENCE that contains the client's username string.

    The line byte generalStringSequence[] = getSequenceBytes (getGeneralStringBytes (userName)); performs these first two steps.

  3. Call the getTagAndLengthBytes() method, passing the SEQUENCE bytes as its content. The getTagAndLengthBytes() method will prepend the name-string tag byte (context-specific tag number 0) and length bytes before the SEQUENCE and return the complete name-string structure.

    The line byte name_string[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 1, generalStringSequence); performs this step.

  4. Author the name-type part of the PrincipalName. The name-type part consists of just one INTEGER, which identifies the type of the username. Kerberos allows several types of names (usernames, unique identifiers, and so on). For the J2ME-based Kerberos client, the only name type I am interested in is usernames, whose name-type identifier is 1. Therefore, I will first construct an INTEGER and then pass on the INTEGER bytes to the getTagAndLengthBytes() method. This method will author the complete name-type part of the PrincipalName. The line byte name_type[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 0, getIntegerBytes (ASN1DataTypes.NT_PRINCIPAL)); in Listing 12 performs this task.

  5. Concatenate the name-type and name-string parts of the PrincipalName and then prepend the SEQUENCE bytes before the concatenated byte array. The line byte principalNameSequence [] = getSequenceBytes (concatenateBytes (name_type, name_string)); performs this task.

  6. Prepend the cname tag byte (context-specific tag number 1) and length bytes before the SEQUENCE of Step 5 above. This will result in the complete cname structure. The line byte cname[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 1, principalNameSequence); performs this task.

The 6-step strategy described above results in the authoring of the complete cname structure.

My next step is to author the realm field, which is of type GeneralString. The strategy to author the realm field is as follows:

  1. Author the GeneralString using a getGeneralStringBytes() method call.
  2. Pass the GeneralString bytes along with a getTagAndLengthBytes() method, which will return the complete byte string representation of the realm field.

The byte realm[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 2, getGeneralStringBytes (realmName)); line in Listing 12 makes these two method calls.

The next task is to author the sname field, which is of the PrincipalName data type. I have already described the strategy for authoring a PrincipalName data structure while discussing the cname field above.

After the sname field, I need to author the till field, which specifies the expiry time of the ticket I am requesting. For the J2ME-based Kerberos client, I don't want to specify any particular expiry time for the ticket; I just want to leave it to the KDC server to issue a ticket with a standard expiry time according to the server's policy. Therefore, I will always send a hard-coded date (January 1, 1970) as the value of the till field. The date that I have chosen lies in the past, which indicates that I don't want to specify an expiry time for the ticket being requested.

The till field is of type KerberosTime, which in turn follows the GeneralizedTime ASN.1 universal data type. The process of authoring a KerberosTime structure is to first call the getGeneralizedTimeBytes() method and pass on the time string along with the method call. For example, the getGeneralizedTimeBytes(new String("19700101000000Z") method call will return the GeneralizedTime structure for January 1, 1970.

Once I have a GeneralizedTime byte array, I can pass it to a getTagAndLengthbytes() method call that will author the complete byte array for the till parameter. The byte till[] = getTagAndLengthBytes (ASN1DataTypes.Context_Specific, 5, getGeneralizedTimeBytes (new String("19700101000000Z").getBytes())); line in the getTicketResponse() method in Listing 12 authors the complete till structure.

Next, I need to author the nonce field, which wraps a random number as an integer. I first generate a random number, then generate the byte array representation of the random number, and finally call the getTagAndLengthBytes() method that will author the complete structure for the nonce field.

The last structure that I have to author in the req-body field is the etype field, which is a sequence of INTEGERs. Each INTEGER in the SEQUENCE specifies an encryption algorithm that the client supports. I just want to support one encryption algorithm (DES in CBC mode) whose INTEGER identifier is 1, 2, or 3, depending upon the message digest algorithm of the client's choice. I'll explain the use of message digest algorithms in the next article of this series, but for now just note that I'm going to use the MD5 message digest algorithm in the Kerberos client.

The identifier for the DES-CBC-MD5 combination is 3. Therefore, I'll first author the INTEGER bytes for 3, then prepend the SEQUENCE bytes before the INTEGER bytes, and finally call the getTagAndLengthBytes() method to get the complete byte array representation of the etype field.

I have now authored all the fields of the req-body field. Thus, I can call the concatenateBytes() method a number of times to concatenate all the individual fields together into one byte array. The next step will be to call the getSequenceBytes() method to put the concatenated byte array into a SEQUENCE. A getTagAndLengthBytes() method will take the SEQUENCE bytes and author the complete req-body structure.

The last step in authoring the TGT request is to concatenate the pvno and msg-fields, which I authored earlier in this section, together with the req-body bytes. I then put these fields in a SEQUENCE, and finally call the getTagAndLengthBytes() method to arrive at the completed ticket request, which can be sent to the Kerberos server.


Summary

I have discussed several basic concepts in this article. I have developed a J2ME class that contains several useful methods for the authoring of ASN.1 data structures, and I have also showed how to generate a Kerberos secret key from a user's password. Finally, I demonstrated how the Kerberos client will author a TGT request.

Next time, I'll set up a KDC server, fetch Kerberos tickets from that server, and use those tickets to exchange cryptographic keys with the e-bank's business logic server.


Resources

About the author

Faheem Khan is an independent software consultant specializing in enterprise application integration (EAI) and B2B solutions. Readers can reach Faheem at fkhan872@yahoo.com.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=10889
ArticleTitle=Lock down J2ME applications with Kerberos, Part 2: Authoring a request for a Kerberos ticket
publish-date=11252003
author1-email=fkhan872@yahoo.com
author1-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Special offers