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

# Keys and the keystore

> Your bot's identity is published once and can never be republished. The keystore is what makes a second run possible.

Because your bot does its own encryption, it holds real cryptographic material: its own identity, plus a conversation key for every server it belongs to. The **keystore** is where the SDK persists that material so your bot keeps working across restarts and reconnects.

<Warning>
  Configure a keystore before the **first** `login()`, not after the first restart. Your bot's identity is published write-once, so a keystore-less first run looks like a complete success and permanently bricks the second one. The rest of this page explains why.
</Warning>

## Set a keystore before the first login

The shortest form is a file path:

```ts theme={null}
import { Client } from '@cloak-software/bot-sdk';

const client = new Client({
  token: process.env.CLOAK_TOKEN!,
  keystorePath: './bot.cloak-keystore.json',
});
```

`keystorePath` is shorthand for `keystore: fileKeystore(path)`. Any other backend is a `KeystoreAdapter` passed as `keystore`. The two options are mutually exclusive, and supplying both throws.

```ts theme={null}
import { Client, fileKeystore } from '@cloak-software/bot-sdk';

// Identical to keystorePath: './bot.cloak-keystore.json'
const client = new Client({
  token: process.env.CLOAK_TOKEN!,
  keystore: fileKeystore('./bot.cloak-keystore.json'),
});
```

## The write-once identity

This is the one thing on this page that cannot be undone, so it is worth reading in full.

On its first login, your bot generates an identity and publishes the public half to Cloak. That publication is **write-once server-side**. There is no republish, no rotate, and no overwrite. From then on, every human member who shares a conversation key with your bot wraps that key to the identity Cloak holds.

If the private half is not persisted:

<Steps>
  <Step title="Run 1 looks perfect">
    The bot generates an identity, publishes it, receives keys, decrypts, and sends. Everything works. The SDK prints one loud warning at the moment it publishes an identity with no keystore, and that warning is not gated behind `debug`.
  </Step>

  <Step title="The process exits">
    The private half died with it. Cloak still holds the public half, forever.
  </Step>

  <Step title="Run 2 fails permanently">
    The bot generates a **new** identity and tries to publish it. The server refuses, because it already holds one. The SDK discards the useless fresh identity and `login()` throws, naming your keystore.
  </Step>
</Steps>

The thrown message is specific, so it is searchable:

> the server already holds a published identity for this bot, but the local keystore (...) is new: its freshly generated identity was rejected (op-238 publication is write-once)

There is no local fix. With no keystore there is nothing to restore from, and the only recovery is resetting the bot's identity server-side.

<Note>
  This replaced an older, worse symptom. The SDK used to start fine and then fail every decryption, so the visible problem was garbled traffic rather than a startup error. If you find a doc or a post describing a "Bad MAC" here, it is describing the behavior this guard was built to remove. Under HPKE the underlying decryption failure text is `open failed: bad tag`, and you should not be seeing it for this reason any more.
</Note>

## Where the failure surfaces

`login()` reads the keystore as its first async step, before it opens any socket and before it makes any identity decision. Failures land there.

```ts theme={null}
const client = new Client({ token, keystorePath: './bot.cloak-keystore.json' });
// The constructor touches no storage. This is where a bad keystore throws.
await client.login();
```

A keystore that exists but cannot be read or parsed throws with a message naming the store, and the SDK refuses to start with a fresh identity. That refusal is deliberate: silently regenerating would walk through the one-way door above.

If you hit it, **restore** the keystore from a backup. Delete it only if you are also resetting the bot's identity server-side.

## What lives in the keystore

| Contents                                   | Purpose                                                                                                                |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| The HPKE identity                          | Published write-once, reused forever. Losing it is the unrecoverable case above.                                       |
| Conversation keys, per epoch               | One key per server per key epoch. This is what decrypts and encrypts messages.                                         |
| Peer identity pins                         | Trust-on-first-use pins for the members who wrap keys to your bot.                                                     |
| Direct-message keys and the peer-to-dm map | So a DM does not re-exchange its key on every start.                                                                   |
| Voice keys, with their scope               | Stored with the scope they were sealed under. An entry missing its scope is dropped and refetched rather than guessed. |

You never read or write any of it by hand. Your job is to make sure it survives restarts and stays private.

### Peer pins and trust on first use

The first wrap your bot receives from a given member pins that member's identity key. Later wraps from the same member must match the pin or they are rejected.

Those pins live in the keystore, so they survive a restart. A bot that keeps its keystore does not silently re-enter trust-on-first-use on every start, which is what makes the mismatch check able to fire at all.

## Protect it

The keystore holds live keys. Anyone who can read it can impersonate your bot and decrypt its traffic.

* Never commit it. Add this to your `.gitignore`:

```bash .gitignore theme={null}
*.cloak-keystore.json
```

* Keep it out of logs and out of backups that leave your control.
* On a host with a writable disk, point `keystorePath` at a **mounted volume**, not at the ephemeral container filesystem. A container that redeploys onto a fresh filesystem is the most common way a bot walks through the one-way door.

<Warning>
  Treat the keystore like a private key, because it contains private keys. Losing it breaks your bot permanently. Leaking it compromises your bot.
</Warning>

## Keystore backends

### `fileKeystore()`

The default, and what `keystorePath` gives you. Two properties belong to this adapter specifically:

* Writes are atomic. Each save goes to a temporary sibling file and is renamed into place, so an interrupted write can never leave you with a truncated keystore.
* The file is written with `0600` permissions, so only your user can read it.

### `envKeystore()`

For hosts with no writable volume. It reads `CLOAK_KEYSTORE` (override with `varName`), accepting either raw keystore JSON or base64 of it, and writes base64 back into `process.env`.

```ts theme={null}
import { Client, envKeystore } from '@cloak-software/bot-sdk';

const client = new Client({
  token: process.env.CLOAK_TOKEN!,
  keystore: envKeystore({
    // Called with a base64 blob after every write.
    onWrite: async (blob) => {
      await myHost.setSecret('CLOAK_KEYSTORE', blob);
    },
  }),
});
```

<Warning>
  `onWrite` is what makes this adapter durable. `process.env` does not survive the process. A new identity written with no `onWrite` hook is gone the moment the process exits, and that is exactly the unrecoverable case. The SDK warns loudly the first time it happens. Do not run past that warning.
</Warning>

### A custom adapter

Implement three methods. Only `describe()` is optional.

```ts theme={null}
import type { KeystoreAdapter } from '@cloak-software/bot-sdk';

const myKeystore: KeystoreAdapter = {
  async read() {
    const row = await db.get('cloak-keystore');
    return row?.contents ?? null; // null ONLY when nothing is stored yet
  },
  async write(contents) {
    await db.put('cloak-keystore', contents);
  },
  describe() {
    return 'db cloak-keystore';
  },
};
```

<Warning>
  The one contract you must honor: `read()` resolves `null` **only** when nothing is stored yet. Every other failure has to reject. A failure that masquerades as a first run makes the bot generate an identity it can never publish, and that is permanent.
</Warning>

Atomic writes and file permissions are the file adapter's properties, not the SDK's. A custom adapter owns its own durability.

## Shutting down

`destroy()` flushes queued keystore writes as well as closing the connection, so always await it. A shutdown immediately after a key landed would otherwise drop that key and cost a re-acquire on the next start.

```ts theme={null}
process.on('SIGTERM', () => {
  client.destroy().catch(console.error);
});
```

## Next

<CardGroup cols={2}>
  <Card title="Connection lifecycle" icon="arrows-rotate" href="/concepts/connection-lifecycle">
    When the keystore is read, and what carries across a reconnect.
  </Card>

  <Card title="Deploying a bot" icon="server" href="/production/deploying">
    Mounted volumes, secret stores, and the one deployment decision that matters.
  </Card>

  <Card title="How bots work" icon="lock" href="/concepts/how-bots-work">
    Why your bot holds keys in the first place.
  </Card>

  <Card title="What is encrypted, and what is not" icon="shield-halved" href="/concepts/encryption-lanes">
    What those keys do and do not cover.
  </Card>
</CardGroup>
