Skip to content

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&nbsp;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

mvn -q compile exec:java\
  -Dexec.mainClass="co.ankatech.ankasecure.sdk.examples.ExampleScenario13"

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


Where next?