JavaEnterprise JavaMessage Authentication: Unlocking the Secrets of the Java Cryptography Extensions

Message Authentication: Unlocking the Secrets of the Java Cryptography Extensions

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

In my article “Unlocking the Secrets of Java Cryptography Extensions: The Basics,” I introduced you to the Java Cryptography Extension and the theory of encrypting and decrypting data. But, how can you be sure of the integrity of your encrypted data? Encryption and decryption are only part of the picture. Here, you will learn about message authentication codes (MAC), and how to verify that the message you received is really what was sent.

What Is Message Authentication?

Message authentication is an algorithm for checking the integrity of a secret message upon receipt and decryption. To authenticate a deciphered message, the recipient applies a mathematical function called a cryptographic hash function to the decrypted plaintext, and checks the final value against an identically computed value that was also received from the message sender. If the values computed agree, the message is legitimate; if not, it is likely that the message was intercepted and changed before reaching the recipient.

Ideally, the hash value from the message sender comes to the recipient separately from the message. This adds an extra layer of security to the exchange because anyone intercepting the message must also intercept the hash code, and must know how to recompute the hash code for their new, different message.

Although this sounds pretty secure, it is certainly not foolproof. Listing 1 shows an absurdly simple example of a “cryptographic” hash function that computes a hash value based on a simple numeric value being assigned to the characters in the message.

Listing 1: SimpleCryptoHash.java

package com.dlt.developer.mac;

import java.io.*;
import java.util.HashMap;

public class SimpleCryptoHash {
   private HashMap codeMap;

   public SimpleCryptoHash() {
      codeMap = new HashMap();
      StringBuffer sb = new
         StringBuffer(" ABCDEFGHIJKLMNOPQRSTUVWXYZ.,?!");
      for (int value = 0; value < sb.length(); value++) {
         String key = String.valueOf(sb.charAt(value));
         Integer val = new Integer(value);
         codeMap.put(key, value);
      }    // for value
   }       // SimpleCryptoHash()

   public String getPlainText() {
      System.out.print("Enter plaintext:");
      String plaintext = "";
      BufferedReader br =
         new BufferedReader(new InputStreamReader(System.in)); 
      try {
         plaintext = br.readLine();
      } catch (IOException ioe) {
         System.out.println("IO error trying to read plaintext!");
         System.exit(1);
      }    // catch
      return plaintext;
   }       // getPlainText()

   public int getHashCode(String plaintext)
      throws IllegalArgumentException {
      int hashCode = 0;
      StringBuffer sb = new StringBuffer(plaintext.toUpperCase());
      for (int i = 0; i < sb.length(); i++) {
         String key = String.valueOf(sb.charAt(i));
         Integer val = (Integer)codeMap.get(key);
         if (val == null) {
            throw new IllegalArgumentException("The character " +
               key + " is not in the code map.");
         }    // if
         hashCode = hashCode + val.intValue();
      }       // for i
      return hashCode;
   }          // getHashCode()

   public static void main(String[] args) {
      System.out.println("This program generates a simple hashcode
                          for the plaintext you enter.");
      SimpleCryptoHash theHash = new SimpleCryptoHash();
      String plaintext = theHash.getPlainText();
      int hashCode = theHash.getHashCode(plaintext);
      System.out.println("The hashcode for the plaintext '" +
                         plaintext + "' is " + hashCode);
   }    // main()

}    // SimpleCryptoHash

Following is sample output from running this program:


This program generates a simple hashcode for the plaintext
   you enter.
Enter plaintext:Attack at dawn!
The hashcode for the plaintext 'Attack at dawn!' is 149

Running the program a second time shows the fatal flaw in my cryptographic hash function:


This program generates a simple hashcode for the plaintext
   you enter.
Enter plaintext:Surrender.
The hashcode for the plaintext 'Surrender.' is 149

In this example, two very different plaintexts give the same cryptographic hash value. Of course, real-world cryptographic hash functions are much more robust. It is much more difficult to generate the same hash code from two different plaintexts as I have done above. However, this simple example demonstrates the fatal flaw in using a cryptographic hash function by itself; the hash code the recipient gets is only as secure as the complexity of the hash function one uses.

Message authentication extends the concept of using a cryptographic hash function, but adds yet another layer of security by basing the hash code itself on a predetermined secret key value. Thus, the sender and recipient of the hash code must agree on the secret key ahead of time. Changing the secret key value completely changes the hash code that would be returned by using the exact same authentication algorithm, so a third party is less likely to be able to figure out what the hash code is, or to be able to produce a hash code of his own to go with an intercepted and modified message.

A Simple Message Authentication Example

Now, I will demonstrate how to perform the same task as in my earlier example using true message authentication. Consider the full listing of SimpleMacExample.java shown in Listing 2.

Listing 2: SimpleMacExample.java

package com.dlt.developer.mac;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import javax.crypto.*;

public class SimpleMacExample {

   public static String getPlainText() {
      System.out.print("Enter plaintext:");
      String plaintext = "";
      BufferedReader br =
         new BufferedReader(new InputStreamReader(System.in));
      try {
         plaintext = br.readLine();
      } catch (IOException ioe) {
         System.out.println("IO error trying to read plaintext!");
         System.exit(1);
      }    // catch
      return plaintext;
   }       // getPlainText()

   public static void main(String[] args) throws Exception {
      System.out.println("This program generates a message
         authentication code for the plaintext you enter.");
      String plaintextString = getPlainText();
      byte[] plaintext = plaintextString.getBytes();

      KeyGenerator keygen = KeyGenerator.getInstance("HmacMD5");
      SecretKey sKey = keygen.generateKey();

      Mac theMac = Mac.getInstance("HmacMD5");
      theMac.init(sKey);

      byte[] theMacCode = theMac.doFinal(plaintext);

      System.out.print("The MAC for the plaintext '" +
                       plaintextString + "' is ");
      for (int i = 0; i < theMacCode.length; i++) {
      System.out.print(theMacCode[i]);
      if (i != theMacCode.length - 1) {
         System.out.print(",");
      }    // if
   }       // for i
   System.out.println();
   }       // main


}          // class SimpleMacExample

The output generated from running this program is shown below:


This program generates a message authentication code for the
   plaintext you enter.
Enter plaintext:Attack at dawn!
The MAC for the plaintext 'Attack at dawn!' is -5,-40,-58,-34,43,
  30,107,61,50,-120,-15,98,109,0,7,121


This program generates a message authentication code for the
   plaintext you enter.
Enter plaintext:Surrender.
The MAC for the plaintext 'Surrender.' is -96,-20,97,73,11,-88,
  16,-123,-108,-100,44,80,127,118,-102,-7

As you can see from the output, the problem demonstrated in the SimpleCryptoHash.java example has been solved. These two plaintexts do not generate the same series of bytes when the MD5 message authentication scheme is used. Before running this example, you will need to install and configure the Java Cryptography Extension and any third-party libraries you might wish to use, as described in my article “Unlocking the Secrets of Java Cryptography Extensions: The Basics.”

Now, examine each step of the process of generating the message authentication code.

First, as in the earlier example, the plaintext is read from the command line. Next, a secret key is generated, which is necessary for generating the MAC:

KeyGenerator keygen = KeyGenerator.getInstance("HmacMD5");
SecretKey sKey = keygen.generateKey();

Note that only particular algorithms are suitable for message authentication. Commonly used algorithms are HmacMD5 and HmacSHA-1. Using an inappropriate algorithm will result in an exception being thrown.

Next, the message authentication code object is initialized with the secret key generated:

Mac theMac = Mac.getInstance("HmacMD5");
theMac.init(sKey);

Finally, the authentication code is generated in one single step:

byte[] theMacCode = theMac.doFinal(plaintext);

Note the similarities of the code above to the code used to encrypt data. Like the Cipher object, the Mac object requires that you get an instance of the class, properly initialized with an acceptable algorithm, and then initialize it with a secret key. The process of generating the MAC is done using the doFinal() method, just as the process of encryption is performed using the Cipher object. These similarities within the JCE make it easier for developers to use all features within the API once they are familiar with a single feature.

Using a third-party library to perform message authentication is just as easy as doing so for a cryptographic cipher. All that would need to be done to use the Bouncy Castle libraries within this code would be to change the lines where the secret key is generated and the Mac object is initialized:

Security.addProvider(new BouncyCastleProvider());
KeyGenerator keygen = KeyGenerator.getInstance("HmacMD5", "BC");
...
Mac theMac = Mac.getInstance("HmacMD5", "BC");

As with encryption algorithms, if the third-party library does not support the algorithm specified, an exception will be thrown. Using this approach means that one can use a third-party message authentication library without being locked into a particular vendor.

A More Realistic Example

The whole concept of message authentication involves a sender and receiver exchanging a secret message, a secret MAC key, and the MAC itself for verifying the message. The previous example only demonstrates the mechanics of generating the MAC, so now, on to a more complicated example.

Listing 3 shows how the sender would generate the MAC key and code for consumption by the recipient.

Listing 3: MACFileExample.java

package com.dlt.developer.mac;

import javax.crypto.*;
import java.io.*;
public class MACFileExample {
   public static void main(String[] args) throws Exception {
      System.out.println("Generating MAC key and code files..");

      KeyGenerator keygen = KeyGenerator.getInstance("HmacMD5");
      SecretKey macKey = keygen.generateKey();

      byte[] keyBytes = macKey.getEncoded();

      BufferedOutputStream out = new
         BufferedOutputStream(new FileOutputStream("mac_key.txt"));
      out.write(keyBytes);
      out.flush();
      out.close();

      Mac theMac = Mac.getInstance("HmacMD5");
      theMac.init(macKey);

      BufferedInputStream in = new
         BufferedInputStream(new FileInputStream("plaintext.txt"));
      while (in.available() > 0) {
         byte[] plaintextBytes = new byte[in.available()];
         in.read(plaintextBytes);
         theMac.update(plaintextBytes);
      }    // while
      in.close();

      BufferedOutputStream macData = new
         BufferedOutputStream(new FileOutputStream(
            "sender_mac_data.txt"));
      macData.write(theMac.doFinal());
      macData.flush();
      macData.close();

      theMac.reset();

      System.out.println("Done!");
   }    // main


}       // class MACFileExample

In Listing 3, the sender first creates the MAC key as shown in the earlier simple example. Then, this value is written to a file to be sent to the message recipient; in this case, mac_key.txt. An alternative to this approach would be for the two parties exchanging data to agree on the secret key value ahead of time; this would eliminate the need for the exchange of the MAC key file.

Next, the code loops through the plaintext.txt file. It grabs chunks of plaintext and applies the message authentication algorithm to each chunk. Note that the update() method is used for each chunk of data because the file could be too large to be handled all at once in memory by the Mac.doFinal() method.

This is very similar to the manner in which the Cipher class encrypts and decrypts data, but with one notable difference. When encrypting or decrypting data, the update() method returns plaintext or ciphertext bytes from the update() method. Here, update() simply applies the MAC algorithm to the current instance of the Mac class; no data is returned.

Finally, when all data has been read, the Mac.doFinal() method is invoked. This method returns the bytes for the MAC that were calculated by all of the prior iterations of the update() method earlier.

The final step of the process is to write this data out to a file, sender_mac_data.txt, which the recipient can verify against his own authentication check.

Listing 4 shows how the recipient would recalculate the MAC on the decrypted message received by using the secret MAC key, and then would compare the calculated MAC to the sender’s MAC to verify the integrity of the decrypted message.

Listing 4: VerifyMACFileExample.java

package com.dlt.developer.mac;

import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;

import javax.crypto.*;import java.security.Key;

import java.io.*;

public class VerifyMACFileExample {
   public static void main(String[] args) throws Exception {
      System.out.println("Calculating MAC and comparing to MAC
                          from sender...");

      BufferedInputStream in = new
         BufferedInputStream(new FileInputStream("mac_key.txt"));
      byte[] keyBytes = new byte[in.available()];
      in.read(keyBytes);
      in.close();
      SecretKeySpec skeySpec =
         new SecretKeySpec(keyBytes, "HmacMD5");

      Mac theMac = Mac.getInstance("HmacMD5");

      theMac.init(skeySpec);


      BufferedInputStream inData = new
         BufferedInputStream(new FileInputStream("plaintext.txt"));
      while (inData.available() > 0) {
         byte[] plaintextBytes = new byte[inData.available()];
         inData.read(plaintextBytes);
         theMac.update(plaintextBytes);
      }    // while
      inData.close();
      byte[] calculatedMacCode = theMac.doFinal();

      in = new BufferedInputStream(new FileInputStream(
         "sender_mac_data.txt"));
      byte[] senderMacCode = new byte[in.available()];
      in.read(senderMacCode);
      in.close();

      boolean macsAgree = true;
      if (calculatedMacCode.length != calculatedMacCode.length) {
         macsAgree = false;
         System.out.println("Sender MAC and calculated MAC length
                             are not the same.");
      } else {

         for (int i = 0; i < senderMacCode.length; i++) {
            if (senderMacCode[i] != calculatedMacCode[i]) {
               macsAgree = false;
               System.out.println("Sender MAC and calculated MAC
                                   are different. Message cannot
                                   be authenticated.");
break;

            }    // if
         }       // for i
      }          // if

      if (macsAgree) {
         System.out.println("Message authenticated successfully.");
      }    // if



      System.out.println("Done!");
   }    // main


}    // class VerifyMACFile

The output from the VerifyMACFileExample.java program is shown below:


Calculating MAC and comparing to MAC from sender...
Message authenticated successfully.
Done!

To verify the integrity of the sender's message, the recipient first reads the mac_key.txt file that contains the agreed-upon secret key value. Then, as in the sender's example, the Mac object is initialized with this secret key.

Next, the recipient loops through the decrypted plaintext.txt file and performs the same calls using the Mac object that the sender did earlier. Of course, in real life, the recipient would have received an encrypted file, and would have had to decrypt this file to get to this point in the process. I have omitted any encryption or decryption from these examples to avoid confusion, but one might well decrypt and authenticate within the same program for efficiency's sake.

After the final invocation of Mac.doFinal(), the recipient has a calculated MAC that should agree with the sender's. Now, the receiver reads the sender's sender_mac_data.txt file into another byte array for comparison.

At last, the time has come to verify the authenticity of the message. The code first checks to see whether the two arrays of bytes are the same length. If so, each array element is compared; if any do not match, authentication fails. Once both of these checks have passed, the message recipient can rest easy that the message the sender sent is the same as the one he has received.

Conclusion

Message authentication is necessary in situations where a message recipient must be guaranteed that the message he received was the one the sender sent. Here, you have learned how to create and verify MACs using the Java Cryptography Extensions. Using the techniques discussed here, you now have the tools to create a secure message exchange between a sender and a recipient, even when the message must pass through unsecured channels.

Download the Code

You can download code examples here.

References

About the Author

David Thurmond is a Sun Certified Developer with over fifteen years of software development experience. He has worked in the agriculture, construction equipment, financial, home improvement, and logistics industries.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories