Integrating via the API
The Key Service gateway is the REST front door to the Tricryption Engine. It exists so that agentic pipelines — services and automations that speak HTTP and JSON — can generate, share, and use keys without linking the native library. If you are building a classic enterprise application in C/C++, use the SDK Reference instead; if you are wiring the engine into an agent, a worker, or any HTTP service, this is your path.
This guide explains the two security models you are integrating against, the error contract you must handle, and then walks the complete worked example — the same sign-in → protect → grant → revoke arc the demo container runs, so you can read it here and watch the demo do exactly this.
Endpoint details live in the API Reference, generated from a committed OpenAPI 3.1 spec you can feed to your own tooling.
Why a gateway (and why these models)
The gateway is small on purpose: 7 endpoints, JSON in and out. What makes it worth understanding is not the surface — it's two deliberate security properties that the REST shape preserves. Lead with those; the mechanics follow.
The auth model — the token never leaves the gateway
The security property first: your client never holds a Tricryption credential or token. It holds an opaque string.
Here is what happens:
POST /loginperforms a real SRP login as the user against the Key Service, issues a per-user Token-Encryption (TE) token, captures the principal's system and principal ids, then closes the login connection and discards the password.- The TE token is stored server-side, in memory, keyed by an opaque random
session id. That session id — and only that id — is returned to the
client as
sessionId. - On every authenticated request the client echoes the id back in the
X-Sessionheader. Per request, the gateway opens a fresh validated-TLS connection, authenticates with the stored token, runs the operation, and closes the connection in afinally. No connection is held at rest — only the token, and the token never leaves the gateway.
X-Session and not Authorization: Bearer?Because there is no bearer token to send. X-Session carries an opaque handle
to a server-held token, not the token itself. Making the credential
un-sendable by construction is the feature — a leaked session id is far less
dangerous than a leaked token, and the token's blast radius never extends past
the gateway.
The key-delivery model — the raw key never crosses the wire in clear
The security property first: when you export a key, the raw key bytes are never transmitted and never become readable in your client.
POST /keys/export wraps the key to a public key you generate per request:
- Your client generates an ephemeral ECDH P-256 keypair and keeps the
private key non-extractable. It sends only the public point
(
clientPubKey). - The gateway performs ECDH (P-256) → HKDF-SHA256 → AES-GCM, wrapping the
key to your public key, and returns the envelope
(
gatewayPubKey,salt,iv,ciphertext,authTag). - Your client derives the same shared secret and unwraps to a non-extractable key. The raw key material never exists as plaintext bytes your code (or anyone on the wire) can read.
The algo and keyLen in the response describe the delivered data key (for
example aes-128-cbc, 16 bytes) — what the unwrapped key is for — not the wrap
envelope, which is always the AES-GCM construction above. One honest caveat:
keyLen is derived from the key, but algo is currently hardcoded to
aes-128-cbc and does not track keyLen. They coincide here only because the
demo creates AES-128 keys exclusively; treat keyLen as authoritative for the
delivered key length.
The error contract — session-expired is not access-denied
This is the part most integrations get wrong. The gateway deliberately distinguishes three failure modes, and a correct client handles each differently:
| You receive | It means | What you do |
|---|---|---|
401 with { "reauth": true } | The server-held token expired/invalid | Re-run POST /login and retry the request |
401 without reauth | Missing or unknown session (X-Session) | Re-login to obtain a fresh session id |
403 | A real authorization denial (RBAC/ACL) | Surface it. Retrying will not help until access is granted |
500 | Some other operation error | Inspect detail; treat as an op failure, not auth |
A 401 { reauth: true } is recoverable by re-authenticating. A 403 is the
system working as designed — the caller genuinely lacks access. Treating a
denial as an expiry (and silently retrying) hides real authorization failures;
treating an expiry as a denial breaks otherwise-valid sessions. The whole point
of a demo that shows real denials is that these stay distinct.
See the API Reference error contract for the
full table, including 400 validation and 502 (gateway-to-kS unreachable).
Worked example — the full arc
The conversation below is the spine of the integration: Alice signs in and protects a field, Bob is denied, Alice grants Bob, Bob succeeds, Alice revokes, Bob is denied again. Tokens and secrets are illustrative/redacted. This is the exact flow the demo container runs.
1. Alice signs in
POST /login
Content-Type: application/json
{ "user": "alice", "password": "********" }
200 OK
{
"sessionId": "<48-hex>",
"user": "alice",
"systemId": 1001,
"principalId": "<16-hex>",
"hasToken": true
}
Alice keeps sessionId and sends it as X-Session from here on.
2. Alice protects a field — generate, then owner export
First generate a key (Alice becomes its owner):
POST /keys
X-Session: <alice-sid>
{}
200 OK
{ "ttag": "<base64>" }
Then export it, wrapped to Alice's ephemeral public key, and unwrap it in-process to encrypt the field:
POST /keys/export
X-Session: <alice-sid>
Content-Type: application/json
{ "ttag": "<base64>", "clientPubKey": "<base64 P-256 point>" }
200 OK
{
"wrapped": {
"gatewayPubKey": "<b64>",
"salt": "<b64>",
"iv": "<b64>",
"ciphertext": "<b64>",
"authTag": "<b64>"
},
"algo": "aes-128-cbc",
"keyLen": 16
}
3. Bob signs in
POST /login
{ "user": "bob", "password": "********" }
200 OK
{ "sessionId": "<bob-sid>", "user": "bob", "systemId": 1002, "principalId": "<16-hex>", "hasToken": true }
4. Bob requests the key — DENIED
Bob is not on the key's ACL, so the export is a real authorization denial:
POST /keys/export
X-Session: <bob-sid>
{ "ttag": "<base64>", "clientPubKey": "<bob b64 point>" }
403 Forbidden
{ "error": "export denied or failed", "detail": "...Authorization Failed..." }
Note this is 403, not 401 — there is nothing to re-authenticate. Bob's
session is fine; Bob simply lacks access.
5. Alice grants Bob
POST /keys/grant
X-Session: <alice-sid>
{ "ttag": "<base64>", "grantee": "bob" }
200 OK
{ "ok": true, "grantee": { "user": "bob", "systemId": 1002, "principalId": "<16-hex>" } }
6. Bob requests again — GRANTED
POST /keys/export
X-Session: <bob-sid>
{ "ttag": "<base64>", "clientPubKey": "<bob b64 point>" }
200 OK
{ "wrapped": { "...": "..." }, "algo": "aes-128-cbc", "keyLen": 16 }
Bob unwraps to a non-extractable key and decrypts the field — round-tripping the oracle value.
7. Alice revokes Bob
POST /keys/revoke
X-Session: <alice-sid>
{ "ttag": "<base64>", "grantee": "bob" }
200 OK
{ "ok": true }
8. Bob requests again — DENIED
POST /keys/export
X-Session: <bob-sid>
{ "ttag": "<base64>", "clientPubKey": "<bob b64 point>" }
403 Forbidden
{ "error": "export denied or failed", "detail": "..." }
Access is gone again — the ACL is authoritative, and the gateway enforces it on every export.
Next steps
- API Reference — every endpoint, request/response schema, and status code.
- Authentication — the
X-Sessionscheme in reference form. openapi.yaml— the machine-readable contract.