CipherStashDocs
Solutions

Provable access control

Bind decryption to an authenticated identity with lock contexts, so access to data is cryptographically demonstrable rather than merely logged.

Traditional access control relies on application logic. If a bug or misconfiguration exposes data, there is no way to prove afterwards whether access was unauthorized. CipherStash binds decryption to an authenticated identity, so access leaves cryptographic evidence rather than only a log entry.

How it works

Two pieces combine.

An auth strategy decides who the client is when it talks to ZeroKMS. OidcFederationStrategy federates a signed-in user's OIDC JWT (from Supabase, Clerk, Auth0, or Okta) into a CipherStash token, so every ZeroKMS request is made as that user rather than as your service.

A lock context binds a value to a claim from that user's JWT, typically sub. ZeroKMS bakes the claim's value into the data key's tag, so only the user who encrypted a value can decrypt it.

The two are layered, not alternatives: lock context requires OidcFederationStrategy, but you can use the strategy without lock context. Authenticating as the user gives you an audit trail; adding lock context gives you the cryptographic binding.

That creates a provable access boundary:

  • If data was decrypted, the user's identity must have been present.
  • ZeroKMS records the identity associated with every key derivation.
  • The record cannot be falsified by tampering with application logs, because the evidence is the derivation itself, not the log line describing it.

A lock context restricts who can decrypt. It does not hide the ciphertext's existence, nor does it prevent a signed-in user from decrypting the values their own claim unlocks. It moves the enforcement point out of your application and into key derivation.

Identity-aware encryption

Add your identity provider to the workspace first, on the OIDC providers page in the Dashboard. The _ in that URL resolves to whichever workspace you have selected.

Authenticate as the end user

Construct the client with OidcFederationStrategy. Pass a function that returns the current provider JWT: the strategy re-invokes it when it needs to re-federate, so do not capture a token once.

import { Encryption, OidcFederationStrategy } from "@cipherstash/stack"
import { patients } from "./schema"

// Every ZeroKMS request is authenticated as the signed-in user
export const client = await Encryption({
  schemas: [patients],
  config: {
    authStrategy: OidcFederationStrategy.create(
      process.env.CS_WORKSPACE_CRN,
      () => getProviderJwt(),
    ),
  },
})

OidcFederationStrategy is re-exported by @cipherstash/stack, so you do not need a separate import from @cipherstash/auth.

OidcFederationStrategy is the only strategy that supports lock contexts. AccessKeyStrategy authenticates as your service rather than as an end user, so there is no user identity for ZeroKMS to resolve the claim against.

Bind encryption to the user's identity

Every example below uses the client from above. Without its authStrategy, .withLockContext() has no end-user identity to bind to.

// `client` is configured with OidcFederationStrategy, as above
import { client } from "./client"
import { patients } from "./schema"

// The data key is tagged with the authenticated provider's `sub` claim
async function encryptDiagnosis(diagnosis: string) {
  const result = await client
    .encrypt(diagnosis, { column: patients.diagnosis, table: patients })
    .withLockContext({ identityClaim: ["sub"] })

  if (result.failure) {
    throw new Error(`Encryption failed: ${result.failure.message}`)
  }

  return result.data
}

Decrypt with the same identity claim

Decryption requires the same lock context, resolved against whichever user the client is currently authenticated as.

// The same `client`, authenticated as the same user
async function decryptDiagnosis(encryptedDiagnosis: unknown) {
  const result = await client
    .decrypt(encryptedDiagnosis)
    .withLockContext({ identityClaim: ["sub"] })

  if (result.failure) {
    // Key derivation fails when the identity does not match
    throw new Error(`Access denied: ${result.failure.message}`)
  }

  return result.data
}

Lock contexts work with every operation: encrypt, decrypt, encryptModel, decryptModel, bulkEncrypt, bulkDecrypt, bulkEncryptModels, and bulkDecryptModels.

lockContext.identify(jwt) is deprecated. Per-operation CTS tokens were removed in protect-ffi 0.25, so identify() no longer affects encryption: code that still calls it compiles and logs a deprecation warning, but the token it fetches is unused. Authenticate the client with OidcFederationStrategy and pass the claim to .withLockContext() instead.

Constructing a LockContext is not deprecated, it is simply optional here. .withLockContext() accepts either a LockContext or a plain { identityClaim }.

Audit logging

Every encryption and decryption through ZeroKMS produces an audit event:

FieldDescription
IdentityThe authenticated user or service
OperationEncrypt or decrypt
TimestampWhen the operation occurred
KeysetWhich keyset was used
ApplicationWhich application performed the operation

Combining with Proxy audit

CipherStash Proxy adds statement-level audit on top: statement fingerprinting to identify unique query patterns against encrypted data, SQL redaction to strip sensitive values from logged queries, primary key injection to track which records were touched, and record reconciliation to map access events back to rows. See Audit logging.

Together these answer who accessed what data, when, and using which query.

Use cases

Healthcare: HIPAA audit requirements

HIPAA requires audit controls recording who accessed protected health information. Authenticating each request as the signed-in provider means the ZeroKMS audit log records the identity, the operation, the keyset, and the timestamp for each derivation, and a lock context ties each patient record to the provider who encrypted it.

Financial services: segregation of duties

Encrypt audit records under a compliance-team member's identity. Someone on the trading desk, authenticated as themselves, resolves a different claim value, so key derivation fails and the records do not decrypt. The separation is enforced at derivation, not by a role check in the application.

Multi-tenant SaaS: tenant isolation

Bind encryption to a per-user claim so only that user's identity can decrypt their data. For coarser isolation that does not depend on a signed-in end user, separate keysets usually fit better: a keyset boundary is per-client or per-connection, where a lock context is per-identity.

Compared to application-level access control

AspectApplication logicLock contexts
EnforcementA software check, which can be bypassedKey derivation, which cannot be skipped
Audit trailApplication logs, which can be editedZeroKMS derivation records
Proof of accessCircumstantial, from log entriesDirect: decryption required the identity
Blast radius of a bugData exposed when the check is bypassedData stays encrypted when application logic fails

Next steps

On this page