> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usesink.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Encryption model

> How Sink stores a secret, and what it can and cannot read.

## Envelope encryption

Every secret value is encrypted with **AES-256-GCM** under a data encryption key (DEK) generated for that value alone. The DEK is then itself encrypted with the deployment's master key before anything is written.

```text theme={null}
value  ──AES-256-GCM(DEK)──→  ciphertext    ┐
                                         ├─→  stored
DEK    ──AES-256-GCM(master key)──→  wrapped DEK  ┘
```

What lands in the database is the ciphertext, its nonce, the wrapped DEK and *its* nonce. Neither the plaintext value nor the unwrapped DEK is ever stored. Reading a secret runs the same steps backwards: unwrap the DEK with the master key, then decrypt the ciphertext with it.

Nonces are 12 random bytes, generated fresh for every encryption. GCM authenticates as well as encrypts, so a ciphertext that has been tampered with fails to decrypt rather than returning something wrong.

### Why per-secret keys

A single key over every value in the database would make one compromise total, and would make rotation an all-or-nothing rewrite. Per-value DEKs keep the blast radius of any one key at one value, and mean the master key is only ever used on 32-byte payloads.

### The master key

The master key is deployment configuration, held in the environment and never in the database. On the hosted service it is managed by Sink; a self-hosted deployment supplies its own. Losing it means losing every value in the deployment — the readiness probe treats an unreadable master key as a **critical** failure and takes the node out of rotation rather than serving requests that cannot decrypt.

<Note>
  This is at-rest encryption with a server-held key, not end-to-end encryption. A Sink deployment can decrypt the values it holds — that is what makes `sink pull`, the dashboard and share links work. If your threat model requires that the operator cannot read your values, Sink is not the right tool.
</Note>

### When a value cannot be decrypted

Listing an environment decrypts each secret independently. If some values fail — a rotated master key, a corrupted row — the call answers **`207 Multi-Status`** with the readable records under `detail.succeeded` and the ids that failed under `detail.failed_ids`, instead of failing the whole read. The CLI prints a warning naming how many were skipped and carries on with the rest.

## Credentials

### API keys

Keys are `sk-` followed by random hex, minted by `POST /keys/`. Only the **SHA-256 digest** is stored, so the token is shown exactly once, at creation, and a lookup miss is indistinguishable from a bad key. Each key records a `last_used` timestamp, can carry an expiry, and can be revoked without touching any other credential.

### Passwords

Passwords are hashed with **bcrypt** and a per-password salt. Hashing runs on a worker thread rather than the event loop, so one login's cost is paid by the caller who provoked it and not by every request in flight.

### Sessions

Browser login issues a short-lived access token and an HttpOnly, `Secure`, `SameSite=Lax` refresh cookie. Sessions are one per device: signing in again from the same user agent revokes that device's previous session, so the refresh token it held stops working. Only the digest of a refresh token is stored, and `GET /sessions/` lists live devices without ever returning it.

## Defences at the edge

<CardGroup cols={2}>
  <Card title="CSRF" icon="shield">
    An origin check stops a page on another origin spending the session cookie in a browser. Credentialed cross-origin calls are limited to an explicit allowlist.
  </Card>

  <Card title="Rate limiting" icon="gauge-high">
    A global backstop on every request, plus tighter budgets on login, OTP, share redemption and billing. Buckets key on API key, then session, then IP.
  </Card>

  <Card title="Interactive docs off" icon="eye-slash">
    `/docs`, `/redoc` and `/openapi.json` are served only when the deployment runs with `DEBUG` enabled — so not on the hosted production instance.
  </Card>

  <Card title="Local file permissions" icon="file-shield">
    Files written by `sink pull`, and the CLI's own config file, are created `0600`. Values print masked unless you ask for them.
  </Card>
</CardGroup>

<Warning>
  Rate limits **fail open**. If the counter store is unreachable the request proceeds and the failure is logged — a secrets manager that deployments read from at boot should lose abuse protection during an outage, not lock every caller out of their own configuration.
</Warning>

## What is written down

|                                        | Stored      | Form                                       |
| -------------------------------------- | ----------- | ------------------------------------------ |
| Secret value                           | Yes         | AES-256-GCM ciphertext + wrapped DEK       |
| Master key                             | No          | Deployment environment only                |
| API key                                | Yes         | SHA-256 digest                             |
| Password                               | Yes         | bcrypt hash                                |
| Refresh token                          | Yes         | SHA-256 digest                             |
| Provider token (Railway/Vercel/Render) | **No**      | Held for one request, then discarded       |
| Share link                             | Temporarily | Expiring entry keyed by the token's digest |

For what Sink retains and for how long, see the [Privacy Policy](https://usesink.co/legal/privacy) and the list of [subprocessors](https://usesink.co/legal/subprocessors).
