Skip to content

Flow 4 – Streaming Re-encrypt RSA-2048 ➜ ML-KEM-1024

This scenario performs a zero-plaintext-exposure migration from a classical RSA key to a post-quantum ML-KEM key:

  1. Generate an RSA-2048 key.
  2. Stream-encrypt a file (detached JWET).
  3. Generate an ML-KEM-1024 key.
  4. Stream-re-encrypt the ciphertext on the server (RSA → KEM).
  5. Stream-decrypt with the ML-KEM key.
  6. Validate the recovered plaintext.

Key points

  • End-to-end streaming pipeline--encryptFileStream → reencryptFileStream → decryptFileStream--so even multi-gigabyte files never hit memory in one piece.
  • Uses a detached JWET (General-JSON header + raw binary part), eliminating Base64 overhead during transfer.
  • Zero-plaintext migration – the server converts RSA ciphertext to ML-KEM ciphertext without ever decrypting or returning sensitive data.
  • Demonstrates live crypto-agility: two independent keys, negotiated algorithms, and rich lifecycle telemetry (actual key used, warnings, migration-mode flag).

When to use it

  • Post-quantum retrofits – you already have archives protected with RSA and need to harden them against future quantum attacks without re-downloading or re-encrypting locally.
  • Massive data lakes & backups – streaming re-encryption lets you upgrade multi-terabyte stores in place, one pass, no staging disk, no plaintext ever written.
  • Regulated environments – proves to auditors that secret material stays encrypted end-to-end while cryptographic strength is increased.
  • Maintenance windows – server-side key migration is quicker than a full decrypt/re-encrypt cycle, reducing downtime for business-critical datasets.

Shared helperExampleUtil (configuration-loading, authentication, JSON, etc.).
If you haven’t copied it yet, fetch example-util.md and place the Java file beside the scenario sources.


Complete Java implementation

src/main/java/co/ankatech/ankasecure/sdk/examples/ExampleScenario4.java

/*
 * Copyright 2025 ANKATech Solutions Inc
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
package co.ankatech.ankasecure.sdk.examples;

import co.ankatech.ankasecure.sdk.AuthenticatedSdk;
import co.ankatech.ankasecure.sdk.model.DecryptResultMetadata;
import co.ankatech.ankasecure.sdk.model.EncryptResult;
import co.ankatech.ankasecure.sdk.model.GenerateKeySpec;
import co.ankatech.ankasecure.sdk.model.ReencryptResult;
import co.ankatech.ankasecure.sdk.util.FileIO;

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

import static co.ankatech.ankasecure.sdk.examples.ExampleUtil.*;

/**
 * Scenario 4 — Streaming Re-encrypt (RSA-2048 → ML-KEM-1024).
 *
 * <p>This scenario demonstrates how to migrate classical RSA-encrypted data to
 * a post-quantum ML-KEM-encrypted format <em>without exposing plaintext</em>.
 * All cryptographic operations use the <strong>streaming</strong> API, meaning
 * the service exchanges a two-part <em>detached&nbsp;JWE</em>
 * (General-JSON header + raw ciphertext stream).</p>
 *
 * <ol>
 *   <li>Generate an {@code RSA-2048} key.</li>
 *   <li>Stream-encrypt a file (detached&nbsp;JWE).</li>
 *   <li>Generate an {@code ML-KEM-1024} key.</li>
 *   <li>Re-encrypt the ciphertext on the server (RSA&nbsp;&rarr;&nbsp;KEM).</li>
 *   <li>Stream-decrypt with the KEM key.</li>
 *   <li>Validate round-trip integrity.</li>
 * </ol>
 *
 * <p>Temporary artefacts are placed under <kbd>temp_files/</kbd>.</p>
 *
 * <p><b>Implementation notes (Java&nbsp;21+):</b></p>
 * <ul>
 *   <li>All file operations rely on the {@link java.nio.file.Path} API.</li>
 *   <li>Encoding is explicitly set to UTF-8 for deterministic behaviour.</li>
 *   <li>Directory creation delegates to {@link ExampleUtil#ensureTempDir(Path)}.</li>
 * </ul>
 *
 * <p><b>Thread-safety:</b> this class is stateless and immutable.</p>
 *
 * @author ANKATech Solutions Inc.
 * @since 3.0.0
 * @see ExampleUtil
 * @see AuthenticatedSdk
 */
public final class ExampleScenario4 {

    /** No instantiation &mdash; this class only exposes {@link #main(String[])}. */
    private ExampleScenario4() { }

    /**
     * Runs the RSA-2048 to ML-KEM-1024 streaming re-encryption scenario.
     *
     * <p>Loads CLI properties, authenticates against ANKASecure&copy;,
     * and delegates to the scenario logic. On any unrecoverable error
     * the JVM terminates via
     * {@link ExampleUtil#fatal(String, Throwable)}.</p>
     *
     * @param args command-line arguments (ignored)
     */
    public static void main(final String[] args) {

        System.out.println("===== SCENARIO 4 START =====");
        System.out.println("""
                Purpose :
                  * RSA-2048 -> ML-KEM-1024 streaming re-encryption
                  * Demonstrates detached-JWE pipeline (encrypt -> re-encrypt -> decrypt)
                Steps   :
                  1) Generate RSA-2048 key
                  2) Create dummy payload
                  3) Encrypt payload (detached JWE)
                  4) Generate ML-KEM-1024 key
                  5) Re-encrypt ciphertext stream
                  6) Decrypt KEM ciphertext
                  7) Validate integrity
                --------------------------------------------------------------""");

        try {
            ensureTempDir(TEMP_DIR);

            Properties props = loadProperties();
            AuthenticatedSdk sdk = authenticate(props);

            runScenario(sdk);

        } catch (Exception ex) {
            fatal("Scenario 4 failed", ex);
        }

        System.out.println("===== SCENARIO 4 END =====");
    }

    /**
     * Executes the 7-step streaming re-encryption scenario.
     *
     * <ol>
     *   <li>Create large plaintext file (1 MB).</li>
     *   <li>Generate RSA-2048 encryption key.</li>
     *   <li>Stream-encrypt under RSA-2048.</li>
     *   <li>Generate ML-KEM-1024 encryption key.</li>
     *   <li>Re-encrypt RSA ciphertext to ML-KEM (server-side).</li>
     *   <li>Stream-decrypt under ML-KEM-1024.</li>
     *   <li>Validate binary integrity via stream comparison.</li>
     * </ol>
     *
     * @param sdk an authenticated {@link AuthenticatedSdk} instance;
     *            must not be {@code null}
     * @throws Exception on any I/O or cryptographic failure
     */
    private static void runScenario(final AuthenticatedSdk sdk) throws Exception {

        /* 1 -- prepare large plaintext file (e.g., 1 Mb) --------------------- */
        Path plainFile = TEMP_DIR.resolve("scenario4_plain_large.bin");
        byte[] dummy = new byte[1 * 1024 * 1024]; // zero-filled 1 Mb buffer
        FileIO.writeBytes(plainFile, dummy);
        System.out.println("[1] Plaintext ready          -> " + plainFile.toAbsolutePath());

        /* 2 -- generate RSA-2048 key ----------------------------------------- */
        String rsaKid = "sc4_rsa_" + System.currentTimeMillis();
        sdk.generateKey(new GenerateKeySpec()
                .setKid(rsaKid)
                .setKty("RSA")
                .setAlg("RSA-2048"));
        System.out.println("[2] RSA-2048 key created     -> kid = " + rsaKid);

        /* 3 -- streaming encrypt under RSA-2048 ------------------------------ */
        Path rsaCipher = TEMP_DIR.resolve("scenario4_rsa_large.enc");
        EncryptResult rsaEncMeta = sdk.encryptFileStream(rsaKid, plainFile, rsaCipher);
        System.out.println("[3] RSA ciphertext written   -> " + rsaCipher.toAbsolutePath());
        printEncryptMeta(rsaEncMeta);

        /* 4 -- generate ML-KEM-1024 key ------------------------------------- */
        String kemKid = "sc4_kem_" + System.currentTimeMillis();
        sdk.generateKey(new GenerateKeySpec()
                .setKid(kemKid)
                .setKty("ML-KEM")
                .setAlg("ML-KEM-1024"));
        System.out.println("[4] ML-KEM-1024 key created  -> kid = " + kemKid);

        /* 5 -- one-pass streaming re-encrypt RSA -> ML-KEM ------------------- */
        Path kemCipher = TEMP_DIR.resolve("scenario4_kem_large.enc");
        ReencryptResult reMeta = sdk.reencryptFileStream(kemKid, null, rsaCipher, kemCipher);
        System.out.println("[5] Re-encrypted file        -> " + kemCipher.toAbsolutePath());
        printReencryptMeta(reMeta);

        /* 6 -- streaming decrypt under ML-KEM-1024 --------------------------- */
        Path decFile = TEMP_DIR.resolve("scenario4_large_decrypted.bin");
        DecryptResultMetadata decMeta = sdk.decryptFileStream(kemCipher, decFile);
        System.out.println("[6] Decrypted file          -> " + decFile.toAbsolutePath());
        printDecryptMeta(decMeta);

        /* 7 -- validation via streaming compare ------------------------------ */
        try (InputStream in1 = Files.newInputStream(plainFile);
             InputStream in2 = Files.newInputStream(decFile)) {

            boolean match = streamsAreEqual(in1, in2);
            System.out.println(match
                    ? "[7] Validation OK - plaintext matches."
                    : "[7] WARNING - plaintext mismatch!");
        }
    }
}

How to run


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

Sequence to watch:

  1. RSA-2048 key creation & stream-encryption.

  2. ML-KEM-1024 key creation.

  3. Server-side re-encrypt (RSA → KEM).

  4. Stream-decryption with KEM key.

  5. Validation OK proving lossless migration.


Where next?