PCI DSS, key management and Keyczar

If your application has to store credit card or other payment card information, it’s important to keep the PCI DSS (Payment Card Industry Data Security Standards) in mind. These are a set of standards from our friends in the payment card industry about handling sensitive data around credit and debit cards.

Among the myriad standards, there are a couple that specify key management requirements. It’s not enough to encrypt the data – the keys themselves must be managed in a way that allow for generation, secure transmission, storage, replacement and revocation of keys (DSS requirements 3.6.1 – 3.6.5 for those of you following along at home). There are a number of commercial solutions that handle this snappily, but they’re not cheap.

I was looking to solve this problem for a project: how can we easily manage keys and key rotation for encrypted field? Ben pointed me towards Google’s open-source Keyczar. After spending a few hours with it, it looks like we’ll be able to use it to manage our requirements. Even if it doesn’t work out for us, it’s useful enough that others might be able to use it.

Keyczar is actually as easy to use as it claims to be. First you create a keyset and add a key to it:

KeyczarTool create --location=/path/to/keys --purpose=crypt
KeyczarTool addkey --location=/path/to/keys --status=primary --size=128

And then in your Java code:

Crypter crypter = new Crypter("/path/to/keys");
String encryptedText = crypter.encrypt("some arbitrary plaintext");

Then say it’s time to use a new encryption key, say a 256-bit key instead of a 128-bit one. We demote the existing key and add a new primary key.

KeyczarTool demote --location=/path/to/keys --version=1
KeyczarTool addkey --location=/path/to/keys --status=primary --size=256

Any text that is encrypted with this keyset now will use the newest key. Any ciphertext that was encoded using the old key can still be decrypted (until/unless that version of the key is revoked). This is possible because the encryption scheme includes dumping a hash of the key into the ciphertext. This allows the correct key to be retrieved from the keyset if it is available.

There are a few issues that I’ve noticed so far:

  • there is no easy way to generate a key programatically for testing. This is by design: Keyczar separates the key management tool from the use of the keys generated by the tool.
  • AES 256-bit encryption is broken in the current build (but there is a source patch). There’s also the issue of enabling 256-bit encryption in the JCE, but that’s not the tool’s fault.
  • it’s not backwards compatible with any other encryption system.

I’m sure I’ll discover more problems with it as I actually try to use it in a real system. It seems pretty solid right now. For the record, it handles asymmetric encryption and hashing as well as the symmetric encryption that I’m using it for.

Here’s the code I’ve actually been testing it with. It’s a snippet from a JUnit test – I find that’s often the easiest way to play with new code in a meaningful way. Note the wacky way that I’m invoking KeyczarTool – it doesn’t have any accessible methods other than main().

private final static String DIRECTORY = "/path/to/keys";

	@Test
	public void testEncrypt() throws Exception {
		String plaintext = "Text to encrypt";

		KeyczarTool.main(new String[] {"create", "--location=" + DIRECTORY, "--purpose=crypt"});
		KeyczarTool.main(new String[] {"addkey", "--location=" + DIRECTORY, "--status=primary"});

		Crypter crypter = new Crypter(DIRECTORY);
		String result1 = crypter.encrypt(plaintext);
		String result1a = crypter.encrypt(plaintext);

		Assert.assertFalse("ciphertext is different when encrypting same plaintext", result1.equals(result1a));
		Assert.assertEquals("decrypt result1", plaintext, crypter.decrypt(result1));
		Assert.assertEquals("decrypt result1a", plaintext, crypter.decrypt(result1a));

		// demote key version 1 to ACTIVE
		KeyczarTool.main(new String[] {"demote", "--location=" + DIRECTORY, "--version=1"});
		crypter = new Crypter(DIRECTORY);
		Assert.assertEquals("decrypt result1 after demotion", plaintext, crypter.decrypt(result1));
		Assert.assertEquals("decrypt result1a after demotion", plaintext, crypter.decrypt(result1a));

		// demote key version 1 to INACTIVE
		KeyczarTool.main(new String[] {"demote", "--location=" + DIRECTORY, "--version=1"});
		crypter = new Crypter(DIRECTORY);
		Assert.assertEquals("decrypt result1 after second demotion", plaintext, crypter.decrypt(result1));
		Assert.assertEquals("decrypt result1a after second demotion", plaintext, crypter.decrypt(result1a));

		// create key version 2
		KeyczarTool.main(new String[] {"addkey", "--location=" + DIRECTORY, "--status=primary"});
		crypter = new Crypter(DIRECTORY);
		String result2 = crypter.encrypt(plaintext);

		// revoke key version 1, the try to decrypt both version 1 and version 2 ciphertext
		KeyczarTool.main(new String[] {"revoke", "--location=" + DIRECTORY, "--version=1"});
		crypter = new Crypter(DIRECTORY);
		try {
			// result 1 cannot be decrypted any more
			crypter.decrypt(result1);
			Assert.fail("expected key not found exception");
		} catch (KeyNotFoundException knfe) {
			// expected
		}
		Assert.assertEquals("result2 decrypts properly", plaintext, crypter.decrypt(result2));
	}

It's only fair to share...
Share on FacebookGoogle+Tweet about this on TwitterShare on LinkedIn

Leave a Reply