In-Memory JWE / JWS Quick-Start (throwaway keys)

Short-lived provisional key pattern — create, use for a burst, and let it archive itself when the session ends. Useful for ephemeral tokens.

⚡ In-Memory JWE / JWS Quick-Start (throwaway keys)

Scenario: a lot of workloads need a short-lived key for a burst of operations — signing every event in a job run, encrypting a batch of items in a pipeline, an ephemeral session key. Rather than long-lived provisioning, create a key with a short expiresAt and let it archive itself when the window closes.

This is not a separate API — it's a pattern on top of POST /admin/…/keys with the expiresAt field set to minutes-to-hours in the future.

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 KID="ephemeral-$(date +%s)"
# ADMIN_TOKEN + APP_TOKEN as in AES-256 recipe

# Step 1: admin creates a key that expires in 1 hour
EXPIRES_AT=$(date -u -d '+1 hour' +%Y-%m-%dT%H:%M:%SZ)
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\"],
    \"expiresAt\":     \"$EXPIRES_AT\",
    \"maxUsageLimit\": 10000,
    \"metadata\":      { \"pattern\": \"in-memory\", \"job\": \"batch-2026-09-10\" }
  }" | jq

# Step 2: burst encrypts (the whole batch of items)
for i in 1 2 3 4 5; do
  DATA_B64=$(printf "Item %d" $i | 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\" }" \
    | jq -r '"item \(.materialVersion): algorithm=\(.algorithmUsed)"'
done

# Step 3: at the end of the batch — either let it expire or explicitly archive
curl -s -X POST "$BASE_URL/api/v3/admin/tenants/$TENANT_ID/keys/$KID/revoke" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq .status
curl -s -X POST "$BASE_URL/api/v3/admin/tenants/$TENANT_ID/keys/$KID/archive" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq .status
# → ARCHIVED — read-only, decryption still works for the retention window
import base64, requests, time
from datetime import datetime, timedelta, timezone

BASE = "https://staging.ankatech.co"
TENANT_ID = "00000000-0000-0000-0000-000000000003"
KID = f"ephemeral-{int(time.time())}"

# Create with 1-hour expiry
expires = (datetime.now(timezone.utc) + timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ")
requests.post(f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys", headers=admin, json={
    "kid": KID, "kty": "oct", "algorithm": "A256GCM",
    "purpose": "ENCRYPT_DECRYPT", "keyOps": ["encrypt","decrypt"],
    "expiresAt":     expires,
    "maxUsageLimit": 10000,
    "metadata":      {"pattern": "in-memory", "job": f"batch-{time.strftime('%Y-%m-%d')}"},
}).raise_for_status()

# Burst encrypt
for i in range(5):
    r = requests.post(f"{BASE}/api/v3/crypto/encrypt", headers=app,
                      json={"kid": KID, "data": base64.b64encode(f"Item {i}".encode()).decode()}).json()
    print(f"item {i}: {r['algorithmUsed']}")

# End-of-batch cleanup — revoke + archive
requests.post(f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys/{KID}/revoke",  headers=admin).raise_for_status()
requests.post(f"{BASE}/api/v3/admin/tenants/{TENANT_ID}/keys/{KID}/archive", headers=admin).raise_for_status()
print(f"Key {KID} archived at end of batch")
import java.net.URI;
import java.net.http.*;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import com.fasterxml.jackson.databind.*;

public class InMemoryQuickstart {
    static final String BASE_URL  = "https://staging.ankatech.co";
    static final String TENANT_ID = "00000000-0000-0000-0000-000000000003";
    static final String KID       = "ephemeral-" + System.currentTimeMillis();

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

    public static void main(String[] args) throws Exception {
        String adminAuth = "Bearer " + System.getenv("ADMIN_TOKEN");
        String appAuth   = "Bearer " + System.getenv("APP_TOKEN");

        String expiresAt = DateTimeFormatter.ISO_INSTANT.format(
            Instant.now().plusSeconds(3600));

        // Create with 1-hour expiry
        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": "oct", "algorithm": "A256GCM",
                  "purpose": "ENCRYPT_DECRYPT", "keyOps": ["encrypt","decrypt"],
                  "expiresAt": "%s", "maxUsageLimit": 10000,
                  "metadata": { "pattern": "in-memory", "job": "batch-%s" } }""",
                KID, expiresAt, LocalDate.now()))).build(),
            HttpResponse.BodyHandlers.ofString());

        // Burst encrypt (5 items)
        for (int i = 0; i < 5; i++) {
            var dataB64 = Base64.getEncoder().encodeToString(("Item " + i).getBytes());
            var r = 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());
            System.out.println("item " + i + ": "
                + json.readTree(r.body()).get("algorithmUsed").asText());
        }

        // End-of-batch: revoke + archive
        http.send(HttpRequest.newBuilder(URI.create(
            BASE_URL + "/api/v3/admin/tenants/" + TENANT_ID + "/keys/" + KID + "/revoke"))
            .header("Authorization", adminAuth)
            .POST(HttpRequest.BodyPublishers.noBody()).build(),
            HttpResponse.BodyHandlers.ofString());
        http.send(HttpRequest.newBuilder(URI.create(
            BASE_URL + "/api/v3/admin/tenants/" + TENANT_ID + "/keys/" + KID + "/archive"))
            .header("Authorization", adminAuth)
            .POST(HttpRequest.BodyPublishers.noBody()).build(),
            HttpResponse.BodyHandlers.ofString());
        System.out.println("Key " + KID + " archived at end of batch");
    }
}
const BASE_URL = "https://staging.ankatech.co";
const TENANT_ID = "00000000-0000-0000-0000-000000000003";
const KID = `ephemeral-${Date.now()}`;

// Create with 1-hour expiry
const expires = new Date(Date.now() + 3600_000).toISOString().replace(/\.\d+Z$/, "Z");
await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys`, {
  method: "POST", headers: jsonH(admin),
  body: JSON.stringify({
    kid: KID, kty: "oct", algorithm: "A256GCM",
    purpose: "ENCRYPT_DECRYPT", keyOps: ["encrypt","decrypt"],
    expiresAt: expires, maxUsageLimit: 10000,
    metadata: { pattern: "in-memory", job: `batch-${new Date().toISOString().slice(0,10)}` },
  }),
});

// Burst encrypt
for (let i = 0; i < 5; i++) {
  const r = await (await fetch(`${BASE_URL}/api/v3/crypto/encrypt`, {
    method: "POST", headers: jsonH(app),
    body: JSON.stringify({ kid: KID, data: Buffer.from(`Item ${i}`).toString("base64") }),
  })).json();
  console.log(`item ${i}: ${r.algorithmUsed}`);
}

// End-of-batch cleanup
await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys/${KID}/revoke`,  { method: "POST", headers: admin });
await fetch(`${BASE_URL}/api/v3/admin/tenants/${TENANT_ID}/keys/${KID}/archive`, { method: "POST", headers: admin });
console.log(`Key ${KID} archived at end of batch`);
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class InMemoryQuickstart
{
    const string BASE_URL  = "https://staging.ankatech.co";
    const string TENANT_ID = "00000000-0000-0000-0000-000000000003";
    static readonly string KID = $"ephemeral-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";

    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")}");

        var expiresAt = DateTime.UtcNow.AddHours(1).ToString("yyyy-MM-ddTHH:mm:ssZ");

        // Create with 1-hour expiry
        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" },
                expiresAt, maxUsageLimit = 10000,
                metadata = new { pattern = "in-memory", job = $"batch-{DateTime.UtcNow:yyyy-MM-dd}" },
            }), Encoding.UTF8, "application/json"));

        // Burst encrypt
        for (int i = 0; i < 5; i++) {
            var dataB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes($"Item {i}"));
            var r = 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 r.Content.ReadAsStringAsync()).RootElement;
            Console.WriteLine($"item {i}: {enc.GetProperty("algorithmUsed").GetString()}");
        }

        // End-of-batch cleanup
        await adminHttp.PostAsync($"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys/{KID}/revoke",
                                  new StringContent(""));
        await adminHttp.PostAsync($"{BASE_URL}/api/v3/admin/tenants/{TENANT_ID}/keys/{KID}/archive",
                                  new StringContent(""));
        Console.WriteLine($"Key {KID} archived at end of batch");
    }
}

When to use this pattern

  • Batch jobs — one key per run, archived at the end.
  • Session keys — one per user session, expires with the session.
  • Per-tenant per-day keys — daily rotation without adding a rotation cron.
  • Data-in-transit encryption where the ciphertext lifetime is short (log shipping, event streaming).

When NOT to use it

  • Long-lived data-at-rest — use rotation instead so the same kid covers historical ciphertexts.
  • Signed contracts with legal retention — use a long-lived signing key with a proper archive policy.

Where to go next


Did this page help you?