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 aclient_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 payloadimport 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
- AES-256 Encrypt / Decrypt — the classical counterpart, byte-identical app code.
- RSA → ML-KEM Rotation — rotate a classical key into ML-KEM without app-code changes.
GET /api/v3/algorithms— full catalogue of every validkty+algorithmcombination in your deployment.
Updated 10 days ago