ML-KEM-1024 Encrypt / Decrypt (post-quantum)

Same round-trip as AES, this time with ML-KEM-1024 (FIPS 203, NIST Level 5). Provisioned by an admin, exercised by an application — identical app code.

🔐 ML-KEM-1024 Encrypt / Decrypt (post-quantum)

Scenario: provision an ML-KEM-1024 key (NIST FIPS 203 post-quantum KEM, security level 5) and use it exactly like a classical algorithm. The application-side code below is byte-for-byte identical to the AES-256 recipe — only the kty and algorithm fields change on the admin's create-key call. That is the core promise of ANKASecure.

Prerequisites

Two identities in the same tenant, exactly as in the AES-256 recipe:

  • Admin (email + password + tenant UUID) — issues a password-grant token used for /api/v3/admin/*.
  • Application (client_id + client_secret) — issues a client_credentials-grant token used for /api/v3/crypto/*.

The two tokens are not interchangeable — see the AES recipe's what just happened section for why.


Step-by-step

# ── Env
export BASE_URL="https://staging.ankatech.co"
export ADMIN_EMAIL="[email protected]"
export ADMIN_PASSWORD="••••••••••••"
export TENANT_ID="00000000-0000-0000-0000-000000000003"
export CLIENT_ID="00000000-0000-0000-0000-000000000000"
export CLIENT_SECRET="••••••••••••••••••••••••••••••••"
export KID="my-mlkem1024-key"

# ── Step 1a: Admin token — password grant
export ADMIN_TOKEN=$(curl -s -X POST "$BASE_URL/api/v3/auth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=password" \
  --data-urlencode "username=$ADMIN_EMAIL" \
  --data-urlencode "password=$ADMIN_PASSWORD" \
  --data-urlencode "tenant_id=$TENANT_ID" \
  | jq -r .access_token)

# ── Step 1b: App token — client_credentials grant
export APP_TOKEN=$(curl -s -X POST "$BASE_URL/api/v3/auth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=client_credentials" \
  --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode "client_secret=$CLIENT_SECRET" \
  | jq -r .access_token)

# ── Step 2: Create the ML-KEM-1024 key — uses ADMIN token
#           Only kty / algorithm differ vs AES — everything else identical.
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\":       \"$KID\",
    \"kty\":       \"ML-KEM\",
    \"algorithm\": \"ML-KEM-1024\",
    \"purpose\":   \"ENCRYPT_DECRYPT\",
    \"keyOps\":    [\"encrypt\", \"decrypt\"],
    \"exportable\": true
  }" | jq

# ── Step 3: Encrypt — uses APP token
export DATA_B64=$(printf "%s" "Post-quantum secured payload" | base64 -w0)
curl -s -X POST "$BASE_URL/api/v3/crypto/encrypt" \
  -H "Authorization: Bearer $APP_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{ \"kid\": \"$KID\", \"data\": \"$DATA_B64\" }" \
  | tee /tmp/enc.json | jq '{keyRequested, algorithmUsed}'
# → algorithmUsed: "ML-KEM-1024+A256GCM"

# ── Step 4: Decrypt — uses APP token
export JWE=$(jq -c .jweToken /tmp/enc.json)
curl -s -X POST "$BASE_URL/api/v3/crypto/decrypt" \
  -H "Authorization: Bearer $APP_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{ \"jweToken\": $JWE }" | jq

# ── Step 5: Verify
curl -s -X POST "$BASE_URL/api/v3/crypto/decrypt" \
  -H "Authorization: Bearer $APP_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{ \"jweToken\": $JWE }" \
  | jq -r .decryptedData \
  | base64 -d
# → Post-quantum secured payload
import base64
import requests

BASE_URL       = "https://staging.ankatech.co"
ADMIN_EMAIL    = "[email protected]"
ADMIN_PASSWORD = "••••••••••••"
TENANT_ID      = "00000000-0000-0000-0000-000000000003"
CLIENT_ID      = "00000000-0000-0000-0000-000000000000"
CLIENT_SECRET  = "••••••••••••••••••••••••••••••••"
KID            = "my-mlkem1024-key"

def token(fields):
    return requests.post(f"{BASE_URL}/api/v3/auth/token",
                         data=fields).json()["access_token"]

# ── Step 1a: Admin token — password grant
admin_auth = {"Authorization": f"Bearer {token({
    'grant_type': 'password',
    'username':   ADMIN_EMAIL,
    'password':   ADMIN_PASSWORD,
    'tenant_id':  TENANT_ID,
})}"}

# ── Step 1b: App token — client_credentials grant
app_auth = {"Authorization": f"Bearer {token({
    'grant_type':    'client_credentials',
    'client_id':     CLIENT_ID,
    'client_secret': CLIENT_SECRET,
})}"}

# ── Step 2: Create the ML-KEM-1024 key (admin) — only kty/algorithm differ vs AES
r = requests.post(
    f"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys",
    headers=admin_auth,
    json={
        "kid":        KID,
        "kty":        "ML-KEM",
        "algorithm":  "ML-KEM-1024",
        "purpose":    "ENCRYPT_DECRYPT",
        "keyOps":     ["encrypt", "decrypt"],
        "exportable": True,
    },
)
r.raise_for_status()
print("PQC key created:", r.json()["kid"])

# ── Step 3: Encrypt (app)
plaintext = "Post-quantum secured payload"
data_b64  = base64.b64encode(plaintext.encode()).decode()

enc = requests.post(
    f"{BASE_URL}/api/v3/crypto/encrypt",
    headers=app_auth,
    json={"kid": KID, "data": data_b64},
).json()
print(f"Encrypted with {enc['algorithmUsed']}")   # → ML-KEM-1024+A256GCM

# ── Step 4: Decrypt (app)
dec = requests.post(
    f"{BASE_URL}/api/v3/crypto/decrypt",
    headers=app_auth,
    json={"jweToken": enc["jweToken"]},
).json()
decrypted = base64.b64decode(dec["decryptedData"]).decode()

# ── Step 5: Verify
assert decrypted == plaintext
print(f"PQC round-trip OK: {decrypted!r}")
import java.net.URI;
import java.net.http.*;
import java.util.Base64;
import com.fasterxml.jackson.databind.*;

public class MlKemRoundTrip {

    static final String BASE_URL       = "https://staging.ankatech.co";
    static final String ADMIN_EMAIL    = "[email protected]";
    static final String ADMIN_PASSWORD = "••••••••••••";
    static final String TENANT_ID      = "00000000-0000-0000-0000-000000000003";
    static final String CLIENT_ID      = "00000000-0000-0000-0000-000000000000";
    static final String CLIENT_SECRET  = "••••••••••••••••••••••••••••••••";
    static final String KID            = "my-mlkem1024-key";

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

    static String token(String body) throws Exception {
        var resp = http.send(
            HttpRequest.newBuilder(URI.create(BASE_URL + "/api/v3/auth/token"))
                .header("Content-Type", "application/x-www-form-urlencoded")
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .build(),
            HttpResponse.BodyHandlers.ofString());
        return json.readTree(resp.body()).get("access_token").asText();
    }

    public static void main(String[] args) throws Exception {
        // ── Step 1a: Admin token — password grant
        var adminAuth = "Bearer " + token(
            "grant_type=password&username=" + ADMIN_EMAIL
            + "&password=" + ADMIN_PASSWORD
            + "&tenant_id=" + TENANT_ID);

        // ── Step 1b: App token — client_credentials grant
        var appAuth = "Bearer " + token(
            "grant_type=client_credentials"
            + "&client_id=" + CLIENT_ID
            + "&client_secret=" + CLIENT_SECRET);

        // ── Step 2: Create ML-KEM-1024 key (admin)
        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("""
                    {
                      "kid":        "%s",
                      "kty":        "ML-KEM",
                      "algorithm":  "ML-KEM-1024",
                      "purpose":    "ENCRYPT_DECRYPT",
                      "keyOps":     ["encrypt", "decrypt"],
                      "exportable": true
                    }""".formatted(KID)))
                .build(),
            HttpResponse.BodyHandlers.ofString());

        // ── Step 3-5: encrypt → decrypt → verify (app)
        var plaintext = "Post-quantum secured payload";
        var dataB64   = Base64.getEncoder().encodeToString(plaintext.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\": \"" + KID + "\", \"data\": \"" + dataB64 + "\" }"))
                .build(),
            HttpResponse.BodyHandlers.ofString());
        JsonNode enc = json.readTree(encResp.body());
        System.out.println("algorithmUsed=" + enc.get("algorithmUsed").asText());

        var jweToken = enc.get("jweToken").toString();
        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\": " + jweToken + " }"))
                .build(),
            HttpResponse.BodyHandlers.ofString());
        var decrypted = new String(Base64.getDecoder().decode(
            json.readTree(decResp.body()).get("decryptedData").asText()));

        assert decrypted.equals(plaintext);
        System.out.println("PQC round-trip OK: " + decrypted);
    }
}
const BASE_URL       = "https://staging.ankatech.co";
const ADMIN_EMAIL    = "[email protected]";
const ADMIN_PASSWORD = "••••••••••••";
const TENANT_ID      = "00000000-0000-0000-0000-000000000003";
const CLIENT_ID      = "00000000-0000-0000-0000-000000000000";
const CLIENT_SECRET  = "••••••••••••••••••••••••••••••••";
const KID            = "my-mlkem1024-key";

async function getToken(fields) {
  const r = await fetch(`${BASE_URL}/api/v3/auth/token`, {
    method:  "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body:    new URLSearchParams(fields),
  });
  return (await r.json()).access_token;
}

// ── Step 1a: Admin token — password grant
const adminAuth = { "Authorization": `Bearer ${await getToken({
  grant_type: "password",
  username:   ADMIN_EMAIL,
  password:   ADMIN_PASSWORD,
  tenant_id:  TENANT_ID,
})}` };

// ── Step 1b: App token — client_credentials grant
const appAuth = { "Authorization": `Bearer ${await getToken({
  grant_type:    "client_credentials",
  client_id:     CLIENT_ID,
  client_secret: CLIENT_SECRET,
})}` };

const jsonH = (auth) => ({ ...auth, "Content-Type": "application/json" });

// ── Step 2: Create ML-KEM-1024 key (admin) — only kty+algorithm differ vs AES
await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys`, {
  method:  "POST",
  headers: jsonH(adminAuth),
  body:    JSON.stringify({
    kid:        KID,
    kty:        "ML-KEM",
    algorithm:  "ML-KEM-1024",
    purpose:    "ENCRYPT_DECRYPT",
    keyOps:     ["encrypt", "decrypt"],
    exportable: true,
  }),
});

// ── Step 3-5: encrypt → decrypt → verify (app)
const plaintext = "Post-quantum secured payload";
const dataB64   = Buffer.from(plaintext).toString("base64");

const enc = await (await fetch(`${BASE_URL}/api/v3/crypto/encrypt`, {
  method:  "POST",
  headers: jsonH(appAuth),
  body:    JSON.stringify({ kid: KID, data: dataB64 }),
})).json();
console.log(`Encrypted with ${enc.algorithmUsed}`);

const dec = await (await fetch(`${BASE_URL}/api/v3/crypto/decrypt`, {
  method:  "POST",
  headers: jsonH(appAuth),
  body:    JSON.stringify({ jweToken: enc.jweToken }),
})).json();
const decrypted = Buffer.from(dec.decryptedData, "base64").toString();

if (decrypted !== plaintext) throw new Error("Mismatch!");
console.log(`PQC round-trip OK: ${decrypted}`);
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class MlKemRoundTrip
{
    const string BASE_URL       = "https://staging.ankatech.co";
    const string ADMIN_EMAIL    = "[email protected]";
    const string ADMIN_PASSWORD = "••••••••••••";
    const string TENANT_ID      = "00000000-0000-0000-0000-000000000003";
    const string CLIENT_ID      = "00000000-0000-0000-0000-000000000000";
    const string CLIENT_SECRET  = "••••••••••••••••••••••••••••••••";
    const string KID            = "my-mlkem1024-key";

    static async Task<string> GetToken(
        HttpClient http, IEnumerable<KeyValuePair<string,string>> fields)
    {
        var resp = await http.PostAsync($"{BASE_URL}/api/v3/auth/token",
            new FormUrlEncodedContent(fields));
        return JsonDocument.Parse(await resp.Content.ReadAsStringAsync())
                           .RootElement.GetProperty("access_token").GetString();
    }

    static async Task Main()
    {
        var http = new HttpClient();

        // ── Step 1a: Admin token — password grant
        var adminToken = await GetToken(http, new[]
        {
            new KeyValuePair<string,string>("grant_type", "password"),
            new KeyValuePair<string,string>("username",   ADMIN_EMAIL),
            new KeyValuePair<string,string>("password",   ADMIN_PASSWORD),
            new KeyValuePair<string,string>("tenant_id",  TENANT_ID),
        });

        // ── Step 1b: App token — client_credentials grant
        var appToken = await GetToken(http, new[]
        {
            new KeyValuePair<string,string>("grant_type",    "client_credentials"),
            new KeyValuePair<string,string>("client_id",     CLIENT_ID),
            new KeyValuePair<string,string>("client_secret", CLIENT_SECRET),
        });

        var adminHttp = new HttpClient();
        adminHttp.DefaultRequestHeaders.Add("Authorization", $"Bearer {adminToken}");
        var appHttp = new HttpClient();
        appHttp.DefaultRequestHeaders.Add("Authorization", $"Bearer {appToken}");

        // ── Step 2: Create ML-KEM-1024 key (admin)
        await adminHttp.PostAsync(
            $"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys",
            new StringContent(JsonSerializer.Serialize(new
            {
                kid        = KID,
                kty        = "ML-KEM",
                algorithm  = "ML-KEM-1024",
                purpose    = "ENCRYPT_DECRYPT",
                keyOps     = new[] { "encrypt", "decrypt" },
                exportable = true,
            }), Encoding.UTF8, "application/json"));

        // ── Step 3-5: encrypt → decrypt → verify (app)
        var plaintext = "Post-quantum secured payload";
        var dataB64   = Convert.ToBase64String(Encoding.UTF8.GetBytes(plaintext));

        var encResp = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/crypto/encrypt",
            new StringContent(
                JsonSerializer.Serialize(new { kid = KID, data = dataB64 }),
                Encoding.UTF8, "application/json"));
        var enc = JsonDocument.Parse(await encResp.Content.ReadAsStringAsync()).RootElement;
        Console.WriteLine($"algorithmUsed={enc.GetProperty("algorithmUsed").GetString()}");

        var jweToken = enc.GetProperty("jweToken").GetRawText();
        var decResp = await appHttp.PostAsync(
            $"{BASE_URL}/api/v3/crypto/decrypt",
            new StringContent($"{{ \"jweToken\": {jweToken} }}",
                              Encoding.UTF8, "application/json"));
        var dec = JsonDocument.Parse(await decResp.Content.ReadAsStringAsync()).RootElement;
        var decrypted = Encoding.UTF8.GetString(
            Convert.FromBase64String(dec.GetProperty("decryptedData").GetString()));

        if (decrypted != plaintext) throw new Exception("Mismatch!");
        Console.WriteLine($"PQC round-trip OK: {decrypted}");
    }
}

Expected output

PQC key created: my-mlkem1024-key
Encrypted with ML-KEM-1024+A256GCM
PQC round-trip OK: 'Post-quantum secured payload'

Why ML-KEM-1024

  • Standard: NIST FIPS 203 (2024), formerly Kyber-1024
  • Security level: NIST Level 5 — the highest tier (equivalent to AES-256 against classical attacks)
  • Design: post-quantum secure — resistant to Shor's algorithm on a large-scale quantum computer
  • Trade-off: larger ciphertexts and keys than classical algorithms — acceptable for anything but very high-throughput streaming

For a hybrid classical+PQC layer (recommended for production migration), see [Composite Hybrid Keys — coming soon].

Where to go next


Did this page help you?