Flow 13 --- Legacy RSA-2048 ➜ ML-KEM-768 Migration (PKCS#12 Import, Server-Side Re-Encryption)
This scenario shows how to lift legacy RSA-2048 ciphertext onto a post-quantum ML-KEM-768 key without ever exposing plaintext on the client:
-
Generate a local PKCS#12 bundle with an RSA-2048 key-pair.
-
Import that bundle into ANKASecure (
legacyRsa_*
). -
Encrypt data inside the service with the imported RSA public key.
-
Generate a fresh ML-KEM-768 key (
pqcKem768_*
). -
Re-encrypt the RSA ciphertext on the server (RSA-2048 → ML-KEM-768).
-
Stream-decrypt the ML-KEM ciphertext and validate integrity.
Key points
End-to-end migration from classical RSA to PQC without plaintext egress.
Demonstrates decrypt-only PKCS#12 import and
reencryptFileStream()
with sourceKidOverride.Uses Bouncy Castle only for local PKCS#12 creation; all production crypto happens on the platform.
All artefacts live in
temp_files/
, keeping the project workspace tidy.
When to use it
-
Regulated archives --- upgrade decades of stored RSA data to PQC while preserving zero-knowledge guarantees.
-
Cloud-first migrations --- move legacy on-prem keys into ANKASecure, then harden them to quantum-safe KEMs without round-tripping data.
-
Compliance audits --- prove that historical ciphertexts can be modernised with no client-side decryption, aligning with NIST post-quantum timelines.
Shared helper – this code imports the utility class from
example_util.md (configuration, authentication, JSON).
Complete Java implementation
src/main/java/co/ankatech/ankasecure/sdk/examples/ExampleScenario13.java
/* *****************************************************************************
* FILE: ExampleScenario13.java
* Copyright © 2025 Anka Technologies.
* SPDX-License-Identifier: MIT
* ---------------------------------------------------------------------------
* Scenario 13 – Legacy RSA-2048 ➜ Post-Quantum ML-KEM-768 Migration
* ---------------------------------------------------------------------------
* 1. Generate a self-signed RSA-2048 PKCS#12 (local, Bouncy Castle).
* 2. Import the PKCS#12 into ANKASecure (decrypt-only legacy key).
* 3. Encrypt a file locally with the exported RSA public key
* → detached ciphertext (no <code>kid</code> header).
* 4. Generate an ML-KEM-768 key on the platform.
* 5. *Stream* re-encrypt on the server (RSA ➜ KEM) – plaintext never
* leaves ANKASecure. Source key is forced via <code>sourceKidOverride</code>.
* 6. *Stream* decrypt the new ML-KEM ciphertext.
* 7. Validate that plaintext before/after matches.
*
* All artefacts are placed in <kbd>temp_files/</kbd>.
* ****************************************************************************/
package co.ankatech.ankasecure.sdk.examples;
import co.ankatech.ankasecure.sdk.AnkaSecureSdk;
import co.ankatech.ankasecure.sdk.model.DecryptResultMetadata;
import co.ankatech.ankasecure.sdk.model.GenerateKeySpec;
import co.ankatech.ankasecure.sdk.model.Pkcs12ImportSpec;
import co.ankatech.ankasecure.sdk.model.ReencryptResult;
import co.ankatech.ankasecure.sdk.util.FileIO;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Reader;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Date;
import java.util.Properties;
import static co.ankatech.ankasecure.sdk.examples.ExampleUtil.*;
/**
* <h2>Scenario 13 – Legacy RSA-2048 ➜ ML-KEM-768 Migration
* (Server-Side Re-encryption)</h2>
*
* <p>This end-to-end scenario demonstrates how to <em>seamlessly</em> migrate
* a legacy RSA-encrypted payload to a post-quantum ML-KEM-768 ciphertext,
* using ANKASecure’s streaming re-encryption API. At no point is the
* plaintext exposed outside the platform.</p>
*
* <p>*Workflow*</p>
* <ol>
* <li>Generate a self-signed RSA-2048 <code>.p12</code> locally.</li>
* <li>Import the PKCS#12 as a <em>decrypt-only</em> key.</li>
* <li>Encrypt a file with the exported RSA public key (utility helper).</li>
* <li>Create an ML-KEM-768 key.</li>
* <li>Server-side streaming re-encrypt (RSA ➜ KEM).</li>
* <li>Stream-decrypt the ML-KEM ciphertext.</li>
* <li>Validate integrity.</li>
* </ol>
*
* @author ANKATech – Security Engineering
* @since 2.1.0
*/
public final class ExampleScenario13 {
/* ====================================================================== */
/**
* Entry-point.
*
* @param args ignored
*/
public static void main(final String[] args) {
System.out.println("===== SCENARIO 13 START =====");
try {
ensureTempDir(TEMP_DIR);
Properties props = loadProperties();
AnkaSecureSdk sdk = authenticate(props);
runScenario(sdk);
} catch (Exception ex) {
fatal("Scenario 13 failed", ex);
}
System.out.println("===== SCENARIO 13 END =====");
}
/* ====================================================================== */
/**
* Executes the scenario.
*
* @param sdk an authenticated {@link AnkaSecureSdk} instance
*
* @throws Exception if any step fails
*/
private static void runScenario(final AnkaSecureSdk sdk) throws Exception {
/* Paths --------------------------------------------------------- */
Path p12File = TEMP_DIR.resolve("sc13_legacy_rsa.p12");
Path rsaPubJson= TEMP_DIR.resolve("sc13_rsa_pub.json");
Path plainFile = TEMP_DIR.resolve("sc13_plain.txt");
Path rsaCt = TEMP_DIR.resolve("sc13_rsa.ct");
Path kemCt = TEMP_DIR.resolve("sc13_kem.ct");
Path kemDec = TEMP_DIR.resolve("sc13_kem.dec.txt");
/* 1 ── RSA-2048 PKCS#12 ***************************************** */
final String p12Pwd = "LegacyP12!";
generatePkcs12(p12File, p12Pwd);
System.out.println("[1] PKCS#12 created -> " + p12File.toAbsolutePath());
/* 2 ── Import as decrypt-only key ******************************* */
String rsaKid = "sc13_legacy_rsa_" + System.currentTimeMillis();
Pkcs12ImportSpec importSpec = new Pkcs12ImportSpec()
.setKid(rsaKid)
.setP12Password(p12Pwd)
.setP12FileBase64(Base64.getEncoder()
.encodeToString(Files.readAllBytes(p12File)));
sdk.importPrivateKeyPkcs12(importSpec);
System.out.println("[2] RSA-2048 key imported -> kid = " + rsaKid);
/* 3 ── Encrypt sample data with RSA public key ****************** */
FileIO.writeUtf8(
plainFile,
"Scenario-13 – legacy RSA ciphertext.");
System.out.println("[3] Plaintext ready -> " + plainFile.toAbsolutePath());
sdk.exportKey(rsaKid, rsaPubJson);
String rsaPubB64 = extractPublicKey(rsaPubJson);
sdk.encryptFileUtilityStream(
"RSA", "RSA-2048", rsaPubB64, plainFile, rsaCt);
System.out.println(" RSA ciphertext created -> " + rsaCt.toAbsolutePath());
/* 4 ── Create ML-KEM-768 key ************************************ */
String kemKid = "sc13_kem768_" + System.currentTimeMillis();
sdk.generateKey(new GenerateKeySpec()
.setKid(kemKid)
.setKty("ML-KEM")
.setAlg("ML-KEM-768"));
System.out.println("[4] ML-KEM-768 key created -> kid = " + kemKid);
/* 5 ── Server-side re-encrypt (RSA ➜ KEM) *********************** */
ReencryptResult rr = sdk.reencryptFileStream(
kemKid, // target PQC key
rsaKid, // sourceKidOverride
rsaCt,
kemCt);
System.out.println("[5] Re-encrypted ciphertext -> " + kemCt.toAbsolutePath());
printReencryptMeta(rr);
/* 6 ── Stream-decrypt PQC ciphertext **************************** */
DecryptResultMetadata decMeta = sdk.decryptFileStream(kemCt, kemDec);
System.out.println("[6] Decrypted ML-KEM file -> " + kemDec.toAbsolutePath());
System.out.println(" Algorithm used : "
+ nullSafe(decMeta.getAlgorithmUsed()));
/* 7 ── Integrity check ****************************************** */
boolean match = FileIO.readUtf8(plainFile)
.equals(FileIO.readUtf8(kemDec));
System.out.println(match
? "[7] SUCCESS – plaintext matches."
: "[7] FAILURE – plaintext mismatch.");
}
/* ====================================================================== */
/* Helper: generate a self-signed RSA-2048 PKCS#12 ************************ */
private static void generatePkcs12(final Path out, final String pwd) throws Exception {
Security.addProvider(new BouncyCastleProvider());
KeyPair kp = KeyPairGenerator.getInstance("RSA").generateKeyPair();
X509Certificate cert = createSelfSigned(kp, "CN=LegacyRSA,O=Demo");
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null, pwd.toCharArray());
ks.setKeyEntry("key", kp.getPrivate(), pwd.toCharArray(),
new java.security.cert.Certificate[]{cert});
try (OutputStream os = Files.newOutputStream(out)) {
ks.store(os, pwd.toCharArray());
}
}
/** Builds a self-signed X.509 certificate for the given key-pair. */
private static X509Certificate createSelfSigned(
final KeyPair kp, final String dn) throws Exception {
X500Name subject = new X500Name(dn);
Instant now = Instant.now();
Date notBefore = Date.from(now);
Date notAfter = Date.from(now.plus(365, ChronoUnit.DAYS));
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.build(kp.getPrivate());
X509CertificateHolder holder = new JcaX509v3CertificateBuilder(
subject, BigInteger.valueOf(now.toEpochMilli()),
notBefore, notAfter, subject, kp.getPublic())
.build(signer);
return new JcaX509CertificateConverter()
.setProvider(new BouncyCastleProvider())
.getCertificate(holder);
}
/* Helper: extract 'publicKey' field from exported JSON ************* */
private static String extractPublicKey(final Path json) throws IOException {
try (Reader r = Files.newBufferedReader(json)) {
JsonObject o = JsonParser.parseReader(r).getAsJsonObject();
return o.get("publicKey").getAsString();
}
}
/**
* Private constructor prevents instantiation.
*/
private ExampleScenario13() {
/* utility class – no instantiation */
}
}
How to run
Console milestones
-
Local PKCS#12 creation (
sc13_legacy_rsa.p12
) -
Import as
legacyRsa_*
-
RSA public-key encryption →
sc13_rsa.ct
-
ML-KEM-768 key generation (
sc13_kem768_*
) -
Server-side re-encryption (RSA → KEM) →
sc13_kem.ct
-
Stream decryption & SUCCESS validation