From 3cb2abd6c2a2eac053fc0e0b3fb43be6d8b23867 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Thu, 9 Jul 2026 15:33:37 +1000 Subject: [PATCH] docs: update identity-aware encryption to the strategy-based API The live docs teach `new LockContext().identify(jwt)`. Per-operation CTS tokens were removed in protect-ffi 0.25, so `identify()` is deprecated: it still compiles and logs a warning, but the token it fetches is no longer used by encryption operations. Readers copying these examples get code that appears to bind an identity and does not. Correct usage authenticates the client as the end user with OidcFederationStrategy (config.authStrategy), then names the claim to bind: const client = await Encryption({ schemas: [users], config: { authStrategy: OidcFederationStrategy.create(crn, () => getJwt()) }, }) await client.encrypt(v, opts).withLockContext({ identityClaim: ["sub"] }) Two pages also used `client.withLockContext({ identityToken })`, a call shape that never existed in any release. Verified against the shipped type definitions of @cipherstash/stack 0.19.0 and @cipherstash/auth 0.42.0. --- .../encryption/bulk-operations.mdx | 11 +- .../encryption/encrypt-decrypt.mdx | 14 +- .../stack/cipherstash/encryption/identity.mdx | 210 ++++++++---------- .../stack/cipherstash/encryption/models.mdx | 13 +- .../stack/cipherstash/encryption/supabase.mdx | 35 ++- content/stack/reference/agent-skills.mdx | 2 +- .../stack/reference/comparisons/aws-kms.mdx | 16 +- content/stack/reference/error-handling.mdx | 30 +-- .../stack/reference/use-cases/compliance.mdx | 9 +- .../reference/use-cases/provable-access.mdx | 48 ++-- 10 files changed, 193 insertions(+), 195 deletions(-) diff --git a/content/stack/cipherstash/encryption/bulk-operations.mdx b/content/stack/cipherstash/encryption/bulk-operations.mdx index 75112e9..5a6d823 100644 --- a/content/stack/cipherstash/encryption/bulk-operations.mdx +++ b/content/stack/cipherstash/encryption/bulk-operations.mdx @@ -160,21 +160,18 @@ For the table setup and single-record insert pattern, see [Storing encrypted dat ## Identity-aware bulk encryption -Lock an entire batch to a user's identity by chaining `.withLockContext()`: +Lock an entire batch to a user's identity by chaining `.withLockContext()`. The client must be authenticated as that user with `OidcFederationStrategy`. ```typescript filename="bulk-encrypt-identity.ts" -import { LockContext } from "@cipherstash/stack/identity" - -const lc = new LockContext() -const lockContext = (await lc.identify(userJwt)).data! +const claim = { identityClaim: ["sub"] } const encrypted = await client .bulkEncrypt(plaintexts, { column: users.email, table: users }) - .withLockContext(lockContext) + .withLockContext(claim) const decrypted = await client .bulkDecrypt(encrypted.data) - .withLockContext(lockContext) + .withLockContext(claim) ``` See [Identity-aware encryption](/stack/cipherstash/encryption/identity) for the full lock context flow. diff --git a/content/stack/cipherstash/encryption/encrypt-decrypt.mdx b/content/stack/cipherstash/encryption/encrypt-decrypt.mdx index 21d3f1d..cda41d7 100644 --- a/content/stack/cipherstash/encryption/encrypt-decrypt.mdx +++ b/content/stack/cipherstash/encryption/encrypt-decrypt.mdx @@ -165,26 +165,28 @@ const back = await client.bulkDecryptModels(encrypted.data) ## Identity-aware operations -Any encrypt or decrypt operation can be scoped to a specific user with a lock context. +Any encrypt or decrypt operation can be bound to a claim from the signed-in user's JWT, provided the client is authenticated as that user with `OidcFederationStrategy`. See [Identity-aware encryption](/stack/cipherstash/encryption/identity) for details. ```typescript filename="identity-encrypt.ts" +const claim = { identityClaim: ["sub"] } + const encrypted = await client .encryptModel(user, users) - .withLockContext(lockContext) + .withLockContext(claim) const decrypted = await client .decryptModel(encryptedUser) - .withLockContext(lockContext) + .withLockContext(claim) // Also works with bulk operations const bulkEncrypted = await client .bulkEncryptModels(userModels, users) - .withLockContext(lockContext) + .withLockContext(claim) const bulkDecrypted = await client .bulkDecryptModels(encryptedUsers) - .withLockContext(lockContext) + .withLockContext(claim) ``` ## Audit logging @@ -202,6 +204,6 @@ Chain `.audit()` with `.withLockContext()` on the same operation: ```typescript filename="audit-model.ts" const result = await client .encryptModel(user, users) - .withLockContext(lockContext) + .withLockContext({ identityClaim: ["sub"] }) .audit({ metadata: { action: "user-signup", requestId: "abc-123" } }) ``` diff --git a/content/stack/cipherstash/encryption/identity.mdx b/content/stack/cipherstash/encryption/identity.mdx index 553a272..48aa767 100644 --- a/content/stack/cipherstash/encryption/identity.mdx +++ b/content/stack/cipherstash/encryption/identity.mdx @@ -1,9 +1,9 @@ --- title: Identity-aware encryption -description: Use LockContext in @cipherstash/stack to tie encryption to a user JWT so only that identity can decrypt their data, including Clerk and Next.js setup. +description: Authenticate as the end user with OidcFederationStrategy and bind encryption to a JWT claim with withLockContext, so only that identity can decrypt their data. --- -Lock encryption to a specific user by requiring a valid JWT for decryption. When a value is encrypted with a lock context, it can only be decrypted by presenting the same user's identity token. +Lock encryption to a specific user, so a value can only be decrypted while the client is authenticated as the identity that encrypted it. ## How it works @@ -11,185 +11,161 @@ Lock encryption to a specific user by requiring a valid JWT for decryption. When Lock contexts require a Business or Enterprise workspace plan. +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** names which claim from that user's JWT to bind the value to, typically `sub`. ZeroKMS resolves the claim's value from the token authenticating the request and bakes it into the data key's tag. + +Lock context is layered on top of the strategy: it requires `OidcFederationStrategy`, but the strategy does not require lock context. Authenticating as the user gives you a per-user audit trail; adding lock context gives you the cryptographic binding. + Lock contexts are useful for: - Multi-tenant applications where each user's data must be isolated - Compliance requirements that demand per-user encryption boundaries - Applications where you need to prove that only authorized users accessed specific records -The flow is: - -1. Create a `LockContext` instance. -2. Identify the user with their JWT. -3. Pass the lock context to encrypt and decrypt operations. - ## Basic usage -```typescript filename="identity.ts" -import { LockContext } from "@cipherstash/stack/identity" +Register your identity provider with the workspace first, at [dashboard.cipherstash.com](https://dashboard.cipherstash.com/workspaces/_/oidc-providers). -// 1. Create a lock context (defaults to the "sub" claim) -const lc = new LockContext() +Construct the client with `OidcFederationStrategy`. Pass a function returning the **current** provider JWT: the strategy calls it again whenever it needs to re-federate, so do not capture a token once. -// 2. Identify the user with their JWT -const identifyResult = await lc.identify(userJwt) +```typescript filename="client.ts" +import { Encryption, OidcFederationStrategy } from "@cipherstash/stack" +import { users } from "./schema" -if (identifyResult.failure) { - throw new Error(identifyResult.failure.message) -} +export const client = await Encryption({ + schemas: [users], + config: { + authStrategy: OidcFederationStrategy.create( + process.env.CS_WORKSPACE_CRN, + () => getUserJwt(), + ), + }, +}) +``` + +`OidcFederationStrategy` and `AccessKeyStrategy` are re-exported from `@cipherstash/stack`, so a separate `@cipherstash/auth` import is not needed. -const lockContext = identifyResult.data +Then bind operations to the user's claim: -// 3. Encrypt with lock context +```typescript filename="identity.ts" +// Encrypt, binding the data key to the user's `sub` claim const encrypted = await client .encrypt("sensitive data", { column: users.email, table: users }) - .withLockContext(lockContext) + .withLockContext({ identityClaim: ["sub"] }) -// 4. Decrypt with the same lock context +// Decrypt with the same claim, as the same user const decrypted = await client .decrypt(encrypted.data) - .withLockContext(lockContext) + .withLockContext({ identityClaim: ["sub"] }) ``` + +`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 }`. + + ## Supported operations Lock contexts work with all encrypt and decrypt operations: ```typescript filename="identity.ts" -// Single operations -const encrypted = await client - .encryptModel(user, users) - .withLockContext(lockContext) +const claim = { identityClaim: ["sub"] } -const decrypted = await client - .decryptModel(encryptedUser) - .withLockContext(lockContext) +// Single operations +const encrypted = await client.encryptModel(user, users).withLockContext(claim) +const decrypted = await client.decryptModel(encryptedUser).withLockContext(claim) // Bulk operations const bulkEncrypted = await client .bulkEncryptModels(userModels, users) - .withLockContext(lockContext) + .withLockContext(claim) const bulkDecrypted = await client .bulkDecryptModels(encryptedUsers) - .withLockContext(lockContext) + .withLockContext(claim) // Query operations const term = await client - .encryptQuery("user@example.com", { - column: users.email, - table: users, - }) - .withLockContext(lockContext) + .encryptQuery("user@example.com", { column: users.email, table: users }) + .withLockContext(claim) ``` ## Custom identity claims -Override the default context by specifying which identity claims to use: - -```typescript filename="identity.ts" -const lc = new LockContext({ - context: { - identityClaim: ["sub"], // this is the default - }, -}) -``` +`identityClaim` selects which claim (or claims) of the user's JWT ZeroKMS binds to. | Identity claim | Description | |---|---| | `sub` | The user's subject identifier | -| `scopes` | The user's scopes set by your IDP policy | +| `scopes` | The user's scopes, set by your IdP policy | -Combine claims for identity and permissions scoping: +Combine claims to scope by identity and permissions together: ```typescript filename="identity.ts" -const lc = new LockContext({ - context: { - identityClaim: ["sub", "scopes"], - }, -}) -``` - -## Using with Clerk and Next.js - -Install the `@cipherstash/nextjs` package for automatic CTS token setup with [Clerk](https://clerk.com/): - -```bash cta cta-type="install" example-id="install-nextjs-identity" -npm install @cipherstash/nextjs -``` - -### Set up middleware - -In your `middleware.ts`, use `protectClerkMiddleware` to automatically generate CTS tokens for every user session: - -```typescript filename="middleware.ts" -import { clerkMiddleware } from "@clerk/nextjs/server" -import { protectClerkMiddleware } from "@cipherstash/nextjs/clerk" - -export default clerkMiddleware(async (auth, req) => { - return protectClerkMiddleware(auth, req) -}) +await client + .encrypt("sensitive data", { column: users.email, table: users }) + .withLockContext({ identityClaim: ["sub", "scopes"] }) ``` -### Retrieve the CTS token - -Use `getCtsToken` to get the CTS token for the current user: - -```typescript filename="page.tsx" -import { getCtsToken } from "@cipherstash/nextjs" - -export default async function Page() { - const ctsToken = await getCtsToken() +The same claim must be supplied to decrypt. A value encrypted under `["sub"]` will not decrypt under `["sub", "scopes"]`. - if (!ctsToken.success) { - // handle error - } +## Using with Clerk and Next.js - // ctsToken is ready to use +Clerk is an OIDC provider like any other: hand `OidcFederationStrategy` a function that returns the current Clerk session token. + +```typescript filename="client.ts" +import { auth } from "@clerk/nextjs/server" +import { Encryption, OidcFederationStrategy } from "@cipherstash/stack" +import { users } from "./schema" + +export async function getClient() { + return Encryption({ + schemas: [users], + config: { + authStrategy: OidcFederationStrategy.create( + process.env.CS_WORKSPACE_CRN, + async () => { + const { getToken } = await auth() + return await getToken() + }, + ), + }, + }) } ``` -### Create a LockContext with an existing CTS token - -Since the CTS token is already available from the middleware, construct the `LockContext` directly. The CTS token has the shape `{ accessToken: string, expiry: number }`. Passing it directly avoids a second round-trip to CTS. - -```typescript filename="page.tsx" -import { LockContext } from "@cipherstash/stack/identity" -import { getCtsToken } from "@cipherstash/nextjs" - -export default async function Page() { - const ctsToken = await getCtsToken() - - if (!ctsToken.success) { - // handle error - } - - const lockContext = new LockContext({ ctsToken }) +Because the callback is re-invoked on every re-federation, it picks up a refreshed Clerk token automatically. - // Use lockContext with encrypt/decrypt operations -} -``` - -`getCtsToken` returns `{ success: true, ctsToken: CtsToken }` on success or `{ success: false, error: string }` on failure. + +The `@cipherstash/nextjs` package (`protectClerkMiddleware`, `getCtsToken`) belongs to the earlier CTS-token flow, in which a token was fetched per request and handed to `new LockContext({ ctsToken })`. Encryption operations no longer consume a CTS token, so that middleware is not required for identity-aware encryption. + ## Error handling -The `identify` method returns a `Result` type: +Encryption operations return a `Result`. ```typescript filename="identity.ts" -const result = await lc.identify(userJwt) +const result = await client + .encrypt("sensitive data", { column: users.email, table: users }) + .withLockContext({ identityClaim: ["sub"] }) if (result.failure) { - // result.failure.type is 'CtsTokenError' - console.error("CTS token exchange failed:", result.failure.message) + console.error("Encryption failed:", result.failure.message) } ``` Common failure scenarios: -| Scenario | Error type | Description | -|---|---|---| -| Invalid JWT | `CtsTokenError` | The JWT token was rejected by CTS | -| Network failure | `CtsTokenError` | Could not reach the CTS endpoint | -| Missing workspace | Runtime error | `CS_WORKSPACE_CRN` is not configured | -| Expired CTS token | `LockContextError` | The CTS token has expired. Call `identify` again | +| Scenario | Description | +|---|---| +| Invalid or expired provider JWT | Federation is rejected. The callback should return a live token, not one captured earlier | +| Provider not registered | The OIDC provider has not been added to the workspace | +| Network failure | The CipherStash API could not be reached | +| Missing workspace | `CS_WORKSPACE_CRN` is not configured | +| Claim mismatch on decrypt | The value was encrypted under a different `identityClaim`, or as a different user | + +See [Error handling](/stack/reference/error-handling) for the full set of error types. diff --git a/content/stack/cipherstash/encryption/models.mdx b/content/stack/cipherstash/encryption/models.mdx index a3f59b3..54d30f7 100644 --- a/content/stack/cipherstash/encryption/models.mdx +++ b/content/stack/cipherstash/encryption/models.mdx @@ -216,27 +216,24 @@ See [Error handling](/stack/reference/error-handling) for the full set of error ## Identity-aware model operations -Chain `.withLockContext()` to bind encryption to a user's JWT: +Chain `.withLockContext()` to bind encryption to a claim from the signed-in user's JWT. The client must be authenticated as that user with `OidcFederationStrategy`. ```typescript filename="model-with-identity.ts" -import { LockContext } from "@cipherstash/stack/identity" - -const lc = new LockContext() -const lockContext = (await lc.identify(userJwt)).data! +const claim = { identityClaim: ["sub"] } // Single record const encrypted = await client .encryptModel(user, users) - .withLockContext(lockContext) + .withLockContext(claim) // Bulk records — one ZeroKMS call, all locked to the same identity const bulkEncrypted = await client .bulkEncryptModels(records, users) - .withLockContext(lockContext) + .withLockContext(claim) const bulkDecrypted = await client .bulkDecryptModels(encryptedRecords) - .withLockContext(lockContext) + .withLockContext(claim) ``` See [Identity-aware encryption](/stack/cipherstash/encryption/identity) for the full flow. diff --git a/content/stack/cipherstash/encryption/supabase.mdx b/content/stack/cipherstash/encryption/supabase.mdx index be8851a..50cfb50 100644 --- a/content/stack/cipherstash/encryption/supabase.mdx +++ b/content/stack/cipherstash/encryption/supabase.mdx @@ -320,15 +320,36 @@ The following transform methods are also supported: ## Lock context and audit -Chain `.withLockContext()` to tie encryption to a specific user's JWT: +Identity-aware encryption has two parts. Authenticate the encryption client as the signed-in user with `OidcFederationStrategy`, then chain `.withLockContext()` to bind values to a claim from that user's JWT. + +Register your Supabase project as an OIDC provider in the [Dashboard](https://dashboard.cipherstash.com/workspaces/_/oidc-providers) first. + +```typescript filename="lib/encryption.ts" +import { Encryption, OidcFederationStrategy } from "@cipherstash/stack" +import { users } from "./schema" + +// Every ZeroKMS request is authenticated as the signed-in Supabase user +export const encryptionClient = await Encryption({ + schemas: [users], + config: { + authStrategy: OidcFederationStrategy.create( + process.env.CS_WORKSPACE_CRN, + async () => { + const { data } = await supabase.auth.getSession() + return data.session?.access_token + }, + ), + }, +}) +``` + +Pass a function that returns the **current** access token. The strategy calls it again whenever it re-federates, so a token captured once will go stale. ```typescript filename="app/actions.ts" import { LockContext } from "@cipherstash/stack/identity" -const lc = new LockContext() -const identified = await lc.identify(userJwt) -if (identified.failure) throw new Error(identified.failure.message) -const lockContext = identified.data +// Bind the data key to the user's `sub` claim +const lockContext = new LockContext({ context: { identityClaim: ["sub"] } }) const { data } = await eSupabase .from("users", users) @@ -337,6 +358,10 @@ const { data } = await eSupabase .select("id") ``` + +`lockContext.identify(jwt)` is **deprecated**. Per-operation CTS tokens were removed in `protect-ffi` 0.25, so calling it no longer affects encryption. Authenticate the client with `OidcFederationStrategy` instead. + + Lock contexts work with all operations (insert, select, update, delete): ```typescript filename="app/queries.ts" diff --git a/content/stack/reference/agent-skills.mdx b/content/stack/reference/agent-skills.mdx index dbafbdd..80aac6c 100644 --- a/content/stack/reference/agent-skills.mdx +++ b/content/stack/reference/agent-skills.mdx @@ -39,7 +39,7 @@ Core field-level encryption with `@cipherstash/stack`. This is the foundational - Single and bulk encrypt/decrypt operations - Model operations (`encryptModel`, `decryptModel`, `bulkEncryptModels`, `bulkDecryptModels`) - Searchable encryption (equality, free-text search, range queries, encrypted JSONB) -- Identity-aware encryption with `LockContext` and JWT-based access control +- Identity-aware encryption with `OidcFederationStrategy` and `.withLockContext()` - Multi-tenant isolation with keysets - Error handling with the `Result` pattern - Migration from `@cipherstash/protect` diff --git a/content/stack/reference/comparisons/aws-kms.mdx b/content/stack/reference/comparisons/aws-kms.mdx index 24a2e35..2fabe0f 100644 --- a/content/stack/reference/comparisons/aws-kms.mdx +++ b/content/stack/reference/comparisons/aws-kms.mdx @@ -127,18 +127,24 @@ const searchTerms = await client.encryptQuery('secret', { **CipherStash Encryption** has built-in identity-aware encryption: ```typescript -import { LockContext } from '@cipherstash/stack/identity'; +import { Encryption, OidcFederationStrategy } from '@cipherstash/stack'; -const lc = new LockContext(); -const lockContext = await lc.identify(userJwt); +// Authenticate every ZeroKMS request as the signed-in user +const client = await Encryption({ + schemas: [users], + config: { + authStrategy: OidcFederationStrategy.create(workspaceCrn, () => getUserJwt()), + }, +}); +// Bind the data key to the user's `sub` claim const encryptResult = await client.encrypt( 'secret@squirrel.example', { column: users.email, table: users } -).withLockContext(lockContext); +).withLockContext({ identityClaim: ['sub'] }); const decryptResult = await client.decrypt(ciphertext) - .withLockContext(lockContext); + .withLockContext({ identityClaim: ['sub'] }); ``` ### 3. Bulk operations: Native API vs manual batching diff --git a/content/stack/reference/error-handling.mdx b/content/stack/reference/error-handling.mdx index 949b77e..2f081c6 100644 --- a/content/stack/reference/error-handling.mdx +++ b/content/stack/reference/error-handling.mdx @@ -27,7 +27,7 @@ if (result.failure) { } ``` -This pattern applies to every async method: `encrypt`, `decrypt`, `encryptModel`, `decryptModel`, `bulkEncrypt`, `bulkDecrypt`, `bulkEncryptModels`, `bulkDecryptModels`, `encryptQuery`, and `LockContext.identify()`. The one exception is `Encryption()` initialization, which throws on failure. +This pattern applies to every async method: `encrypt`, `decrypt`, `encryptModel`, `decryptModel`, `bulkEncrypt`, `bulkDecrypt`, `bulkEncryptModels`, `bulkDecryptModels`, and `encryptQuery`. The one exception is `Encryption()` initialization, which throws on failure. ## Error types @@ -119,25 +119,15 @@ if (result.failure) { ## Handling identity errors -When using [identity-aware encryption](/stack/cipherstash/encryption/identity), errors can occur during JWT identification or when using lock contexts. +When using [identity-aware encryption](/stack/cipherstash/encryption/identity), errors can occur while federating the user's JWT or while binding a lock context. Both surface on the operation's `Result`. ```typescript title="identity-error.ts" -import { LockContext } from "@cipherstash/stack/identity" +const result = await client + .encrypt("sensitive data", { column: users.email, table: users }) + .withLockContext({ identityClaim: ["sub"] }) -const lc = new LockContext() -const identifyResult = await lc.identify(userJwt) - -if (identifyResult.failure) { - switch (identifyResult.failure.type) { - case "LockContextError": - // JWT is invalid or missing required claims - console.error("Identity error:", identifyResult.failure.message) - break - case "CtsTokenError": - // Token exchange with CTS failed - console.error("CTS error:", identifyResult.failure.message) - break - } +if (result.failure) { + console.error("Identity-aware encryption failed:", result.failure.message) } ``` @@ -145,9 +135,9 @@ if (identifyResult.failure) { | Symptom | Cause | Fix | |---|---|---| -| `LockContextError` with missing claim | JWT doesn't contain the configured identity claim | Check your JWT contains the `sub` claim (or your custom claims) | -| `CtsTokenError` with connection error | CTS endpoint unreachable | Verify `CS_CTS_ENDPOINT` is set correctly | -| `CtsTokenError` with rejection | JWT is expired or not trusted by CTS | Ensure JWT is fresh and from a [configured identity provider](/stack/cipherstash/kms/cts) | +| Missing claim | The JWT doesn't contain the configured identity claim | Check your JWT contains the `sub` claim (or your custom claims) | +| Federation rejected | The JWT is expired, or the provider is not registered | Return a live token from the `OidcFederationStrategy` callback, and add the provider to your workspace | +| Decryption fails for a valid user | The value was encrypted under a different `identityClaim`, or as a different user | Supply the same claim used at encrypt time | ## Error type constants and utilities diff --git a/content/stack/reference/use-cases/compliance.mdx b/content/stack/reference/use-cases/compliance.mdx index ab403cb..ba582e0 100644 --- a/content/stack/reference/use-cases/compliance.mdx +++ b/content/stack/reference/use-cases/compliance.mdx @@ -55,10 +55,11 @@ Encrypted data with CipherStash supports crypto-shredding: revoke the keyset or Encrypt all personal data fields and use [Lock Contexts](/stack/cipherstash/encryption/identity) to restrict which application components can decrypt specific records: ```typescript filename="minimize-access.ts" -// Only the billing service can decrypt payment data +// The client is authenticated as the signed-in user via OidcFederationStrategy; +// the data key is bound to their `sub` claim const result = await client - .withLockContext({ identityToken: billingServiceJWT }) .decrypt(encryptedPaymentData) + .withLockContext({ identityClaim: ["sub"] }) ``` ### Data portability (Article 20) @@ -72,13 +73,13 @@ Encrypted values are stored as standard JSON objects ([CipherCells](/stack/refer Use identity-aware encryption to bind decryption to authenticated healthcare providers: ```typescript filename="hipaa-access.ts" -// Encrypt patient records with identity binding +// Encrypt patient records, bound to the authenticated provider's identity const encrypted = await client - .withLockContext({ identityToken: providerJWT }) .encrypt(patientRecord.diagnosis, { column: patients.diagnosis, table: patients, }) + .withLockContext({ identityClaim: ["sub"] }) ``` ### Audit controls (§ 164.312(b)) diff --git a/content/stack/reference/use-cases/provable-access.mdx b/content/stack/reference/use-cases/provable-access.mdx index 24ee55f..9b690a6 100644 --- a/content/stack/reference/use-cases/provable-access.mdx +++ b/content/stack/reference/use-cases/provable-access.mdx @@ -19,23 +19,30 @@ This creates a provable access boundary: ### Bind encryption to an identity +Authenticate the client as the signed-in provider, then bind values to a claim from their JWT. + ```typescript filename="identity-encrypt.ts" -import { Encryption } from "@cipherstash/stack" +import { Encryption, OidcFederationStrategy } from "@cipherstash/stack" import { patients } from "./schema" -const client = await Encryption({ schemas: [patients] }) - -// The JWT identifies the healthcare provider performing the action -async function encryptPatientRecord( - record: { diagnosis: string }, - providerJWT: string -) { +// Every ZeroKMS request is made as the signed-in healthcare provider +const client = await Encryption({ + schemas: [patients], + config: { + authStrategy: OidcFederationStrategy.create( + process.env.CS_WORKSPACE_CRN, + () => getProviderJwt(), + ), + }, +}) + +async function encryptPatientRecord(record: { diagnosis: string }) { const result = await client - .withLockContext({ identityToken: providerJWT }) .encrypt(record.diagnosis, { column: patients.diagnosis, table: patients, }) + .withLockContext({ identityClaim: ["sub"] }) if (result.failure) { throw new Error(`Encryption failed: ${result.failure.message}`) @@ -48,13 +55,10 @@ async function encryptPatientRecord( ### Decrypt with identity verification ```typescript filename="identity-decrypt.ts" -async function decryptPatientRecord( - encryptedDiagnosis: unknown, - providerJWT: string -) { +async function decryptPatientRecord(encryptedDiagnosis: unknown) { const result = await client - .withLockContext({ identityToken: providerJWT }) .decrypt(encryptedDiagnosis) + .withLockContext({ identityClaim: ["sub"] }) if (result.failure) { // Decryption fails if the identity doesn't have access @@ -95,10 +99,10 @@ Together, these provide an end-to-end audit trail: **who** accessed **what data* HIPAA requires audit controls that record who accessed Protected Health Information (PHI). With Lock Contexts, every access to patient data is cryptographically tied to the authenticated provider: ```typescript filename="hipaa-audit.ts" -// Each access is provably tied to the authenticated provider +// The client is authenticated as Dr Smith, so each access is provably tied to her const diagnosis = await client - .withLockContext({ identityToken: drSmithJWT }) .decrypt(patient.encryptedDiagnosis) + .withLockContext({ identityClaim: ["sub"] }) // ZeroKMS audit log shows: // - Identity: dr.smith@hospital.example (from JWT) @@ -112,13 +116,13 @@ const diagnosis = await client Ensure that only authorized roles can access specific data categories: ```typescript filename="segregation.ts" -// Compliance team can decrypt audit records +// A client authenticated as a compliance-team member can decrypt audit records const auditData = await client - .withLockContext({ identityToken: complianceTeamJWT }) .decrypt(encryptedAuditRecord) + .withLockContext({ identityClaim: ["sub"] }) -// Trading team uses a different Lock Context — cannot decrypt audit records -// The key derivation will fail because the identity doesn't match +// Someone on the trading desk, authenticated as themselves, resolves a different +// claim value, so key derivation fails and the records do not decrypt ``` ### Multi-tenant SaaS: Tenant isolation @@ -126,13 +130,13 @@ const auditData = await client Bind encryption to tenant identity to provide cryptographic tenant isolation: ```typescript filename="tenant-isolation.ts" -// Encrypt data with tenant-scoped identity +// Encrypt data bound to the signed-in tenant user's identity const encrypted = await client - .withLockContext({ identityToken: tenantAJWT }) .encrypt(sensitiveData, { column: tenants.data, table: tenants, }) + .withLockContext({ identityClaim: ["sub"] }) // Only Tenant A's identity can decrypt this data // Tenant B's JWT will fail key derivation