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.
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);
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.
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.
Copyright © 2001 O'Reilly & Associates. All rights reserved.