[ldap-sdk-commits] SF.net SVN: ldap-sdk:[1587] trunk
A Java-based LDAP API
Brought to you by:
dirmgr,
kennethleo
|
From: <di...@us...> - 2023-01-27 23:48:06
|
Revision: 1587
http://sourceforge.net/p/ldap-sdk/code/1587
Author: dirmgr
Date: 2023-01-27 23:48:04 +0000 (Fri, 27 Jan 2023)
Log Message:
-----------
Passphrase-encrypted output stream improvements
Updated the passphrase-encrypted output stream to use a higher
key factory iteration count by default. When using the strongest
available 256-bit AES encryption, it now follows the latest OWASP
recommendation of 600,000 PBKDF2 iterations. The key factory
iteration count can still be explicitly specified when creating a
new output stream if an alternative iteration count is desired, and
the default iteration count can now be overridden with a system
property.
Updated the passphrase-encrypted output stream to make it possible
to create a new output stream with the encryption header from a
previously created encryption header. This will make it possible to
reuse the previously derived key (with a different initialization
vector), which will be substantially faster when using the same
passphrase to encrypt multiple output streams than needing to
re-derive the key for each stream.
Modified Paths:
--------------
trunk/docs/release-notes.html
trunk/messages/unboundid-ldapsdk-util.properties
trunk/src/com/unboundid/util/PassphraseEncryptedOutputStream.java
trunk/src/com/unboundid/util/PassphraseEncryptedStreamHeader.java
trunk/src/com/unboundid/util/PassphraseEncryptionCipherType.java
trunk/tests/unit/src/com/unboundid/util/PassphraseEncryptedStreamsTestCase.java
Modified: trunk/docs/release-notes.html
===================================================================
--- trunk/docs/release-notes.html 2023-01-16 20:46:51 UTC (rev 1586)
+++ trunk/docs/release-notes.html 2023-01-27 23:48:04 UTC (rev 1587)
@@ -40,6 +40,26 @@
</li>
<li>
+ Updated the passphrase-encrypted output stream to use a higher key factory
+ iteration count by default. When using the strongest available 256-bit AES
+ encryption, it now follows the latest OWASP recommendation of 600,000 PBKDF2
+ iterations. The key factory iteration count can still be explicitly specified
+ when creating a new output stream if an alternative iteration count is desired,
+ and the default iteration count can now be overridden with a system property.
+ <br><br>
+ </li>
+
+ <li>
+ Updated the passphrase-encrypted output stream to make it possible to create a
+ new output stream with the encryption header from a previously created
+ encryption header. This will make it possible to reuse the previously derived
+ key (with a different initialization vector), which will be substantially faster
+ when using the same passphrase to encrypt multiple output streams than needing
+ to re-derive the key for each stream.
+ <br><br>
+ </li>
+
+ <li>
Updated documentation to include the latest versions of
draft-howard-gssapi-aead, draft-ietf-kitten-scram-2fa, draft-melnikov-scram-bis,
and draft-reitzenstein-kitten-opaque in the set of LDAP-related specifications.
Modified: trunk/messages/unboundid-ldapsdk-util.properties
===================================================================
--- trunk/messages/unboundid-ldapsdk-util.properties 2023-01-16 20:46:51 UTC (rev 1586)
+++ trunk/messages/unboundid-ldapsdk-util.properties 2023-01-27 23:48:04 UTC (rev 1587)
@@ -953,6 +953,9 @@
ERR_PW_ENCRYPTED_HEADER_NO_KEY_AVAILABLE=Unable to create a cipher from the \
passphrase-encrypted stream header because no passphrase was provided when \
decoding the header.
+ERR_PW_ENCRYPTED_STREAM_HEADER_COPY_WITHOUT_SECRET_KEY=Unable to create a copy \
+ of an existing passphrase-encrypted stream header with a new initialization \
+ vector because the provided header does not include a secret key.
ERR_CLOSEABLE_LOCK_TRY_LOCK_TIMEOUT=Unable to acquire the closeable lock with \
a timeout of {0}.
ERR_CLOSEABLE_RW_LOCK_TRY_LOCK_WRITE_TIMEOUT=Unable to acquire the closeable \
Modified: trunk/src/com/unboundid/util/PassphraseEncryptedOutputStream.java
===================================================================
--- trunk/src/com/unboundid/util/PassphraseEncryptedOutputStream.java 2023-01-16 20:46:51 UTC (rev 1586)
+++ trunk/src/com/unboundid/util/PassphraseEncryptedOutputStream.java 2023-01-27 23:48:04 UTC (rev 1587)
@@ -84,6 +84,90 @@
public final class PassphraseEncryptedOutputStream
extends OutputStream
{
+ /**
+ * The default PBKDF2 iteration count that should be used for the
+ * {@link PassphraseEncryptionCipherType#AES_128} cipher type.
+ */
+ public static final int DEFAULT_AES_128_CIPHER_TYPE_ITERATION_COUNT;
+
+
+
+ /**
+ * The default PBKDF2 iteration count that should be used for the
+ * {@link PassphraseEncryptionCipherType#AES_256} cipher type.
+ */
+ public static final int DEFAULT_AES_256_CIPHER_TYPE_ITERATION_COUNT;
+
+
+
+ /**
+ * The name of a system property that can be used to override the default
+ * PBKDF2 iteration count for the
+ * {@link PassphraseEncryptionCipherType#AES_128} cipher type.
+ */
+ @NotNull public static final String
+ PROPERTY_DEFAULT_AES_128_CIPHER_TYPE_ITERATION_COUNT =
+ PassphraseEncryptedOutputStream.class.getName() +
+ ".defaultAES128CipherTypeIterationCount";
+
+
+
+ /**
+ * The name of a system property that can be used to override the default
+ * PBKDF2 iteration count for the
+ * {@link PassphraseEncryptionCipherType#AES_256} cipher type.
+ */
+ @NotNull public static final String
+ PROPERTY_DEFAULT_AES_256_CIPHER_TYPE_ITERATION_COUNT =
+ PassphraseEncryptedOutputStream.class.getName() +
+ ".defaultAES256CipherTypeIterationCount";
+
+
+
+ static
+ {
+ int defaultAES128IterationCount = 100_000;
+ final String defaultAES128IterationCountPropertyValue =
+ StaticUtils.setSystemProperty(
+ PROPERTY_DEFAULT_AES_128_CIPHER_TYPE_ITERATION_COUNT, null);
+ if (defaultAES128IterationCountPropertyValue != null)
+ {
+ try
+ {
+ defaultAES128IterationCount =
+ Integer.parseInt(defaultAES128IterationCountPropertyValue);
+ }
+ catch (final Exception e)
+ {
+ Debug.debugException(e);
+ }
+ }
+
+ DEFAULT_AES_128_CIPHER_TYPE_ITERATION_COUNT = defaultAES128IterationCount;
+
+
+ int defaultAES256IterationCount = 600_000;
+ final String defaultAES256IterationCountPropertyValue =
+ StaticUtils.setSystemProperty(
+ PROPERTY_DEFAULT_AES_256_CIPHER_TYPE_ITERATION_COUNT, null);
+ if (defaultAES256IterationCountPropertyValue != null)
+ {
+ try
+ {
+ defaultAES256IterationCount =
+ Integer.parseInt(defaultAES256IterationCountPropertyValue);
+ }
+ catch (final Exception e)
+ {
+ Debug.debugException(e);
+ }
+ }
+
+ DEFAULT_AES_256_CIPHER_TYPE_ITERATION_COUNT = defaultAES256IterationCount;
+ }
+
+
+
// The cipher output stream that will be used to actually write the
// encrypted output.
@NotNull private final CipherOutputStream cipherOutputStream;
@@ -537,6 +621,58 @@
/**
+ * Creates a new passphrase-encrypted output stream that wraps the provided
+ * output stream and reuses the same derived secret key as the given
+ * stream header (although with a newly computed initialization vector). This
+ * can dramatically reduce the cost of creating a new passphrase-encrypted
+ * output stream with the same underlying password and settings without the
+ * need to recompute the key.
+ *
+ * @param header
+ * The existing passphrase-encrypted stream header that contains
+ * the details to use for the encryption. It must not be
+ * {@code null}, and it must have an associated secret key.
+ * @param wrappedOutputStream
+ * The output stream to which the encrypted data (optionally
+ * preceded by a header with details about the encryption) will
+ * be written. It must not be {@code null}.
+ * @param writeHeaderToStream
+ * Indicates whether to write the generated
+ * {@link PassphraseEncryptedStreamHeader} to the provided
+ * {@code wrappedOutputStream} before any encrypted data so that
+ * a {@link PassphraseEncryptedInputStream} can read it to obtain
+ * information necessary for decrypting the data. If this is
+ * {@code false}, then the {@link #getEncryptionHeader()} method
+ * must be used to obtain the encryption header so that it can be
+ * stored elsewhere and provided to the
+ * {@code PassphraseEncryptedInputStream} constructor.
+ *
+ * @throws GeneralSecurityException If a problem is encountered while
+ * initializing the encryption.
+ *
+ * @throws IOException If a problem is encountered while writing the
+ * encryption header to the underlying output stream.
+ */
+ public PassphraseEncryptedOutputStream(
+ @NotNull final PassphraseEncryptedStreamHeader header,
+ @NotNull final OutputStream wrappedOutputStream,
+final boolean writeHeaderToStream)
+ throws GeneralSecurityException, IOException
+ {
+ encryptionHeader = header.withNewCipherInitializationVector();
+
+ final Cipher cipher = encryptionHeader.createCipher(Cipher.ENCRYPT_MODE);
+ if (writeHeaderToStream)
+ {
+ encryptionHeader.writeTo(wrappedOutputStream);
+ }
+
+ cipherOutputStream = new CipherOutputStream(wrappedOutputStream, cipher);
+ }
+
+
+
+ /**
* Writes an encrypted representation of the provided byte to the underlying
* output stream.
*
Modified: trunk/src/com/unboundid/util/PassphraseEncryptedStreamHeader.java
===================================================================
--- trunk/src/com/unboundid/util/PassphraseEncryptedStreamHeader.java 2023-01-16 20:46:51 UTC (rev 1586)
+++ trunk/src/com/unboundid/util/PassphraseEncryptedStreamHeader.java 2023-01-27 23:48:04 UTC (rev 1587)
@@ -1077,6 +1077,48 @@
/**
+ * Creates a copy of this passphrase-encrypted stream header that use a
+ * newly-computed initialization vector. The new stream header can be used to
+ * create a new passphrase-encrypted output stream that safely leverages an
+ * already computed secret key to dramatically reduce the cost of creating a
+ * new stream from the same underlying passphrase.
+ *
+ * @return The new passphrase-encrypted stream header that was created.
+ *
+ * @throws GeneralSecurityException If a problem occurs while creating a
+ * copy of this passphrase-encrypted stream
+ * header with a new initialization vector.
+ */
+ @NotNull()
+ PassphraseEncryptedStreamHeader withNewCipherInitializationVector()
+ throws GeneralSecurityException
+ {
+ if (secretKey == null)
+ {
+ throw new InvalidKeyException(
+ ERR_PW_ENCRYPTED_STREAM_HEADER_COPY_WITHOUT_SECRET_KEY.get());
+ }
+
+ final byte[] newInitializationVector =
+ new byte[cipherInitializationVector.length];
+ ThreadLocalSecureRandom.get().nextBytes(newInitializationVector);
+
+ final ObjectPair<byte[],byte[]> headerPair = encode(keyFactoryAlgorithm,
+ keyFactoryIterationCount, this.keyFactorySalt, keyFactoryKeyLengthBits,
+ cipherTransformation, newInitializationVector, keyIdentifier,
+ secretKey, macAlgorithm);
+ final byte[] newEncodedHeader = headerPair.getFirst();
+ final byte[] newMACValue = headerPair.getSecond();
+
+ return new PassphraseEncryptedStreamHeader(keyFactoryAlgorithm,
+ keyFactoryIterationCount, keyFactorySalt, keyFactoryKeyLengthBits,
+ cipherTransformation, newInitializationVector, keyIdentifier,
+ secretKey, macAlgorithm, newMACValue, newEncodedHeader);
+ }
+
+
+
+ /**
* Retrieves a string representation of this passphrase-encrypted stream
* header.
*
Modified: trunk/src/com/unboundid/util/PassphraseEncryptionCipherType.java
===================================================================
--- trunk/src/com/unboundid/util/PassphraseEncryptionCipherType.java 2023-01-16 20:46:51 UTC (rev 1586)
+++ trunk/src/com/unboundid/util/PassphraseEncryptionCipherType.java 2023-01-27 23:48:04 UTC (rev 1587)
@@ -52,8 +52,10 @@
/**
* Cipher settings that use a 128-bit AES cipher.
*/
- AES_128("AES/CBC/PKCS5Padding", 128, "PBKDF2WithHmacSHA1", 16_384, 16, 16,
- "HmacSHA256"),
+ AES_128("AES/CBC/PKCS5Padding", 128, "PBKDF2WithHmacSHA1",
+ PassphraseEncryptedOutputStream.
+ DEFAULT_AES_128_CIPHER_TYPE_ITERATION_COUNT,
+ 16, 16, "HmacSHA256"),
@@ -60,8 +62,10 @@
/**
* Cipher settings that use a 256-bit AES cipher.
*/
- AES_256("AES/CBC/PKCS5Padding", 256, "PBKDF2WithHmacSHA512", 131_072, 16, 16,
- "HmacSHA512");
+ AES_256("AES/CBC/PKCS5Padding", 256, "PBKDF2WithHmacSHA512",
+ PassphraseEncryptedOutputStream.
+ DEFAULT_AES_256_CIPHER_TYPE_ITERATION_COUNT,
+ 16, 16, "HmacSHA512");
Modified: trunk/tests/unit/src/com/unboundid/util/PassphraseEncryptedStreamsTestCase.java
===================================================================
--- trunk/tests/unit/src/com/unboundid/util/PassphraseEncryptedStreamsTestCase.java 2023-01-16 20:46:51 UTC (rev 1586)
+++ trunk/tests/unit/src/com/unboundid/util/PassphraseEncryptedStreamsTestCase.java 2023-01-27 23:48:04 UTC (rev 1587)
@@ -585,7 +585,9 @@
assertNotNull(header.getKeyFactoryAlgorithm());
assertEquals(header.getKeyFactoryAlgorithm(), "PBKDF2WithHmacSHA1");
- assertEquals(header.getKeyFactoryIterationCount(), 16_384);
+ assertEquals(header.getKeyFactoryIterationCount(),
+ PassphraseEncryptionCipherType.AES_128.
+ getKeyFactoryIterationCount());
assertNotNull(header.getKeyFactorySalt());
assertEquals(header.getKeyFactorySalt().length, 16);
@@ -1115,4 +1117,157 @@
assertNotNull(header.getKeyIdentifier());
assertEquals(header.getKeyIdentifier(), "different-key-id");
}
+
+
+
+ /**
+ * Tests the behavior when creating a passphrase-encrypted output streams from
+ * an existing passphrase-encrypted stream header, which allows for reusing
+ * the same derived key without the need to recompute it.
+ *
+ * @throws Exception If an unexpected problem occurs.
+ */
+ @Test()
+ public void testDerivedKeyReuse()
+ throws Exception
+ {
+ // Define the data to be encrypted.
+ final List<String> linesToEncrypt = Arrays.asList(
+ "This is some data that will be encrypted.",
+ "So is this.",
+ "And this.");
+
+ // Get the path to a file to which encrypted data will be written.
+ final File encryptedFile = createTempFile();
+ assertTrue(encryptedFile.delete());
+
+
+ // Create the properties that will be used for the encryption.
+ final PassphraseEncryptedOutputStreamProperties properties =
+ new PassphraseEncryptedOutputStreamProperties(
+ PassphraseEncryptionCipherType.getStrongestAvailableCipherType());
+ properties.setKeyIdentifier("the-key-identifier");
+ properties.setWriteHeaderToStream(true);
+
+
+ // Create an initial passphrase-encrypted output stream and use it to
+ // encrypt the data.
+ final PassphraseEncryptedStreamHeader encryptionHeader1;
+ final char[] encryptionPassphrase =
+ "this-is-the-encryption-passphrase".toCharArray();
+ final File outputFile1 = createTempFile();
+ assertTrue(outputFile1.delete());
+ try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile1);
+ PassphraseEncryptedOutputStream encryptedOutputStream =
+ new PassphraseEncryptedOutputStream(encryptionPassphrase,
+ fileOutputStream, properties);
+ PrintStream printStream = new PrintStream(encryptedOutputStream))
+ {
+ for (final String line : linesToEncrypt)
+ {
+ printStream.println(line);
+ }
+
+ encryptionHeader1 = encryptedOutputStream.getEncryptionHeader();
+ }
+
+
+ // Create a second passphrase-encrypted output stream with the same header
+ // as the first stream and also use it to encrypt the data.
+ final PassphraseEncryptedStreamHeader encryptionHeader2;
+ final File outputFile2 = createTempFile();
+ assertTrue(outputFile2.delete());
+ try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile2);
+ PassphraseEncryptedOutputStream encryptedOutputStream =
+ new PassphraseEncryptedOutputStream(encryptionHeader1,
+ fileOutputStream, true);
+ PrintStream printStream = new PrintStream(encryptedOutputStream))
+ {
+ for (final String line : linesToEncrypt)
+ {
+ printStream.println(line);
+ }
+
+ encryptionHeader2 = encryptedOutputStream.getEncryptionHeader();
+ }
+
+
+ // Make sure that the two output files are comprised of different sets of
+ // bytes.
+ final byte[] file1Bytes = StaticUtils.readFileBytes(outputFile1);
+ final byte[] file2Bytes = StaticUtils.readFileBytes(outputFile2);
+ assertFalse(Arrays.equals(file1Bytes, file2Bytes));
+
+
+ // Make sure that the two encryption headers are the same, except for the
+ // initialization vector.
+ assertEquals(encryptionHeader1.getKeyFactoryAlgorithm(),
+ encryptionHeader2.getKeyFactoryAlgorithm());
+ assertEquals(encryptionHeader1.getKeyFactoryIterationCount(),
+ encryptionHeader2.getKeyFactoryIterationCount());
+ assertEquals(encryptionHeader1.getKeyFactorySalt(),
+ encryptionHeader2.getKeyFactorySalt());
+ assertEquals(encryptionHeader1.getKeyFactoryKeyLengthBits(),
+ encryptionHeader2.getKeyFactoryKeyLengthBits());
+ assertEquals(encryptionHeader1.getCipherTransformation(),
+ encryptionHeader2.getCipherTransformation());
+ assertEquals(encryptionHeader1.getKeyIdentifier(),
+ encryptionHeader2.getKeyIdentifier());
+ assertEquals(encryptionHeader1.getMACAlgorithm(),
+ encryptionHeader2.getMACAlgorithm());
+ assertFalse(Arrays.equals(encryptionHeader1.getCipherInitializationVector(),
+ encryptionHeader2.getCipherInitializationVector()));
+
+
+ // Make sure that we can decrypt both files with the same passphrase, and
+ // that the decrypted contents are identical.
+ final ArrayList<String> decryptedLines = new ArrayList<>();
+ try (FileInputStream fileInputStream = new FileInputStream(outputFile1);
+ PassphraseEncryptedInputStream encryptedInputStream =
+ new PassphraseEncryptedInputStream(encryptionPassphrase,
+ fileInputStream);
+ InputStreamReader encryptedStreamReader =
+ new InputStreamReader(encryptedInputStream);
+ BufferedReader bufferedReader =
+ new BufferedReader(encryptedStreamReader))
+ {
+ while (true)
+ {
+ final String line = bufferedReader.readLine();
+ if (line == null)
+ {
+ break;
+ }
+
+ decryptedLines.add(line);
+ }
+
+ assertEquals(decryptedLines, linesToEncrypt);
+ }
+
+
+ decryptedLines.clear();
+ try (FileInputStream fileInputStream = new FileInputStream(outputFile2);
+ PassphraseEncryptedInputStream encryptedInputStream =
+ new PassphraseEncryptedInputStream(encryptionPassphrase,
+ fileInputStream);
+ InputStreamReader encryptedStreamReader =
+ new InputStreamReader(encryptedInputStream);
+ BufferedReader bufferedReader =
+ new BufferedReader(encryptedStreamReader))
+ {
+ while (true)
+ {
+ final String line = bufferedReader.readLine();
+ if (line == null)
+ {
+ break;
+ }
+
+ decryptedLines.add(line);
+ }
+
+ assertEquals(decryptedLines, linesToEncrypt);
+ }
+ }
}
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
|