Repository: wultra/powerauth-crypto (java)

Issue:
Improve crypto agility for PQC KEM and DSA
### Description

Improve crypto agility by switching from concrete classes to interfaces for:
- PQC KEM
- PQC DSA
- associated key convertors

Allow instantiation based on PQC algorithms with generic interfaces and swappable implementations.

### Acceptance criteria

It is possible to swith different PQC algorithms easily.

### Technical specification

Follow PQC algorithm names based on standards.

### QA specification

Tested by unit tests.

Pull request:
Fix #768: Improve crypto agility for PQC KEM and DSA
I switched to interfaces so we can change algorithms easily in the future.

Patch:
diff --git a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/client/v4/activation/PowerAuthClientActivation.java b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/client/v4/activation/PowerAuthClientActivation.java
index ac6a36e2..485b64f9 100644
--- a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/client/v4/activation/PowerAuthClientActivation.java
+++ b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/client/v4/activation/PowerAuthClientActivation.java
@@ -26,6 +26,7 @@ import com.wultra.security.powerauth.crypto.lib.util.*;
 import com.wultra.security.powerauth.crypto.lib.v4.PqcDsa;
 import com.wultra.security.powerauth.crypto.lib.v4.kdf.CustomString;
 import com.wultra.security.powerauth.crypto.lib.v4.kdf.Kmac;
+import com.wultra.security.powerauth.crypto.lib.v4.ml.MlDsa;
 
 import javax.crypto.SecretKey;
 import java.nio.ByteBuffer;
@@ -50,7 +51,7 @@ public class PowerAuthClientActivation {
 
     private static final SignatureUtils SIGNATURE_UTILS = new SignatureUtils();
     private static final KeyGenerator KEY_GENERATOR = new KeyGenerator();
-    private static final PqcDsa PQC_DSA = new PqcDsa();
+    private static final PqcDsa PQC_DSA = new MlDsa();
 
     /**
      * Custom bytes for MAC for counter data.
diff --git a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/util/PqcDsaKeyConvertor.java b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/util/PqcDsaKeyConvertor.java
index 61f1d3e4..10d47ab8 100644
--- a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/util/PqcDsaKeyConvertor.java
+++ b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/util/PqcDsaKeyConvertor.java
@@ -34,6 +34,23 @@ public class PqcDsaKeyConvertor {
 
     private static final Logger logger = LoggerFactory.getLogger(PqcDsaKeyConvertor.class);
 
+    private final String algorithmName;
+
+    /**
+     * Constructs convertor for ML-DSA algorithm.
+     */
+    public PqcDsaKeyConvertor() {
+        this("ML-DSA");
+    }
+
+    /**
+     * Constructs convertor for a specific PQC DSA algorithm.
+     * @param algorithmName Algorithm name (e.g., "ML-DSA")
+     */
+    public PqcDsaKeyConvertor(String algorithmName) {
+        this.algorithmName = algorithmName;
+    }
+
     /**
      * Converts public key to byte array.
      *
@@ -47,7 +64,7 @@ public class PqcDsaKeyConvertor {
         }
         return publicKey.getEncoded();
     }
-    
+
     /**
      * Converts byte array to an ML-DSA public key.
      *
@@ -60,7 +77,7 @@ public class PqcDsaKeyConvertor {
             throw new GenericCryptoException("Missing public key bytes");
         }
         try {
-            final KeyFactory keyFactory = KeyFactory.getInstance("ML-DSA", "BC");
+            final KeyFactory keyFactory = KeyFactory.getInstance(algorithmName, "BC");
             final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
             return keyFactory.generatePublic(keySpec);
         } catch (NoSuchProviderException | NoSuchAlgorithmException | InvalidKeySpecException e) {
@@ -70,7 +87,7 @@ public class PqcDsaKeyConvertor {
     }
 
     /**
-     * Converts public key to byte array.
+     * Converts private key to byte array.
      *
      * @param privateKey An ML-DSA private key to be converted.
      * @return A byte array representation of the ML-DSA private key.
@@ -95,7 +112,7 @@ public class PqcDsaKeyConvertor {
             throw new GenericCryptoException("Missing public key bytes");
         }
         try {
-            final KeyFactory keyFactoryMlDsa = KeyFactory.getInstance("ML-DSA", "BC");
+            final KeyFactory keyFactoryMlDsa = KeyFactory.getInstance(algorithmName, "BC");
             final PKCS8EncodedKeySpec keySpecMlDsa = new PKCS8EncodedKeySpec(keyBytes);
             return keyFactoryMlDsa.generatePrivate(keySpecMlDsa);
         } catch (NoSuchProviderException | NoSuchAlgorithmException | InvalidKeySpecException e) {
@@ -103,5 +120,4 @@ public class PqcDsaKeyConvertor {
             throw new GenericCryptoException("Key conversion failed", e);
         }
     }
-
 }
diff --git a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/util/PqcKemKeyConvertor.java b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/util/PqcKemKeyConvertor.java
index 770acd86..21e49e18 100644
--- a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/util/PqcKemKeyConvertor.java
+++ b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/util/PqcKemKeyConvertor.java
@@ -34,6 +34,23 @@ import java.security.spec.X509EncodedKeySpec;
  */
 public class PqcKemKeyConvertor {
 
+    private final String algorithmName;
+
+    /**
+     * Constructs convertor for ML-KEM algorithm.
+     */
+    public PqcKemKeyConvertor() {
+        this("ML-KEM");
+    }
+
+    /**
+     * Constructs convertor for a specific PQC KEM algorithm.
+     * @param algorithmName Algorithm name (e.g., "ML-KEM")
+     */
+    public PqcKemKeyConvertor(String algorithmName) {
+        this.algorithmName = algorithmName;
+    }
+
     /**
      * Convert public key for ML-KEM into bytes.
      * @param publicKey Public key.
@@ -77,7 +94,7 @@ public class PqcKemKeyConvertor {
             throw new GenericCryptoException("Missing public key bytes");
         }
         try {
-            final KeyFactory keyFactory = KeyFactory.getInstance("ML-KEM", "BC");
+            final KeyFactory keyFactory = KeyFactory.getInstance(algorithmName, "BC");
             final PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
             if (!(publicKey instanceof MLKEMPublicKey)) {
                 throw new GenericCryptoException("Invalid public key");
@@ -99,7 +116,7 @@ public class PqcKemKeyConvertor {
             throw new GenericCryptoException("Missing private key bytes");
         }
         try {
-            final KeyFactory keyFactory = KeyFactory.getInstance("ML-KEM", "BC");
+            final KeyFactory keyFactory = KeyFactory.getInstance(algorithmName, "BC");
             final PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
             if (!(privateKey instanceof MLKEMPrivateKey)) {
                 throw new GenericCryptoException("Invalid private key");
@@ -111,8 +128,7 @@ public class PqcKemKeyConvertor {
     }
 
     /**
-     * Converts a shared secret key (usually used for AES based operations) to a
-     * byte array.
+     * Converts a shared secret key (usually used for AES based operations) to a byte array.
      *
      * @param sharedSecretKey A shared key to be converted to bytes.
      * @return A byte array representation of the shared secret key.
@@ -126,8 +142,7 @@ public class PqcKemKeyConvertor {
     }
 
     /**
-     * Converts a byte array to the secret shared key (usually used for AES
-     * based operations).
+     * Converts a byte array to the secret shared key (usually used for AES based operations).
      *
      * @param bytesSecretKey Bytes representing the shared key.
      * @return An instance of the secret key by decoding from provided bytes.
@@ -139,5 +154,4 @@ public class PqcKemKeyConvertor {
         }
         return new SecretKeySpec(bytesSecretKey, "AES");
     }
-
 }
diff --git a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/PqcDsa.java b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/PqcDsa.java
index e1cea2fb..411727e3 100644
--- a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/PqcDsa.java
+++ b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/PqcDsa.java
@@ -14,23 +14,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.wultra.security.powerauth.crypto.lib.v4;
 
 import com.wultra.security.powerauth.crypto.lib.model.exception.CryptoProviderException;
 import com.wultra.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
-import lombok.NoArgsConstructor;
-import org.bouncycastle.jcajce.spec.MLDSAParameterSpec;
 
-import java.security.*;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
 
 /**
- * Post-quantum digital signature algorithm.
+ * Post-quantum digital signature algorithm interface.
  *
  * @author Roman Strobl, roman.strobl@wultra.com
  */
-@NoArgsConstructor
-public class PqcDsa {
+public interface PqcDsa {
 
     /**
      * Generate PQC DSA keypair.
@@ -38,15 +36,7 @@ public class PqcDsa {
      * @return Keypair.
      * @throws CryptoProviderException Thrown in case the cryptography provider is incorrectly initialized.
      */
-    public KeyPair generateKeyPair() throws CryptoProviderException {
-        try {
-            final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("MLDSA", "BC");
-            keyPairGenerator.initialize(MLDSAParameterSpec.ml_dsa_65);
-            return keyPairGenerator.generateKeyPair();
-        } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
-            throw new CryptoProviderException("Error generating key pair", e);
-        }
-    }
+    KeyPair generateKeyPair() throws CryptoProviderException;
 
     /**
      * Sign a message using PQC DSA.
@@ -56,22 +46,7 @@ public class PqcDsa {
      * @return Signature.
      * @throws GenericCryptoException Thrown in case of any cryptography error.
      */
-    public byte[] sign(PrivateKey privateKey, byte[] message) throws GenericCryptoException {
-        if (privateKey == null) {
-            throw new GenericCryptoException("Missing private key when signing a message");
-        }
-        if (message == null) {
-            throw new GenericCryptoException("Missing message to sign");
-        }
-        try {
-            final Signature mlDsa = Signature.getInstance("MLDSA", "BC");
-            mlDsa.initSign(privateKey);
-            mlDsa.update(message);
-            return mlDsa.sign();
-        } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) {
-            throw new GenericCryptoException("Error during signature calculation", e);
-        }
-    }
+    byte[] sign(PrivateKey privateKey, byte[] message) throws GenericCryptoException;
 
     /**
      * Verify a message signature using PQC DSA.
@@ -81,24 +56,5 @@ public class PqcDsa {
      * @return True if signature was correct, false otherwise.
      * @throws GenericCryptoException Thrown in case of any cryptography error.
      */
-    public boolean verify(PublicKey publicKey, byte[] message, byte[] signature) throws GenericCryptoException {
-        if (publicKey == null) {
-            throw new GenericCryptoException("Missing public key when verifying a signature");
-        }
-        if (message == null) {
-            throw new GenericCryptoException("Missing message when verifying a signature");
-        }
-        if (signature == null) {
-            throw new GenericCryptoException("Missing signature to verify");
-        }
-        try {
-            final Signature mlDsa = Signature.getInstance("MLDSA", "BC");
-            mlDsa.initVerify(publicKey);
-            mlDsa.update(message);
-            return mlDsa.verify(signature);
-        } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) {
-            throw new GenericCryptoException("Error during signature verification", e);
-        }
-    }
-
+    boolean verify(PublicKey publicKey, byte[] message, byte[] signature) throws GenericCryptoException;
 }
diff --git a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/PqcKem.java b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/PqcKem.java
index f99ae7ad..509a5746 100644
--- a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/PqcKem.java
+++ b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/PqcKem.java
@@ -14,27 +14,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.wultra.security.powerauth.crypto.lib.v4;
 
 import com.wultra.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
-import lombok.NoArgsConstructor;
 import org.bouncycastle.jcajce.SecretKeyWithEncapsulation;
-import org.bouncycastle.jcajce.spec.KEMExtractSpec;
-import org.bouncycastle.jcajce.spec.KEMGenerateSpec;
-import org.bouncycastle.jcajce.spec.MLKEMParameterSpec;
 
-import javax.crypto.KeyGenerator;
 import javax.crypto.SecretKey;
-import java.security.*;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
 
 /**
- * Post-quantum key encapsulation mechanism.
+ * Post-quantum key encapsulation mechanism interface.
  *
  * @author Roman Strobl, roman.strobl@wultra.com
  */
-@NoArgsConstructor
-public class PqcKem {
+public interface PqcKem {
 
     /**
      * Generate a PQC keypair.
@@ -42,15 +37,7 @@ public class PqcKem {
      * @return Keypair.
      * @throws GenericCryptoException Thrown in case of any cryptography error.
      */
-    public KeyPair generateKeyPair() throws GenericCryptoException {
-        try {
-            final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ML-KEM", "BC");
-            keyPairGenerator.initialize(MLKEMParameterSpec.ml_kem_768);
-            return keyPairGenerator.generateKeyPair();
-        } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
-            throw new GenericCryptoException("Error generating key pair", e);
-        }
-    }
+    KeyPair generateKeyPair() throws GenericCryptoException;
 
     /**
      * Encapsulate a shared secret key using a public key.
@@ -59,18 +46,7 @@ public class PqcKem {
      * @return Secret key with encapsulation.
      * @throws GenericCryptoException Thrown in case of any cryptography error.
      */
-    public SecretKeyWithEncapsulation encapsulate(PublicKey encapsulationKey) throws GenericCryptoException {
-        if (encapsulationKey == null) {
-            throw new GenericCryptoException("Missing public key during encapsulation");
-        }
-        try {
-            final KeyGenerator keyGenerator = KeyGenerator.getInstance("ML-KEM", "BC");
-            keyGenerator.init(new KEMGenerateSpec.Builder(encapsulationKey, "RAW", 256).withNoKdf().build());
-            return (SecretKeyWithEncapsulation) keyGenerator.generateKey();
-        } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
-            throw new GenericCryptoException("Error during encapsulation", e);
-        }
-    }
+    SecretKeyWithEncapsulation encapsulate(PublicKey encapsulationKey) throws GenericCryptoException;
 
     /**
      * Decapsulate a shared secret key using a private key.
@@ -79,20 +55,5 @@ public class PqcKem {
      * @return Secret key.
      * @throws GenericCryptoException Thrown in case of any cryptography error.
      */
-    public SecretKey decapsulate(PrivateKey decapsulationKey, byte[] ciphertext) throws GenericCryptoException {
-        if (decapsulationKey == null) {
-            throw new GenericCryptoException("Missing public key during decapsulation");
-        }
-        if (ciphertext == null) {
-            throw new GenericCryptoException("Missing ciphertext during decapsulation");
-        }
-        try {
-            final KeyGenerator keyGenerator = KeyGenerator.getInstance("ML-KEM", "BC");
-            keyGenerator.init(new KEMExtractSpec.Builder(decapsulationKey, ciphertext, "RAW", 256).withNoKdf().build());
-            return keyGenerator.generateKey();
-        } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
-            throw new GenericCryptoException("Error during decapsulation", e);
-        }
-    }
-
+    SecretKey decapsulate(PrivateKey decapsulationKey, byte[] ciphertext) throws GenericCryptoException;
 }
diff --git a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/ml/MlDsa.java b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/ml/MlDsa.java
new file mode 100644
index 00000000..aa3acb3e
--- /dev/null
+++ b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/ml/MlDsa.java
@@ -0,0 +1,104 @@
+/*
+ * PowerAuth Crypto Library
+ * Copyright 2025 Wultra s.r.o.
+ *
+ * Licensed 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 com.wultra.security.powerauth.crypto.lib.v4.ml;
+
+import com.wultra.security.powerauth.crypto.lib.model.exception.CryptoProviderException;
+import com.wultra.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
+import com.wultra.security.powerauth.crypto.lib.v4.PqcDsa;
+import org.bouncycastle.jcajce.spec.MLDSAParameterSpec;
+
+import java.security.*;
+
+/**
+ * ML-DSA implementation of the post-quantum digital signature algorithm.
+ *
+ * @author Roman Strobl, roman.strobl@wultra.com
+ */
+public class MlDsa implements PqcDsa {
+
+
+    private final MLDSAParameterSpec dsaParameterSpec;
+
+    /**
+     * Construct with default parameter spec (ml_dsa_65).
+     */
+    public MlDsa() {
+        this.dsaParameterSpec = MLDSAParameterSpec.ml_dsa_65;
+    }
+
+    /**
+     * Construct with a specific parameter spec.
+     * @param dsaParameterSpec Algorithm parameter spec.
+     * @throws GenericCryptoException In case of missing parameter specification.
+     */
+    public MlDsa(MLDSAParameterSpec dsaParameterSpec) throws GenericCryptoException {
+        if (dsaParameterSpec == null) {
+            throw new GenericCryptoException("Missing ML-DSA parameter specification");
+        }
+        this.dsaParameterSpec = dsaParameterSpec;
+    }
+
+    @Override
+    public KeyPair generateKeyPair() throws CryptoProviderException {
+        try {
+            final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("MLDSA", "BC");
+            keyPairGenerator.initialize(dsaParameterSpec);
+            return keyPairGenerator.generateKeyPair();
+        } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
+            throw new CryptoProviderException("Error generating key pair", e);
+        }
+    }
+
+    @Override
+    public byte[] sign(PrivateKey privateKey, byte[] message) throws GenericCryptoException {
+        if (privateKey == null) {
+            throw new GenericCryptoException("Missing private key when signing a message");
+        }
+        if (message == null) {
+            throw new GenericCryptoException("Missing message to sign");
+        }
+        try {
+            final Signature mlDsa = Signature.getInstance("MLDSA", "BC");
+            mlDsa.initSign(privateKey);
+            mlDsa.update(message);
+            return mlDsa.sign();
+        } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) {
+            throw new GenericCryptoException("Error during signature calculation", e);
+        }
+    }
+
+    @Override
+    public boolean verify(PublicKey publicKey, byte[] message, byte[] signature) throws GenericCryptoException {
+        if (publicKey == null) {
+            throw new GenericCryptoException("Missing public key when verifying a signature");
+        }
+        if (message == null) {
+            throw new GenericCryptoException("Missing message when verifying a signature");
+        }
+        if (signature == null) {
+            throw new GenericCryptoException("Missing signature to verify");
+        }
+        try {
+            final Signature mlDsa = Signature.getInstance("MLDSA", "BC");
+            mlDsa.initVerify(publicKey);
+            mlDsa.update(message);
+            return mlDsa.verify(signature);
+        } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) {
+            throw new GenericCryptoException("Error during signature verification", e);
+        }
+    }
+}
diff --git a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/ml/MlKem.java b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/ml/MlKem.java
new file mode 100644
index 00000000..25f71ff5
--- /dev/null
+++ b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/ml/MlKem.java
@@ -0,0 +1,100 @@
+/*
+ * PowerAuth Crypto Library
+ * Copyright 2025 Wultra s.r.o.
+ *
+ * Licensed 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 com.wultra.security.powerauth.crypto.lib.v4.ml;
+
+import com.wultra.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
+import com.wultra.security.powerauth.crypto.lib.v4.PqcKem;
+import org.bouncycastle.jcajce.SecretKeyWithEncapsulation;
+import org.bouncycastle.jcajce.spec.KEMExtractSpec;
+import org.bouncycastle.jcajce.spec.KEMGenerateSpec;
+import org.bouncycastle.jcajce.spec.MLKEMParameterSpec;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import java.security.*;
+
+/**
+ * ML-KEM implementation of the post-quantum key encapsulation mechanism.
+ *
+ * @author Roman Strobl, roman.strobl@wultra.com
+ */
+public class MlKem implements PqcKem {
+
+    private final MLKEMParameterSpec kemParameterSpec;
+
+    /**
+     * Construct with default parameter spec (ml_kem_768).
+     */
+    public MlKem() {
+        this.kemParameterSpec = MLKEMParameterSpec.ml_kem_768;
+    }
+
+    /**
+     * Construct with a specific parameter spec.
+     * @param kemParameterSpec Algorithm parameter spec.
+     * @throws GenericCryptoException In case of missing parameter specification.
+     */
+    public MlKem(MLKEMParameterSpec kemParameterSpec) throws GenericCryptoException {
+        if (kemParameterSpec == null) {
+            throw new GenericCryptoException("Missing ML-KEM parameter specification");
+        }
+        this.kemParameterSpec = kemParameterSpec;
+    }
+
+    @Override
+    public KeyPair generateKeyPair() throws GenericCryptoException {
+        try {
+            final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ML-KEM", "BC");
+            keyPairGenerator.initialize(kemParameterSpec);
+            return keyPairGenerator.generateKeyPair();
+        } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
+            throw new GenericCryptoException("Error generating key pair", e);
+        }
+    }
+
+    @Override
+    public SecretKeyWithEncapsulation encapsulate(PublicKey encapsulationKey) throws GenericCryptoException {
+        if (encapsulationKey == null) {
+            throw new GenericCryptoException("Missing public key during encapsulation");
+        }
+        try {
+            final KeyGenerator keyGenerator = KeyGenerator.getInstance("ML-KEM", "BC");
+            keyGenerator.init(new KEMGenerateSpec.Builder(encapsulationKey, "RAW", 256).withNoKdf().build());
+            return (SecretKeyWithEncapsulation) keyGenerator.generateKey();
+        } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
+            throw new GenericCryptoException("Error during encapsulation", e);
+        }
+    }
+
+    @Override
+    public SecretKey decapsulate(PrivateKey decapsulationKey, byte[] ciphertext) throws GenericCryptoException {
+        if (decapsulationKey == null) {
+            throw new GenericCryptoException("Missing public key during decapsulation");
+        }
+        if (ciphertext == null) {
+            throw new GenericCryptoException("Missing ciphertext during decapsulation");
+        }
+        try {
+            final KeyGenerator keyGenerator = KeyGenerator.getInstance("ML-KEM", "BC");
+            keyGenerator.init(new KEMExtractSpec.Builder(decapsulationKey, ciphertext, "RAW", 256).withNoKdf().build());
+            return keyGenerator.generateKey();
+        } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
+            throw new GenericCryptoException("Error during decapsulation", e);
+        }
+    }
+
+}
diff --git a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/sharedsecret/SharedSecretHybrid.java b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/sharedsecret/SharedSecretHybrid.java
index e5d3f121..17216b08 100644
--- a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/sharedsecret/SharedSecretHybrid.java
+++ b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/sharedsecret/SharedSecretHybrid.java
@@ -28,6 +28,7 @@ import com.wultra.security.powerauth.crypto.lib.v4.PqcKem;
 import com.wultra.security.powerauth.crypto.lib.v4.api.SharedSecret;
 import com.wultra.security.powerauth.crypto.lib.v4.api.SharedSecretClientContext;
 import com.wultra.security.powerauth.crypto.lib.v4.kdf.KeyFactory;
+import com.wultra.security.powerauth.crypto.lib.v4.ml.MlKem;
 import com.wultra.security.powerauth.crypto.lib.v4.model.*;
 import com.wultra.security.powerauth.crypto.lib.v4.model.context.SharedSecretAlgorithm;
 import com.wultra.security.powerauth.crypto.lib.v4.model.request.RequestCryptogram;
@@ -55,7 +56,7 @@ public class SharedSecretHybrid implements SharedSecret<SharedSecretRequestHybri
     private static final KeyConvertor KEY_CONVERTOR_EC = new KeyConvertor();
     private static final PqcKemKeyConvertor KEY_CONVERTOR_PQC = new PqcKemKeyConvertor();
 
-    private static final PqcKem PQC_KEM = new PqcKem();
+    private static final PqcKem PQC_KEM = new MlKem();
 
     @Override
     public SharedSecretAlgorithm getAlgorithm() {
diff --git a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/sharedsecret/SharedSecretPqc.java b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/sharedsecret/SharedSecretPqc.java
index ee820203..1a724f6c 100644
--- a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/sharedsecret/SharedSecretPqc.java
+++ b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/lib/v4/sharedsecret/SharedSecretPqc.java
@@ -22,6 +22,7 @@ import com.wultra.security.powerauth.crypto.lib.util.KeyConvertor;
 import com.wultra.security.powerauth.crypto.lib.util.PqcKemKeyConvertor;
 import com.wultra.security.powerauth.crypto.lib.v4.PqcKem;
 import com.wultra.security.powerauth.crypto.lib.v4.api.SharedSecret;
+import com.wultra.security.powerauth.crypto.lib.v4.ml.MlKem;
 import com.wultra.security.powerauth.crypto.lib.v4.model.SharedSecretClientContextPqc;
 import com.wultra.security.powerauth.crypto.lib.v4.model.context.SharedSecretAlgorithm;
 import com.wultra.security.powerauth.crypto.lib.v4.model.request.RequestCryptogram;
@@ -46,7 +47,7 @@ public class SharedSecretPqc implements SharedSecret<SharedSecretRequestPqc, Sha
     private static final KeyConvertor KEY_CONVERTOR = new KeyConvertor();
     private static final PqcKemKeyConvertor KEY_CONVERTOR_PQC = new PqcKemKeyConvertor();
 
-    private static final PqcKem PQC_KEM = new PqcKem();
+    private static final PqcKem PQC_KEM = new MlKem();
 
     @Override
     public SharedSecretAlgorithm getAlgorithm() {
diff --git a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/server/v4/activation/PowerAuthServerActivation.java b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/server/v4/activation/PowerAuthServerActivation.java
index 4f85886b..2e2a19fe 100644
--- a/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/server/v4/activation/PowerAuthServerActivation.java
+++ b/powerauth-java-crypto/src/main/java/com/wultra/security/powerauth/crypto/server/v4/activation/PowerAuthServerActivation.java
@@ -26,6 +26,7 @@ import com.wultra.security.powerauth.crypto.lib.util.*;
 import com.wultra.security.powerauth.crypto.lib.v4.PqcDsa;
 import com.wultra.security.powerauth.crypto.lib.v4.kdf.CustomString;
 import com.wultra.security.powerauth.crypto.lib.v4.kdf.Kmac;
+import com.wultra.security.powerauth.crypto.lib.v4.ml.MlDsa;
 
 import javax.crypto.SecretKey;
 import java.nio.ByteBuffer;
@@ -50,7 +51,7 @@ public class PowerAuthServerActivation {
 
     private static final KeyGenerator KEY_GENERATOR = new KeyGenerator();
     private static final SignatureUtils SIGNATURE_UTILS = new SignatureUtils();
-    private static final PqcDsa PQC_DSA = new PqcDsa();
+    private static final PqcDsa PQC_DSA = new MlDsa();
 
     /**
      * Custom bytes for MAC for counter data.


Test patch:
diff --git a/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/util/PqcDsaKeyConvertorTest.java b/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/util/PqcDsaKeyConvertorTest.java
index 513e28ba..54d1282c 100644
--- a/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/util/PqcDsaKeyConvertorTest.java
+++ b/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/util/PqcDsaKeyConvertorTest.java
@@ -17,6 +17,7 @@
 package com.wultra.security.powerauth.crypto.lib.util;
 
 import com.wultra.security.powerauth.crypto.lib.v4.PqcDsa;
+import com.wultra.security.powerauth.crypto.lib.v4.ml.MlDsa;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
@@ -34,7 +35,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 class PqcDsaKeyConvertorTest {
 
 	private final PqcDsaKeyConvertor KEY_CONVERTOR = new PqcDsaKeyConvertor();
-	private final PqcDsa PQC_DSA = new PqcDsa();
+	private final PqcDsa PQC_DSA = new MlDsa();
 
 	private static final String PUBLIC_KEY_BASE64 = "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";
 	private static final String PRIVATE_KEY_BASE64 = "MDICAQAwCwYJYIZIAWUDBAMSBCAtie/N/WXwodhUNcaNUDOOHL8StjZUn+vU7MzwrDhrFQ==";
diff --git a/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/v4/PqcDsaTest.java b/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/v4/PqcDsaTest.java
index c5677c4d..5c3a4bdb 100644
--- a/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/v4/PqcDsaTest.java
+++ b/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/v4/PqcDsaTest.java
@@ -19,6 +19,7 @@ package com.wultra.security.powerauth.crypto.lib.v4;
 
 import com.wultra.security.powerauth.crypto.lib.model.exception.CryptoProviderException;
 import com.wultra.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
+import com.wultra.security.powerauth.crypto.lib.v4.ml.MlDsa;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.junit.jupiter.api.Test;
 
@@ -45,7 +46,7 @@ class PqcDsaTest {
      */
     @Test
     public void testPqcDsa_Success() throws GenericCryptoException, CryptoProviderException {
-        final PqcDsa pqcDsa = new PqcDsa();
+        final PqcDsa pqcDsa = new MlDsa();
         final KeyPair keyPair = pqcDsa.generateKeyPair();
         final byte[] testMessage = "test_message".getBytes(StandardCharsets.UTF_8);
         byte[] signature = pqcDsa.sign(keyPair.getPrivate(), testMessage);
@@ -58,7 +59,7 @@ class PqcDsaTest {
      */
     @Test
     public void testPqcDsa_InvalidSignature() throws GenericCryptoException, CryptoProviderException {
-        final PqcDsa pqcDsa = new PqcDsa();
+        final PqcDsa pqcDsa = new MlDsa();
         final KeyPair keyPair = pqcDsa.generateKeyPair();
         final byte[] testMessage = "test_message".getBytes(StandardCharsets.UTF_8);
         assertFalse(pqcDsa.verify(keyPair.getPublic(), testMessage, "invalid".getBytes(StandardCharsets.UTF_8)));
@@ -70,7 +71,7 @@ class PqcDsaTest {
      */
     @Test
     public void testPqcDsa_InvalidPrivateKey() throws GenericCryptoException, CryptoProviderException {
-        final PqcDsa pqcDsa = new PqcDsa();
+        final PqcDsa pqcDsa = new MlDsa();
         final KeyPair keyPair = pqcDsa.generateKeyPair();
         final KeyPair keyPair2 = pqcDsa.generateKeyPair();
         final byte[] testMessage = "test_message".getBytes(StandardCharsets.UTF_8);
@@ -80,11 +81,10 @@ class PqcDsaTest {
 
     /**
      * PQC DSA null message test.
-     * @throws GenericCryptoException Thrown in case of any cryptography error.
      */
     @Test
     public void testPqcDsa_NullMessage() throws CryptoProviderException {
-        final PqcDsa pqcDsa = new PqcDsa();
+        final PqcDsa pqcDsa = new MlDsa();
         final KeyPair keyPair = pqcDsa.generateKeyPair();
         final Exception ex = assertThrows(GenericCryptoException.class, () -> pqcDsa.sign(keyPair.getPrivate(), null));
         assertEquals("Missing message to sign", ex.getMessage());
@@ -96,7 +96,7 @@ class PqcDsaTest {
      */
     @Test
     public void testPqcDsa_EmptyMessage() throws GenericCryptoException, CryptoProviderException {
-        final PqcDsa pqcDsa = new PqcDsa();
+        final PqcDsa pqcDsa = new MlDsa();
         final KeyPair keyPair = pqcDsa.generateKeyPair();
         final byte[] emptyMessage = new byte[0];
         byte[] signature = pqcDsa.sign(keyPair.getPrivate(), emptyMessage);
diff --git a/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/v4/PqcKemTest.java b/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/v4/PqcKemTest.java
index a81b379e..6b14242f 100644
--- a/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/v4/PqcKemTest.java
+++ b/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/v4/PqcKemTest.java
@@ -18,6 +18,7 @@
 package com.wultra.security.powerauth.crypto.lib.v4;
 
 import com.wultra.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
+import com.wultra.security.powerauth.crypto.lib.v4.ml.MlKem;
 import org.bouncycastle.jcajce.SecretKeyWithEncapsulation;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.junit.jupiter.api.Test;
@@ -46,7 +47,7 @@ class PqcKemTest {
      */
     @Test
     public void testPqcKem_Success() throws GenericCryptoException {
-        final PqcKem kem = new PqcKem();
+        final PqcKem kem = new MlKem();
         final KeyPair keyPair = kem.generateKeyPair();
         final SecretKeyWithEncapsulation secret = kem.encapsulate(keyPair.getPublic());
         final SecretKey sharedKey = kem.decapsulate(keyPair.getPrivate(), secret.getEncapsulation());
@@ -61,7 +62,7 @@ class PqcKemTest {
      */
     @Test
     public void testPqcKem_InvalidPrivateKey() throws GenericCryptoException {
-        final PqcKem kem = new PqcKem();
+        final PqcKem kem = new MlKem();
         final KeyPair keyPair = kem.generateKeyPair();
         final SecretKeyWithEncapsulation secret = kem.encapsulate(keyPair.getPublic());
         final KeyPair keyPair2 = kem.generateKeyPair();
diff --git a/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/v4/jwt/TemporaryKeyTest.java b/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/v4/jwt/TemporaryKeyTest.java
index c4fa6b42..a66dd33d 100644
--- a/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/v4/jwt/TemporaryKeyTest.java
+++ b/powerauth-java-crypto/src/test/java/com/wultra/security/powerauth/crypto/lib/v4/jwt/TemporaryKeyTest.java
@@ -26,6 +26,7 @@ import com.wultra.security.powerauth.crypto.lib.util.KeyConvertor;
 import com.wultra.security.powerauth.crypto.lib.util.PqcDsaKeyConvertor;
 import com.wultra.security.powerauth.crypto.lib.util.SignatureUtils;
 import com.wultra.security.powerauth.crypto.lib.v4.PqcDsa;
+import com.wultra.security.powerauth.crypto.lib.v4.ml.MlDsa;
 import com.wultra.security.powerauth.crypto.lib.v4.model.context.SharedSecretAlgorithm;
 import lombok.Data;
 import org.bouncycastle.asn1.ASN1EncodableVector;
@@ -59,7 +60,7 @@ public class TemporaryKeyTest {
 
     private static final JSONParser JSON_PARSER = new JSONParser();
     private static final SignatureUtils SIGNATURE_UTILS = new SignatureUtils();
-    private static final PqcDsa PQC_DSA = new PqcDsa();
+    private static final PqcDsa PQC_DSA = new MlDsa();
     private static final KeyConvertor KEY_CONVERTOR = new KeyConvertor();
     private static final PqcDsaKeyConvertor PQC_DSA_KEY_CONVERTOR = new PqcDsaKeyConvertor();