Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bouncy Castle 1.80: Encryption / Decryption not working anymore with NoIvGenerator #1985

Open
wolfgangs-px opened this issue Feb 4, 2025 · 15 comments
Assignees

Comments

@wolfgangs-px
Copy link

wolfgangs-px commented Feb 4, 2025

Since bouncy castle version 1.80 the encryption and decryption for Ciphers like PBEWITHSHA256AND256BITAES-CBC-BC fail in our setup.

Example to reproduce the issue with Jasypt ( http://www.jasypt.org/bouncy-castle.html ) in Java:

StandardPBEStringEncryptor stringEncryptor = new StandardPBEStringEncryptor();
stringEncryptor.setAlgorithm("PBEWITHSHA256AND256BITAES-CBC-BC");
stringEncryptor.setPassword(“secretPassword”);
stringEncryptor.setProvider(new BouncyCastleProvider());

String encryptedText = stringEncryptor.encrypt("plainText");

The underlying exception is: java.security.InvalidAlgorithmParameterException: IV must be 16 bytes long

We use Java 21, Jasypt 1.9.3 and org.bouncycastle:bcprov-jdk18on 1.80. Everything worked fine with bouncy castle 1.79, the error occurs since the 1.80 update.

It seems the issue is related to the IvGenerator. By default, a NoIvGenerator is added by Jasypt, if not specified differently.

If in the above code example you add a RandomIvGenerator , encrpytion and decryption work fine again.

But: We can not just switch to using a RandomIvGenerator, because we have stored the encrypted strings in a database and these can not be correctly decrpyted with a different IvGenerator than the NoIvGenerator used for encryption.

Why did the behaviour change and for the same algorithm there are now new requirements for the IvGenerator? Could this be fixed? Thanks!

@uszeiss
Copy link

uszeiss commented Feb 4, 2025

I can confirm the behaviour described by Wolfgang. We too have existing encrypted strings that cannot be decrypted with the 1.80 version. Everything worked as expected with 1.79.

I would appreciate any support on this issue. Thanks!

@ligefeiBouncycastle ligefeiBouncycastle self-assigned this Feb 6, 2025
@ligefeiBouncycastle
Copy link
Collaborator

Thank you for raising this issue. This issue is triggered by an improperly set IV.

In earlier versions, if no IV was provided, a backup IV was generated internally. Starting from version 1.80, a validation check was introduced to ensure the IV size is correct. If an IV is missing, this check triggers an exception.

To prevent this, please ensure that a valid IV is explicitly set when using Jasypt. I have added the following code to ensure the program runs properly:

stringEncryptor.setIvGenerator(new RandomIvGenerator());

We are currently discussing whether to introduce a mechanism that automatically uses a backup IV when none is provided.

@uszeiss
Copy link

uszeiss commented Feb 7, 2025

Thank you for your feedback and for looking into this.

How can a message that was encrypted with BC 1.79 without explicitly setting an IV be decrypted with BC 1.80? I have not been able to do so even when I set an IV before performing the decryption.

I can provide a minimal example if required.

@wolfgangs-px
Copy link
Author

wolfgangs-px commented Feb 7, 2025

Yes, thank you @ligefeiBouncycastle for looking into this.

I understand the issue and that it works when a RandomIvGenerator is used for encryption and decryption.

However, just as @uszeiss has explained, we have messages, that have been encrypted with BC 1.79 and without the RandomIvGenerator. We need to be able to decrypt these messages with BC 1.80, otherwise we can't upgrade BC.

How can we do this? Can we use the "internal backup Iv generator" that was added in 1.79?

Thanks again!

@ligefeiBouncycastle
Copy link
Collaborator

@wolfgangs-px @uszeiss Thank you for raising this critical compatibility concern.

BC 1.80 introduced breaking changes for NoIvGenerator scenarios as part of our security hardening efforts in issue #1846. This change was implemented because no external IV overwrite was provided. To address this, we will be releasing a beta version soon for testing.

Please note that the internal backup IV generator is not purely random. Instead, it derives a pseudo-random IV from the password and salt based on PKCS12 (RFC 7292) guidelines, using PBE.makePBEParameters. For further details, you can refer to the following:

Thank you again for your feedback.

@uszeiss
Copy link

uszeiss commented Feb 10, 2025

Thank you for these additional information, @ligefeiBouncycastle .

Based on your input, I was able to reconstruct the IV that was generated internally and I successfully used version 1.80 to decrypt a message encrypted with version 1.79.

Is the beta version you mentioned going to provide full compatibility with 1.79 (in that we won't need to adjust our code)?

@wolfgangs-px
Copy link
Author

Based on your input, I was able to reconstruct the IV that was generated internally and I successfully used version 1.80 to decrypt a message encrypted with version 1.79.

@uszeiss Sounds good! Could you (only if easily possible) maybe share how you reconstructed the IV (by source code or explanation). It would help us and probably other developers facing this issue 😉 Thank you!

@uszeiss
Copy link

uszeiss commented Feb 10, 2025

Here's a JUnit test that decrypts a ciphertext that was encrypted using Jasypt and BC 1.79:

package com.example.bc;

import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;

import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.jcajce.provider.symmetric.util.PBE;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jasypt.contrib.org.apache.commons.codec_1_3.binary.Base64;
import org.jasypt.normalization.Normalizer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class DecryptionTest {

    @Test
    void decrypt_with_computed_salt_and_iv() throws Exception {

        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.insertProviderAt(new BouncyCastleProvider(), 1);
        }

        // given
        int iterationCount = 1000;
        String algorithm = "PBEWithMD5AndDES";
        String password = "5oddwa3FeNwvXY9y";
        String encodedCiphertext = "Ky/9NyLdkJMlwWR4KcY9BIat/jzwbqBK3GWuBiPkQdY=";
        String expectedPlaintext = "d1IYhc2vCFLLDfL2";

        byte[] decodedCiphertext = Base64.decodeBase64(encodedCiphertext.getBytes(StandardCharsets.US_ASCII));
        byte[] salt = Arrays.copyOfRange(decodedCiphertext, 0, 8); // {43, 47, -3, 55, 34, -35, -112, -109};
        byte[] encryptedMessage = Arrays.copyOfRange(decodedCiphertext, 8, decodedCiphertext.length);

        SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm);
        char[] normalizedPassword = Normalizer.normalizeToNfc(password.toCharArray());
        PBEKeySpec pbeKeySpec = new PBEKeySpec(normalizedPassword);
        SecretKey key = factory.generateSecret(pbeKeySpec);

        PBEParameterSpec initialParameterSpecWithoutIv = new PBEParameterSpec(salt, iterationCount, new IvParameterSpec(new byte[0]));
        ParametersWithIV ivParameters = (ParametersWithIV) PBE.Util.makePBEParameters(key.getEncoded(),
                                                                                      0,
                                                                                      0,
                                                                                      64,
                                                                                      64,
                                                                                      initialParameterSpecWithoutIv,
                                                                                      "DES/CBC");
        PBEParameterSpec parameterSpecWithIv = new PBEParameterSpec(salt, iterationCount, new IvParameterSpec(ivParameters.getIV()));

        Cipher decryptCipher = Cipher.getInstance(algorithm);
        decryptCipher.init(Cipher.DECRYPT_MODE, key, parameterSpecWithIv);

        // when
        byte[] plaintextBytes = decryptCipher.doFinal(encryptedMessage);

        // then
        String plaintext = new String(plaintextBytes, StandardCharsets.US_ASCII);
        Assertions.assertEquals(expectedPlaintext, plaintext);
    }
}

Note that the inputs (plaintext, password) were chosen randomly for this example and the parameters like iterationCount, algorithm, passsword length, etc. could be different in your case. Please adjust according to your needs.

If you want to use the Jasypt API, derive the ivParameters as above and then use stringEncryptor.setIvGenerator(new ByteArrayFixedIvGenerator(ivParameters.getIV()));

@wolfgangs-px
Copy link
Author

@uszeiss Many thanks!

@ligefeiBouncycastle
Copy link
Collaborator

We invite you to try BC 1.81 beta by downloading it from Bouncy Castle Betas. BC 1.81 beta is designed with backward compatibility in mind, so in theory, your existing code should work seamlessly with messages encrypted using 1.79. We’d greatly appreciate any feedback you have and any details you can share about your process.

@uszeiss
Copy link

uszeiss commented Feb 11, 2025

@ligefeiBouncycastle Thanks a lot for this beta version! I can confirm it allows us to decrypt the messages encrypted with BC 1.79 via Jasypt without any changes of our code. From this point of view, 1.81 is compatible with 1.79. Very good news!

I have one test case, however, that works with 1.79 but fails with 1.81. It is not directly related to Jasypt:

package com.example.bc;

import java.security.Security;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class CompatibilityTest {

    @Test
    void minimal_example_for_incompatibility() throws Exception {
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.insertProviderAt(new BouncyCastleProvider(), 1);
        }

        // given
        String algorithm = "PBEWithMD5AndDES";
        char[] password = new char[]{'5', 'o', 'd', 'd', 'w', 'a', '3', 'F', 'e', 'N', 'w', 'v', 'X', 'Y', '9', 'y'};
        byte[] salt = new byte[]{43, 47, -3, 55, 34, -35, -112, -109};
        byte[] iv = new byte[]{-14, -53, 122, 86, 6, -37, 31, -93};
        byte[] emptyIv = new byte[8];
        byte[] message = {100, 49, 73, 89, 104, 99, 50, 118, 67, 70, 76, 76, 68, 102, 76, 50};

        SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm);
        SecretKey key = factory.generateSecret(new PBEKeySpec(password));
        PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, 1000, new IvParameterSpec(iv));

        Cipher encryptCipher = Cipher.getInstance(algorithm);
        encryptCipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);

        Cipher decryptCipher = Cipher.getInstance(algorithm);
        decryptCipher.init(Cipher.DECRYPT_MODE, key, new PBEParameterSpec(salt, 1000, new IvParameterSpec(emptyIv)));

        // when
        byte[] encryptedMessage = encryptCipher.doFinal(message);
        byte[] decryptedMessage = decryptCipher.doFinal(encryptedMessage);

        // then (works with BC 1.79, fails with BC 1.80+)
        Assertions.assertEquals(new String(message), new String(decryptedMessage));
    }
}

This test succeeds if BC 1.79 is on the classpath, but fails if BC 1.80 or 1.8.1-SNAPSHOT is on the classpath. As you can see, with 1.79, we could use an empty IV of the expected length to decrypt a message encrypted with a non-empty IV. This is not possible in 1.80+.

I'm not sure if this is an undesired behaviour in 1.79 or if this is something that is expected to still work in 1.80+.

Thank you again and keep up the great work!

@ligefeiBouncycastle
Copy link
Collaborator

Thank you for the detailed report and test case. In earlier versions, allowing an empty IV to be used for decryption—even when a non-empty IV was used during encryption—was an oversight, as the IV was overwritten by an internally generated one. With BC 1.80 and later, we enforce the use of the user’s predefined IV, except when an IV of length 0 is provided.

The issue in your code arises because the IVs used during encryption and decryption do not match. To fix the code, please update the decryption initialization as follows:
decryptCipher.init(Cipher.DECRYPT_MODE, key, new PBEParameterSpec(salt, 1000, new IvParameterSpec(iv)));

If the encryption was performed using the legacy behaviour (BC 1.79), you can resolve the incompatibility by either:

  • Modifying your code to extract the IV from the encrypted message, or
  • Setting the length of the empty IV to 0.

Thank you for your understanding and for helping us improve the library. Please let us know if you have any further questions or need additional guidance on adapting your code.

@uszeiss
Copy link

uszeiss commented Feb 11, 2025

Good to know, thanks @ligefeiBouncycastle. I adjusted my code to use an IV of length 0 for decryption and it works as expected.

Looking forward to the official release of 1.81.

@wolfgangs-px
Copy link
Author

Hello, I can confirm, that with the 1.81-SNAPSHOT version, the decryption of messages encrypted with 1.79 is working again in our setup.

So we'll just skip 1.80 and wait for 1.81 to be officially released.

Thank you very much for your quick reaction and the great support!

@domer-sz
Copy link

Hi @ligefeiBouncycastle , when do you expect to release 1.81?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants