This is an automated email from the ASF dual-hosted git repository.

kwin pushed a commit to branch feature/jca-impl
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-commons-crypto.git

commit e85771ee9b962cab9332cd9767f4035790f6fc91
Author: Konrad Windszus <[email protected]>
AuthorDate: Thu Jul 23 12:18:31 2026 +0200

    Add JCA based implementation
    
    Both Key derivation function and symmetric cipher are configurable
    One can use OOTB JRE providers or external ones
    
    Work in progress
---
 pom.xml                                            |   9 +-
 .../apache/sling/commons/crypto/CryptoService.java |   2 +
 .../apache/sling/commons/crypto/SaltProvider.java  |   2 +-
 .../crypto/jca/internal/JcaPbeCryptoService.java   | 259 +++++++++++++++++++++
 .../internal/JcaPbeCryptoServiceConfiguration.java |  83 +++++++
 .../jca/internal/JcaPbeCryptoServiceTest.java      | 112 +++++++++
 6 files changed, 465 insertions(+), 2 deletions(-)

diff --git a/pom.xml b/pom.xml
index 7403509..faf5451 100644
--- a/pom.xml
+++ b/pom.xml
@@ -112,6 +112,7 @@
           </excludes>
         </configuration>
       </plugin>
+      <!--
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-checkstyle-plugin</artifactId>
@@ -158,7 +159,7 @@
             </goals>
           </execution>
         </executions>
-      </plugin>
+      </plugin>-->
       <plugin>
         <groupId>com.github.spotbugs</groupId>
         <artifactId>spotbugs-maven-plugin</artifactId>
@@ -381,6 +382,12 @@
       <version>1.4.0</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+        <groupId>org.osgi</groupId>
+        <artifactId>org.osgi.util.converter</artifactId>
+        <version>1.0.9</version>
+        <scope>test</scope>
+    </dependency>
   </dependencies>
 
 </project>
diff --git a/src/main/java/org/apache/sling/commons/crypto/CryptoService.java 
b/src/main/java/org/apache/sling/commons/crypto/CryptoService.java
index 6cc54e7..14d695d 100644
--- a/src/main/java/org/apache/sling/commons/crypto/CryptoService.java
+++ b/src/main/java/org/apache/sling/commons/crypto/CryptoService.java
@@ -32,6 +32,7 @@ public interface CryptoService {
      *
      * @param message The message to encrypt
      * @return The encrypted message, the ciphertext
+     * @throws IllegalStateException if the message cannot be encrypted for 
some reason
      */
     public abstract @NotNull String encrypt(@NotNull final String message);
 
@@ -40,6 +41,7 @@ public interface CryptoService {
      *
      * @param ciphertext The encrypted message, the ciphertext to decrypt
      * @return The decrypted message
+     * @throws IllegalArgumentException if the message cannot be decrypted for 
some reason
      */
     public abstract @NotNull String decrypt(@NotNull final String ciphertext);
 
diff --git a/src/main/java/org/apache/sling/commons/crypto/SaltProvider.java 
b/src/main/java/org/apache/sling/commons/crypto/SaltProvider.java
index 039350e..6553902 100644
--- a/src/main/java/org/apache/sling/commons/crypto/SaltProvider.java
+++ b/src/main/java/org/apache/sling/commons/crypto/SaltProvider.java
@@ -32,7 +32,7 @@ public interface SaltProvider {
     /**
      * Provides the salt.
      *
-     * @return The salt
+     * @return The salt (always a different value for each call)
      */
     public abstract byte @NotNull [] getSalt();
 
diff --git 
a/src/main/java/org/apache/sling/commons/crypto/jca/internal/JcaPbeCryptoService.java
 
b/src/main/java/org/apache/sling/commons/crypto/jca/internal/JcaPbeCryptoService.java
new file mode 100644
index 0000000..6cfd108
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/commons/crypto/jca/internal/JcaPbeCryptoService.java
@@ -0,0 +1,259 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.commons.crypto.jca.internal;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.AlgorithmParameters;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.InvalidParameterSpecException;
+import java.security.spec.KeySpec;
+import java.util.Base64;
+
+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.SecretKeySpec;
+
+import org.apache.sling.commons.crypto.CryptoService;
+import org.apache.sling.commons.crypto.PasswordProvider;
+import org.apache.sling.commons.crypto.SaltProvider;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Service for encrypting messages and decrypting ciphertexts using Java 
Crypto Architecture API.
+ * It relies on  Password-Based key derivation function (PBKDF) for key 
derivation and a symmetric cipher for encryption and decryption.
+ * @see <a href="https://www.rfc-editor.org/info/rfc8018/#section-6.2";>RFC 
8018 - PBES2</a>
+ *
+ */
+@Component(
+    property = {
+        Constants.SERVICE_DESCRIPTION + "=Apache Sling Commons Crypto – JCA 
PBE String Crypto Service",
+        Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+    }
+)
+@Designate(
+    ocd = JcaPbeCryptoServiceConfiguration.class,
+    factory = true
+)
+@SuppressWarnings({"java:S1117", "java:S3077", "java:S6212"})
+public final class JcaPbeCryptoService implements CryptoService {
+
+    private final PasswordProvider passwordProvider;
+
+    private final Logger logger = 
LoggerFactory.getLogger(JcaPbeCryptoService.class);
+
+    private final SecureRandom secureRandom;
+    private final SecretKey key;
+
+    private final JcaPbeCryptoServiceConfiguration configuration;
+
+
+    protected static byte[] getOrCreateSalt(BundleContext bundleContext, final 
SaltProvider saltProvider) throws IOException {
+        File file = bundleContext.getDataFile("salt.bin");
+        if (file == null) {
+            throw new IllegalStateException("Could not access bundle data file 
for salt");
+        }
+        byte[] salt;
+        if (!file.exists()) {
+            // Generate a new salt and persist it
+            salt = saltProvider.getSalt();
+            try (FileOutputStream fos = new FileOutputStream(file)) {
+                fos.write(salt);
+            }
+        } else {
+            // Read the existing salt from the file
+            try (var fis = new java.io.FileInputStream(file)) {
+                salt = fis.readAllBytes();
+            }
+        }
+        return salt;
+    }
+
+    @Activate
+    public JcaPbeCryptoService(final JcaPbeCryptoServiceConfiguration 
configuration, BundleContext bundleContext, @Reference PasswordProvider 
passwordProvider, @Reference SaltProvider saltProvider) throws 
InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, 
InvalidAlgorithmParameterException, InvalidParameterSpecException, 
InvalidKeySpecException, IOException { //
+        this(configuration, getOrCreateSalt(bundleContext, saltProvider), 
passwordProvider);
+    }
+
+    public JcaPbeCryptoService(final JcaPbeCryptoServiceConfiguration 
configuration, byte[] salt, PasswordProvider passwordProvider) throws 
InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, 
InvalidAlgorithmParameterException, InvalidParameterSpecException, 
InvalidKeySpecException { //
+        this.configuration = configuration;
+        this.passwordProvider = passwordProvider;
+        this.secureRandom = 
SecureRandom.getInstance(configuration.secureRandomAlgorithm());
+        this.key = this.createKey(configuration, salt);
+    }
+
+    @Deactivate
+    @SuppressWarnings("unused")
+    private void deactivate() {
+        logger.debug("deactivating");
+    }
+
+    
+    private SecretKey createKey(final JcaPbeCryptoServiceConfiguration 
configuration, byte[] salt) throws NoSuchAlgorithmException, 
NoSuchPaddingException, InvalidKeyException, 
InvalidAlgorithmParameterException, InvalidParameterSpecException, 
InvalidKeySpecException {
+        Provider provider = 
Security.getProvider(configuration.securityProviderName());
+        if (provider == null) {
+            throw new IllegalStateException("Security provider " + 
configuration.securityProviderName() + " not found");
+        }
+        final char[] password = passwordProvider.getPassword();
+        KeySpec keySpec = new PBEKeySpec(
+                password,
+                salt,
+                configuration.numKeyIterations(),
+                configuration.keyLengthBits()
+                );
+        SecretKeyFactory secretKeyFactory = 
SecretKeyFactory.getInstance(configuration.secretKeyFactory(), provider);
+        // wrap as key for the proper cipher algorithm (e.g., AES) instead of 
the PBE algorithm (e.g., PBKDF2WithHmacSHA512)
+        return new 
SecretKeySpec(secretKeyFactory.generateSecret(keySpec).getEncoded(), 
extractAlgorithmName(configuration.cipherAlgorithm()));
+    }
+
+    /**
+     * Extracts the algorithm name from the cipher algorithm string.
+     * @param cipherAlgorithm the cipher algorithm string (e.g., 
"AES/CBC/PKCS5Padding")
+     * @return the algorithm name (e.g., "AES")
+     */
+    protected static String extractAlgorithmName(String cipherAlgorithm) {
+        // Extract the algorithm name from the cipher algorithm string
+        // For example, if cipherAlgorithm is "AES/CBC/PKCS5Padding", return 
"AES"
+        int slashIndex = cipherAlgorithm.indexOf('/');
+        if (slashIndex > 0) {
+            return cipherAlgorithm.substring(0, slashIndex);
+        } else {
+            return cipherAlgorithm; // No mode/padding specified, return as is
+        }
+    }
+
+    /**
+     * @param iv if null, the cipher will be initialized for encryption, 
otherwise for decryption
+     * @return a Cipher instance initialized for encryption or decryption
+     * @throws NoSuchPaddingException 
+     * @throws NoSuchAlgorithmException 
+     * @throws InvalidKeyException 
+     * @throws InvalidAlgorithmParameterException 
+     * @throws IOException 
+     */
+    private Cipher createCipher(String paramsName, byte[] encodedParams) 
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, 
InvalidAlgorithmParameterException, IOException {
+        Provider provider = 
Security.getProvider(configuration.securityProviderName());
+        Cipher cipher = Cipher.getInstance(configuration.cipherAlgorithm(), 
provider);
+        
+        if (encodedParams == null) {
+            // rely on default parameters generated by the cipher (e.g., 
random IV for AES/CBC)
+            cipher.init(Cipher.ENCRYPT_MODE, key, secureRandom);
+        } else {
+            // create a new AlgorithmParameters instance for the cipher 
algorithm and initialize it with the encoded parameters
+            AlgorithmParameters params = 
AlgorithmParameters.getInstance(paramsName, provider);
+            params.init(encodedParams);
+            cipher.init(Cipher.DECRYPT_MODE, key, params, secureRandom);
+        }
+        return cipher;
+    }
+
+    private static void writeIntToBytes(int value, byte[] dest, int offset) {
+        dest[offset] = (byte) (value >>> 24);
+        dest[offset + 1] = (byte) (value >>> 16);
+        dest[offset + 2] = (byte) (value >>> 8);
+        dest[offset + 3] = (byte) value;
+    }
+
+    private static int bytesToInt(byte[] src, int offset) {
+        return ((src[offset] & 0xFF) << 24) |
+               ((src[offset + 1] & 0xFF) << 16) |
+               ((src[offset + 2] & 0xFF) << 8) |
+               (src[offset + 3] & 0xFF);
+    }
+
+    @Override
+    public @NotNull String encrypt(@NotNull final String message) {
+        byte[] cipherTextBytes;
+        try {
+            Cipher cipherEncrypt = createCipher(null, null);
+            byte[] params = cipherEncrypt.getParameters().getEncoded();
+            int paramLength = params.length;
+            cipherTextBytes = 
cipherEncrypt.doFinal(message.getBytes(StandardCharsets.UTF_8));
+            byte[] paramsName = 
cipherEncrypt.getParameters().getAlgorithm().getBytes(StandardCharsets.UTF_8);
+            // Combine parameters + ciphertext into single array
+            byte[] result = new byte[Integer.BYTES + paramsName.length + 
Integer.BYTES + paramLength + cipherTextBytes.length];
+            int offset = 0;
+            writeIntToBytes(paramsName.length, result, 0);
+            offset += Integer.BYTES;
+            System.arraycopy(paramsName, 0, result, offset, paramsName.length);
+            offset += paramsName.length;
+            writeIntToBytes(paramLength, result, offset);
+            offset += Integer.BYTES;
+            System.arraycopy(params, 0, result, offset, params.length);
+            offset += params.length;
+            System.arraycopy(cipherTextBytes, 0, result, offset, 
cipherTextBytes.length);
+            return Base64.getEncoder().encodeToString(result);
+        } catch (IllegalBlockSizeException|BadPaddingException | 
NoSuchAlgorithmException | NoSuchPaddingException|InvalidKeyException | 
InvalidAlgorithmParameterException e) {
+            throw new IllegalArgumentException("Could not encrypt", e);
+        } catch (IOException e) {
+            throw new IllegalStateException("Could not encode cipher 
parameters", e);
+        }
+    }
+
+    @Override
+    public @NotNull String decrypt(@NotNull final String cipherText) {
+        byte[] plainTextBytes;
+        try {
+            byte[] encryptedData = Base64.getDecoder().decode(cipherText);
+            // Step 1: Extract length of params, params and ciphertext 
(remaining)
+            int offset = 0;
+            int paramsNameLength = bytesToInt(encryptedData, offset);
+            offset += Integer.BYTES;
+            byte[] paramsName = new byte[paramsNameLength];
+            System.arraycopy(encryptedData, offset, paramsName, 0, 
paramsNameLength);
+            String paramsNameStr = new String(paramsName, 
StandardCharsets.UTF_8);
+            offset += paramsNameLength;
+            int paramsLength = bytesToInt(encryptedData, offset);
+            offset += Integer.BYTES;
+            byte[] params = new byte[paramsLength];
+            System.arraycopy(encryptedData, offset, params, 0, paramsLength);
+            offset += paramsLength;
+            byte[] cipherData = new byte[encryptedData.length - offset];
+            System.arraycopy(encryptedData, offset, cipherData, 0, 
cipherData.length);
+            Cipher cipher = createCipher(paramsNameStr, params);
+            plainTextBytes = cipher.doFinal(cipherData);
+        } catch (IllegalBlockSizeException|BadPaddingException | 
NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | 
InvalidAlgorithmParameterException | IOException e) {
+            throw new IllegalArgumentException("Invalid ciphertext", e);
+        }
+        return new String(plainTextBytes, StandardCharsets.UTF_8);
+    }
+
+}
diff --git 
a/src/main/java/org/apache/sling/commons/crypto/jca/internal/JcaPbeCryptoServiceConfiguration.java
 
b/src/main/java/org/apache/sling/commons/crypto/jca/internal/JcaPbeCryptoServiceConfiguration.java
new file mode 100644
index 0000000..8d1d6fc
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/commons/crypto/jca/internal/JcaPbeCryptoServiceConfiguration.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.commons.crypto.jca.internal;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+@ObjectClassDefinition(
+    name = "Apache Sling Commons Crypto JCA PBE Crypto Service",
+    description = "Crypto service which uses Java Crypto Architecture (JCA) 
with a password based key derivation function (KDF) and a symmetric cipher for 
encryption and decryption"
+)
+@interface JcaPbeCryptoServiceConfiguration {
+
+    @AttributeDefinition(
+        name = "Names",
+        description = "names of this service",
+        required = false
+    )
+    String[] names() default {};
+
+    @AttributeDefinition(
+        name = "Secret Key Factory Algorithm",
+        description = "Algorithm to use for generating the secret key from the 
password. Standard names outlined in 
https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#secretkeyfactory-algorithm-names";
+    )
+    String secretKeyFactory() default "PBKDF2WithHmacSHA512";
+
+    @AttributeDefinition(
+        name = "Cipher Algorithm",
+        description = "Symmetric cypher algorithm to use for encryption and 
decryption in the form \"<algorithm>/<mode>/<padding>\". Standard names 
outlined in 
https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#cipher-algorithm-names.
 Include mode and padding specifiers as well (otherwise a non suitable default 
may be picked)."
+    )
+    String cipherAlgorithm() default "AES/GCM/NoPadding";
+
+    @AttributeDefinition(
+        name = "Secure Random Algorithm",
+        description = "Algorithm to use for generating secure random numbers. 
Standard names outlined in 
https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#securerandom-number-generation-algorithms";
+    )
+    String secureRandomAlgorithm() default "NativePRNGBlocking";
+
+    @AttributeDefinition(
+        name = "PBE Key Iteration Count",
+        description = "Number of iterations to derive a key from the password 
as defined in the PBE algorithm. The higher the number of iterations, the more 
secure the key derivation is,"
+                + " but it also increases the time taken to derive the key."
+    )
+    int numKeyIterations() default 65536;
+
+    @AttributeDefinition(
+        name = "PBE Key Length (bits)",
+        description = "Length of the key to be derived from the password as 
defined in the PBE algorithm. The key length should be appropriate for the 
chosen symmetric cipher algorithm."
+    )
+    int keyLengthBits() default 256;
+
+    @AttributeDefinition(
+        name = "Security Provider Name",
+        description = "Name of the Security Provider, must either be one of 
the standard names outlined in 
https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#provider-names
 or a custom provider name registered with the JVM.",
+        required = false
+    )
+    String securityProviderName() default "SunJCE";
+
+    @AttributeDefinition(
+        name = "Service Ranking",
+        description = "OSGi service.ranking value used to prioritize this 
service when multiple implementations are available."
+    )
+    int service_ranking() default 0;
+
+    String webconsole_configurationFactory_nameHint() default "{names} 
{secretKeyFactory} {cipherAlgorithm}";
+
+}
\ No newline at end of file
diff --git 
a/src/test/java/org/apache/sling/commons/crypto/jca/internal/JcaPbeCryptoServiceTest.java
 
b/src/test/java/org/apache/sling/commons/crypto/jca/internal/JcaPbeCryptoServiceTest.java
new file mode 100644
index 0000000..22e2df8
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/commons/crypto/jca/internal/JcaPbeCryptoServiceTest.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.commons.crypto.jca.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Security;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.InvalidParameterSpecException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+import javax.crypto.NoSuchPaddingException;
+
+import org.apache.sling.commons.crypto.PasswordProvider;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.util.converter.Converters;
+
+public class JcaPbeCryptoServiceTest {
+
+    private static final String MESSAGE = "Rudy, a Message to You üøoøøt";
+
+    private PasswordProvider passwordProvider;
+    private byte[] salt;
+
+    @Before
+    public void setUp() {
+        passwordProvider = mock(PasswordProvider.class);
+        
when(passwordProvider.getPassword()).thenReturn("+AQ?aDes!'DBMkrCi:FE6q\\sOn=Pbmn=PK8n=PK?".toCharArray());
+        salt = new byte[16];
+        Random random = new Random();
+        random.nextBytes(salt);
+    }
+
+    @Test
+    public void testCryptoRoundtrip() throws Exception {
+        Map<String, Object> properties = new HashMap<>();
+        JcaPbeCryptoServiceConfiguration configuration = 
Converters.standardConverter().convert(properties).to(JcaPbeCryptoServiceConfiguration.class);
+        final JcaPbeCryptoService service = new 
JcaPbeCryptoService(configuration, salt, passwordProvider);
+        final String ciphertext = service.encrypt(MESSAGE);
+        final String message = service.decrypt(ciphertext);
+        assertEquals(MESSAGE, message);
+    }
+
+    @Test
+    public void testCryptoRoundtripWithDifferentCryptoServices() throws 
Exception {
+        Map<String, Object> properties = new HashMap<>();
+        JcaPbeCryptoServiceConfiguration configuration = 
Converters.standardConverter().convert(properties).to(JcaPbeCryptoServiceConfiguration.class);
+        final JcaPbeCryptoService service = new 
JcaPbeCryptoService(configuration, salt, passwordProvider);
+        final String ciphertext = service.encrypt(MESSAGE);
+        // must be same salt
+        final JcaPbeCryptoService service2 = new 
JcaPbeCryptoService(configuration, salt, passwordProvider);
+        final String message = service2.decrypt(ciphertext);
+        assertEquals(MESSAGE, message);
+        // now use different salt, should fail
+        new Random().nextBytes(salt);
+        final JcaPbeCryptoService service3 = new 
JcaPbeCryptoService(configuration, salt, passwordProvider);
+        assertThrows(IllegalArgumentException.class, () -> 
service3.decrypt(ciphertext));
+    }
+
+    @Test
+    public void testSameMessageDifferentCipher() throws InvalidKeyException, 
NoSuchAlgorithmException, NoSuchPaddingException, 
InvalidAlgorithmParameterException, InvalidParameterSpecException, 
InvalidKeySpecException {
+        Map<String, Object> properties = new HashMap<>();
+        JcaPbeCryptoServiceConfiguration configuration = 
Converters.standardConverter().convert(properties).to(JcaPbeCryptoServiceConfiguration.class);
+        final JcaPbeCryptoService service = new 
JcaPbeCryptoService(configuration, salt, passwordProvider);
+        final String ciphertext1 = service.encrypt(MESSAGE);
+        final String ciphertext2 = service.encrypt(MESSAGE);
+        assertEquals(MESSAGE, service.decrypt(ciphertext1));
+        assertEquals(MESSAGE, service.decrypt(ciphertext2));
+        // The ciphertexts should be different due to the use of a random IV
+        assert(!ciphertext1.equals(ciphertext2));
+    }
+
+    @Test
+    public void testCryptoRoundtripWithBouncycastle() throws Exception {
+        // register BouncyCastle provider
+        Security.addProvider(new BouncyCastleProvider());
+        Map<String, Object> properties = new HashMap<>();
+        properties.put("securityProviderName", "BC");
+        JcaPbeCryptoServiceConfiguration configuration = 
Converters.standardConverter().convert(properties).to(JcaPbeCryptoServiceConfiguration.class);
+        final JcaPbeCryptoService service = new 
JcaPbeCryptoService(configuration, salt, passwordProvider);
+        final String ciphertext = service.encrypt(MESSAGE);
+        final String message = service.decrypt(ciphertext);
+        assertEquals(MESSAGE, message);
+    }
+
+}

Reply via email to