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:

  • password grant → issues a HUMAN token (audience=ankasecure-admin) used for key provisioning (/api/v3/admin/*).
  • client_credentials grant → 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:

PlaceholderWhat it isUsed for
$ADMIN_EMAILAdmin user emailProvisioning the key
$ADMIN_PASSWORDAdmin user passwordProvisioning the key
$TENANT_IDTenant UUIDBoth flows
$CLIENT_IDApplication UUID (workload registered in the same tenant)Runtime encrypt / decrypt
$CLIENT_SECRETApplication secretRuntime encrypt / decrypt
$BASE_URLhttps://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-trip
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-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 A256GCM algorithm (AES-256 in Galois/Counter Mode) was resolved by the tenant policy for kty=oct — you never picked it explicitly.
  • The algorithmUsed field 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 algorithmUsed and materialVersion in the response change. That is crypto agility.

Where to go next


Did this page help you?