From 382dc0c47ad79c241afa8e11ca8274c5a9f6e675 Mon Sep 17 00:00:00 2001
From: david_page <david_page@localhost>
Date: Fri, 07 Sep 2007 05:00:12 +0000
Subject: [PATCH] Interim update to allow stream ciphers where no initialization vector is allowed (e.g., "RC4"). Cipher.getBlockSize equal to 0 is used to detect this case, hence this implementation likely still does not support block cipher modes that do not accept initialization vectors (e.g., ECB).
---
opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/types/CryptoManagerTestCase.java | 147 ++++++++++++++++++
opendj-sdk/opends/src/server/org/opends/server/types/CryptoManager.java | 292 ++++++++++++++++++++++++++----------
2 files changed, 356 insertions(+), 83 deletions(-)
diff --git a/opendj-sdk/opends/src/server/org/opends/server/types/CryptoManager.java b/opendj-sdk/opends/src/server/org/opends/server/types/CryptoManager.java
index aae5be2..1e8f79a 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/types/CryptoManager.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/types/CryptoManager.java
@@ -94,17 +94,16 @@
private final Random pseudoRandom
= new Random(secureRandom.nextLong());
- // The map from encryption key ID to cipher algorithm name.
- private final HashMap<ByteArray, String> cipherTransformationMap
- = new HashMap<ByteArray, String>();
-
- // The map from encryption key ID to cipher key.
- private final HashMap<ByteArray, byte[]> secretKeyMap
- = new HashMap<ByteArray, byte[]>();
+ // The map from encryption key ID to KeyEntry (cache).
+ private final HashMap<ByteArray, KeyEntry> keyEntryMap
+ = new HashMap<ByteArray, KeyEntry>();
// The preferred cipher for the Directory Server.
private final String preferredCipherTransformation;
+ // The preferred key length for the preferred cipher.
+ private final int preferredCipherTransformationKeyLength;
+
// The preferred message digest algorithm for the Directory Server.
private final String preferredDigestAlgorithm;
@@ -147,6 +146,7 @@
preferredDigestAlgorithm = "SHA-1";
preferredMACAlgorithm = "HmacSHA1";
preferredCipherTransformation = "AES/CBC/PKCS5Padding";
+ preferredCipherTransformationKeyLength = 128;
sslCertNickname = cfg.getSSLCertNickname();
sslEncryption = cfg.isSSLEncryption();
@@ -646,14 +646,72 @@
/**
+ * This class corresponds to the encryption key entry in ADS. It is
+ * used for the local cache of key entries requested by
+ * CryptoManager users.
+ */
+ private static class KeyEntry
+ {
+ /**
+ * Construct an instance of KeyEntry using the specified
+ * parameters.
+ *
+ * @param keyID The unique identifier of this cipher
+ * transformation/key pair.
+ * @param transformation The cipher transformation name.
+ * @param encryptionKey The cipher key.
+ * @param initializationVectorLength The length of the required
+ * initialization vector or 0 if none is required.
+ */
+ public KeyEntry( byte[] keyID,
+ String transformation,
+ byte[] encryptionKey,
+ int initializationVectorLength){
+ this.keyID = new byte[keyID.length];
+ System.arraycopy(keyID, 0, this.keyID, 0, keyID.length);
+ this.transformation = new String(transformation);
+ this.encryptionKey = new byte[encryptionKey.length];
+ System.arraycopy(encryptionKey, 0,
+ this.encryptionKey, 0, encryptionKey.length);
+ this.initializationVectorLength = initializationVectorLength;
+ }
+
+ /**
+ * The unique identifier of this cipher transformation/key pair.
+ */
+ public final byte[] keyID;
+
+ /**
+ * The cipher transformation name.
+ */
+ public final String transformation;
+
+ /**
+ * The cipher key.
+ */
+ public final byte[] encryptionKey;
+
+ /**
+ * The initializationVectorLength (may be 0).
+ */
+ public final int initializationVectorLength;
+ }
+
+
+
+ /**
* Retrieves an encryption mode cipher using the specified
* algorithm.
*
* @param cipherTransformation The algorithm/mode/padding to use
* for the cipher.
*
- * @param keyIdentifier The key identifier assigned to the cipher
- * key used by the cipher.
+ * @param keyLength The length in bits of the encryption key this
+ * method will generate. Note the specified key length must
+ * be compatible with the transformation.
+ *
+ * @param keyIdentifier The key identifier this method assigns to
+ * the cipher transformation/key/initializationVectorLength.
*
* @return An encryption mode cipher using the preferred algorithm.
*
@@ -674,9 +732,10 @@
* initialization vector used.
*
* @throws CryptoManagerException If there is a problem managing
- * the encryption key.
+ * or generating an encryption key.
*/
private Cipher getEncryptionCipher(String cipherTransformation,
+ int keyLength,
byte[] keyIdentifier)
throws NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeyException,
@@ -685,26 +744,35 @@
{
Validator.ensureNotNull(cipherTransformation, keyIdentifier);
- byte[] keyID = null;
- for (Map.Entry<ByteArray, String> cipherEntry
- : cipherTransformationMap.entrySet()) {
- if (cipherEntry.getValue().equals(cipherTransformation)) {
- keyID = cipherEntry.getKey().array();
+ KeyEntry keyEntry = null;
+ for (Map.Entry<ByteArray, KeyEntry> keyEntryRow
+ : keyEntryMap.entrySet()) {
+ KeyEntry keyEntryIterator = keyEntryRow.getValue();
+ if (keyEntryIterator.transformation.equals(cipherTransformation)
+ && keyEntryIterator.encryptionKey.length == keyLength) {
+ assert Arrays.equals(keyEntryRow.getKey().array(),
+ keyEntryIterator.keyID);
+ keyEntry = keyEntryIterator;
break;
}
}
- byte[] secretKey;
- if (null != keyID) {
- secretKey = secretKeyMap.get(new ByteArray(keyID));
- } else {
+ final Cipher cipher = Cipher.getInstance(cipherTransformation);
+ if (null == keyEntry) {
// generate a new key
- secretKey = new byte[16]; // FIXME: not all keys are 128-bits
+ if (0 != keyLength % Byte.SIZE) {
+ throw new CryptoManagerException(
+ // TODO: i18n
+ Message.raw("keyLength parameter must be evenly " +
+ "divisible by %d.", Byte.SIZE));
+ }
+ final byte[] keyID = uuidToBytes(UUID.randomUUID());
+ byte[] secretKey = new byte[keyLength/Byte.SIZE];
secureRandom.nextBytes(secretKey);
- keyID = uuidToBytes(UUID.randomUUID());
- final ByteArray mapKey = new ByteArray(keyID);
- secretKeyMap.put(mapKey, secretKey);
- cipherTransformationMap.put(mapKey, cipherTransformation);
+ final int ivLength = (0 == cipher.getBlockSize())
+ ? 0 : keyLength;
+ keyEntry = new KeyEntry(keyID, cipherTransformation,
+ secretKey, ivLength);
}
// E.g., AES/CBC/PKCS5Padding -> AES
@@ -713,17 +781,22 @@
? cipherTransformation.substring(0, separatorIndex)
: cipherTransformation;
- // TODO: initialization vector length: key length?
- final byte[] initializationVector = new byte[16];
- pseudoRandom.nextBytes(initializationVector);
-
- final Cipher cipher = Cipher.getInstance(cipherTransformation);
- cipher.init(Cipher.ENCRYPT_MODE,
- new SecretKeySpec(secretKey, cipherAlgorithm),
- new IvParameterSpec(initializationVector));
+ if (0 < keyEntry.initializationVectorLength) {
+ final byte[] initializationVector
+ = new byte[keyEntry.initializationVectorLength/Byte.SIZE];
+ pseudoRandom.nextBytes(initializationVector);
+ cipher.init(Cipher.ENCRYPT_MODE,
+ new SecretKeySpec(keyEntry.encryptionKey, cipherAlgorithm),
+ new IvParameterSpec(initializationVector));
+ }
+ else {
+ cipher.init(Cipher.ENCRYPT_MODE,
+ new SecretKeySpec(keyEntry.encryptionKey, cipherAlgorithm));
+ }
try {
- System.arraycopy(keyID, 0, keyIdentifier, 0, keyID.length);
+ System.arraycopy(keyEntry.keyID, 0, keyIdentifier, 0,
+ keyIdentifier.length);
}
catch (Exception ex) {
throw new CryptoManagerException(
@@ -732,6 +805,8 @@
ex);
}
+ keyEntryMap.put(new ByteArray(keyEntry.keyID), keyEntry);
+
return cipher;
}
@@ -742,7 +817,9 @@
* encryption key specified by the key identifier and the supplied
* initialization vector.
*
- * @param keyID The ID of the key to be used for decryption.
+ * @param keyEntry The keyEntry containing the key to be used for
+ * decryption.
+ *
* @param initializationVector The initialization vector to be used
* for decryption.
*
@@ -766,36 +843,33 @@
*
* @throws CryptoManagerException If an invalid keyID is supplied.
*/
- private Cipher getDecryptionCipher(byte[] keyID,
+ private Cipher getDecryptionCipher(KeyEntry keyEntry,
byte[] initializationVector)
throws NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeyException,
InvalidAlgorithmParameterException,
CryptoManagerException
{
- Validator.ensureNotNull(keyID, initializationVector);
-
- final ByteArray mapKey = new ByteArray(keyID);
- final String cipherTransformation
- = cipherTransformationMap.get(mapKey);
- final byte[] secretKey = secretKeyMap.get(mapKey);
- if (null == cipherTransformation || null == secretKey) {
- throw new CryptoManagerException(
- // TODO: i18n
- Message.raw("Invalid encryption key identifier %s.",
- mapKey));
- }
+ Validator.ensureNotNull(keyEntry);
+ Validator.ensureTrue(0 == keyEntry.initializationVectorLength
+ || null != initializationVector);
+ final Cipher cipher = Cipher.getInstance(keyEntry.transformation);
// E.g., AES/CBC/PKCS5Padding -> AES
- final int separatorIndex = cipherTransformation.indexOf('/');
+ final int separatorIndex = keyEntry.transformation.indexOf('/');
final String cipherAlgorithm = (0 < separatorIndex)
- ? cipherTransformation.substring(0, separatorIndex)
- : cipherTransformation;
+ ? keyEntry.transformation.substring(0, separatorIndex)
+ : keyEntry.transformation;
- final Cipher cipher = Cipher.getInstance(cipherTransformation);
- cipher.init(Cipher.DECRYPT_MODE,
- new SecretKeySpec(secretKey, cipherAlgorithm),
- new IvParameterSpec(initializationVector));
+ if (0 < keyEntry.initializationVectorLength) {
+ cipher.init(Cipher.DECRYPT_MODE,
+ new SecretKeySpec(keyEntry.encryptionKey, cipherAlgorithm),
+ new IvParameterSpec(initializationVector));
+ }
+ else {
+ cipher.init(Cipher.DECRYPT_MODE,
+ new SecretKeySpec(keyEntry.encryptionKey, cipherAlgorithm));
+ }
return cipher;
}
@@ -822,7 +896,8 @@
throws GeneralSecurityException,
CryptoManagerException
{
- return encrypt(getPreferredCipherTransformation(), data);
+ return encrypt(preferredCipherTransformation,
+ preferredCipherTransformationKeyLength, data);
}
@@ -833,7 +908,10 @@
*
* @param cipherTransformation The algorithm to use to encrypt the
* data.
- * @param data The data to be encrypted.
+ *
+ * @param keyLength The length of the encrytion key to use.
+ *
+ * @param data The data to be encrypted.
*
* @return A byte array containing the encrypted representation of
* the provided data.
@@ -845,20 +923,24 @@
* @throws CryptoManagerException If a problem occurs managing the
* encryption key.
*/
- public byte[] encrypt(String cipherTransformation, byte[] data)
+ public byte[] encrypt(String cipherTransformation, int keyLength,
+ byte[] data)
throws GeneralSecurityException, CryptoManagerException
{
Validator.ensureNotNull(cipherTransformation, data);
final byte[] keyID = new byte[16];// FIXME: key id length constant
- final Cipher cipher
- = getEncryptionCipher(cipherTransformation, keyID);
+ final Cipher cipher = getEncryptionCipher(cipherTransformation,
+ keyLength, keyID);
final byte[] iv = cipher.getIV();
- final int prologueLength = keyID.length + iv.length;
+ final int prologueLength
+ = keyID.length + ((null == iv) ? 0 : iv.length);
final int dataLength = cipher.getOutputSize(data.length);
final byte[] cipherText = new byte[prologueLength + dataLength];
System.arraycopy(keyID, 0, cipherText, 0, keyID.length);
- System.arraycopy(iv, 0, cipherText, keyID.length, iv.length);
+ if (null != iv) {
+ System.arraycopy(iv, 0, cipherText, keyID.length, iv.length);
+ }
System.arraycopy(cipher.doFinal(data), 0, cipherText,
prologueLength, dataLength);
return cipherText;
@@ -887,8 +969,8 @@
OutputStream outputStream)
throws GeneralSecurityException, CryptoManagerException
{
- return getCipherOutputStream(getPreferredCipherTransformation(),
- outputStream);
+ return getCipherOutputStream(preferredCipherTransformation,
+ preferredCipherTransformationKeyLength, outputStream);
}
@@ -899,6 +981,9 @@
*
* @param cipherTransformation The algorithm to use to encrypt the
* data.
+ *
+ * @param keyLength The length of the encrytion key to use.
+ *
* @param outputStream The output stream to be wrapped by the
* returned output stream.
*
@@ -913,15 +998,18 @@
* encryption key.
*/
public CipherOutputStream getCipherOutputStream(
- String cipherTransformation, OutputStream outputStream)
+ String cipherTransformation, int keyLength,
+ OutputStream outputStream)
throws GeneralSecurityException, CryptoManagerException
{
final byte[] keyID = new byte[16];// FIXME: key id length constant
- final Cipher cipher
- = getEncryptionCipher(cipherTransformation, keyID);
+ final Cipher cipher = getEncryptionCipher(cipherTransformation,
+ keyLength, keyID);
try {
outputStream.write(keyID);
- outputStream.write(cipher.getIV());
+ if (null != cipher.getIV()) {
+ outputStream.write(cipher.getIV());
+ }
}
catch (IOException ioe) {
throw new CryptoManagerException(
@@ -956,21 +1044,41 @@
throws GeneralSecurityException,
CryptoManagerException
{
- final byte[] keyID = new byte[16];
- final byte[] iv = new byte[16]; // FIXME: always 128 bits?
- final int prologueLength = keyID.length + iv.length;
+ final byte[] keyID = new byte[16]; //FIXME: key length constant
try {
System.arraycopy(data, 0, keyID, 0, keyID.length);
- System.arraycopy(data, keyID.length, iv, 0, iv.length);
}
catch (Exception ex) {
throw new CryptoManagerException(
- // TODO: i18n
- Message.raw("Exception when reading CryptoManager prologue."),
- ex);
+ // TODO: i18n
+ Message.raw(
+ "Exception when reading key identifier from data prologue."),
+ ex);
+ }
+ KeyEntry keyEntry = keyEntryMap.get(new ByteArray(keyID));
+ if (null == keyEntry) {
+ throw new CryptoManagerException(
+ // TODO: i18N
+ Message.raw("Invalid key identifier in data prologue."));
}
- Cipher cipher = getDecryptionCipher(keyID, iv);
+ byte[] iv = null;
+ if (0 < keyEntry.initializationVectorLength) {
+ iv = new byte[keyEntry.initializationVectorLength/Byte.SIZE];
+ try {
+ System.arraycopy(data, keyID.length, iv, 0, iv.length);
+ }
+ catch (Exception ex) {
+ throw new CryptoManagerException(
+ // TODO: i18n
+ Message.raw("Exception when reading initialization" +
+ " vector from data prologue."), ex);
+ }
+ }
+
+ final Cipher cipher = getDecryptionCipher(keyEntry, iv);
+ final int prologueLength
+ = keyID.length + ((null == iv) ? 0 : iv.length);
return cipher.doFinal(data, prologueLength,
data.length - prologueLength);
}
@@ -1012,15 +1120,33 @@
InvalidAlgorithmParameterException,
CryptoManagerException
{
- byte[] keyID = new byte[16];
- byte[] iv = new byte[16]; // FIXME: not all ivs are key length
+ KeyEntry keyEntry;
+ byte[] iv = null;
try {
- if (keyID.length != inputStream.read(keyID)
- || iv.length != inputStream.read(iv)){
+ final byte[] keyID = new byte[16]; //FIXME: key length constant
+ if (keyID.length != inputStream.read(keyID)){
throw new CryptoManagerException(
- // TODO: i18n
- Message.raw(
- "Stream underflow when reading CryptoManager prologue."));
+ // TODO: i18n
+ Message.raw("Stream underflow when reading key" +
+ " identifier from data prologue."));
+ }
+
+ keyEntry = keyEntryMap.get(new ByteArray(keyID));
+ if (null == keyEntry) {
+ throw new CryptoManagerException(
+ // TODO: i18N
+ Message.raw("Invalid key identifier in data prologue."));
+ }
+
+ if (0 < keyEntry.initializationVectorLength) {
+ iv = new byte[keyEntry.initializationVectorLength/Byte.SIZE];
+ if (iv.length != inputStream.read(iv)) {
+ throw new CryptoManagerException(
+ // TODO: i18n
+ Message.raw("Stream underflow when reading" +
+ " initialization vector from data prologue."));
+ }
+
}
}
catch (IOException ioe) {
@@ -1032,7 +1158,7 @@
}
return new CipherInputStream(inputStream,
- getDecryptionCipher(keyID, iv));
+ getDecryptionCipher(keyEntry, iv));
}
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/types/CryptoManagerTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/types/CryptoManagerTestCase.java
index 26bff26..9c98052 100644
--- a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/types/CryptoManagerTestCase.java
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/types/CryptoManagerTestCase.java
@@ -45,6 +45,8 @@
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
+import javax.crypto.Cipher;
+
/**
This class tests the CryptoManager.
*/
@@ -105,6 +107,8 @@
os.write(secretMessage.getBytes());
os.close();
+ // TODO: check tempfile for plaintext.
+
InputStream is = new FileInputStream(tempFile);
is = cm.getCipherInputStream(is);
byte[] plainText = new byte[secretMessage.getBytes().length];
@@ -115,4 +119,147 @@
}
// TODO: other-than-preferred cipher algorithms, failure cases...
+ /**
+ Tests a simple encryption-decryption cycle.
+
+ @throws Exception If an exceptional condition arises.
+ */
+ @Test
+ public void testEncryptDecryptSuccessX() throws Exception {
+ final CryptoManager cm = DirectoryServer.getCryptoManager();
+ final String secretMessage = "1234";
+
+ final byte[] cipherText = cm.encrypt("Blowfish/CFB/NoPadding", 64,
+ secretMessage.getBytes());
+ assertEquals(-1, (new String(cipherText)).indexOf(secretMessage));
+
+ final byte[] plainText = cm.decrypt(cipherText);
+ assertEquals((new String(plainText)), secretMessage);
+ }
+
+ /**
+ Tests a simple cipher stream encryption-decryption cycle.
+
+ @throws Exception If an exceptional condition arises.
+ */
+ @Test
+ public void testCipherEncryptDecryptSuccessX() throws Exception {
+ final CryptoManager cm = DirectoryServer.getCryptoManager();
+ final String secretMessage = "56789";
+
+ final File tempFile
+ = File.createTempFile(cm.getClass().getName(), null);
+ tempFile.deleteOnExit();
+
+ OutputStream os = new FileOutputStream(tempFile);
+ os = cm.getCipherOutputStream("Blowfish/CFB/NoPadding", 64, os);
+ os.write(secretMessage.getBytes());
+ os.close();
+
+ // TODO: check tempfile for plaintext.
+
+ InputStream is = new FileInputStream(tempFile);
+ is = cm.getCipherInputStream(is);
+ byte[] plainText = new byte[secretMessage.getBytes().length];
+ assertEquals(is.read(plainText), secretMessage.getBytes().length);
+ assertEquals(is.read(), -1);
+ is.close();
+ assertEquals(new String(plainText), secretMessage);
+ }
+
+ /**
+ Tests a simple encryption-decryption cycle.
+
+ @throws Exception If an exceptional condition arises.
+ */
+ @Test
+ public void testEncryptDecryptSuccessY() throws Exception {
+ final CryptoManager cm = DirectoryServer.getCryptoManager();
+ final String secretMessage = "1234";
+
+ final byte[] cipherText = cm.encrypt("RC4", 104,
+ secretMessage.getBytes());
+ assertEquals(-1, (new String(cipherText)).indexOf(secretMessage));
+
+ final byte[] plainText = cm.decrypt(cipherText);
+ assertEquals((new String(plainText)), secretMessage);
+ }
+
+ /**
+ Tests a simple cipher stream encryption-decryption cycle.
+
+ @throws Exception If an exceptional condition arises.
+ */
+ @Test
+ public void testCipherEncryptDecryptSuccessY() throws Exception {
+ final CryptoManager cm = DirectoryServer.getCryptoManager();
+ final String secretMessage = "56789";
+
+ final File tempFile
+ = File.createTempFile(cm.getClass().getName(), null);
+ tempFile.deleteOnExit();
+
+ OutputStream os = new FileOutputStream(tempFile);
+ os = cm.getCipherOutputStream("RC4", 104, os);
+ os.write(secretMessage.getBytes());
+ os.close();
+
+ // TODO: check tempfile for plaintext.
+
+ InputStream is = new FileInputStream(tempFile);
+ is = cm.getCipherInputStream(is);
+ byte[] plainText = new byte[secretMessage.getBytes().length];
+ assertEquals(is.read(plainText), secretMessage.getBytes().length);
+ assertEquals(is.read(), -1);
+ is.close();
+ assertEquals(new String(plainText), secretMessage);
+ }
+
+ /**
+ Tests a simple encryption-decryption cycle.
+
+ @throws Exception If an exceptional condition arises.
+ */
+ @Test
+ public void testEncryptDecryptSuccessZ() throws Exception {
+ final CryptoManager cm = DirectoryServer.getCryptoManager();
+ final String secretMessage = "1234";
+
+ final byte[] cipherText = cm.encrypt("DES/CFB/NoPadding", 64,
+ secretMessage.getBytes());
+ assertEquals(-1, (new String(cipherText)).indexOf(secretMessage));
+
+ final byte[] plainText = cm.decrypt(cipherText);
+ assertEquals((new String(plainText)), secretMessage);
+ }
+
+ /**
+ Tests a simple cipher stream encryption-decryption cycle.
+
+ @throws Exception If an exceptional condition arises.
+ */
+ @Test
+ public void testCipherEncryptDecryptSuccessZ() throws Exception {
+ final CryptoManager cm = DirectoryServer.getCryptoManager();
+ final String secretMessage = "56789";
+
+ final File tempFile
+ = File.createTempFile(cm.getClass().getName(), null);
+ tempFile.deleteOnExit();
+
+ OutputStream os = new FileOutputStream(tempFile);
+ os = cm.getCipherOutputStream("DES/CFB/NoPadding", 64, os);
+ os.write(secretMessage.getBytes());
+ os.close();
+
+ // TODO: check tempfile for plaintext.
+
+ InputStream is = new FileInputStream(tempFile);
+ is = cm.getCipherInputStream(is);
+ byte[] plainText = new byte[secretMessage.getBytes().length];
+ assertEquals(is.read(plainText), secretMessage.getBytes().length);
+ assertEquals(is.read(), -1);
+ is.close();
+ assertEquals(new String(plainText), secretMessage);
+ }
}
--
Gitblit v1.10.0