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 windowimport 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
kidcovers historical ciphertexts. - Signed contracts with legal retention — use a long-lived signing key with a proper archive policy.
Where to go next
- In-Place Key Metadata Update — extend
expiresAtif a batch runs long. - Key Lifecycle Management — the state machine underlying archive.
Updated 10 days ago
Did this page help you?