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..7ff65ef 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,165 @@ 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: +## Basic usage -1. Create a `LockContext` instance. -2. Identify the user with their JWT. -3. Pass the lock context to encrypt and decrypt operations. +Register your identity provider with the workspace first, on the [OIDC providers](https://dashboard.cipherstash.com/workspaces/_/oidc-providers) page in the Dashboard. The `_` in that URL resolves to whichever workspace you have selected. -## Basic usage +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. -```typescript filename="identity.ts" -import { LockContext } from "@cipherstash/stack/identity" +```typescript filename="client.ts" +import { Encryption, OidcFederationStrategy } from "@cipherstash/stack" +import { users } from "./schema" -// 1. Create a lock context (defaults to the "sub" claim) -const lc = new LockContext() +export const client = await Encryption({ + schemas: [users], + config: { + authStrategy: OidcFederationStrategy.create( + process.env.CS_WORKSPACE_CRN, + () => getUserJwt(), + ), + }, +}) +``` -// 2. Identify the user with their JWT -const identifyResult = await lc.identify(userJwt) +`OidcFederationStrategy` is re-exported from `@cipherstash/stack`, so a separate `@cipherstash/auth` import is not needed. -if (identifyResult.failure) { - throw new Error(identifyResult.failure.message) -} + +`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. + -const lockContext = identifyResult.data +Then bind operations to the user's claim. Every example below uses the `client` above: without its `authStrategy`, `.withLockContext()` has no end-user identity to bind to. -// 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..ff433da 100644 --- a/content/stack/cipherstash/encryption/supabase.mdx +++ b/content/stack/cipherstash/encryption/supabase.mdx @@ -320,15 +320,42 @@ 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 on the [OIDC providers](https://dashboard.cipherstash.com/workspaces/_/oidc-providers) page in the Dashboard first. The `_` in that URL resolves to whichever workspace you have selected. + + +`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. + + +```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. + +Build `eSupabase` from that `encryptionClient`. Without its `authStrategy`, `.withLockContext()` has no end-user identity to bind to. ```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 +364,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 diff --git a/next.config.mjs b/next.config.mjs index 990a7b3..3ffa9e2 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -333,6 +333,16 @@ const config = { destination: "/stack/cipherstash/kms", permanent: false, }, + // === Pre-publish placeholder for the v2 docs restructure === + // The v2 branch will host the Supabase docs at /docs/integrations/supabase, + // but that branch isn't published yet. Until it ships, temporarily (307) + // point the future URL at the current Supabase overview page so external + // links to it resolve. Remove this once v2 owns /integrations/supabase. + { + source: "/integrations/supabase", + destination: "/stack/cipherstash/supabase", + permanent: false, + }, ]; }, async rewrites() {