P-521 → ML-KEM-768 Re-Encrypt

Re-encrypt an existing JWE from P-521 (ECC) to ML-KEM-768 (PQC), without the plaintext ever leaving the platform.

🔄 P-521 → ML-KEM-768 Re-Encrypt

Scenario: you have JWE tokens encrypted with P-521 (NIST elliptic curve, ECIES). Migrate the ciphertext to ML-KEM-768 (PQC) using the re-encrypt endpoint — the platform decrypts and re-encrypts internally, plaintext never surfaces. Different from key rotation: rotation replaces the key under a stable kid; re-encrypt creates a fresh ciphertext under a different target kid.

Prerequisites

Same two identities as the AES-256 recipe.


Step-by-step

export BASE_URL="https://staging.ankatech.co"
export TENANT_ID="00000000-0000-0000-0000-000000000003"
export SRC_KID="p521-source-key"
export TGT_KID="mlkem768-target-key"
# ADMIN_TOKEN + APP_TOKEN as in AES-256 recipe

# Step 2: admin provisions both keys
curl -s -X POST "$BASE_URL/api/v3/admin/tenants/$TENANT_ID/keys" \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d "{ \"kid\":\"$SRC_KID\", \"kty\":\"EC\", \"algorithm\":\"ECDH-ES+A256KW\",
        \"purpose\":\"ENCRYPT_DECRYPT\", \"keyOps\":[\"encrypt\",\"decrypt\"] }" | jq

curl -s -X POST "$BASE_URL/api/v3/admin/tenants/$TENANT_ID/keys" \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d "{ \"kid\":\"$TGT_KID\", \"kty\":\"ML-KEM\", \"algorithm\":\"ML-KEM-768\",
        \"purpose\":\"ENCRYPT_DECRYPT\", \"keyOps\":[\"encrypt\",\"decrypt\"] }" | jq

# Step 3: app encrypts with the classical key (represents existing ciphertext)
export DATA_B64=$(printf "%s" "Legacy P-521 ciphertext" | base64 -w0)
export SRC_JWE=$(curl -s -X POST "$BASE_URL/api/v3/crypto/encrypt" \
  -H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
  -d "{ \"kid\":\"$SRC_KID\", \"data\":\"$DATA_B64\" }" | jq -c .jweToken)

# Step 4: re-encrypt to the PQC target — plaintext never surfaces
curl -s -X POST "$BASE_URL/api/v3/crypto/reencrypt" \
  -H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
  -d "{ \"jweToken\": $SRC_JWE, \"newKid\":\"$TGT_KID\" }" \
  | tee /tmp/re.json | jq '{keyRequested, algorithmUsed, materialVersion}'
# → algorithmUsed: "ML-KEM-768+A256GCM"    keyRequested: "mlkem768-target-key"

# Step 5: decrypt the re-encrypted JWE with the target key
curl -s -X POST "$BASE_URL/api/v3/crypto/decrypt" \
  -H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
  -d "{ \"jweToken\": $(jq -c .jweToken /tmp/re.json) }" \
  | jq -r .decryptedData | base64 -d
# → Legacy P-521 ciphertext
import base64, requests

BASE = "https://staging.ankatech.co"
TENANT_ID = "00000000-0000-0000-0000-000000000003"
SRC_KID, TGT_KID = "p521-source-key", "mlkem768-target-key"
# admin / app tokens as in AES-256

for spec in [
    {"kid": SRC_KID, "kty": "EC",     "algorithm": "ECDH-ES+A256KW"},
    {"kid": TGT_KID, "kty": "ML-KEM", "algorithm": "ML-KEM-768"},
]:
    requests.post(f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys", headers=admin,
                  json={**spec, "purpose": "ENCRYPT_DECRYPT",
                        "keyOps": ["encrypt","decrypt"]}).raise_for_status()

# Encrypt with source key
src = requests.post(f"{BASE}/api/v3/crypto/encrypt", headers=app,
                    json={"kid": SRC_KID, "data": base64.b64encode(b"Legacy P-521 ciphertext").decode()}).json()

# Re-encrypt to the PQC target
re = requests.post(f"{BASE}/api/v3/crypto/reencrypt", headers=app,
                   json={"jweToken": src["jweToken"], "newKid": TGT_KID}).json()
print(f"Re-encrypted with {re['algorithmUsed']}")

# Decrypt with target key
dec = requests.post(f"{BASE}/api/v3/crypto/decrypt", headers=app,
                    json={"jweToken": re["jweToken"]}).json()
assert base64.b64decode(dec["decryptedData"]) == b"Legacy P-521 ciphertext"
print("Re-encrypt round-trip PASSED")
import java.net.URI;
import java.net.http.*;
import java.util.Base64;
import com.fasterxml.jackson.databind.*;

public class P521ToMlKemReencrypt {
    static final String BASE_URL  = "https://staging.ankatech.co";
    static final String TENANT_ID = "00000000-0000-0000-0000-000000000003";
    static final String SRC_KID   = "p521-source-key";
    static final String TGT_KID   = "mlkem768-target-key";

    static final HttpClient   http = HttpClient.newHttpClient();
    static final ObjectMapper json = new ObjectMapper();

    public static void main(String[] args) throws Exception {
        // ADMIN_TOKEN + APP_TOKEN acquired as in the AES-256 recipe
        String adminAuth = "Bearer " + System.getenv("ADMIN_TOKEN");
        String appAuth   = "Bearer " + System.getenv("APP_TOKEN");

        // Admin creates both keys
        for (String[] spec : new String[][] {
            { SRC_KID, "EC",     "ECDH-ES+A256KW" },
            { TGT_KID, "ML-KEM", "ML-KEM-768"     },
        }) {
            http.send(HttpRequest.newBuilder(URI.create(
                BASE_URL + "/api/v3/admin/tenants/" + TENANT_ID + "/keys"))
                .header("Authorization", adminAuth).header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(String.format(
                    "{ \"kid\": \"%s\", \"kty\": \"%s\", \"algorithm\": \"%s\","
                    + " \"purpose\": \"ENCRYPT_DECRYPT\", \"keyOps\": [\"encrypt\",\"decrypt\"] }",
                    spec[0], spec[1], spec[2]))).build(),
                HttpResponse.BodyHandlers.ofString());
        }

        // Encrypt with SRC
        var dataB64 = Base64.getEncoder().encodeToString("Legacy P-521 ciphertext".getBytes());
        var encResp = http.send(HttpRequest.newBuilder(URI.create(BASE_URL + "/api/v3/crypto/encrypt"))
            .header("Authorization", appAuth).header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(
                "{ \"kid\": \"" + SRC_KID + "\", \"data\": \"" + dataB64 + "\" }")).build(),
            HttpResponse.BodyHandlers.ofString());
        JsonNode src = json.readTree(encResp.body());

        // Re-encrypt to TGT (PQC)
        var reResp = http.send(HttpRequest.newBuilder(URI.create(BASE_URL + "/api/v3/crypto/reencrypt"))
            .header("Authorization", appAuth).header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(String.format(
                "{ \"jweToken\": %s, \"newKid\": \"%s\" }", src.get("jweToken"), TGT_KID))).build(),
            HttpResponse.BodyHandlers.ofString());
        JsonNode re = json.readTree(reResp.body());
        System.out.println("Re-encrypted with " + re.get("algorithmUsed").asText());

        // Decrypt with TGT
        var decResp = http.send(HttpRequest.newBuilder(URI.create(BASE_URL + "/api/v3/crypto/decrypt"))
            .header("Authorization", appAuth).header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(
                "{ \"jweToken\": " + re.get("jweToken") + " }")).build(),
            HttpResponse.BodyHandlers.ofString());
        String plaintext = new String(Base64.getDecoder().decode(
            json.readTree(decResp.body()).get("decryptedData").asText()));
        if (!"Legacy P-521 ciphertext".equals(plaintext)) throw new RuntimeException("Fail");
        System.out.println("Re-encrypt round-trip PASSED");
    }
}
const BASE_URL = "https://staging.ankatech.co";
const TENANT_ID = "00000000-0000-0000-0000-000000000003";
const SRC_KID = "p521-source-key", TGT_KID = "mlkem768-target-key";

// Admin creates both keys
for (const spec of [
  { kid: SRC_KID, kty: "EC",     algorithm: "ECDH-ES+A256KW" },
  { kid: TGT_KID, kty: "ML-KEM", algorithm: "ML-KEM-768" },
]) {
  await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys`, {
    method: "POST", headers: jsonH(admin),
    body: JSON.stringify({ ...spec, purpose: "ENCRYPT_DECRYPT", keyOps: ["encrypt","decrypt"] }),
  });
}

// App encrypts with SRC
const src = await (await fetch(`${BASE_URL}/api/v3/crypto/encrypt`, {
  method: "POST", headers: jsonH(app),
  body: JSON.stringify({ kid: SRC_KID, data: Buffer.from("Legacy P-521 ciphertext").toString("base64") }),
})).json();

// Re-encrypt to TGT (PQC)
const re = await (await fetch(`${BASE_URL}/api/v3/crypto/reencrypt`, {
  method: "POST", headers: jsonH(app),
  body: JSON.stringify({ jweToken: src.jweToken, newKid: TGT_KID }),
})).json();
console.log(`Re-encrypted with ${re.algorithmUsed}`);

// Decrypt with TGT
const dec = await (await fetch(`${BASE_URL}/api/v3/crypto/decrypt`, {
  method: "POST", headers: jsonH(app),
  body: JSON.stringify({ jweToken: re.jweToken }),
})).json();
if (Buffer.from(dec.decryptedData, "base64").toString() !== "Legacy P-521 ciphertext")
  throw new Error("Fail");
console.log("Re-encrypt round-trip PASSED");
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class P521ToMlKemReencrypt
{
    const string BASE_URL  = "https://staging.ankatech.co";
    const string TENANT_ID = "00000000-0000-0000-0000-000000000003";
    const string SRC_KID   = "p521-source-key";
    const string TGT_KID   = "mlkem768-target-key";

    static async Task Main()
    {
        var adminHttp = new HttpClient();
        adminHttp.DefaultRequestHeaders.Add("Authorization",
            $"Bearer {Environment.GetEnvironmentVariable("ADMIN_TOKEN")}");
        var appHttp = new HttpClient();
        appHttp.DefaultRequestHeaders.Add("Authorization",
            $"Bearer {Environment.GetEnvironmentVariable("APP_TOKEN")}");

        // Admin creates both keys
        foreach (var spec in new[] {
            new { kid = SRC_KID, kty = "EC",     algorithm = "ECDH-ES+A256KW" },
            new { kid = TGT_KID, kty = "ML-KEM", algorithm = "ML-KEM-768"     },
        }) {
            await adminHttp.PostAsync(
                $"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys",
                new StringContent(JsonSerializer.Serialize(new {
                    spec.kid, spec.kty, spec.algorithm,
                    purpose = "ENCRYPT_DECRYPT", keyOps = new[] { "encrypt", "decrypt" },
                }), Encoding.UTF8, "application/json"));
        }

        // Encrypt with SRC
        var dataB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("Legacy P-521 ciphertext"));
        var encResp = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/crypto/encrypt",
            new StringContent(JsonSerializer.Serialize(new { kid = SRC_KID, data = dataB64 }),
                              Encoding.UTF8, "application/json"));
        var src = JsonDocument.Parse(await encResp.Content.ReadAsStringAsync()).RootElement;
        var srcJwe = src.GetProperty("jweToken").GetRawText();

        // Re-encrypt to TGT (PQC)
        var reResp = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/crypto/reencrypt",
            new StringContent($"{{ \"jweToken\": {srcJwe}, \"newKid\": \"{TGT_KID}\" }}",
                              Encoding.UTF8, "application/json"));
        var re = JsonDocument.Parse(await reResp.Content.ReadAsStringAsync()).RootElement;
        Console.WriteLine($"Re-encrypted with {re.GetProperty("algorithmUsed").GetString()}");

        // Decrypt with TGT
        var reJwe = re.GetProperty("jweToken").GetRawText();
        var decResp = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/crypto/decrypt",
            new StringContent($"{{ \"jweToken\": {reJwe} }}",
                              Encoding.UTF8, "application/json"));
        var dec = JsonDocument.Parse(await decResp.Content.ReadAsStringAsync()).RootElement;
        var plaintext = Encoding.UTF8.GetString(
            Convert.FromBase64String(dec.GetProperty("decryptedData").GetString()));
        if (plaintext != "Legacy P-521 ciphertext") throw new Exception("Fail");
        Console.WriteLine("Re-encrypt round-trip PASSED");
    }
}

Re-encrypt vs Rotation

AspectRe-encryptRotation
Endpoint/api/v3/crypto/reencrypt/api/v3/admin/…/keys/{kid}/rotations
App identityApplication (client_creds)Admin (password)
Target kidDifferent kid you specifySame kid, new material version
App code changesYes — must reference new kidNo — same kid keeps working
Use caseCiphertext migration in bulkZero-touch algorithm change

Prefer rotation when you can — the app code stays untouched. Re-encrypt is for migrations where you want each ciphertext to be independently addressable under a new kid.

Where to go next


Did this page help you?