Book Home Java Distributed Computing Search this book

5.6. Data Encryption

Now we've seen how you can authenticate a remote agent talking to you over the network, but what about the security of the data you're exchanging? Our AuthCreditAgent checks the identity of the requesting agent before sending account information to them, but once it does, the data is sent unencoded over the network as a message:

String msg = nextMsg();
    ...
dout.writeUTF(msg);

So if the data is all an attacker is after, and he couldn't easily bypass our authentication system, he could eavesdrop on the network communications and collect the data that way. To prevent this, you want to encode, or encrypt, the data before it's transmitted, in such a way that only the intended recipient can decode the data.

5.6.1. Ciphers for Secure Data Transfers

The Java Cryptography Extension to the Java Security API provides the java.security.Cipher class for implementing secure, encrypted data transfers. A Cipher can be used on the sending end of a transmission to encrypt data, and on the receiving end to decrypt data. A Cipher is created using the getInstance() method common to the Java Security API, passing in the name of an algorithm to be used for encryption:

Cipher sendCipher = Cipher.getInstance("DES");

In this example, we're creating a Cipher that uses the DES algorithm to encrypt data. This algorithm is a symmetric encryption algorithm, which means that it needs to use the same secret key for both the encryption and decryption of data at either end of the transmission link. Other encryption schemes are considered asymmetric, in that they use different keys for encryption and decryption. This term is usually used to refer to encryption algorithms that are based on public key methods. With an asymmetric cipher, a message might be encrypted using an agent's public key, and can only be decrypted using the corresponding private key. The advantage of asymmetric encryption is that we can transmit public keys to other agents in the clear. Symmetric encryption requires that we securely transmit a secret key between two parties. The advantage of symmetric encryption is performance; symmetric algorithms usually take less CPU time to encrypt and decrypt data. Where performance is an issue, you can use a combination of symmetric and asymmetric encryption. A secret key can be transmitted between two parties first, using their public keys and asymmetric encryption on the secret key. Then the rest of the communication can be carried out using symmetric encryption, with the secret key.

For sending data, the Cipher that we create needs to be initialized with a key to use to encrypt the data. This is done by passing the Key into the initEncrypt() method on the Cipher:

Key encodeKey = ...;   // Get key to be used for encrypting data
sendCipher.initEncrypt(encodeKey);

The data to be encrypted is passed into the Cipher's crypt() method next:

byte[] sensitiveData = ...;
byte[] encodedData = sendCipher.crypt(sensitiveData);

The encoded data can now be transmitted safely to another party, where it can be decoded only if the right key and algorithm are used.

The procedure for decrypting the message is similar. A Cipher object is created just as in the encryption stage, but it is initialized for decryption using the proper key:

Key decodeKey = ...;   // Get the key to be used for decrypting data
receiveCipher.initDecrypt(decodeKey);

Once the encoded data is received, either over a Socket connection or through some other means, the receiver can decrypt it using the crypt() methods on the decrypting Cipher:

byte[] sensitiveData = receiveCipher.crypt(encodedData);

5.6.2. Back to Our Credit Agent

Now we can add data encryption to our credit agent, so that the account information sent back to the remote agent is safe from prying eyes. First, we'll make a new subclass of our SimpleAgent, called SecureAgent, that includes all of the authentication abilities of the AuthAgent from Example 5-3, plus the ability to encrypt and decrypt messages. The SecureAgent is shown in Example 5-5. The SecureAgent has an extra data member, cryptKey, which is a Key to be used to encrypt and decrypt messages sent to and from the remote agent. For this example, we'll assume that we're using a symmetric, secret key encryption algorithm to encode messages. The key used for the cipher may have been initialized with a handshaking process involving an asymmetric algorithm, as we described earlier.

Example 5-5. An Agent with Authentication and Encryption

package dcj.examples.security;

import java.lang.*;
import java.net.*;
import java.io.*;
import java.security.*;

public class SecureAgent extends SimpleAgent {

  // The Identity of the agent we're connected to
  Identity remoteAgent = null;
  // A secret key used to encode/decode messages
  Key cryptKey = null;

  public SecureAgent(String host, int port)
      throws IllegalArgumentException {

    super(host, port);
    DataInputStream din = new DataInputStream(inStream);

    // Try to authenticate the remote agent
    try {
      String agentId = din.readUTF();
      int dataLen = din.readInt();
      byte[] data = new byte[dataLen];
      din.read(data);
      int sigLen = din.readInt();
      byte[] sig = new byte[sigLen];
      din.read(sig);

      if (!authenticate(agentId, data, sig)) {
        // Failed to authenticate: write error message, close socket and
        // return
        System.out.println("Failed to authenticate remote agent "
                           + agentId);
        closeConnection();
      }
      else {
        // Remote agent is authenticated, first message is a welcome
        addMsg("HELLO " + agentId);
      }
    }
    catch (Exception e) {
      closeConnection();
    }
  }

  protected boolean authenticate(String id, byte[] data, byte[] sig) {
    boolean success = false;
    PublicKey key = lookupKey(id);
    try {
      // Set up a signature with the agent's public key
      Signature agentSig = Signature.getInstance(key.getAlgorithm());
      agentSig.initVerify(key);
      // Try to verify the signature message from the agent
      agentSig.update(data);
      success = agentSig.verify(sig);

      if (success) {
        // Agent checks out, so initialize an identity for it
        remoteAgent = new Identity(id);
        remoteAgent.setPublicKey(key);
        // Get the agent's secret encryption key, too
        cryptKey = lookupSecretKey(id);
      }
    }
    catch (Exception e) {
      System.err.println("Failed to verify agent signature.");
      success = false;
    }

    return success;
  }

  public void run() {
    // Go into infinite loop, sending messages, receiving responses, and
    // processing them...

    DataInputStream din = (DataInputStream)inStream;
    DataOutputStream dout = (DataOutputStream)outStream;

    // Make an encryption Cipher for sending messages...
    String cryptAlgName = cryptKey.getAlgorithm();
    Cipher sendCipher = Cipher.getInstance(cryptAlgName);
    sendCipher.initEncrypt(cryptKey);
    // ...and a decryption Cipher for receiving them.
    Cipher receiveCipher = Cipher.getInstance(cryptAlgName);
    receiveCipher.initDecrypt(cryptKey);

    while (true) {
      String msg = nextMsg();
      if (msg != null) {
        String inMsg = "", inToken = "";
        try {
          // Send encrypted message to agent
          byte[] eData = sendCipher.crypt(msg.getBytes());
          dout.write(eData);

          // Read and decrypt message from agent
          int dataLen = din.readInt();
          eData = new byte[dataLen];
          din.read(eData);
          byte[] clearData = receiveCipher.crypt(eData);
          inMsg = new String(clearData);

          // Process the incoming message
          processMsg(inMsg);
        }
        catch (Exception e) {}
      }
    }
  }
}

The SecureAgent constructor is identical to the AuthAgent, reading a digital signature from the input stream and passing it to the authenticate() method. The authenticate() method is almost the same: the digital signature is checked, and if it is verified, then the agent's Identity is initialized with their PublicKey. An extra step is added, though, to look up the agent's secret key using the aptly named lookupSecretKey() method. The secret key might be stored in a local database, or on a key-ring in memory. A very simple way to store a key-ring would be to put keys in a Hashtable indexed by the identity name, and then serialize the Hashtable object to a file on disk. In this case, the lookupSecretKey() method on our AuthAgent might look something like this:

protected SecretKey lookupSecretKey(String id) {
    SecretKey key = null;

    // Get the key-ring file name from the property list for this agent
    String keyringFile = System.getProperty("keyring", "keyring.dat");

    // Try reading the key-ring and looking up the key for the id
    try {
        // Read the key-ring from disk
        ObjectInputStream in =
            new ObjectInputStream(new FileInputStream(keyringFile));
        Hashtable keyring = (Hashtable) in.readObject();
        // Lookup the id's key on the ring
        key = (SecretKey)keyring.get(id);
    }
    catch (Exception e) {
        System.err.println("Failure looking up key on keyring.");
        e.printStackTrace(System.err);
    }

    return key;
}

The big difference between the AuthAgent and the SecureAgent is in their run() methods. Before the SecureAgent starts sending and receiving messages in the run() method's while loop, it initializes two Ciphers, one for encrypting outgoing messages and one for decrypting incoming messages. It uses the cryptKey to initialize both Ciphers. In the message-passing loop, the Secure-Agent encrypts each outgoing message with the send Cipher, and all incoming messages are read as byte arrays, and decrypted using the receive Cipher.

To make our credit agent use the new encryption abilities offered by the SecureAgent, we can just change the class definition and have the agent extend the SecureAgent rather than the AuthAgent. The processMsg() method will work unchanged since the incoming messages have been decrypted in the Se-cureAgent.run() method before being passed into the processMsg() method.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.