AES-256 Encrypt / Decrypt (basic)
Provision an AES-256 symmetric key (as admin), then encrypt / decrypt with an application token. Full end-to-end round-trip using both auth flows.
🔑 AES-256 Encrypt / Decrypt (basic)
Scenario: provision an AES-256 symmetric key in your tenant and use it for an encrypt / decrypt round-trip. The recipe uses two OAuth 2.0 grant types — this is important to understand:
passwordgrant → issues a HUMAN token (audience=ankasecure-admin) used for key provisioning (/api/v3/admin/*).client_credentialsgrant → issues an APPLICATION token (audience=ankasecure-core) used for runtime crypto (/api/v3/crypto/*).
The two tokens are not interchangeable. Attempting POST /api/v3/admin/… with an application token, or POST /api/v3/crypto/… with an admin token, is rejected. This mirrors how the platform separates the provisioning plane from the runtime plane.
Prerequisites
You need two identities in the same tenant on staging — usually issued by your platform administrator:
| Placeholder | What it is | Used for |
|---|---|---|
$ADMIN_EMAIL | Admin user email | Provisioning the key |
$ADMIN_PASSWORD | Admin user password | Provisioning the key |
$TENANT_ID | Tenant UUID | Both flows |
$CLIENT_ID | Application UUID (workload registered in the same tenant) | Runtime encrypt / decrypt |
$CLIENT_SECRET | Application secret | Runtime encrypt / decrypt |
$BASE_URL | https://staging.ankatech.co (or your on-prem hostname) | Both flows |
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-aes256-key"
# ── Step 1a: Admin token — password grant (for /api/v3/admin/*)
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)
echo "Admin token: ${ADMIN_TOKEN:0:40}... (audience=ankasecure-admin)"
# ── Step 1b: App token — client_credentials grant (for /api/v3/crypto/*)
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)
echo "App token: ${APP_TOKEN:0:40}... (audience=ankasecure-core)"
# ── Step 2: Create the AES-256 key — uses ADMIN_TOKEN
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\": \"oct\",
\"algorithm\": \"A256GCM\",
\"purpose\": \"ENCRYPT_DECRYPT\",
\"keyOps\": [\"encrypt\", \"decrypt\"]
}" | jq
# ── Step 3: Prepare payload (Base64 is required by the API)
export DATA_B64=$(printf "%s" "Hello ANKASecure — AES-256 round-trip" | base64 -w0)
# ── Step 4: Encrypt — uses APP_TOKEN
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, materialVersion}'
# ── Step 5: Decrypt — uses APP_TOKEN (pass back the whole jweToken object)
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 6: Verify the round-trip
DECRYPTED_B64=$(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)
echo "$DECRYPTED_B64" | base64 -d
# → Hello ANKASecure — AES-256 round-tripimport 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-aes256-key"
# ── Step 1a: Admin token (password grant) — for /api/v3/admin/*
r = requests.post(f"{BASE_URL}/api/v3/auth/token", data={
"grant_type": "password",
"username": ADMIN_EMAIL,
"password": ADMIN_PASSWORD,
"tenant_id": TENANT_ID,
})
admin_auth = {"Authorization": f"Bearer {r.json()['access_token']}"}
# ── Step 1b: App token (client_credentials grant) — for /api/v3/crypto/*
r = requests.post(f"{BASE_URL}/api/v3/auth/token", data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
})
app_auth = {"Authorization": f"Bearer {r.json()['access_token']}"}
# ── Step 2: Create the AES-256 key — uses ADMIN token
r = requests.post(
f"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys",
headers=admin_auth,
json={
"kid": KID,
"kty": "oct",
"algorithm": "A256GCM",
"purpose": "ENCRYPT_DECRYPT",
"keyOps": ["encrypt", "decrypt"],
},
)
r.raise_for_status()
print("Key created:", r.json()["kid"])
# ── Step 3: Encrypt — uses APP token
plaintext = "Hello ANKASecure — AES-256 round-trip"
data_b64 = base64.b64encode(plaintext.encode()).decode()
r = requests.post(
f"{BASE_URL}/api/v3/crypto/encrypt",
headers=app_auth,
json={"kid": KID, "data": data_b64},
)
enc = r.json()
print(f"Encrypted with {enc['algorithmUsed']} (material v{enc['materialVersion']})")
# ── Step 4: Decrypt — uses APP token
r = requests.post(
f"{BASE_URL}/api/v3/crypto/decrypt",
headers=app_auth,
json={"jweToken": enc["jweToken"]},
)
dec = r.json()
decrypted = base64.b64decode(dec["decryptedData"]).decode()
# ── Step 5: Verify
assert decrypted == plaintext, "Round-trip mismatch!"
print(f"Round-trip OK: {decrypted!r}")import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class AesRoundTrip {
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-aes256-key";
static final HttpClient http = HttpClient.newHttpClient();
static final ObjectMapper json = new ObjectMapper();
/** POSTs a form-urlencoded token request and returns the access_token. */
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 (for /api/v3/admin/*)
var adminAuth = "Bearer " + token(
"grant_type=password"
+ "&username=" + ADMIN_EMAIL
+ "&password=" + ADMIN_PASSWORD
+ "&tenant_id=" + TENANT_ID);
// ── Step 1b: App token — client_credentials grant (for /api/v3/crypto/*)
var appAuth = "Bearer " + token(
"grant_type=client_credentials"
+ "&client_id=" + CLIENT_ID
+ "&client_secret=" + CLIENT_SECRET);
// ── Step 2: Create the AES-256 key — uses ADMIN token
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": "oct",
"algorithm": "A256GCM",
"purpose": "ENCRYPT_DECRYPT",
"keyOps": ["encrypt", "decrypt"]
}""".formatted(KID)))
.build(),
HttpResponse.BodyHandlers.ofString());
// ── Step 3: Encrypt — uses APP token
var plaintext = "Hello ANKASecure — AES-256 round-trip";
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());
// ── Step 4: Decrypt — uses APP token (send the whole jweToken)
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()));
// ── Step 5: Verify
assert decrypted.equals(plaintext) : "Round-trip mismatch!";
System.out.println("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-aes256-key";
// Small helper — issues a token by grant_type
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 (for /api/v3/admin/*)
const adminToken = await getToken({
grant_type: "password",
username: ADMIN_EMAIL,
password: ADMIN_PASSWORD,
tenant_id: TENANT_ID,
});
const adminAuth = { "Authorization": `Bearer ${adminToken}` };
// ── Step 1b: App token — client_credentials grant (for /api/v3/crypto/*)
const appToken = await getToken({
grant_type: "client_credentials",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
});
const appAuth = { "Authorization": `Bearer ${appToken}` };
const jsonH = (auth) => ({ ...auth, "Content-Type": "application/json" });
// ── Step 2: Create the AES-256 key — uses ADMIN token
await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys`, {
method: "POST",
headers: jsonH(adminAuth),
body: JSON.stringify({
kid: KID,
kty: "oct",
algorithm: "A256GCM",
purpose: "ENCRYPT_DECRYPT",
keyOps: ["encrypt", "decrypt"],
}),
});
// ── Step 3: Encrypt — uses APP token
const plaintext = "Hello ANKASecure — AES-256 round-trip";
const dataB64 = Buffer.from(plaintext).toString("base64"); // Node.js
// Browser: btoa(unescape(encodeURIComponent(plaintext)))
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} (v${enc.materialVersion})`);
// ── Step 4: Decrypt — uses APP token
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();
// ── Step 5: Verify
if (decrypted !== plaintext) throw new Error("Round-trip mismatch!");
console.log(`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 AesRoundTrip
{
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-aes256-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));
var body = await resp.Content.ReadAsStringAsync();
return JsonDocument.Parse(body).RootElement.GetProperty("access_token").GetString();
}
static async Task Main()
{
var http = new HttpClient();
// ── Step 1a: Admin token — password grant (for /api/v3/admin/*)
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 (for /api/v3/crypto/*)
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),
});
// Two separate HttpClients avoid header collisions between the two identities
var adminHttp = new HttpClient();
adminHttp.DefaultRequestHeaders.Add("Authorization", $"Bearer {adminToken}");
var appHttp = new HttpClient();
appHttp.DefaultRequestHeaders.Add("Authorization", $"Bearer {appToken}");
// ── Step 2: Create the AES-256 key — uses ADMIN identity
await adminHttp.PostAsync(
$"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys",
new StringContent(JsonSerializer.Serialize(new
{
kid = KID,
kty = "oct",
algorithm = "A256GCM",
purpose = "ENCRYPT_DECRYPT",
keyOps = new[] { "encrypt", "decrypt" },
}), Encoding.UTF8, "application/json"));
// ── Step 3: Encrypt — uses APP identity
var plaintext = "Hello ANKASecure — AES-256 round-trip";
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()}");
// ── Step 4: Decrypt — uses APP identity
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()));
// ── Step 5: Verify
if (decrypted != plaintext) throw new Exception("Round-trip mismatch!");
Console.WriteLine($"Round-trip OK: {decrypted}");
}
}Expected output
Admin token: eyJhbGciOiJSUzI1NiIs... (audience=ankasecure-admin)
App token: eyJhbGciOiJSUzI1NiIs... (audience=ankasecure-core)
Key created: my-aes256-key
Encrypted with A256GCM (material v1)
Round-trip OK: 'Hello ANKASecure — AES-256 round-trip'
What just happened
- Two separate OAuth 2.0 flows issued two tokens with different audiences — the platform enforces the separation between admin and runtime concerns.
- The
A256GCMalgorithm (AES-256 in Galois/Counter Mode) was resolved by the tenant policy forkty=oct— you never picked it explicitly. - The
algorithmUsedfield on encrypt confirms what actually ran — useful for audit trails. - If the admin rotates the key to a new algorithm tomorrow, this application code keeps working. Only
algorithmUsedandmaterialVersionin the response change. That is crypto agility.
Where to go next
- ML-KEM-1024 Encrypt / Decrypt — same flow with a post-quantum KEM instead of AES.
- RSA → ML-KEM Rotation — demonstrate transparent key rotation across the two auth flows.
POST /api/v3/auth/token— full details on all four supported OAuth 2.0 grants.POST /api/v3/admin/tenants/{tenantId}/keys— create-key endpoint reference.POST /api/v3/crypto/encrypt— runtime encrypt endpoint reference.
Updated 10 days ago
Did this page help you?