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

# KMS

> Customer-managed encryption keys: envelope encryption, signing, and the deletion window that makes a key recoverable.

KMS holds encryption keys that belong to your account. You create a key, and
from then on the platform will encrypt, decrypt, sign and verify **with** it —
but never hand it to you. There is no endpoint that exports the key itself.

The service is **regional** — `kms.sa-saopaulo-1.basaltic.sh`. A key exists in
one region and can only be used from there, so a ciphertext produced in one
region cannot be opened in another.

<CardGroup cols={2}>
  <Card title="Create a key" icon="key-round" href="#creating-a-key">
    Specs, usages, and the one choice you cannot change afterwards.
  </Card>

  <Card title="Envelope encryption" icon="package" href="#envelope-encryption">
    What a data key is, and why you should almost never call encrypt
    directly.
  </Card>

  <Card title="Sign and verify" icon="pen-tool" href="#signing">
    Algorithms per spec, and why you must not pre-hash.
  </Card>

  <Card title="Disable and delete" icon="trash-2" href="#turning-a-key-off">
    The recovery window, what cancelling gives you back, and what it does
    not.
  </Card>
</CardGroup>

## Creating a key

<Tabs>
  <Tab title="Console">
    Go to **Encryption Keys** and choose **Create Key**. Under **Key details**
    give it a **Name** and an optional **Description**; under **Cryptography**
    pick a **Key spec** and a **Key usage**.

    The spec choices are **AES-256 symmetric (recommended)**, **RSA-2048
    asymmetric**, **RSA-4096 asymmetric** and **ECDSA P-256 asymmetric (sign
    only)**. **Key usage** is locked to whatever the chosen spec supports — an
    RSA spec is the only one that lets you choose between **Encrypt /
    Decrypt** and **Sign / Verify**.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST https://kms.sa-saopaulo-1.basaltic.sh/v1/keys
    {
      "name": "prod-master",
      "key_spec": "aes-256",
      "description": "Master key for the payments data store"
    }
    ```
  </Tab>
</Tabs>

The call is synchronous. The response carries the key already in `enabled`,
ready for crypto operations — there is nothing to poll.

`name` is unique per account and lands in the CRN, so it has to be URL-safe:
`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$`.

### Specs and usages

A key is pinned to one **usage** at creation: either `encrypt_decrypt` or
`sign_verify`. The **spec** decides which usages are even possible.

| `key_spec`   | Can encrypt       | Can sign      | Default usage         |
| ------------ | ----------------- | ------------- | --------------------- |
| `aes-256`    | Yes — AES-GCM     | No            | `encrypt_decrypt`     |
| `rsa-2048`   | Yes — OAEP-SHA256 | Yes           | none, you must choose |
| `rsa-4096`   | Yes — OAEP-SHA256 | Yes           | none, you must choose |
| `ecdsa-p256` | No                | Yes — SHA-256 | `sign_verify`         |

Omit `key_usage` for `aes-256` or `ecdsa-p256` and the only usage the spec
supports is applied. Omit it for an RSA spec and the request is rejected — RSA
can do both, so the service will not guess.

<Warning>
  An RSA key pinned to `encrypt_decrypt` **refuses sign and verify**, and the
  other way round, even though the algorithm could do either. The refusal is
  `400 KMS_INVALID_KEY_USAGE`. Pick the usage deliberately: neither the spec
  nor the usage can be changed later, and `PATCH /v1/keys/{key_id}` only edits
  `name`, `description` and `tags`. To change either one, create a new key.
</Warning>

## Envelope encryption

<Note>
  **Cryptographic operations are API only.** Encrypt, decrypt,
  generate-data-key, sign and verify have no console controls. The console
  creates keys, inspects them and turns them off; the operations that *use* a
  key run from your application, next to the plaintext they act on.
</Note>

`POST /v1/keys/{key_id}/encrypt` sends your plaintext to KMS and gets
ciphertext back. That is fine for something small and rare — a config value, an
API token. It is the wrong shape for anything else, because every byte crosses
the network twice and every operation costs a round trip.

The alternative is a **data key**: KMS mints a fresh random key, hands you two
copies of it, and never stores it.

```bash theme={null}
POST /v1/keys/{key_id}/generate-data-key
{ "number_of_bytes": 32 }
```

```json theme={null}
{
  "plaintext":  "<base64 — the raw key bytes>",
  "ciphertext": "<base64 — the same key, wrapped under your KMS key>"
}
```

You encrypt your data locally with `plaintext`, then throw `plaintext` away and
store `ciphertext` beside the data it protects. To read the data back, send
`ciphertext` to `POST /v1/keys/{key_id}/decrypt` and you have the data key
again.

```mermaid theme={null}
sequenceDiagram
    participant App as Your application
    participant KMS
    participant Store as Your storage
    App->>KMS: generate-data-key
    KMS-->>App: plaintext + ciphertext
    App->>App: encrypt data with plaintext, then discard it
    App->>Store: store ciphertext beside the encrypted data
    Note over App,Store: later, on read
    Store-->>App: encrypted data + ciphertext
    App->>KMS: decrypt(ciphertext)
    KMS-->>App: plaintext data key
    App->>App: decrypt data locally
```

Your bulk data never leaves your process, one KMS call covers a whole batch,
and the KMS key stays a *key-encrypting* key — the only thing it ever wraps is
other keys.

<Warning>
  Never persist the `plaintext` data key. Storing it next to `ciphertext`
  defeats the entire arrangement: anyone who reaches your storage then has
  both the lock and the key, and revoking the KMS key no longer protects
  anything.
</Warning>

`number_of_bytes` accepts **16, 32 or 64** and nothing else — 16 for AES-128,
32 for AES-256 (the default), 64 for HMAC-SHA512. Any other value fails.

### When direct encrypt runs out

<Warning>
  An **RSA key cannot encrypt more than a few hundred bytes.** RSA-OAEP can
  only carry a message smaller than the modulus: with SHA-256 that is
  `k - 2·32 - 2` bytes, so **190 bytes** for `rsa-2048` and **446 bytes** for
  `rsa-4096` ([RFC 8017 §7.1.1](https://datatracker.ietf.org/doc/html/rfc8017#section-7.1.1)).
  There is no chunking behind the API. Past that size the operation fails and a
  data key is the only route.
</Warning>

A symmetric key has no comparable algorithmic ceiling, but the request body
still has to fit in one HTTP call and you still pay a round trip per operation.
Treat direct encrypt as a convenience for small, infrequent values, and reach
for a data key for everything else.

### Encryption context

`aad` is optional additional authenticated data. It is bound into the AES-GCM
tag, so a ciphertext will only open if the same context is presented again —
useful for pinning a blob to the thing it belongs to, so a stolen ciphertext
cannot be replayed against a different record.

```json theme={null}
{ "plaintext": "<base64>", "aad": "<base64 of e.g. tenant=42>" }
```

<Warning>
  The context must match at decrypt **exactly, including its absence**.
  Supplying `aad` to decrypt a ciphertext that was sealed without one is
  refused rather than ignored — a context that is only sometimes checked is
  not a check. Encrypting without `aad` and decrypting with it fails with
  `400 INVALID_INPUT`.
</Warning>

<Warning>
  Only a symmetric key can bind a context. Sending `aad` to encrypt under an
  RSA key is refused with `400 KMS_INVALID_KEY_SPEC` — RSA-OAEP has nowhere to
  carry one, so accepting it would drop the binding while you went on treating
  it as an integrity check. The refusal happens at the seal, where you can
  still pick a different key.
</Warning>

## Signing

```bash theme={null}
POST /v1/keys/{key_id}/sign
{ "message": "<base64 of the raw payload>" }
```

<Warning>
  Pass the **raw message**, not a digest. The service hashes it with SHA-256
  server-side, so a pre-hashed input gets hashed twice and produces a signature
  nothing will verify.
</Warning>

`signing_algorithm` is optional; omitted, the default for the spec is used, and
the algorithm actually applied comes back in the response.

| `key_spec`             | Accepted algorithms                               | Default              |
| ---------------------- | ------------------------------------------------- | -------------------- |
| `rsa-2048`, `rsa-4096` | `RSASSA_PSS_SHA_256`, `RSASSA_PKCS1_V1_5_SHA_256` | `RSASSA_PSS_SHA_256` |
| `ecdsa-p256`           | `ECDSA_SHA_256`                                   | `ECDSA_SHA_256`      |

An algorithm that does not match the spec is rejected with
`400 KMS_UNSUPPORTED_SIGNING_ALGORITHM` rather than being attempted.

Signatures come back in the standard encodings, so nothing downstream needs
special handling: RSA-PSS uses `saltLen = hashLen = 32`, and ECDSA returns the
ASN.1 DER `(r, s)` sequence of ANSI X9.62.

`POST /v1/keys/{key_id}/verify` answers `{"signature_valid": false}` for a
signature that simply does not match. That is a `200`, not an error — an error
means the request or the backend was wrong, not that the signature was.

<Note>
  The API does not expose the public half of an asymmetric key, so verification
  goes through `verify` rather than offline against a published key. Plan on a
  call per check — and note that `verify` is a separate IAM action, so a party
  that should only check signatures never needs `kms:Sign`.
</Note>

## Turning a key off

```mermaid theme={null}
stateDiagram-v2
    [*] --> enabled: create
    enabled --> disabled: disable
    disabled --> enabled: enable
    enabled --> pending_deletion: schedule-deletion
    disabled --> pending_deletion: schedule-deletion
    pending_deletion --> disabled: cancel-deletion
    pending_deletion --> [*]: window elapses
```

<Columns cols={2}>
  <Card title="Disable" icon="pause">
    `POST /v1/keys/{key_id}/disable` refuses every crypto operation with
    `409 KMS_KEY_DISABLED` while leaving the material intact. This is the
    reversible move: stop a suspected-compromised key now, keep the ability to
    read historical ciphertext after re-enabling.
  </Card>

  <Card title="Schedule deletion" icon="clock">
    `POST /v1/keys/{key_id}/schedule-deletion` starts a countdown.
    `pending_window_in_days` is **7 to 30, defaulting to 7**. The key refuses
    crypto operations for the whole window, then the material and the record
    are destroyed.
  </Card>
</Columns>

<Tabs>
  <Tab title="Console">
    Open the key from **Encryption Keys**. **Disable** and **Enable** are in
    the header. **Schedule key deletion** sits in the **Danger zone** on the
    **Settings** tab: it takes a **Pending window (days)** and makes you type
    the key's name back before **Schedule Deletion** is accepted.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/keys/{key_id}/schedule-deletion
    { "pending_window_in_days": 30 }
    ```

    `pending_window_in_days` is optional — omit it and you get the 7-day
    minimum, which is the shortest window, not the safest one.
  </Tab>
</Tabs>

<Warning>
  Deletion destroys the key material. Every ciphertext ever produced under the
  key — including every data key you wrapped with it — becomes permanently
  unreadable. The window exists because that is not undoable afterwards, so
  use it: schedule the deletion, watch for what breaks, and only let it elapse
  when nothing does.
</Warning>

### Cancelling

<Tabs>
  <Tab title="Console">
    A key inside the window shows **Cancel Deletion** in its header, where
    **Disable** or **Enable** would otherwise be.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/keys/{key_id}/cancel-deletion
    ```
  </Tab>
</Tabs>

Cancelling exits the window at any point before `deletion_scheduled_at`.

<Note>
  A cancelled key comes back **`disabled`, not `enabled`**. Nothing starts
  working again until you explicitly call `enable`. The window was entered
  because someone wanted the key gone; recovering it should not silently
  restore traffic to it.
</Note>

Two consequences of how quota is accounted are worth knowing before you rely on
cancelling:

* Scheduling a deletion **releases the key's quota immediately**, so you can
  create a replacement inside the same limit without waiting out the window.
* Cancelling therefore has to take that quota back, and **fails with
  `403 QUOTA_EXCEEDED` if your account is now at its limit**. If you created a
  replacement key, free a slot before you cancel.

<Warning>
  A key in `pending_deletion` no longer reserves its **name**, so a new key can
  be created with the same one right away. Because a KMS CRN is built from the
  name (`crn:kms:<region>:<account>:key/<name>`), the old key and the new key
  then share a CRN, and an IAM policy naming it matches both. Give the
  replacement a different name if that distinction matters to your policies.
</Warning>

## Controlling who may use a key

Every operation checks a distinct IAM action, and every operation on a specific
key is authorized against **that key's CRN** with the key's tags available as
condition context. Only the two collection-level operations are not.

| Action                                              | Scope      | Guards                  |
| --------------------------------------------------- | ---------- | ----------------------- |
| `kms:ListKeys`                                      | collection | Listing keys            |
| `kms:CreateKey`                                     | collection | Creating one            |
| `kms:GetKey`                                        | key CRN    | Reading metadata        |
| `kms:UpdateKey`                                     | key CRN    | Name, description, tags |
| `kms:EnableKey` / `kms:DisableKey`                  | key CRN    | Availability            |
| `kms:ScheduleKeyDeletion` / `kms:CancelKeyDeletion` | key CRN    | The deletion window     |
| `kms:Encrypt`                                       | key CRN    | Sealing data            |
| `kms:Decrypt`                                       | key CRN    | **Opening data**        |
| `kms:GenerateDataKey`                               | key CRN    | **Minting a data key**  |
| `kms:Sign`                                          | key CRN    | Producing signatures    |
| `kms:Verify`                                        | key CRN    | Checking them           |

<Tip>
  `kms:Decrypt` and `kms:GenerateDataKey` are separate actions from
  `kms:GetKey` precisely so the operations that return usable key material can
  be granted narrowly. The same pattern appears on
  [certificates](/certificates#certificate-material), where
  `certificate:GetCertificateMaterial` is split out from reading a certificate.
  A service that only needs to *seal* data should get `kms:Encrypt` and
  `kms:GenerateDataKey` and nothing else — it can then write, but never read.
</Tip>

A write path that can seal but not open:

```json theme={null}
{
  "version": "2024-01-01",
  "statements": [{
    "sid": "IngestSealsOnly",
    "effect": "allow",
    "actions": ["kms:GenerateDataKey", "kms:Encrypt"],
    "resources": ["crn:kms:sa-saopaulo-1:my-account:key/prod-*"]
  }]
}
```

Because tags on the key are available as condition context, a fleet can be
fenced by label rather than by name:

```json theme={null}
{
  "sid": "StagingKeysOnly",
  "effect": "allow",
  "actions": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
  "resources": ["*"],
  "conditions": [
    { "operator": "equals", "key": "basalt:ResourceTag/env", "values": ["staging"] }
  ]
}
```

<Note>
  `kms:ListKeys` is authorized against the collection, not against individual
  keys, so restricting it by CRN or tag has no effect. Scope the operations
  that *use* a key; listing tells a caller a key exists and nothing more.
</Note>

See [writing policies](/iam/policies) for the full document format and every
condition operator.

## Keys other services can use

Some services will encrypt their data under a key of yours instead of a
platform-managed one, which puts the kill switch in your hands: disable the
key and that service stops being able to read what it stored.

<CardGroup cols={2}>
  <Card title="Secrets" icon="lock" href="/secrets#encrypting-under-your-own-key">
    Bind a secret to one of your keys at creation and every version is sealed
    under it.
  </Card>

  <Card title="Telemetry" icon="activity" href="/api-reference/introduction">
    A log group or trace setting takes a `kms_key_crn`; each ingest batch gets
    its own data key wrapped under yours.
  </Card>
</CardGroup>

<Note>
  Not everything encrypted at rest uses a key of yours. A certificate's private
  key, for example, is encrypted under a platform-managed regional key you do
  not control. Where a service supports your own key, it exposes a field for
  it — if there is no such field, there is no such binding.
</Note>

## Errors

| Code                                | Status | Means                                                                                            |
| ----------------------------------- | ------ | ------------------------------------------------------------------------------------------------ |
| `KMS_KEY_NOT_FOUND`                 | 404    | No key with that id **in your account**. A key belonging to another account reads the same way.  |
| `KMS_KEY_NAME_EXISTS`               | 409    | Another key of yours already holds the name.                                                     |
| `KMS_KEY_DISABLED`                  | 409    | Crypto operation on a `disabled` key.                                                            |
| `KMS_KEY_PENDING_DELETION`          | 409    | Crypto, enable, disable or update on a key inside the deletion window. Cancel first.             |
| `KMS_INVALID_KEY_USAGE`             | 400    | Operation from the usage the key is not pinned to, or a spec/usage pair the spec cannot satisfy. |
| `KMS_INVALID_KEY_SPEC`              | 400    | Unknown spec, or an encryption context supplied for an asymmetric key.                           |
| `KMS_UNSUPPORTED_SIGNING_ALGORITHM` | 400    | `signing_algorithm` does not match the key spec.                                                 |
| `KMS_INVALID_DELETION_WINDOW`       | 400    | `pending_window_in_days` outside 7–30.                                                           |

## Next

<CardGroup cols={2}>
  <Card title="Secrets" icon="lock" href="/secrets">
    Versioned application secrets, and how to put one behind your own key.
  </Card>

  <Card title="Writing policies" icon="file-text" href="/iam/policies">
    Scoping `kms:Decrypt` to the keys and tags it should reach.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/introduction">
    Every KMS operation, with request and response schemas.
  </Card>

  <Card title="Authentication" icon="key" href="/authentication">
    Signing requests to a regional endpoint.
  </Card>
</CardGroup>
