mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

david_page
07.00.2007 2b576336c489fa56c61fafc564ee10419bff1c60
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).

One additional remaining inadequacy is the case where the initialization vector is not the same length as the encryption key (e.g., ""Blowfish/CFB/NoPadding" allows a key length up to 448 bits, but the initialization vector is always 64 bits).
2 files modified
417 ■■■■ changed files
opends/src/server/org/opends/server/types/CryptoManager.java 270 ●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/types/CryptoManagerTestCase.java 147 ●●●●● patch | view | raw | blame | history
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];
    if (0 < keyEntry.initializationVectorLength) {
      final byte[] initializationVector
            = new byte[keyEntry.initializationVectorLength/Byte.SIZE];
    pseudoRandom.nextBytes(initializationVector);
    final Cipher cipher = Cipher.getInstance(cipherTransformation);
    cipher.init(Cipher.ENCRYPT_MODE,
                new SecretKeySpec(secretKey, cipherAlgorithm),
           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);
    if (0 < keyEntry.initializationVectorLength) {
    cipher.init(Cipher.DECRYPT_MODE,
                new SecretKeySpec(secretKey, cipherAlgorithm),
           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,6 +908,9 @@
   *
   * @param  cipherTransformation  The algorithm to use to encrypt the
   *                          data.
   *
   * @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
@@ -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);
    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,16 +998,19 @@
   *          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);
      if (null != cipher.getIV()) {
      outputStream.write(cipher.getIV());
    }
    }
    catch (IOException ioe) {
      throw new CryptoManagerException(
        // TODO: i18n
@@ -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);
    }
    catch (Exception ex) {
      throw new CryptoManagerException(
              // 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."));
    }
    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 CryptoManager prologue."),
        ex);
                Message.raw("Exception when reading initialization" +
                        " vector from data prologue."), ex);
      }
    }
    Cipher cipher = getDecryptionCipher(keyID, iv);
    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."));
                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));
  }
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);
  }
}