http://docker:8085ML-KEM-768 key encapsulation derives an AES-256-GCM key. Message body encrypted with AES. Only the intended recipient can decrypt.
ML-KEM-768 + AES-256-GCM (FIPS 203)ML-DSA-65 signature on the message body. Proves the sender's identity and ensures the message wasn't tampered with.
ML-DSA-65 (FIPS 204)Full S/MIME-like workflow: sender signs with ML-DSA-65, then encrypts the signed payload with ML-KEM-768 for the recipient.
ML-DSA-65 + ML-KEM-768The Email Encryption Service demonstrates quantum-safe email protection using two PQC algorithms:
Encryption (ML-KEM-768) — The recipient's ML-KEM public key is used to encapsulate a shared secret, which derives an AES-256-GCM key to encrypt the message body. Only the recipient's private key can recover it.
Signing (ML-DSA-65) — The sender signs the message with ML-DSA-65, proving authenticity and preventing tampering. The signature is verified using the sender's public key.
Send Secure — Combines both: sign the message with ML-DSA-65, then encrypt the signed message with ML-KEM-768. This mirrors the S/MIME sign-then-encrypt pattern used in enterprise email.
/api/email/encrypt
Encrypt a message
/api/email/decrypt
Decrypt a message
/api/email/sign
Sign a message
/api/email/verify
Verify a signature
/api/email/send-secure
Send secure (sign + encrypt)
/api/email/receive-secure
Receive secure (decrypt + verify)
/api/email/health
Service health + supported algorithms
Migrate email encryption from classical S/MIME (RSA/ECDH) to quantum-safe ML-KEM + ML-DSA using the Qudo Cryptographic Module. Use this playground to understand PQC email patterns, then integrate the Qudo JNI provider directly into your own system — no dependency on this REST API.
Sender (Alice) Recipient (Bob) | | | 1. Bob publishes ML-KEM-768 public key | | Alice has ML-DSA-65 signing keypair | | | | 2. provider.sign(message, alicePrivKey, "ML-DSA-65")| | 3. provider.kemEncapsulate(bobPubKey, "ML-KEM-768") | | -> per-message ciphertext + shared secret | | 4. Derive AES-256 key from shared secret (SHA-256) | | 5. AES-256-GCM encrypt signed message | | | |---- KEM ciphertext + AES ciphertext + IV ----------->| | | | 6. provider.kemDecapsulate(kemCt, bobPrivKey, | | "ML-KEM-768") -> recover shared secret | | 7. Derive AES-256 key, decrypt AES-256-GCM | | 8. provider.verify(message, sig, alicePubKey, | | "ML-DSA-65") -> true
Each message gets a unique shared secret via ML-KEM encapsulation (FIPS 203) through the QUDO JNI provider. The shared secret derives the AES-256-GCM key. Compromising one message does not reveal any other — true per-message forward secrecy.
The sender signs the plaintext with ML-DSA before encryption (sign-then-encrypt, per S/MIME). The recipient verifies the signature after decryption. Supports ML-DSA-44 (Level 2), ML-DSA-65 (Level 3, default), and ML-DSA-87 (Level 5).
Recipients generate ML-KEM keypairs with provider.generateKeyPair("ML-KEM-768"), store private keys in HSM, and publish public keys via a directory (LDAP, certificate, or key server). Senders retrieve recipient public keys before encapsulation.
Test PQC email operations directly. Each workflow shows the full request/response with PQC algorithm details.
Encrypt a message with ML-KEM-768 + AES-256-GCM, then decrypt it.
Sign a message with ML-DSA-65, then verify the signature. Try tampering to see verification fail.
Complete end-to-end workflow: sign with ML-DSA-65, encrypt with ML-KEM-768 encapsulation, then decrypt and verify on the recipient side.
The playground above uses a REST API for convenience. In your production system, use the Qudo JNI provider directly — no HTTP overhead, no dependency on this portal.
import com.qudo.crypto.QudoCrypto;
import com.qudo.crypto.QudoKeyPair;
import com.qudo.crypto.QudoKemResult;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.SecureRandom;
// ============================================================
// Initialize the Qudo provider once at application startup
// ============================================================
QudoCrypto provider = QudoCrypto.create();
// ============================================================
// 1. KEY GENERATION — generate ML-KEM and ML-DSA keypairs
// ============================================================
// Recipient generates an ML-KEM-768 keypair (store privKey securely, publish pubKey)
try (QudoKeyPair recipientKeys = provider.generateKeyPair("ML-KEM-768")) {
byte[] recipientPubKey = recipientKeys.getPublicKeyPem();
byte[] recipientPrivKey = recipientKeys.getPrivateKeyPem();
// Sender generates an ML-DSA-65 signing keypair
try (QudoKeyPair senderKeys = provider.generateKeyPair("ML-DSA-65")) {
byte[] message = "Q4 revenue exceeded projections by 15%".getBytes();
// ============================================================
// 2. SIGN — sender signs the message with ML-DSA-65
// ============================================================
byte[] signature = provider.sign(message, senderKeys.getPrivateKeyPem(), "ML-DSA-65");
// ============================================================
// 3. ENCRYPT — sender encapsulates + encrypts for recipient
// ============================================================
// ML-KEM encapsulate: produces a per-message shared secret
QudoKemResult encap = provider.kemEncapsulate(recipientPubKey, "ML-KEM-768");
byte[] kemCiphertext = encap.getCiphertext(); // send this to recipient
byte[] sharedSecret = encap.getSharedSecret(); // use locally for AES key
encap.destroy(); // zero shared secret from KEM result
// Derive AES-256 key from shared secret, then zero the secret
byte[] aesKey = MessageDigest.getInstance("SHA-256").digest(sharedSecret);
java.util.Arrays.fill(sharedSecret, (byte) 0);
// Encrypt with AES-256-GCM
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey, "AES"),
new GCMParameterSpec(128, iv));
byte[] ciphertext = cipher.doFinal(message);
// Send to recipient: kemCiphertext, ciphertext, iv, signature, senderPubKey
// ============================================================
// 4. DECRYPT — recipient decapsulates + decrypts
// ============================================================
// ML-KEM decapsulate: recover the shared secret with private key
byte[] recoveredSecret = provider.kemDecapsulate(
kemCiphertext, recipientPrivKey, "ML-KEM-768");
byte[] recoveredAesKey = MessageDigest.getInstance("SHA-256").digest(recoveredSecret);
Cipher decCipher = Cipher.getInstance("AES/GCM/NoPadding");
decCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(recoveredAesKey, "AES"),
new GCMParameterSpec(128, iv));
byte[] plaintext = decCipher.doFinal(ciphertext);
// ============================================================
// 5. VERIFY — recipient verifies the sender's signature
// ============================================================
boolean valid = provider.verify(
plaintext, signature, senderKeys.getPublicKeyPem(), "ML-DSA-65");
System.out.println("Decrypted: " + new String(plaintext));
System.out.println("Signature valid: " + valid);
}
}
// Cleanup at shutdown
provider.close();
com.qudo:qudo-jni-crypto to your Maven/Gradle build and set -Djava.library.path to the native library location.
# Key exchange: RSA key transport openssl rsautl -encrypt -pubin -inkey recipient.pub \ -in aes.key -out wrapped.key # Sign: RSA-SHA256 openssl dgst -sha256 -sign sender.key \ -out sig.bin message.txt
// Key exchange: ML-KEM-768 encapsulation
QudoKemResult e = provider.kemEncapsulate(
recipientPubKey, "ML-KEM-768");
byte[] sharedSecret = e.getSharedSecret();
// -> derive AES key, encrypt with AES-256-GCM
// Sign: ML-DSA-65 (FIPS 204)
byte[] sig = provider.sign(
message, senderPrivKey, "ML-DSA-65");
// Verify
boolean ok = provider.verify(
message, sig, senderPubKey, "ML-DSA-65");
| Component | Changes? | Details |
|---|---|---|
| Key transport / exchange | Yes | RSA encrypt / ECDH → ML-KEM-768 encapsulation |
| Message encryption | No | AES-256-GCM stays the same — symmetric crypto is quantum-safe |
| Digital signature | Yes | RSA-SHA256 / ECDSA → ML-DSA-65 |
| S/MIME workflow | No | Sign-then-encrypt pattern stays the same |
| CMS / PKCS#7 format | Minimal | Same ASN.1 structure, new algorithm OIDs for ML-KEM and ML-DSA |
| Recipient key management | Yes | Recipients need ML-KEM-768 keypairs instead of RSA/ECDH keypairs |
| Sender key management | Yes | Senders need ML-DSA-65 signing keys instead of RSA/ECDSA |
| Operation | PQC (ML-KEM + ML-DSA) | Classical (RSA + ECDSA) |
|---|---|---|
| Encrypt (KEM + AES) | ~130ms | ~5ms |
| Decrypt (KEM + AES) | ~30ms | ~3ms |
| Sign (ML-DSA-65) | ~120ms | ~2ms (ECDSA) |
| Verify (ML-DSA-65) | ~40ms | ~2ms (ECDSA) |
| Full sign + encrypt | ~200ms | ~10ms |
| Signature size | 3,309 bytes | 64 bytes (ECDSA) |
| Encapsulated key size | ~1KB | ~256 bytes (RSA) |
| Component | Minimum Version | Notes |
|---|---|---|
| OpenSSL | 3.6.0+ | Required for ML-KEM keypair generation and ML-DSA signing |
| Qudo Module | 1.0.0+ | Provides ML-KEM-768 and ML-DSA-65 algorithms |
| Java | 17+ | For the Qudo JNI provider runtime. AES-256-GCM via javax.crypto |
| Email clients | None natively | No major email client supports ML-KEM/ML-DSA natively yet. Use the Qudo provider in your email pipeline. |
| Thunderbird | Future | PQC S/MIME support expected as IETF standards finalize |
| Outlook | Future | Microsoft tracking NIST PQC standardization for S/MIME integration |
provider.kemEncapsulate() / provider.sign() directly before sending, and provider.kemDecapsulate() / provider.verify() upon receipt. No middleware needed — the provider runs in-process.
Cause: ML-KEM requires the sender to have the recipient's public key before encapsulation. Unlike RSA key transport, there's no way to encrypt without a pre-shared public key.
Fix: Set up a key directory (LDAP, database, or certificate-based) where recipients publish their ML-KEM public keys. Generate keypairs with provider.generateKeyPair("ML-KEM-768"), store private keys in HSM, distribute public keys via your directory.
Cause: ML-DSA-65 signature (3.3KB) + ML-KEM encapsulated key (~1KB) = ~4.4KB overhead. A 10-byte "ok" message becomes 4.4KB after PQC protection.
Mitigation: For very short messages, consider batching or using ML-DSA-44 (2.4KB signature). For typical email (1KB+), the overhead is proportionally small.
Cause: ML-KEM private keys are larger than RSA keys (~3KB for ML-KEM-768). Existing key storage may need updates.
Fix: Store ML-KEM/ML-DSA private keys in an HSM, secure keystore, or KMS. The Qudo provider outputs standard PEM-encoded keys compatible with PKCS#8 storage. Call provider.generateKeyPair("ML-KEM-768") and persist getPrivateKeyPem() securely. The playground stores keys in memory for convenience — never do this in production.
A: Not with PQC alone. During migration, implement a dual-mode path in your email pipeline: use classical S/MIME (RSA/ECDH) for recipients without PQC keys, and ML-KEM + ML-DSA for PQC-capable recipients. Check the recipient's key type in your directory and route accordingly.
A: Add the Qudo JNI provider to your email pipeline:
1. Outgoing: Before SMTP send, call provider.sign() + provider.kemEncapsulate() + AES encrypt
2. Incoming: After IMAP fetch, call provider.kemDecapsulate() + AES decrypt + provider.verify()
The provider runs in-process — no external service dependency. See Section 3 for the complete Java code.
A: IETF is finalizing PQC-in-CMS standards (draft-ietf-lamps-cms-kyber, draft-ietf-lamps-dilithium-certificates). Once ratified, email clients will add native support. Timeline is likely 2025-2026 for early adopters. The Qudo provider lets you start using PQC email today in your backend without waiting for client-side support.
A: Yes. S/MIME uses sign-then-encrypt: the sender signs the plaintext, then encrypts the signed message. The recipient decrypts first, then verifies the signature on the plaintext. This ensures the signature is bound to the actual message content, not the ciphertext.
A: Yes. Pass the algorithm name directly to the Qudo provider: provider.kemEncapsulate(pubKey, "ML-KEM-1024") or provider.sign(data, privKey, "ML-DSA-87"). Supported: ML-KEM-512/768/1024 (encryption), ML-DSA-44/65/87 (signing). In the playground, pass "algorithm":"ML-KEM-1024" in the JSON body.
A: Options for key distribution:
1. Key directory — LDAP/Active Directory with ML-KEM-768 public key attribute
2. Certificate-based — Issue ML-DSA-65 certificates via the CA service, publish in a directory
3. Key server — Dedicated key distribution endpoint (similar to PGP key servers)
4. First-contact — Exchange public keys on first encrypted communication (like Signal)
Analyze any endpoint's cryptographic configuration. Enter a host:port to scan its TLS setup and identify what's quantum-safe vs what needs migration.