Skip to content

Flow 4 – Streaming Re-encrypt RSA-3072 ➜ ML-KEM-768

This scenario performs a zero-plaintext-exposure migration from a classical RSA key to a post-quantum ML-KEM key, operating on the pre-provisioned granted cross-kid pair (cli-reencrypt2-source / cli-reencrypt2-target):

  1. Resolve the granted RSA-3072 source kid (cli-reencrypt2-source).
  2. Stream-encrypt a file (detached JWET).
  3. Resolve the granted ML-KEM-768 target kid (cli-reencrypt2-target).
  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 (keys requested, warnings, migration-mode flag).

Prerequisites

The flow examples are data-plane only. They never create, rotate, or delete keys — they resolve pre-provisioned playground keys and operate on them. Before running any flow example you must:

  1. Provision a fresh demo-cli playground. Use the ankasecure-demo-provisioning tool to provision the demo-cli playground. This seeds the cryptographic keys the examples operate on and the cli-reference@demo-cli actor that holds capability grants on them.
  2. Use the emitted cli.properties. The provisioning tool writes a cli.properties file that carries the ankasecure.demo.kids catalogue line — the comma-separated list of provisioned key ids, in YAML file order. The examples load this file to discover which keys exist.
  3. Authenticate as cli-reference@demo-cli. Authenticate the SDK as the cli-reference@demo-cli actor — the all-operations actor of the demo-cli playground — so the resolved keys carry the capability grants each operation needs.

Key selection in the examples is by algorithm or by exact name:

  • kidForAlgorithm(alg) returns a pre-provisioned key id matching the requested algorithm — used by the single-key operation flows.
  • requireKid(name) asserts that an exact, named cross-kid endpoint is present in the ankasecure.demo.kids catalogue — used by the cross-kid REENCRYPT / RESIGN flows that must operate on a specific granted source/target pair.

If cli.properties is missing the ankasecure.demo.kids line, the examples fail fast with a clear message instructing you to (re-)provision the demo-cli playground — they will not silently fall back.

PKCS#7 / CMS examples (Flow 19 & Flow 20)

The PKCS#7 interop examples operate on a packaged, non-sensitive CMS EnvelopedData fixture — you do not supply a PKCS#7 file:

  • Fixture: src/main/resources/pkcs7/enveloped-data-sample.p7m, loaded from the classpath by Pkcs7ExampleFixture. It is a single-recipient, zero-signer EnvelopedData artifact from the QA test signer; it carries no private key and no secret.
  • Flow 19 (analysis) needs no key. Structural analysis (analyzePkcs7 / analyzePkcs7Stream) inspects the CMS envelope metadata only, so it runs with no decryption key and no extra configuration.
  • Flow 20 (conversion) needs a pre-provisioned recipient key. Converting EnvelopedData to JWE requires the platform to decrypt the envelope, so it needs the recipient's private key referenced by the pkcs7.decryptionKid property in cli.properties. That key is matched by issuer DN + serial number and is imported by a control-plane step (outside the data-plane SDK); until it is provisioned, Flow 20's conversion calls fail at the server while Flow 19 still runs. The calling actor also needs the in-place REENCRYPT grant on that kid, requested with the kid twice (assetKids: [kid, kid]) and stored as [kid]: a conversion is authorized by REENCRYPT / RESIGN grants, never by DECRYPT, and no wildcard REENCRYPT grant exists.
  • Data-plane only. Like every flow example, these never import keystores or perform any key-lifecycle operation.

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 helper — ExampleUtil (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.
 */

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.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-3072 → ML-KEM-768).
 *
 * <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>
 *
 * <p>The SDK is data-plane-only: it does not create keys. This scenario operates
 * on the <strong>pre-provisioned granted cross-kid pair</strong> of the
 * demo playground &mdash; source {@code cli-reencrypt2-source}
 * ({@code RSA-3072}) and target {@code cli-reencrypt2-target} ({@code ML-KEM-768}).
 * The playground actor holds the cross-kid REENCRYPT grant on this pair
 * plus the atomic encrypt/decrypt grants on each endpoint.</p>
 *
 * <ol>
 *   <li>Resolve the granted {@code RSA-3072} source and {@code ML-KEM-768} target kids.</li>
 *   <li>Stream-encrypt a file under the source (detached&nbsp;JWE).</li>
 *   <li>Re-encrypt the ciphertext on the server (RSA&nbsp;&rarr;&nbsp;KEM).</li>
 *   <li>Stream-decrypt with the KEM target 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>
 *
 * @see ExampleUtil
 * @see ExamplePlaygroundKeys
 * @see AuthenticatedSdk
 */
public final class ExampleScenario4 {

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

    /**
     * Runs the RSA-3072 to ML-KEM-768 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-3072 -> ML-KEM-768 streaming re-encryption
                  * Uses the pre-provisioned granted cross-kid pair
                    (cli-reencrypt2-source / cli-reencrypt2-target)
                  * Demonstrates detached-JWE pipeline (encrypt -> re-encrypt -> decrypt)
                Steps   :
                  1) Resolve granted RSA-3072 source + ML-KEM-768 target kids
                  2) Create dummy payload
                  3) Encrypt payload under source (detached JWE)
                  4) Re-encrypt ciphertext stream to target
                  5) Decrypt KEM ciphertext under target
                  6) Validate integrity
                --------------------------------------------------------------""");

        try {
            prepareWorkingDir();

            Properties            props      = loadProperties();
            AuthenticatedSdk      sdk        = authenticate(props);
            ExamplePlaygroundKeys playground = ExamplePlaygroundKeys.from(props, sdk);

            runScenario(sdk, playground);

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

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

    /**
     * Executes the streaming re-encryption scenario on the granted cross-kid pair.
     *
     * <ol>
     *   <li>Create large plaintext file (1 MB).</li>
     *   <li>Resolve the granted RSA-3072 source and ML-KEM-768 target kids.</li>
     *   <li>Stream-encrypt under the RSA-3072 source.</li>
     *   <li>Re-encrypt RSA ciphertext to ML-KEM (server-side).</li>
     *   <li>Stream-decrypt under the ML-KEM-768 target.</li>
     *   <li>Validate binary integrity via stream comparison.</li>
     * </ol>
     *
     * @param sdk        an authenticated {@link AuthenticatedSdk} instance;
     *                   must not be {@code null}
     * @param playground the pre-provisioned playground key resolver; must not be {@code null}
     * @throws Exception on any I/O or cryptographic failure
     */
    private static void runScenario(final AuthenticatedSdk sdk,
                                    final ExamplePlaygroundKeys playground) 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 -- resolve granted cross-kid pair (data-plane SDK: no key creation) */
        final String rsaKid = playground.requireKid("cli-reencrypt2-source"); // RSA-3072
        final String kemKid = playground.requireKid("cli-reencrypt2-target"); // ML-KEM-768
        System.out.println("[2] Source kid (RSA-3072)    -> " + rsaKid);
        System.out.println("[2] Target kid (ML-KEM-768)  -> " + kemKid);

        /* 3 -- streaming encrypt under RSA-3072 source ----------------------- */
        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 -- 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("[4] Re-encrypted file        -> " + kemCipher.toAbsolutePath());
        printReencryptMeta(reMeta);

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

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

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

How to run


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

Sequence to watch:

  1. RSA-3072 source-kid resolution & stream-encryption.

  2. ML-KEM-768 target-kid resolution.

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

  4. Stream-decryption with KEM key.

  5. Validation OK proving lossless migration.


Where next?