Skip to main content

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:

  1. POST /login performs 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.
  2. 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.
  3. On every authenticated request the client echoes the id back in the X-Session header. Per request, the gateway opens a fresh validated-TLS connection, authenticates with the stored token, runs the operation, and closes the connection in a finally. No connection is held at rest — only the token, and the token never leaves the gateway.
info
Why 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:

  1. Your client generates an ephemeral ECDH P-256 keypair and keeps the private key non-extractable. It sends only the public point (clientPubKey).
  2. 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).
  3. 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 receiveIt meansWhat you do
401 with { "reauth": true }The server-held token expired/invalidRe-run POST /login and retry the request
401 without reauthMissing or unknown session (X-Session)Re-login to obtain a fresh session id
403A real authorization denial (RBAC/ACL)Surface it. Retrying will not help until access is granted
500Some other operation errorInspect detail; treat as an op failure, not auth
Do not collapse 401-reauth and 403

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