Hello, zoT1wy1njA0=!

Let’s jump right into Java cryptography with some examples. The first example can be run by anyone who has the Java Development Kit (JDK) 1.1 or later installed. The second example uses classes from the Java Cryptography Extension (JCE). To run it, you will need to download and install the JCE, which is available in the United States and Canada only at http://java.sun.com/products/jdk/1.2/jce/. Chapter 3, discusses these pieces of software and how they fit together.

Don’t worry if you don’t understand everything in these programs. They are demonstrations of what you can do with cryptography in Java, and everything in them will be explained in more detail elsewhere in the book.

Masher

Our first example demonstrates how a message digest works. A message digest takes an arbitrary amount of input data and creates a short, digested version of the data, sometimes called a digital fingerprint, secure hash, or cryptographic hash. Chapter 2 and Chapter 6 contain more detail about message digests. This program creates a message digest from a file:

import java.io.*;
import java.security.*;

import sun.misc.*;

public class Masher {
  public static void main(String[] args) throws Exception {
    // Check arguments.
    if (args.length != 1) {
      System.out.println("Usage: Masher filename");
      return;
    }

    // Obtain a message digest object.
    MessageDigest md = MessageDigest.getInstance("MD5");

    // Calculate the digest for the given file.
    FileInputStream in = new FileInputStream(args[0]);
    byte[] buffer = new byte[8192];
    int length;
    while ((length = in.read(buffer)) != -1)
      md.update(buffer, 0, length);
    byte[] raw = md.digest();

    // Print out the digest in base64.
    BASE64Encoder encoder = new BASE64Encoder();
    String base64 = encoder.encode(raw);
    System.out.println(base64);
  }
}

To use this program, just compile it and give it a file to digest. Here, I use the source code, Masher.java, as the file:

C:\ java Masher Masher.java
nfEOH/5M+yDLaxaJ+XpJ5Q==

Now try changing one character of your input file, and calculate the digest again. It looks completely different! Try to create a different file that produces the same message digest. Although it’s not impossible, you probably have a better chance of winning the lottery. Likewise, given a message digest, it’s very hard to figure out what input produced it. Just as a fingerprint identifies a human, a message digest identifies data but reveals little about it. Unlike fingerprints, message digests are not unique.

A message digest is sometimes called a cryptographic hash. It’s an example of a one-way function , which means that although you can calculate a message digest, given some data, you can’t figure out what data produced a given message digest. Let’s say that your friend, Josephine, wants to send you a file. She’s afraid that your mutual enemy, Edith, will modify the file before it gets to you. If Josephine sends the original file and the message digest, you can check the validity of the file by calculating your own message digest and comparing it to the one Josephine sent you. If Edith changes the file at all, your calculated message digest will be different and you’ll know there’s something awry. Of course, there’s a way around this: Edith changes the file, calculates a new message digest for the changed file, and sends the whole thing to you. You have no way of knowing whether Edith has changed the file or not. Digital signatures extend message digests to solve this problem; I’ll get to them in Chapter 6.

So how does this program work? It operates in four distinct steps, indicated by the source comments:

  1. Check command-line arguments. Masher expects one argument, a filename.

  2. Obtain the message digest object. We use a factory method, a special static method that returns an instance of MessageDigest. This factory method accepts the name of an algorithm. In this case, we use an algorithm called MD5.

    	MessageDigest md = MessageDigest.getInstance("MD5");

    This type of factory method is used throughout the Security API.

  3. Calculate the message digest. Here we open the file and read it in 8-kilobyte chunks. Each chunk is passed to the MessageDigest object’s update() method. Finally, the message digest value is calculated with a call to digest().

  4. Make the result readable. The digest() method returns an array of bytes. To convert this to a screen-printable form, we use the sun.misc.BASE64Encoder class. This class converts an array of bytes to a String, which we print.

SecretWriting

The next example uses classes that are found only in the Java Cryptography Extension (JCE). The JCE contains cryptographic software whose export is limited by the U.S. government. If you live outside the United States or Canada, it is not legal to download this software. Within the United States and Canada, you can get the JCE from http://java.sun.com/products/jdk/1.2/jce/.

The SecretWriting program encrypts and decrypts text. Here is a sample session:

C:\ java SecretWriting -e Hello, world!
Lc4WKHP/uCls8mFcyTw1pQ==

C:\ java SecretWriting -d Lc4WKHP/uCls8mFcyTw1pQ==
Hello, world!

The -e option encrypts data, and the -d option decrypts it. A cipher is used to do this work. The cipher uses a key. Different keys will produce different results. SecretWriting stores its key in a file called SecretKey.ser. The first time you run the program, SecretWriting generates a key and stores it in the file. Subsequently, the key is loaded from the file. If you remove the file, SecretWriting will create a new key. Note that you must use the same key to encrypt and decrypt data. This is a property of a symmetric cipher. We’ll talk more about different flavors of ciphers in Chapter 7.

“Hello, world!” can be encrypted to many different values, depending on the key that you use. Here are a few sample ciphertexts:

Lc4WKHP/uCls8mFcyTw1pQ==
xyOoLnWOH0eqRwUu3rQHJw==
hevNJLNowIzrocxplKI7dQ==

The source code for this example is longer than the last one, but it’s also a more capable program:

import java.io.*;
import java.security.*;

import javax.crypto.*;

import sun.misc.*;

public class SecretWriting {
  public static void main(String[] args) throws Exception {
    // Check arguments.
    if (args.length < 2) {
      System.out.println("Usage: SecretWriting -e|-d text");
      return;
    }

    // Get or create key.
    Key key;
    try {
      ObjectInputStream in = new ObjectInputStream(
          new FileInputStream("SecretKey.ser"));
      key = (Key)in.readObject();
      in.close();
    }
    catch (FileNotFoundException fnfe) {
      KeyGenerator generator = KeyGenerator.getInstance("DES");
      generator.init(new SecureRandom());
      key = generator.generateKey();
      ObjectOutputStream out = new ObjectOutputStream(
          new FileOutputStream("SecretKey.ser"));
      out.writeObject(key);
      out.close();
    }

    // Get a cipher object.
    Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");

    // Encrypt or decrypt the input string.
    if (args[0].indexOf("e") != -1) {
      cipher.init(Cipher.ENCRYPT_MODE, key);
      String amalgam = args[1];
      for (int i = 2; i < args.length; i++)
        amalgam += " " + args[i];
      byte[] stringBytes = amalgam.getBytes("UTF8");
      byte[] raw = cipher.doFinal(stringBytes);
      BASE64Encoder encoder = new BASE64Encoder();
      String base64 = encoder.encode(raw);
      System.out.println(base64);
    }
    else if (args[0].indexOf("d") != -1) {
      cipher.init(Cipher.DECRYPT_MODE, key);
      BASE64Decoder decoder = new BASE64Decoder();
      byte[] raw = decoder.decodeBuffer(args[1]);
      byte[] stringBytes = cipher.doFinal(raw);
      String result = new String(stringBytes, "UTF8");
      System.out.println(result);
    }
  }
}

SecretWriting has to generate a key the first time you use it. This can take a few seconds, so be prepared to wait.

In the meantime, let’s look at the steps in this program:

  1. Check command-line arguments. We expect an option, either -e or -d, and a string.

  2. Next we need a key to use the cipher. We first attempt to deserialize the key from a file named SecretKey.ser. If this fails, we need to create a new key. A KeyGenerator object creates keys. We obtain a KeyGenerator by using a factory method, in just the same way that we obtained a MessageDigest in the Masher example. In this case, we ask for a key for the DES (Data Encryption Standard) cipher algorithm:

    	KeyGenerator generator = KeyGenerator.getInstance("DES");

    The key generator must be initialized with a random number to produce a random new key. It takes a few seconds to initialize the SecureRandom, so be patient.

    	generator.init(new SecureRandom());

    This done, we are set to generate a key. We serialize the key to the SecretKey.ser file so that we can use the same key the next time we run the program.

  3. Having obtained our key, we obtain a cipher in much the same way:

    	Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");

    This specifies the DES algorithm and some other parameters the Cipher needs. We’ll talk about these in detail in Chapter 7.

  4. Finally, we encrypt or decrypt the input data. The Cipher is created in an uninitialized state; it must be initialized, with a key, to either encryption mode or decryption mode. This is accomplished by calling init(). When encrypting, we take all of the command-line arguments after the -e option and concatenate them into one string, amalgam.

    Then we get a byte array from this string and encrypt it in the call to Cipher’s doFinal() method:

    	byte[] stringBytes = amalgam.getBytes("UTF8");
    	byte[] raw = cipher.doFinal(stringBytes);

    Finally, as in the Masher example, we convert the raw encrypted bytes to base64 and display them.

    Decrypting is the same process in reverse. We convert the command-line argument from base64 to an array of bytes. We then use our Cipher object to decrypt this:

    	byte[] stringBytes = cipher.doFinal(raw);

    We create a new String from the resulting byte array and display it. Note that we specify an encoding for converting between a String and a byte array. If we just used the default encoding (by calling getBytes() with no argument), then the ciphertext produced by this program might not be portable from one machine to another. We use UTF8 as a standard encoding because it can express all Unicode characters. For more information on UTF8, see http://www.stonehand.com/unicode/standard/wg2n1036.html. You don’t really have to understand how UTF8 works; just think of it as a standard way to convert from a string to a byte array and back.

This is only a demonstration program. Note that its key management is not secure. SecretWriting silently writes the secret key to a disk file. A secret key must be kept secret—writing it to a file without notifying the user is not wise. In a multiuser system, other users might be able to copy the key file, enabling them to decode your secret messages. A better approach would be to prompt the user for a safe place to put the key, either in a protected directory, in some sort of protected database, on a floppy disk, or on a smart card, perhaps. Another approach is to encrypt the key itself before writing it to disk. A good way to do this is using password-based encryption, which is covered in Chapter 7.

Although SecretWriting doesn’t do a whole lot, you can see how it could be expanded to implement a cryptographically enabled email application. I’ll develop such an application in Chapter 11.

Get Java Cryptography now with O’Reilly online learning.

O’Reilly members experience live online training, plus books, videos, and digital content from 200+ publishers.