[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. |