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

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


The following commit(s) were added to refs/heads/master by this push:
     new 5431d49  SLING-13280 Add JCA based implementation of CryptoService (#6)
5431d49 is described below

commit 5431d49bfbbe1ba3954bf32edbf4b4d078edc025
Author: Konrad Windszus <[email protected]>
AuthorDate: Wed Aug 19 09:48:43 2026 +0200

    SLING-13280 Add JCA based implementation of CryptoService (#6)
    
    Both Key derivation function and symmetric cipher are configurable
    One can use OOTB JRE providers or external ones
    Automatically register external security providers bundles
    Add WebConsole for listing all providers with their supported algorithms.
---
 pom.xml                                            |  42 ++-
 .../apache/sling/commons/crypto/CryptoService.java |  16 +
 .../apache/sling/commons/crypto/SaltProvider.java  |   2 +-
 .../OsgiAwareSecurityProviderInstaller.java        | 155 ++++++++++
 .../crypto/internal/SecureRandomSaltProvider.java  |  33 +-
 .../SecureRandomSaltProviderConfiguration.java     |   8 +-
 .../JasyptStandardPbeStringCryptoService.java      |  12 +-
 .../crypto/jca/internal/JcaPbeCryptoService.java   | 343 +++++++++++++++++++++
 .../internal/JcaPbeCryptoServiceConfiguration.java | 100 ++++++
 .../apache/sling/commons/crypto/package-info.java  |   2 +-
 .../internal/EncryptWebConsolePlugin.java          |  12 +-
 .../JcaProviderAlgorithmsWebConsolePlugin.java     | 201 ++++++++++++
 .../internal/SecureRandomSaltProviderTest.java     |  45 +--
 .../EncryptWebConsolePluginHttpWhiteboardIT.java   |   8 +-
 .../crypto/it/tests/EncryptWebConsolePluginIT.java |  24 +-
 .../crypto/it/tests/ReversingCryptoService.java    |   5 +
 .../jca/internal/JcaPbeCryptoServiceTest.java      | 144 +++++++++
 17 files changed, 1055 insertions(+), 97 deletions(-)

diff --git a/pom.xml b/pom.xml
index 7403509..ba5ebae 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,7 +29,7 @@
   </parent>
 
   <artifactId>org.apache.sling.commons.crypto</artifactId>
-  <version>1.2.1-SNAPSHOT</version>
+  <version>1.3.0-SNAPSHOT</version>
 
   <name>Apache Sling Commons Crypto</name>
   <description>Apache Sling Commons Crypto</description>
@@ -112,6 +112,7 @@
           </excludes>
         </configuration>
       </plugin>
+      <!-- very opinionated styling
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-checkstyle-plugin</artifactId>
@@ -141,7 +142,7 @@
             </goals>
           </execution>
         </executions>
-      </plugin>
+      </plugin>-->
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-pmd-plugin</artifactId>
@@ -159,6 +160,7 @@
           </execution>
         </executions>
       </plugin>
+      <!-- lots of false positives, therefore disable and rely on SonarQube 
Cloud 
       <plugin>
         <groupId>com.github.spotbugs</groupId>
         <artifactId>spotbugs-maven-plugin</artifactId>
@@ -181,7 +183,7 @@
             </goals>
           </execution>
         </executions>
-      </plugin>
+      </plugin>-->
       <plugin>
         <groupId>com.diffplug.spotless</groupId>
         <artifactId>spotless-maven-plugin</artifactId>
@@ -241,6 +243,11 @@
       <artifactId>org.osgi.util.tracker</artifactId>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <scope>test</scope>
+    </dependency>
     <!-- Apache Commons -->
     <dependency>
       <groupId>org.apache.commons</groupId>
@@ -265,7 +272,7 @@
     <dependency>
       <groupId>org.bouncycastle</groupId>
       <artifactId>bcprov-jdk18on</artifactId>
-      <version>1.83</version>
+      <version>1.85</version>
       <scope>test</scope>
     </dependency>
     <!-- Jasypt -->
@@ -297,6 +304,21 @@
       <scope>provided</scope>
     </dependency>
     <!-- testing -->
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.platform</groupId>
+      <artifactId>junit-platform-engine</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
@@ -314,6 +336,12 @@
       <version>5.23.0</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <version>1.7.0</version><!-- must be compatible with API -->
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>com.github.stefanbirkner</groupId>
       <artifactId>system-lambda</artifactId>
@@ -381,6 +409,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..2370f82 100644
--- a/src/main/java/org/apache/sling/commons/crypto/CryptoService.java
+++ b/src/main/java/org/apache/sling/commons/crypto/CryptoService.java
@@ -19,6 +19,7 @@
 package org.apache.sling.commons.crypto;
 
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.osgi.annotation.versioning.ProviderType;
 
 /**
@@ -32,6 +33,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,7 +42,21 @@ 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
+     * @throws IllegalStateException if the key used to decrypt the message is 
not available or cannot be used for some reason
      */
     public abstract @NotNull String decrypt(@NotNull final String ciphertext);
+    
+    /**
+     * Returns a description of the algorithm(s) used by this service.
+     * <p>
+     * This method is optional and may return {@code null} if no description 
is available.
+     *
+     * @return A description of the algorithm used by this service, or {@code 
null} if not available
+     * @since 1.2.0 (Bundle version 1.3.0)
+     */
+    default @Nullable String getAlgorithmDescription() {
+        return null;
+    }
 
 }
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/internal/OsgiAwareSecurityProviderInstaller.java
 
b/src/main/java/org/apache/sling/commons/crypto/internal/OsgiAwareSecurityProviderInstaller.java
new file mode 100644
index 0000000..6c495a5
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/commons/crypto/internal/OsgiAwareSecurityProviderInstaller.java
@@ -0,0 +1,155 @@
+/*
+ * 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.internal;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.charset.StandardCharsets;
+import java.security.Provider;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Hashtable;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.SynchronousBundleListener;
+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.propertytypes.ServiceDescription;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This DS component listens for bundle events and automatically installs or 
uninstalls security providers
+ * based on the presence of a service registration file {@value 
#SECURITY_PROVIDER_CONFIGURATION_FILE} in the 
+ * started/stopping bundle.
+ */
+@Component(immediate = true, service= {}, name = 
"org.apache.sling.commons.crypto.internal.AutoRegisterSecurityProvider")
+@ServiceDescription("Apache Sling Commons Crypto – Auto Register Security 
Provider")
+public final class OsgiAwareSecurityProviderInstaller implements 
SynchronousBundleListener {
+    private static final String SECURITY_PROVIDER_CONFIGURATION_FILE = 
"META-INF/services/java.security.Provider";
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(OsgiAwareSecurityProviderInstaller.class);
+
+    @Activate
+    public OsgiAwareSecurityProviderInstaller(BundleContext bundleContext) {
+        bundleContext.addBundleListener(this);
+        for (Bundle bundle : bundleContext.getBundles()) {
+            if (bundle.getState() == Bundle.ACTIVE) {
+                addOrRemoveProviders(true, bundle);
+            }
+        }
+    }
+
+    @Deactivate
+    public void deactivate(BundleContext bundleContext) {
+        bundleContext.removeBundleListener(this);
+        for (Bundle bundle : bundleContext.getBundles()) {
+            if (bundle.getState() == Bundle.ACTIVE) {
+                addOrRemoveProviders(false, bundle);
+            }
+        }
+    }
+
+    @Override
+    public void bundleChanged(BundleEvent event) {
+        Bundle bundle = event.getBundle();
+        final boolean isAdd;
+        if (event.getType() == BundleEvent.STARTED) {
+            isAdd = true;
+        } else if (event.getType() == BundleEvent.STOPPING) {
+            isAdd = false;
+        } else {
+            LOGGER.debug("Ignoring bundle event {} for bundle {}", 
event.getType(), bundle.getSymbolicName());
+            return;
+        }
+        addOrRemoveProviders(isAdd, bundle);
+    }
+
+    protected void addOrRemoveProviders(boolean isAdd, Bundle bundle) {
+        try {
+            Collection<String> classNames = 
collectClassNamesFromProviderConfigurationFile(bundle);
+            for (String className : classNames) {
+                try {
+                    addOrRemoveProvider(isAdd, bundle, className);
+                } catch (ClassNotFoundException e) {
+                    LOGGER.error("Class {} not found in bundle {}: {}", 
className, bundle.getSymbolicName(), e.getMessage(), e);
+                } catch (Exception e) {
+                    LOGGER.error("Error adding/removing security provider 
class {} from bundle {}: {}", className, bundle.getSymbolicName(), 
e.getMessage(), e);
+                }
+            }
+        } catch (IOException e) {
+            LOGGER.error("Error reading provider configuration file from 
bundle {}: {}", bundle.getSymbolicName(), e.getMessage(), e);
+        }
+    }
+
+    protected Collection<String> 
collectClassNamesFromProviderConfigurationFile(Bundle bundle) throws 
IOException {
+        var serviceRegistrationResource = 
bundle.getEntry(SECURITY_PROVIDER_CONFIGURATION_FILE);
+        Collection<String> classNames = new ArrayList<>();
+        if (serviceRegistrationResource != null) {
+            try (InputStream inputStream = 
serviceRegistrationResource.openStream();
+                 BufferedReader reader = new BufferedReader(new 
InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    line = line.trim();
+                    if (!line.isEmpty() && !line.startsWith("#")) {
+                        classNames.add(line);
+                    }
+                }
+            }
+        } else {
+            LOGGER.debug("No service registration file found in bundle {}", 
bundle);
+        }
+        return classNames;
+    }
+
+    protected void addOrRemoveProvider(boolean isAdd, Bundle bundle, String 
providerClassName)
+            throws ClassNotFoundException, InstantiationException, 
IllegalAccessException, IllegalArgumentException,
+            InvocationTargetException, NoSuchMethodException, 
SecurityException {
+        Class<?> clazz = bundle.loadClass(providerClassName);
+        if (!Provider.class.isAssignableFrom(clazz)) {
+            // Handle the case where the class is not a Provider
+            LOGGER.warn("Class {} in bundle {} is not a subclass of 
java.security.Provider", providerClassName, bundle);
+        }
+        Provider provider = (Provider) 
clazz.getDeclaredConstructor().newInstance();
+        if (isAdd) {
+            int position = Security.addProvider(provider);
+            if (position == -1) {
+                LOGGER.warn("Failed to add security provider {} (name {}) from 
bundle {} to the security providers list. Provider with that name already 
registered.", providerClassName, provider.getName(), bundle);
+            }
+            // also add service registration for the provider so that other 
services can defer loading until the provider is available
+            Hashtable<String, String> props = new Hashtable<>();
+            props.put("provider.name", provider.getName());
+            bundle.getBundleContext().registerService(Provider.class, 
provider, props);
+            LOGGER.info("Added security provider {} (name {}) from bundle {} 
to last position {}", providerClassName, provider.getName(), bundle, position);
+        } else {
+            if (Security.getProvider(provider.getName()) != null) {
+                Security.removeProvider(provider.getName());
+                LOGGER.info("Removed security provider {} (name {}) from 
bundle {}", providerClassName, provider.getName(), bundle);
+            } else {
+                LOGGER.warn("Security provider {} (name {}) not found for 
removal", providerClassName, provider.getName());
+            }
+        }
+    }
+}
diff --git 
a/src/main/java/org/apache/sling/commons/crypto/internal/SecureRandomSaltProvider.java
 
b/src/main/java/org/apache/sling/commons/crypto/internal/SecureRandomSaltProvider.java
index a8ad8a9..886069f 100644
--- 
a/src/main/java/org/apache/sling/commons/crypto/internal/SecureRandomSaltProvider.java
+++ 
b/src/main/java/org/apache/sling/commons/crypto/internal/SecureRandomSaltProvider.java
@@ -27,8 +27,6 @@ import org.jetbrains.annotations.NotNull;
 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.Modified;
 import org.osgi.service.metatype.annotations.Designate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -51,36 +49,21 @@ import org.slf4j.LoggerFactory;
 @SuppressWarnings({"java:S1117", "java:S6212"})
 public final class SecureRandomSaltProvider implements SaltProvider {
 
-    private SecureRandom secureRandom;
+    private final SecureRandom secureRandom;
 
-    private SecureRandomSaltProviderConfiguration configuration;
+    private final SecureRandomSaltProviderConfiguration configuration;
 
     private final Logger logger = 
LoggerFactory.getLogger(SecureRandomSaltProvider.class);
 
-    public SecureRandomSaltProvider() { //
-    }
-
     @Activate
-    @SuppressWarnings("unused")
-    private void activate(final SecureRandomSaltProviderConfiguration 
configuration) throws NoSuchAlgorithmException {
+    public SecureRandomSaltProvider(final 
SecureRandomSaltProviderConfiguration configuration) throws 
NoSuchAlgorithmException { //
         logger.debug("activating");
         this.configuration = configuration;
-        secureRandom = SecureRandom.getInstance(configuration.algorithm());
-
-    }
-
-    @Modified
-    @SuppressWarnings("unused")
-    private void modified(final SecureRandomSaltProviderConfiguration 
configuration) throws NoSuchAlgorithmException {
-        logger.debug("modifying");
-        this.configuration = configuration;
-        secureRandom = SecureRandom.getInstance(configuration.algorithm());
-    }
-
-    @Deactivate
-    @SuppressWarnings("unused")
-    private void deactivate() {
-        logger.debug("deactivating");
+        if (configuration.algorithm().isBlank()) {
+            secureRandom = new SecureRandom();
+        } else {
+            secureRandom = SecureRandom.getInstance(configuration.algorithm());
+        }
     }
 
     @Override
diff --git 
a/src/main/java/org/apache/sling/commons/crypto/internal/SecureRandomSaltProviderConfiguration.java
 
b/src/main/java/org/apache/sling/commons/crypto/internal/SecureRandomSaltProviderConfiguration.java
index 3346127..fe27010 100644
--- 
a/src/main/java/org/apache/sling/commons/crypto/internal/SecureRandomSaltProviderConfiguration.java
+++ 
b/src/main/java/org/apache/sling/commons/crypto/internal/SecureRandomSaltProviderConfiguration.java
@@ -37,13 +37,13 @@ import 
org.osgi.service.metatype.annotations.ObjectClassDefinition;
 
     @AttributeDefinition(
         name = "Algorithm",
-        description = "secure random number generation algorithm"
+        description = "Secure random number generation algorithm. The standard 
ones are outlined in 
https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#securerandom-number-generation-algorithms.
 Leave empty to use the JRE's default SecureRandom implementation."
     )
-    String algorithm() default "SHA1PRNG";
+    String algorithm() default "";
 
     @AttributeDefinition(
-        name = "Key Length",
-        description = "length of the key"
+        name = "Salt Length",
+        description = "Length of the generated salt in bytes"
     )
     int keyLength() default 8;
 
diff --git 
a/src/main/java/org/apache/sling/commons/crypto/jasypt/internal/JasyptStandardPbeStringCryptoService.java
 
b/src/main/java/org/apache/sling/commons/crypto/jasypt/internal/JasyptStandardPbeStringCryptoService.java
index bb2a714..08eff49 100644
--- 
a/src/main/java/org/apache/sling/commons/crypto/jasypt/internal/JasyptStandardPbeStringCryptoService.java
+++ 
b/src/main/java/org/apache/sling/commons/crypto/jasypt/internal/JasyptStandardPbeStringCryptoService.java
@@ -30,6 +30,7 @@ import org.jasypt.iv.IvGenerator;
 import org.jasypt.registry.AlgorithmRegistry;
 import org.jasypt.salt.SaltGenerator;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.osgi.framework.Constants;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
@@ -77,8 +78,11 @@ public final class JasyptStandardPbeStringCryptoService 
implements CryptoService
 
     private StandardPBEStringEncryptor encryptor;
 
+    private String algorithm;
+
     private final Logger logger = 
LoggerFactory.getLogger(JasyptStandardPbeStringCryptoService.class);
 
+    @SuppressWarnings("unused")
     public JasyptStandardPbeStringCryptoService() { //
     }
 
@@ -103,7 +107,7 @@ public final class JasyptStandardPbeStringCryptoService 
implements CryptoService
     }
 
     private void setupEncryptor(final 
JasyptStandardPbeStringCryptoServiceConfiguration configuration) {
-        final String algorithm = configuration.algorithm();
+        algorithm = configuration.algorithm();
         final Set<?> algorithms = AlgorithmRegistry.getAllPBEAlgorithms();
         if (!algorithms.contains(algorithm)) {
             logger.warn("Configured algorithm {} for password based encryption 
is not available. {}", algorithm, algorithms);
@@ -146,4 +150,10 @@ public final class JasyptStandardPbeStringCryptoService 
implements CryptoService
         return encryptor.decrypt(ciphertext);
     }
 
+    @Override
+    public @Nullable String getAlgorithmDescription() {
+        return algorithm;
+    }
+
+    
 }
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..b394aa2
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/commons/crypto/jca/internal/JcaPbeCryptoService.java
@@ -0,0 +1,343 @@
+/*
+ * 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.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.AlgorithmParameters;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+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.util.Base64;
+import java.util.Objects;
+import java.util.Optional;
+
+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.PBEKeySpec;
+import javax.crypto.spec.PBEParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.security.auth.DestroyFailedException;
+
+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.jetbrains.annotations.Nullable;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.propertytypes.ServiceDescription;
+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 a {@link SecretKeyFactory} with
+ * {@link PBEKeySpec} 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>
+ * @see <a 
href="https://docs.oracle.com/en/java/javase/21/security/java-cryptography-architecture-jca-reference-guide.html";>Java
+ *      Cryptography Architecture (JCA) Reference Guide</a> */
+@Component(service = CryptoService.class)
+@Designate(ocd = JcaPbeCryptoServiceConfiguration.class, factory = true)
+@ServiceDescription("Apache Sling Commons Crypto – JCA PBE String Crypto 
Service")
+@SuppressWarnings({ "java:S1117", "java:S3077", "java:S6212" })
+public final class JcaPbeCryptoService implements CryptoService {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(JcaPbeCryptoService.class);
+    private final PasswordProvider passwordProvider;
+    // not thread safe!
+    private final SecureRandom secureRandom;
+    private final Optional<Provider> securityProvider;
+
+    private final JcaPbeCryptoServiceConfiguration configuration;
+
+    /**
+     * This salt is only relevant for the encryption key, for decryption keys 
the salt is extracted from the cipher data.
+     * The salt is initialized once during service activation and is used for 
all encryption operations. 
+     * As the salt is not used for the IV (initialization vector) of the 
cipher, it does not need to be unique for each encryption operation.
+     */
+    private final byte[] salt;
+
+    @Activate
+    public JcaPbeCryptoService(final JcaPbeCryptoServiceConfiguration 
configuration, BundleContext bundleContext,
+            @Reference(name="passwordProvider") PasswordProvider 
passwordProvider, @Reference(name="saltProvider") SaltProvider saltProvider)
+            throws NoSuchAlgorithmException { //
+        this(configuration, saltProvider.getSalt(), passwordProvider);
+    }
+
+    protected JcaPbeCryptoService(final JcaPbeCryptoServiceConfiguration 
configuration, byte[] salt, PasswordProvider passwordProvider)
+            throws NoSuchAlgorithmException { //
+        this.configuration = configuration;
+        this.passwordProvider = passwordProvider;
+        if (configuration.secureRandomAlgorithm() != null && 
!configuration.secureRandomAlgorithm().isBlank()) {
+            this.secureRandom = 
SecureRandom.getInstance(configuration.secureRandomAlgorithm());
+        } else {
+            this.secureRandom = new SecureRandom();
+        }
+        securityProvider = 
Optional.ofNullable(configuration.securityProviderName()).filter(name -> 
!name.isBlank())
+                .map(Security::getProvider);
+        if (securityProvider.isPresent()) {
+            LOGGER.debug("Using security provider {} for JCA PBE Crypto 
Service", securityProvider.get());
+        } 
+        this.salt = salt;
+    }
+
+    private static void destroyKey(SecretKey key) {
+        try {
+            // not implemented for all relevant keys, 
https://bugs.openjdk.org/browse/JDK-8389121
+            key.destroy();
+        } catch (DestroyFailedException e) {
+            // log and ignore
+            LOGGER.debug("Could not destroy key {} as implementation does not 
implement destroy()", key, e);
+        }
+    }
+
+    private @NotNull SecretKey createKey(byte[] salt) throws 
NoSuchAlgorithmException, InvalidKeySpecException {
+        final char[] password = passwordProvider.getPassword();
+        // for regular PBE key this is completely ignored except for the 
password (as all logic is encapsulated in the actual cipher
+        // implementation, see
+        // 
https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/com/sun/crypto/provider/PBEKeyFactory.java
+        PBEKeySpec keySpec = new PBEKeySpec(
+                password,
+                salt,
+                configuration.numKeyIterations(),
+                configuration.keyLengthBits());
+        SecretKeyFactory secretKeyFactory = securityProvider.isPresent()
+                ? 
SecretKeyFactory.getInstance(configuration.secretKeyFactoryAlgorithm(), 
securityProvider.get())
+                : 
SecretKeyFactory.getInstance(configuration.secretKeyFactoryAlgorithm());
+        SecretKey originalKey = secretKeyFactory.generateSecret(keySpec);
+        keySpec.clearPassword(); // clear password from memory after use
+        if 
(configuration.secretKeyFactoryAlgorithm().equals(configuration.cipherAlgorithm()))
 {
+            // if the cipher algorithm is the same as the secret key factory 
algorithm then the cipher takes care of the actual logic and
+            // uses the key as is (which is just a wrapper around the given 
password)
+            return originalKey;
+        } else {
+            // wrap as key for the proper cipher algorithm (e.g., AES) instead 
of the PBE algorithm (e.g., PBKDF2WithHmacSHA512)
+            SecretKey derivedKey = new SecretKeySpec(originalKey.getEncoded(), 
extractAlgorithmName(configuration.cipherAlgorithm()));
+            destroyKey(originalKey); // destroy the original key as it is no 
longer needed
+            return derivedKey;
+        }
+    }
+
+    /** 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 paramsName the name of the algorithm parameters (e.g., "AES"), 
{@code null} if cipher should be used for encryption and
+     *            default parameters should be generated (e.g., random IV for 
AES/CBC)
+     * @param isForEncryption {@code true} if the cipher should be initialized 
for encryption, {@code false} if it should be initialized for decryption
+     * @param params the algorithm parameters if the cipher should be used for 
decryption, {@code null} if cipher should be used for encryption and default 
parameters should be used instead
+     * @return a Cipher instance initialized for encryption or decryption
+     * @throws NoSuchPaddingException
+     * @throws NoSuchAlgorithmException
+     * @throws InvalidKeyException
+     * @throws InvalidAlgorithmParameterException
+     * @throws IOException */
+    private Cipher createCipher(@NotNull Key key, boolean isForEncryption, 
@Nullable AlgorithmParameters params)
+            throws NoSuchAlgorithmException, NoSuchPaddingException, 
InvalidKeyException, InvalidAlgorithmParameterException, IOException {
+        Cipher cipher = securityProvider.isPresent() ? 
Cipher.getInstance(configuration.cipherAlgorithm(), securityProvider.get())
+                : Cipher.getInstance(configuration.cipherAlgorithm());
+        if (isForEncryption) {
+            // rely on default parameters generated by the cipher (e.g., 
random IV for AES/CBC)
+            cipher.init(Cipher.ENCRYPT_MODE, key, secureRandom);
+        } else {
+            Objects.requireNonNull(params, "AlgorithmParameters must not be 
null for decryption");
+            cipher.init(Cipher.DECRYPT_MODE, key, params, secureRandom);
+        }
+        return cipher;
+    }
+
+    private AlgorithmParameters createAlgorithmParameters(String paramsName, 
byte[] encodedParams)
+            throws NoSuchAlgorithmException, IOException {
+        // create a new AlgorithmParameters instance for the cipher algorithm 
and initialize it with the encoded parameters
+        AlgorithmParameters params = securityProvider.isPresent()
+                ? AlgorithmParameters.getInstance(paramsName, 
securityProvider.get())
+                : AlgorithmParameters.getInstance(paramsName);
+        params.init(encodedParams);
+        return params;
+    }
+
+    protected AlgorithmParameters createDefaultAlgorithmParameters() throws 
InvalidKeyException, NoSuchAlgorithmException,
+            NoSuchPaddingException, InvalidAlgorithmParameterException, 
IOException, InvalidKeySpecException {
+        SecretKey key = createKey(salt);
+        try {
+            Cipher cipher = createCipher(key, true, null);
+            return cipher.getParameters();
+        } finally {
+            destroyKey(key);
+        }
+    }
+
+    @Override
+    public @NotNull String encrypt(@NotNull final String message) {
+        try {
+            SecretKey key = createKey(salt);
+            try {
+                return encrypt(key, message);
+            } catch (NoSuchAlgorithmException | InvalidKeyException | 
NoSuchPaddingException | InvalidAlgorithmParameterException
+                    | IOException | IllegalBlockSizeException | 
BadPaddingException e) {
+                throw new IllegalStateException("Could not encrypt message", 
e);
+            } finally {
+                destroyKey(key);
+            }
+        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+            throw new IllegalStateException("Could not create key for 
encryption", e);
+        }
+    }
+
+    private @NotNull String encrypt(@NotNull final Key key, @NotNull final 
String message) throws InvalidKeyException, NoSuchAlgorithmException,
+            NoSuchPaddingException, InvalidAlgorithmParameterException, 
IOException, IllegalBlockSizeException, BadPaddingException {
+        Cipher cipherEncrypt = createCipher(key, true, null);
+        final byte[] params;
+        final byte[] paramsName;
+        if (cipherEncrypt.getParameters() == null) {
+            params = new byte[0];
+            paramsName = new byte[0];
+        } else {
+            params = cipherEncrypt.getParameters().getEncoded();
+            paramsName = 
cipherEncrypt.getParameters().getAlgorithm().getBytes(StandardCharsets.UTF_8);
+        }
+        byte[] cipherTextBytes = 
cipherEncrypt.doFinal(message.getBytes(StandardCharsets.UTF_8));
+
+        // Combine paramsName + parameters + ciphertext into single array
+        int totalLength = Integer.BYTES + paramsName.length + Integer.BYTES + 
params.length + cipherTextBytes.length;
+        boolean isSaltIncludedInParams = 
isSaltIncludedInParams(cipherEncrypt.getParameters());
+        if (!isSaltIncludedInParams) {
+            totalLength += Integer.BYTES + salt.length; // Include salt in the 
output if not included in params
+        }
+        ByteBuffer bb = ByteBuffer.allocate(totalLength)
+            .putInt(paramsName.length)
+            .put(paramsName)
+            .putInt(params.length)
+            .put(params);
+        if (!isSaltIncludedInParams) {
+            bb.putInt(salt.length)
+              .put(salt);
+        }
+        bb.put(cipherTextBytes);
+        
+        return Base64.getEncoder().encodeToString(bb.array());
+    }
+
+    @Override
+    public @NotNull String decrypt(@NotNull final String cipherText) {
+        try {
+            byte[] encryptedData = Base64.getDecoder().decode(cipherText);
+            ByteBuffer buffer = ByteBuffer.wrap(encryptedData);
+            final byte[] cipherData;
+            // Split up into paramsName, params, and cipherData
+            int paramsNameLength = buffer.getInt();
+            if (paramsNameLength < 0 || paramsNameLength > 255) {
+                throw new IllegalArgumentException("Invalid params name length 
" + paramsNameLength);
+            }
+            byte[] paramsName = new byte[paramsNameLength];
+            buffer.get(paramsName);
+            String paramsNameStr = new String(paramsName, 
StandardCharsets.UTF_8);
+            int paramsLength = buffer.getInt();
+            if (paramsLength < 0 || paramsLength > 65535) {
+                throw new IllegalArgumentException("Invalid params length " + 
paramsLength);
+            }
+            byte[] params = new byte[paramsLength];
+            buffer.get(params);
+            AlgorithmParameters algorithmParams = 
createAlgorithmParameters(paramsNameStr, params);
+            final byte[] salt;
+            if (!isSaltIncludedInParams(algorithmParams)) {
+                // If the salt is not included in the parameters, it is part 
of the cipher data and needs to be extracted and used to create the algorithm 
parameters
+                int saltLength = buffer.getInt(); // Skip the salt length
+                salt = new byte[saltLength];
+                buffer.get(salt);
+            } else {
+                salt = this.salt; // Use the salt from the service 
configuration
+            }
+            cipherData = new byte[buffer.remaining()];
+            buffer.get(cipherData);
+            SecretKey key = createKey(salt);
+            try {
+                return decrypt(key, algorithmParams, cipherData);
+            } catch (NoSuchAlgorithmException | InvalidKeyException | 
NoSuchPaddingException | InvalidAlgorithmParameterException
+                    | IOException | IllegalBlockSizeException | 
BadPaddingException e) {
+                throw new IllegalArgumentException("Could not decrypt cipher 
text", e);
+            } finally {
+                destroyKey(key);
+            }
+        } catch (NoSuchAlgorithmException | InvalidKeySpecException | 
IOException e) {
+            throw new IllegalStateException("Could not create key for 
decryption", e);
+        }
+    }
+
+    private @NotNull String decrypt(final SecretKey key, AlgorithmParameters 
params, final byte[] cipherData) throws InvalidKeyException, 
NoSuchAlgorithmException,
+            NoSuchPaddingException, InvalidAlgorithmParameterException, 
IOException, IllegalBlockSizeException, BadPaddingException {
+        final Cipher cipher = createCipher(key, false, params);
+        byte[] plainTextBytes = cipher.doFinal(cipherData);
+        return new String(plainTextBytes, StandardCharsets.UTF_8);
+    }
+
+    static boolean isSaltIncludedInParams(AlgorithmParameters params) {
+        // Check if the salt is included in the parameters (e.g., for PBE 
algorithms)
+        if (params == null) {
+            return false;
+        }
+        try {
+            params.getParameterSpec(PBEParameterSpec.class);
+            return true;
+        } catch (InvalidParameterSpecException e) {
+            return false;
+        } // This will throw an exception if the parameters do not include the 
salt
+    }
+
+    @Override
+    public @Nullable String getAlgorithmDescription() {
+        StringBuilder sb = new StringBuilder();
+        
sb.append("secretKeyFactory=").append(configuration.secretKeyFactoryAlgorithm());
+        sb.append(", cipher=").append(configuration.cipherAlgorithm());
+        if (!configuration.securityProviderName().isBlank()) {
+            sb.append(", 
provider=").append(configuration.securityProviderName());
+        }
+        return sb.toString();
+    }
+    @Override
+    public String toString() {
+        return "JcaPbeCryptoService [" + getAlgorithmDescription() + "]";
+    }
+}
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..a41e208
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/commons/crypto/jca/internal/JcaPbeCryptoServiceConfiguration.java
@@ -0,0 +1,100 @@
+/*
+ * 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) based on SecretKeyFactory 
parameterized with PBEKeySpec 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.
 Must support key spec of type \"javax.crypto.spec.PBEKeySpec\"."
+    )
+    String secretKeyFactoryAlgorithm() 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). If a PBE algorithm (format \"PBEWith<digest>And<encryption>\" 
or \"PBEWith<prf>And<encryption>\" is used for the secret key factory  [...]
+    )
+    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.
 Leave empty to use the default SecureRandom implementation provided by the 
JVM.",
+        required = false
+    )
+    String secureRandomAlgorithm() default "";
+
+    @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. Ignored for PBE algorithms which implicitly 
contain the key length already."
+    )
+    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. If left empty, the first 
registered provider for the given algorithm will be used.",
+        required = false
+    )
+    String securityProviderName() default "SunJCE";
+
+    // automatically evaluated 
(https://docs.osgi.org/specification/osgi.cmpn/8.0.0/service.component.html#service.component-target.property)
+    @AttributeDefinition(
+        name = "Password Provider Target",
+        description = "Filter expression to target a Password Provider 
(usually by name with a pattern like \"(names=*)\"). If not specified, the 
first available Password Provider will be used.",
+        required = false
+    )
+    String passwordProvider_target();
+
+    // automatically evaluated 
(https://docs.osgi.org/specification/osgi.cmpn/8.0.0/service.component.html#service.component-target.property)
+    @AttributeDefinition(
+        name = "Salt Provider Target",
+        description = "Filter expression to target a Salt Provider (usually by 
name with a pattern like \"(names=*)\"). If not specified, the first available 
Salt Provider will be used.",
+        required = false
+    )
+    String saltProvider_target();
+
+    @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: {secretKeyFactory}, Cipher: {cipherAlgorithm}";
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/commons/crypto/package-info.java 
b/src/main/java/org/apache/sling/commons/crypto/package-info.java
index 25bccb7..7d9983a 100644
--- a/src/main/java/org/apache/sling/commons/crypto/package-info.java
+++ b/src/main/java/org/apache/sling/commons/crypto/package-info.java
@@ -20,7 +20,7 @@
 /**
  * Provides the Apache Sling Commons Crypto API.
  */
-@Version("1.1.0")
+@Version("1.2.0")
 package org.apache.sling.commons.crypto;
 
 import org.osgi.annotation.versioning.Version;
diff --git 
a/src/main/java/org/apache/sling/commons/crypto/webconsole/internal/EncryptWebConsolePlugin.java
 
b/src/main/java/org/apache/sling/commons/crypto/webconsole/internal/EncryptWebConsolePlugin.java
index 56d0a81..94db866 100644
--- 
a/src/main/java/org/apache/sling/commons/crypto/webconsole/internal/EncryptWebConsolePlugin.java
+++ 
b/src/main/java/org/apache/sling/commons/crypto/webconsole/internal/EncryptWebConsolePlugin.java
@@ -62,8 +62,6 @@ public final class EncryptWebConsolePlugin extends 
HttpServlet {
 
     private static final String ATTRIBUTE_CIPHERTEXT = 
"org.apache.sling.commons.crypto.webconsole.internal.EncryptWebConsolePlugin.ciphertext";
 
-    private BundleContext bundleContext;
-
     private ServiceTracker<CryptoService, CryptoService> tracker;
 
     public EncryptWebConsolePlugin() { //
@@ -72,7 +70,6 @@ public final class EncryptWebConsolePlugin extends 
HttpServlet {
     @Activate
     @SuppressWarnings("unused")
     private void activate(final BundleContext bundleContext) {
-        this.bundleContext = bundleContext;
         tracker = new ServiceTracker<>(bundleContext, CryptoService.class, 
null);
         tracker.open();
     }
@@ -80,7 +77,6 @@ public final class EncryptWebConsolePlugin extends 
HttpServlet {
     @Deactivate
     @SuppressWarnings("unused")
     private void deactivate() {
-        this.bundleContext = null;
         if (Objects.nonNull(tracker)) {
             tracker.close();
             tracker = null;
@@ -158,9 +154,11 @@ public final class EncryptWebConsolePlugin extends 
HttpServlet {
         builder.append("<select id=\"service-id\" name=\"service-id\">");
         for (final ServiceReference<CryptoService> reference : references) {
             final String id = 
reference.getProperty(Constants.SERVICE_ID).toString();
+            final String description = 
Objects.toString(reference.getProperty(Constants.SERVICE_DESCRIPTION), "");
             final String[] names = (String[]) reference.getProperty("names");
-            final String algorithm = 
reference.getProperty("algorithm").toString();
-            final String label = String.format("Service id %s, names: %s, 
algorithm: %s", id, Arrays.toString(names), algorithm);
+            CryptoService service = findCryptoService(id);
+            final String algorithm = 
Objects.toString(service.getAlgorithmDescription(), "");
+            final String label = String.format("Service id %s (%s), names: %s, 
algorithm(s): %s", id, description, Arrays.toString(names), algorithm);
             builder.append("<option value=\"").append(id).append("\">");
             builder.append(label);
             builder.append("</option>");
@@ -180,7 +178,7 @@ public final class EncryptWebConsolePlugin extends 
HttpServlet {
         }
         for (final ServiceReference<CryptoService> reference : references) {
             if 
(id.equals(reference.getProperty(Constants.SERVICE_ID).toString())) {
-                return bundleContext.getService(reference);
+                return tracker.getService(reference);
             }
         }
         return null;
diff --git 
a/src/main/java/org/apache/sling/commons/crypto/webconsole/internal/JcaProviderAlgorithmsWebConsolePlugin.java
 
b/src/main/java/org/apache/sling/commons/crypto/webconsole/internal/JcaProviderAlgorithmsWebConsolePlugin.java
new file mode 100644
index 0000000..de3cbda
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/commons/crypto/webconsole/internal/JcaProviderAlgorithmsWebConsolePlugin.java
@@ -0,0 +1,201 @@
+/*
+ * 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.webconsole.internal;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.security.Provider;
+import java.security.Provider.Service;
+import java.security.Security;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * Web Console plugin to list the algorithms for service types of JCA 
providers.
+ */
+@Component(
+    service = Servlet.class,
+    property = {
+        "felix.webconsole.label=sling-commons-crypto-jca-provider-algorithms",
+        "felix.webconsole.title=Sling Commons Crypto JCA Provider Algorithms",
+        "felix.webconsole.category=Crypto"
+    }
+)
+@SuppressWarnings({"java:S1989", "java:S2226", "java:S6212"})
+public final class JcaProviderAlgorithmsWebConsolePlugin extends HttpServlet {
+
+    private static final String PARAMETER_PROVIDER_NAME = "provider";
+    private static final String PARAMETER_SERVICE_TYPE = "servicetype";
+    private static final String PARAMETER_VALUE_ALL = "all";
+
+
+    public JcaProviderAlgorithmsWebConsolePlugin() { //
+    }
+
+
+    @Override
+    protected void doGet(final HttpServletRequest request, final 
HttpServletResponse response) throws ServletException, IOException {
+        final Provider[] providers = Security.getProviders();
+        final String providerName = 
Objects.toString(request.getParameter(PARAMETER_PROVIDER_NAME), 
PARAMETER_VALUE_ALL);
+        final String serviceType = 
Objects.toString(request.getParameter(PARAMETER_SERVICE_TYPE), 
PARAMETER_VALUE_ALL);
+        
+        final PrintWriter writer = response.getWriter();
+        Collection<Service> services = new LinkedList<>();
+        Consumer<Service> serviceConsumer = services::add;
+        writeForm(writer, serviceConsumer, providerName, serviceType, 
providers);
+        
+        writer.println("<table class='tablesorter nicetable'>");
+        writer.println("<thead>");
+        writer.println("<tr><th class='col-provider'>Provider</th><th 
class='col-typr'>Type</th><th class='col-algo'>Algorithm</th><th 
class='col-class'>Class Name</th></tr>");
+        writer.println("</thead>");
+        writer.println("<tbody>");
+        for (Service service : services) {
+            writer.println("<tr>");
+            writeTableCell(writer, service.getProvider().getName());
+            writeTableCell(writer, service.getType());
+            writeTableCell(writer, service.getAlgorithm());
+            writeTableCell(writer, service.getClassName());
+            writer.println("</tr>");
+        }
+        writer.println("</tbody>");
+        writer.println("</table>");
+    }
+
+    private void writeTableCell(final PrintWriter writer, String text) {
+        writer.println("<td>" + escapeHtml(text) + "</td>");
+    }
+
+    private @NotNull void writeForm(@NotNull PrintWriter writer, @NotNull 
Consumer<Service> serviceConsumer, @NotNull String selectedProviderName, 
@NotNull String selectedServiceType, Provider...providers) {
+        writer.append("<form method=\"GET\">");
+        writer.append("<div class=\"ui-widget-header ui-corner-top 
buttonGroup\">");
+        writer.append("<label 
for=\"").append(PARAMETER_PROVIDER_NAME).append("\">Provider</label>");
+        writer.append("<select 
id=\"").append(PARAMETER_PROVIDER_NAME).append("\" 
name=\"").append(PARAMETER_PROVIDER_NAME).append("\">");
+        // add an entry for all providers
+        Provider selectedProvider = null;
+        writeOption(writer, PARAMETER_VALUE_ALL, null, "All Providers", null);
+        for (Provider provider : Security.getProviders()) {
+            writeOption(writer, provider.getName(), selectedProviderName, 
provider.getName(), provider.getInfo());
+            if (provider.getName().equals(selectedProviderName)) {
+                selectedProvider = provider;
+            }
+        }
+        writer.append("</select>");
+        writer.append("<br>");
+        writer.append("<label 
for=\"").append(PARAMETER_SERVICE_TYPE).append("\">Service Type</label>");
+        writer.append("<select 
id=\"").append(PARAMETER_SERVICE_TYPE).append("\" 
name=\"").append(PARAMETER_SERVICE_TYPE).append("\">");
+        writeOption(writer, PARAMETER_VALUE_ALL, null, "All Types", null);
+        ServiceTypePredicate serviceTypePredicate = new 
ServiceTypePredicate(selectedServiceType);
+        // entry for service types for that provider
+        if (Objects.nonNull(selectedProvider)) {
+            selectedProvider.getServices().stream()
+                .filter(serviceTypePredicate)
+                .forEach(serviceConsumer);
+            selectedProvider.getServices().stream()
+                .map(Provider.Service::getType)
+                .distinct()
+                .sorted()
+                .forEach(serviceType -> 
+                    writeOption(writer, serviceType, selectedServiceType, 
serviceType, null)
+                );
+        } else {
+            
Stream.of(providers).map(Provider::getServices).flatMap(Collection::stream).filter(serviceTypePredicate).forEach(serviceConsumer);
+            
Stream.of(providers).map(Provider::getServices).flatMap(Collection::stream)
+                .map(Provider.Service::getType).distinct().sorted()
+                    .forEach(serviceType -> 
+                        writeOption(writer, serviceType, selectedServiceType, 
serviceType, null)
+                    );
+        }
+        writer.append("</select>");
+        writer.append("</div>");
+        writer.append("</form>");
+        
+        writer.append("<script>")
+               .append("(function(){")
+               .append("var 
providerSelect=document.getElementById('").append(PARAMETER_PROVIDER_NAME).append("');")
+               .append("var 
serviceTypeSelect=document.getElementById('").append(PARAMETER_SERVICE_TYPE).append("');")
+               .append("if(!providerSelect){return;}")
+               .append("function updateUrl(){")
+               .append("var url=new URL(window.location.href);")
+               
.append("url.searchParams.set('").append(PARAMETER_PROVIDER_NAME).append("',providerSelect.value);")
+               .append("if(serviceTypeSelect){")
+               .append("var stValue=serviceTypeSelect.value;")
+               
.append("if(stValue==='").append(PARAMETER_VALUE_ALL).append("'){url.searchParams.delete('").append(PARAMETER_SERVICE_TYPE).append("');}else{url.searchParams.set('").append(PARAMETER_SERVICE_TYPE).append("',stValue);}}")
+               
.append("else{url.searchParams.delete('").append(PARAMETER_SERVICE_TYPE).append("');}")
+               .append("window.location.href=url.toString();}")
+               .append("providerSelect.addEventListener('change',function(){")
+               .append("var url=new URL(window.location.href);")
+               
.append("url.searchParams.set('").append(PARAMETER_PROVIDER_NAME).append("',providerSelect.value);")
+               
.append("url.searchParams.delete('").append(PARAMETER_SERVICE_TYPE).append("');")
+               .append("window.location.href=url.toString();")
+               .append("});")
+               .append("if(serviceTypeSelect){")
+               
.append("serviceTypeSelect.addEventListener('change',updateUrl);")
+               .append("}")
+               .append("})();")
+               .append("</script>");
+    }
+
+    static class ServiceTypePredicate implements 
java.util.function.Predicate<Provider.Service> {
+        private final String serviceType;
+
+        public ServiceTypePredicate(String serviceType) {
+            this.serviceType = serviceType;
+        }
+
+        @Override
+        public boolean test(Provider.Service service) {
+            return PARAMETER_VALUE_ALL.equals(serviceType) || 
service.getType().equals(serviceType);
+        }
+    }
+
+    private void writeOption(@NotNull PrintWriter writer, @NotNull String 
value, @Nullable String selectedValue, @NotNull String label, @Nullable String 
title) {
+        writer.append("<option 
value=\"").append(escapeHtml(value)).append("\"");
+        if (value.equals(selectedValue)) {
+            writer.append("selected");
+        }
+        if (title != null && !title.isEmpty()) {
+            writer.append(" title=\"").append(escapeHtml(title)).append("\"");
+        }
+        writer.append(">");
+        writer.append(escapeHtml(label));
+        writer.append("</option>");
+    }
+
+    protected static String escapeHtml(@NotNull String input) {
+        return input.replace("&", "&amp;")
+                    .replace("<", "&lt;")
+                    .replace(">", "&gt;")
+                    .replace("\"", "&quot;")
+                    .replace("'", "&#x27;")
+                    .replace("/", "&#x2F;");
+    }
+}
\ No newline at end of file
diff --git 
a/src/test/java/org/apache/sling/commons/crypto/internal/SecureRandomSaltProviderTest.java
 
b/src/test/java/org/apache/sling/commons/crypto/internal/SecureRandomSaltProviderTest.java
index 61b79a8..2096686 100644
--- 
a/src/test/java/org/apache/sling/commons/crypto/internal/SecureRandomSaltProviderTest.java
+++ 
b/src/test/java/org/apache/sling/commons/crypto/internal/SecureRandomSaltProviderTest.java
@@ -18,53 +18,22 @@
  */
 package org.apache.sling.commons.crypto.internal;
 
-import java.io.IOException;
-import java.security.NoSuchAlgorithmException;
-
-import org.apache.commons.lang3.reflect.MethodUtils;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-public class SecureRandomSaltProviderTest {
-
-    @Rule
-    public ExpectedException exception = ExpectedException.none();
+import org.junit.Test;
 
-    @Test
-    public void testMissingConfiguration() throws IOException, 
NoSuchAlgorithmException {
-        final SecureRandomSaltProvider provider = new 
SecureRandomSaltProvider();
-        exception.expect(NullPointerException.class);
-        exception.expectMessage("Configuration must not be null");
-        provider.getSalt();
-    }
+public class SecureRandomSaltProviderTest {
 
     @Test
     public void testComponentLifecycle() throws Exception {
-        final SecureRandomSaltProvider provider = new 
SecureRandomSaltProvider();
-        { // activate
-            final SecureRandomSaltProviderConfiguration configuration = 
mock(SecureRandomSaltProviderConfiguration.class);
-            when(configuration.algorithm()).thenReturn("SHA1PRNG");
-            when(configuration.keyLength()).thenReturn(8);
-            MethodUtils.invokeMethod(provider, true, "activate", 
configuration);
-            assertThat(provider.getSalt().length, is(8));
-        }
-        { // modified
-            final SecureRandomSaltProviderConfiguration configuration = 
mock(SecureRandomSaltProviderConfiguration.class);
-            when(configuration.algorithm()).thenReturn("SHA1PRNG");
-            when(configuration.keyLength()).thenReturn(16);
-            MethodUtils.invokeMethod(provider, true, "modified", 
configuration);
-            assertThat(provider.getSalt().length, is(16));
-        }
-        { // deactivate
-            MethodUtils.invokeMethod(provider, true, "deactivate");
-            assertThat(provider.getSalt().length, is(16));
-        }
+        final SecureRandomSaltProviderConfiguration configuration = 
mock(SecureRandomSaltProviderConfiguration.class);
+        when(configuration.algorithm()).thenReturn("SHA1PRNG");
+        when(configuration.keyLength()).thenReturn(8);
+        SecureRandomSaltProvider provider = new 
SecureRandomSaltProvider(configuration);
+        assertThat(provider.getSalt().length, is(8));
     }
 
 }
diff --git 
a/src/test/java/org/apache/sling/commons/crypto/it/tests/EncryptWebConsolePluginHttpWhiteboardIT.java
 
b/src/test/java/org/apache/sling/commons/crypto/it/tests/EncryptWebConsolePluginHttpWhiteboardIT.java
index 67add13..f7d01ac 100644
--- 
a/src/test/java/org/apache/sling/commons/crypto/it/tests/EncryptWebConsolePluginHttpWhiteboardIT.java
+++ 
b/src/test/java/org/apache/sling/commons/crypto/it/tests/EncryptWebConsolePluginHttpWhiteboardIT.java
@@ -18,15 +18,14 @@
  */
 package org.apache.sling.commons.crypto.it.tests;
 
+import static org.ops4j.pax.exam.OptionUtils.combine;
+
 import org.apache.sling.testing.paxexam.SlingOptions;
 import org.ops4j.pax.exam.Configuration;
 import org.ops4j.pax.exam.Option;
 import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
 import org.ops4j.pax.exam.spi.reactors.PerMethod;
 
-import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
-import static org.ops4j.pax.exam.OptionUtils.combine;
-
 @ExamReactorStrategy(PerMethod.class)
 public class EncryptWebConsolePluginHttpWhiteboardIT extends 
EncryptWebConsolePluginIT {
 
@@ -36,8 +35,7 @@ public class EncryptWebConsolePluginHttpWhiteboardIT extends 
EncryptWebConsolePl
         SlingOptions.versionResolver.setVersionFromProject("org.apache.felix", 
"org.apache.felix.http.servlet-api");
         SlingOptions.versionResolver.setVersionFromProject("org.apache.felix", 
"org.apache.felix.webconsole");
         return combine(
-            super.configuration(),
-            
mavenBundle().groupId("org.owasp.encoder").artifactId("encoder").versionAsInProject()
+            super.configuration()
         );
     }
 
diff --git 
a/src/test/java/org/apache/sling/commons/crypto/it/tests/EncryptWebConsolePluginIT.java
 
b/src/test/java/org/apache/sling/commons/crypto/it/tests/EncryptWebConsolePluginIT.java
index fbb9626..41be926 100644
--- 
a/src/test/java/org/apache/sling/commons/crypto/it/tests/EncryptWebConsolePluginIT.java
+++ 
b/src/test/java/org/apache/sling/commons/crypto/it/tests/EncryptWebConsolePluginIT.java
@@ -18,11 +18,20 @@
  */
 package org.apache.sling.commons.crypto.it.tests;
 
+import static org.apache.sling.testing.paxexam.SlingOptions.webconsole;
+import static org.hamcrest.CoreMatchers.endsWith;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.options;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
+
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Base64;
 import java.util.Dictionary;
 import java.util.Hashtable;
+import java.util.Objects;
 
 import javax.inject.Inject;
 
@@ -44,14 +53,6 @@ import org.osgi.framework.Constants;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
 
-import static org.apache.sling.testing.paxexam.SlingOptions.webconsole;
-import static org.hamcrest.CoreMatchers.endsWith;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
-import static org.ops4j.pax.exam.CoreOptions.options;
-import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
-
 @RunWith(PaxExam.class)
 @ExamReactorStrategy(PerMethod.class)
 public class EncryptWebConsolePluginIT extends CryptoTestSupport {
@@ -73,7 +74,6 @@ public class EncryptWebConsolePluginIT extends 
CryptoTestSupport {
     private void registerCryptoService() {
         final Dictionary<String, Object> properties = new Hashtable<>();
         properties.put("names", new String[]{"reverse"});
-        properties.put("algorithm", "reverse");
         registration = bundleContext.registerService(CryptoService.class, 
cryptoService, properties);
     }
 
@@ -85,7 +85,9 @@ public class EncryptWebConsolePluginIT extends 
CryptoTestSupport {
             newConfiguration("org.apache.felix.http")
                 .put("org.osgi.service.http.port", httpPort)
                 .asOption(),
+            // missing the OWASP encoder dependency (added in 
https://github.com/apache/sling-org-apache-sling-testing-paxexam/commit/a9974736b45677a52c2cf117b1525aac3f535b5b,
 but not yet released) so we need to add it here
             webconsole(),
+            
mavenBundle().groupId("org.owasp.encoder").artifactId("encoder").versionAsInProject(),
             
mavenBundle().groupId("org.jsoup").artifactId("jsoup").versionAsInProject()
         );
     }
@@ -110,9 +112,9 @@ public class EncryptWebConsolePluginIT extends 
CryptoTestSupport {
     public void testGetFormCryptoServiceAvailable() throws IOException {
         final ServiceReference<CryptoService> reference = 
registration.getReference();
         final String id = 
reference.getProperty(Constants.SERVICE_ID).toString();
+        final String description = 
Objects.toString(reference.getProperty(Constants.SERVICE_DESCRIPTION), "");
         final String[] names = (String[]) reference.getProperty("names");
-        final String algorithm = reference.getProperty("algorithm").toString();
-        final String label = String.format("Service id %s, names: %s, 
algorithm: %s", id, Arrays.toString(names), algorithm);
+        final String label = String.format("Service id %s (%s), names: %s, 
algorithm(s): reverse", id, description, Arrays.toString(names));
         final Document document = Jsoup.connect(url)
             .header("Authorization", String.format("Basic %s", CREDENTIALS))
             .get();
diff --git 
a/src/test/java/org/apache/sling/commons/crypto/it/tests/ReversingCryptoService.java
 
b/src/test/java/org/apache/sling/commons/crypto/it/tests/ReversingCryptoService.java
index 3c30108..9ae3c89 100644
--- 
a/src/test/java/org/apache/sling/commons/crypto/it/tests/ReversingCryptoService.java
+++ 
b/src/test/java/org/apache/sling/commons/crypto/it/tests/ReversingCryptoService.java
@@ -20,6 +20,7 @@ package org.apache.sling.commons.crypto.it.tests;
 
 import org.apache.sling.commons.crypto.CryptoService;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 public class ReversingCryptoService implements CryptoService {
 
@@ -35,4 +36,8 @@ public class ReversingCryptoService implements CryptoService {
         return sb.reverse().toString();
     }
 
+    @Override
+    public @Nullable String getAlgorithmDescription() {
+        return "reverse";
+    }
 }
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..348c827
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/commons/crypto/jca/internal/JcaPbeCryptoServiceTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Security;
+import java.security.spec.InvalidKeySpecException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.stream.Stream;
+
+import javax.crypto.NoSuchPaddingException;
+
+import org.apache.sling.commons.crypto.PasswordProvider;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.Parameter;
+import org.junit.jupiter.params.ParameterizedClass;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.osgi.util.converter.Converters;
+
+@ParameterizedClass(name="{index} => {0}")
+@MethodSource("provideAlgorithms")
+class JcaPbeCryptoServiceTest {
+
+    // returns a stream of each 4 parameters: name, providerName, 
secretKeyFactoryAlgorithm, cipherAlgorithm
+    @SuppressWarnings("unused")
+    private static Stream<Arguments> provideAlgorithms() {
+        return Stream.of(
+          Arguments.of("Default (PBKDF2 with AES cipher)", "", "", "", false), 
// empty means default algorithms
+          Arguments.of("PBES1 (PBEWithMD5AndDES)", "", "PBEWithMD5AndDES", 
"PBEWithMD5AndDES", true),
+          Arguments.of("PBES2 (PBEWithHmacSHA256AndAES_128)", "", 
"PBEWithHmacSHA256AndAES_128", "PBEWithHmacSHA256AndAES_128", true),
+          Arguments.of("BC: (PBKDF2 with ChaCha20)", "BC", "PBKDF2", 
"CHACHA20-POLY1305", false)
+          // Arguments.of("BC: SCRIPT with BLOWFISH", "BC", "SCRYPT", 
"BLOWFISH"), fails as requiring ScryptKeySpec
+          // Arguments.of("BC: ARGON2 with BLOWFISH", "BC", "ARGON2", 
"BLOWFISH"), fails as requiring Argon2KeySpec
+        );
+    }
+
+    @Parameter(0)
+    String name;
+
+    @Parameter(1)
+    String providerName;
+
+    @Parameter(2)
+    String secretKeyFactoryAlgorithm;
+
+    @Parameter(3)
+    String cipherAlgorithm;
+
+    @Parameter(4)
+    boolean paramsIncludeSalt;
+
+    private static final String MESSAGE = "Rudy, a Message to You üøoøøt";
+
+    private PasswordProvider passwordProvider;
+    private byte[] salt;
+    private JcaPbeCryptoServiceConfiguration configuration;
+    private JcaPbeCryptoService service;
+
+    @BeforeEach
+    void setUp() throws NoSuchAlgorithmException {
+        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);
+        Map<String, Object> properties = new HashMap<>();
+        if (!providerName.isEmpty()) {
+            properties.put("securityProviderName", providerName);
+            if (providerName.equals("BC") && Security.getProvider("BC") == 
null) {
+                Security.addProvider(new BouncyCastleProvider());
+            }
+        }
+        if (!secretKeyFactoryAlgorithm.isEmpty()) {
+            properties.put("secretKeyFactoryAlgorithm", 
secretKeyFactoryAlgorithm);
+        }
+        if (!cipherAlgorithm.isEmpty()) {
+            properties.put("cipherAlgorithm", cipherAlgorithm);
+        }
+        configuration = 
Converters.standardConverter().convert(properties).to(JcaPbeCryptoServiceConfiguration.class);
+        service = new JcaPbeCryptoService(configuration, salt, 
passwordProvider);
+    }
+
+    @Test
+    void testCryptoRoundtrip() {
+        final String ciphertext = service.encrypt(MESSAGE);
+        final String message = service.decrypt(ciphertext);
+        assertEquals(MESSAGE, message);
+        assertNotEquals(MESSAGE, ciphertext);
+    }
+
+    @Test
+    void testCryptoRoundtripWithCryptoServicesHavingDifferentSalts() throws 
Exception {
+        final String ciphertext = service.encrypt(MESSAGE);
+        // now use different salt, affects only encryption key, salt is part 
of the ciphertext, so decryption should still work
+        new Random().nextBytes(salt);
+        final JcaPbeCryptoService service2 = new 
JcaPbeCryptoService(configuration, salt, passwordProvider);
+        assertEquals(MESSAGE, service2.decrypt(ciphertext));
+        assertEquals(MESSAGE, service.decrypt(service2.encrypt(MESSAGE)));
+    }
+
+    @Test
+    void testSameMessageDifferentCipher() {
+        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
+    void testIfSaltIsIncludedInParams() throws InvalidKeyException, 
NoSuchAlgorithmException, NoSuchPaddingException, 
InvalidAlgorithmParameterException, InvalidKeySpecException, IOException {
+        assertEquals(paramsIncludeSalt, 
JcaPbeCryptoService.isSaltIncludedInParams(service.createDefaultAlgorithmParameters()));
+    }
+}

Reply via email to