eb applications today must often offer a convenience feature called automatic authentication?the ability to “remember” users between visits, so that they don’t have to log in for every visit to the site. To do that, the server must store a cookie on the user’s machine that serves to identify that user the next time a browser running on that machine requests a page from the Web application. Unfortunately, while automatic authentication is a wonderful user convenience, it’s also a security risk, because when you rely on stored data for authentication rather than user input, you’re not authenticating a user?you’re authenticating the machine from which a user last successfully logged in. Obviously, that can be dangerous when users log in from shared computers, such as those in a library or classroom. You should only implement automatic authentication if you’re willing to accept that security risk.
The simplest way to implement automatic authentication is to create a cookie on the user’s machine containing an identifying value, such as a UserID after the user logs in successfully the first time. On subsequent visits, the browser will send the UserID cookie, and your application can read the UserID value to log the user in. However, that exposes you to another security hole; it’s possible for people to log in as other users simply by guessing their UserIDs.
One popular solution is to use some form of encryption to create a signature for the user. You store the signature in the cookie along with the UserID. However, ColdFusion’s (CF’s) encryption abilities aren’t the best around. What you need is a solution that is not only secure, but also standards-based, so you can share authentication information with other applications (for example, non-CF based applications).
Extending CF Encryption
Because of CF’s weak encryption abilities you will need to extend them in some fashion. There are numerous options available for extending CF, but in this article, I’ll show you how to use Java, because it’s popular and cross-platform support. However, you could just as easily use C++ or any other language capable of writing COM or CORBA components.
To use Java with CF, you must be using CF 4.5 or higher. Additionally, you will need some version of the Java 2 Development Kit (JDK). While the JDK version shouldn’t matter for this implementation, it is important to point out that all the code referenced here was tested with version 1.3. See the CF documentation for information on configuring CF to work with Java.
Creating a Shell Java Encryption Class
The first step is to create a shell class to handle the encryption. For lack of imagination I’ll call this class “Encryption.” You need to import the Java Security API package, which implements many different types of encryption. In this example, you’ll use public/private key encryption and create two private variables to store our public and private keys. Here’s the code for the Encryption class.
import java.security.*; public class Encryption { private PublicKey publicKey = null; private PrivateKey privateKey = null; }
Generating a Key PairWith the shell class in place you can get right down to implementation. Next, you need to generate a key pair. The key pair lets you create signatures with your private key and verify them with your public key. It is perfectly secure to share the public key with third parties to verify the signatures. Sharing your public key with partners or other applications is one common way to share authentication information. If you are going to share your public key with third parties, they need to be able to ensure or “trust” that your public key is valid, but because this example is only concerned with internal use of the public key, I won’t address trust in this article. You will need to create only a single key pair for all of the example’s encryption needs. While the public key can be shared, the private key needs to be secured. The JDK contains a program to manage your keys securely named keyring. For simplicity however, this example just stores the keys in the CF application server file system, under the assumption that only authorized people can access them.
You need a method to generate the keys. I’ll call this method generateKeys. The generateKeys method needs to create a key pair generator instance. To do that, you must tell it which encryption algorithm to use?the example uses DSA for simplicity. Optionally, you can also specify a security provider. For simplicity, the example doesn’t specify a provider; instead, it lets the key pair generator pick the provider based on the security properties setup of the Java Runtime Environment (JRE). The following line instantiates a new key pair generator.
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(“DSA”);
The preceding code sets the keyGen variable to a new KeyPairGenerator instance using the Digital Signature Algorithm (DSA). The next step is to initialize the key pair generator. The initialize method takes two parameters: bit length, and an instance of a random object. The bit length has a major effect on the strength of the encryption. Shorter bit lengths are less secure while longer bit lengths can lead to performance problems. The example uses a 1024-bit key, which should be sufficiently secure but is short enough to avoid major performance problems. Again, for simplicity, the example uses the default secure random object?for DSA that’s the Secure Hash Algorithm (SHA1). After initializing the key pair generator you can call the generateKeyPair method to create a KeyPair instance. keyGen.initialize(1024, new SecureRandom()); KeyPair pair = keyGen.generateKeyPair(); this.privateKey = pair.getPrivate(); this.publicKey = pair.getPublic();
The preceding code initializes the key pair generator, generates a new key pair, and calls the getPrivate and getPublic methods to set the two private variables. Now that you have a key pair you’re ready to create and verify signatures. However, because you don’t want to generate a new key pair each time you create or verify signatures you need some way of persisting the keys. The easiest solution is to write the keys to the file system. Luckily, the National Institute of Standards and Technology (NIST) has defined standards on how to store public and private keys. These standards are known as Private-key Information Syntax Standard (PKCS8) for private keys and X.509 for public keys.Reading and Writing the Keys
The Java Security API provides the java.security.spec package that implements these standards. You can write to the file system without the assistance of java.security.spec, but reading is a completely different matter, because the key objects implement the serializable interface. Therefore, you need to import two additional packages, java.io and java.security.spec.
import java.io.*; import java.security.spec.*;
After importing those two packages you can create methods to read and write keys using the file system. The code below shows the implementation for reading and writing keys. public void writeKeys(String publicURI, String privateURI) throws Exception { writePublicKey(publicURI); writePrivateKey(privateURI); } public void writePublicKey(String URI) throws Exception { byte[] enckey = publicKey.getEncoded(); FileOutputStream keyfos = new FileOutputStream(URI); keyfos.write(enckey); keyfos.close(); } public void writePrivateKey(String URI) throws Exception { byte[] enckey = privateKey.getEncoded(); FileOutputStream keyfos = new FileOutputStream(URI); keyfos.write(enckey); keyfos.close(); } public void readKeys(String publicURI, String privateURI) throws Exception { readPublicKey(publicURI); readPrivateKey(privateURI); } public void readPublicKey(String URI) throws Exception { FileInputStream keyfis = new FileInputStream(URI); byte[] encKey = new byte[keyfis.available()]; keyfis.read(encKey); keyfis.close(); X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encKey); KeyFactory keyFactory = KeyFactory.getInstance(“DSA”); publicKey = keyFactory.generatePublic(pubKeySpec); } public void readPrivateKey(String URI) throws Exception { FileInputStream keyfis = new FileInputStream(URI); byte[] encKey = new byte[keyfis.available()]; keyfis.read(encKey); keyfis.close(); PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(encKey); KeyFactory keyFactory = KeyFactory.getInstance(“DSA”); privateKey = keyFactory.generatePrivate(privKeySpec); }
Creating a Digital Signature
Now that you have the ability to generate a key pair and persist and retrieve them from the file system you’re ready to create a digital signature. First, use the getInstance method to obtain an instance of the Signature object. Like the key pair generator, the method expects a parameter indicating which algorithm to use, and also accepts an optional provider parameter, but the example code skips the provider for simplicity. After creating an instance of the Signature object you need to initialize it for signing. To do that, call the initSign method, which expects a privateKey object parameter.
After initializing the Signature object you need to pass it the data to be signed. This data must be unique for each user, so we will want to pass it the user’s ID, which in this case is simply their email address. The Signature object has an update method that you can use to pass it the data to be signed. The update method expects a byte array as a parameter, but that won’t work well for passing data from CF, because CF can’t pass structured data to Java. Fortunately, the Java String class provides a method called getBytes, which accepts a string and returns a byte array. You can call the update method as many times as you like to feed the Signature object data to sign. When you’re done feeding it data you call the sign method to get the digital signature. Again, you have a problem because the sign method returns a byte array, but again, the String class saves you by offering a constructor that accepts a byte array. Here’s the sign method.
public String sign(String buffer) throws Exception { Signature dsa = Signature.getInstance(“SHA/DSA”); dsa.initSign(privateKey); dsa.update(buffer.getBytes()); String signature = new String(dsa.sign()); return signature; }
Verifying a Digital Signature
The Java Encryption class is almost complete. All you need now is a method to verify signatures, and again, you can use the Signature class. Create a Signature instance and initialize it for signing. Next, pass the same data you used to create the previous signature through the update method. Again, you can call the update method as many times as you want. After feeding all the data to the Signature object you can verify it by calling the verify method. Pass the signature you created previously with the sign method. The verify method expects the signature to be a byte array, so you use the String getBytes method to work around CF’s shortcomings. The verify method returns a Boolean value indicating whether the signature is valid for the data supplied to it. Here’s the verify method implementation.
public boolean verify(String signature, String buffer) throws Exception { Signature dsa = Signature.getInstance(“SHA/DSA”); dsa.initVerify(publicKey); dsa.update(buffer.getBytes()); return dsa.verify(signature.getBytes()); }
Adding the verify method completes the Encryption class. Before you can call it from CF, you need to compile the class. You will also need to update CF’s classpath environment variable in the CF administrator under the Java settings section. Take a look at the documentation for help on how to do this.Calling the Encryption Class from CF
Using the Encryption class from CF is rather easy thanks to the CFObject tag. It lets you create Java class instances and call their public methods. There are a few problems related to use of CFObject and Java classes. One of those problems is how CF deals with overloaded methods. Defining more than one method in a single class with the same name is known as method overloading. Java uses the method argument variable types to determine which method to use for any given call. However, because CF is a typeless language, it is often hard or impossible for CF to resolve overloaded methods. For this reason I recommend not using overloaded methods in classes that are to be called from CF. Another problem with CFObject to beware of is that you have to restart the CF application server anytime you make changes to any Java class files that you call.
First, create an Encryption class instance and use it to generate a key pair and write them to disk.
crypt.generateKeys(); crypt.writeKeys(“c:public.key”, “c:private.key”);
The preceding example uses the CFObject tag to create an instance of class Encryption and assign it to a local CF variable. You can also create a pointer to the Encryption class using the CreateObject function; however, you will need to call the class’ init method to instantiate it. Here’s an example. crypt = CreateObject(“JAVA”, “Encryption”); crypt.init();
After creating the instance of the Encryption class you can call methods in much the same way you would in Java. After executing the preceding code you have the public and private keys written to disk. With those in hand you can create a signature. Assuming that you’re going to create the signature in a different CS script you need to create a new instance of the Encryption class. You won’t be generating a key pair this time, but you need to read in the private key before creating the signature. You don’t need the public key to create a signature. crypt.readPrivateKey(“c:private.key”); signature = ToBase64(crypt.sign(“[email protected]”));
The preceding code successfully signs my email address and assigns the newly created digital signature it to the local CF variable signature. However, it may be hard to use the signature in its current form because it is binary data. The CF function ToBase64 converts binary strings into base64 strings. You can display Base64 strings as plain text with no problems. To verify the signature you need to convert it back into binary by calling the CF ToBinary function. After converting it back to binary, verification is a simple matter of calling the verify method. However, the verify method needs the public key to verify the signature, so you need to read first. crypt.readPublicKey(“c:public.key”); verified = crypt.verify(ToBinary(signature), “[email protected]”);
After executing the preceding code, the local CF variable verified contains a Boolean value indicating whether the signature is valid. Next, you need to be able to persist the signature on the user’s machine using a cookie. Because you need to write both the user’s email address and signature you’ll need to make two calls to the CFCookie tag.
Automatically logging in a user is now a simple matter of verifying their signature. For example, in a separate script you would write: crypt = CreateObject(“Java”, “Encryption”); crypt.init(); crypt.readPublicKey(“c:public.key”); verified = crypt.verify(ToBinary(cookie.signature), cookie.email);
At this point you should be able to generate key pairs, generate and verify signatures for arbitrary data, and ultimately, use Java’s encryption classes instead of the built-in CF encryption functions.