# Encryption

URL: https://shellz.dev/docs/security/encryption

> The keys, algorithms and formats shellz uses to encrypt, sign and verify recordings.

This page describes exactly how a recording is protected, from the server to your browser. The same design applies to servers and to containers.

## Keys at a glance

| Key | Type | Where it lives | Used for |
|---|---|---|---|
| Session data key | 32 random bytes | Only in the recorder's memory during the session, and wrapped in each segment header | Encrypting one session's content (AES-256-GCM) |
| Org encryption key | X25519 key pair | Public half on every server. Private half: sealed by shellz (standard) or only in your team's browsers (Perfect Secrecy) | Wrapping session data keys |
| Org signing key | Ed25519 key pair | Same as the org encryption key | Signing policies and org key rotations |
| Server key | Ed25519 key pair | Created on the server at enrollment; the private half never leaves it (containers keep it in memory) | Signing segments and requests to shellz |
| Release key | Ed25519 | Pinned in the agent and installers | Verifying agent downloads and updates |

All signatures use a domain-separation prefix (for example `shellz:segment:v1:`), so a signature made for one purpose can never be valid for another.

## Encrypting a session

When a session starts, the recorder:

1. Generates a 32-byte random **data key** for the session.
2. **Wraps** the data key to the org's current X25519 public key:
   - generates an ephemeral X25519 key pair and computes the shared secret with the org public key;
   - derives a wrapping key with HKDF-SHA256, using the ephemeral and org public keys as salt and `shellz:session-key:v1:<sessionId>` as info;
   - encrypts the data key with AES-256-GCM under the wrapping key, with the org key ID as associated data.
3. Records the session as two streams: an asciicast v2 event stream (output, input, resizes) and a plain-text transcript.

About once a second, the recorder closes a **segment**:

1. The new events are serialized as JSON and compressed with gzip.
2. The result is encrypted with AES-256-GCM under the data key, with a fresh 12-byte nonce, and with `shellz:segment-content:v1:<sessionId>:<seq>` as associated data. A segment can't be moved to another session or position.
3. A **header** is built: org, server and session IDs, the sequence number, the SHA-256 hash of the ciphertext, the hash of the previous segment's header, the wrapped data key, the org key ID, a timestamp, and on the first segment the session details (kind, user, source address, terminal, container).
4. The header is signed with the server's Ed25519 key.

Segments are at most 1 MiB. The recorder discards the data key when the session ends. Only the wrapped copy in the headers remains, so the server can't decrypt a session once it's over.

### Hidden input

Keystrokes are only recorded while the terminal echoes them. Passwords typed at `sudo`, `passwd`, `ssh` or `read -s` prompts turn echo off, so they're never written to the recording.

## The hash chain

Each header contains `prevHash`, the SHA-256 hash of the previous segment's header. The first segment of a session references a fixed genesis value, and the last one is marked `final`. Together they form a chain:

```text
seq 0                 seq 1                 seq 2 (final)
┌──────────────┐      ┌──────────────┐      ┌──────────────┐
│ prevHash: 0… │◀─────│ prevHash: h0 │◀─────│ prevHash: h1 │
│ cipherHash   │      │ cipherHash   │      │ cipherHash   │
│ signature    │      │ signature    │      │ signature    │
└──────────────┘      └──────────────┘      └──────────────┘
```

When a recording is played, every segment is checked:

- the server's signature over the header;
- that the ciphertext matches `cipherHash`;
- that sequence numbers are consecutive and every `prevHash` matches;
- that nothing follows the final segment.

The result is shown as the integrity badge: **verified**, **gap** (a segment is missing, for example because a server lost its disk before uploading) or **tampered** (a signature or hash didn't match).

## Upload and storage

The server uploads segments over HTTPS. Before accepting a segment, shellz checks that it comes from an enrolled server of your org, that the server's signature is valid, and that it extends the session's chain:

- a segment with a bad signature is rejected and raises `segment-signature-invalid`;
- a segment that conflicts with one already stored (same position, different content) is rejected, the session is shown as **tampered**, and a `segment-conflict` alert is raised;
- a segment that skips ahead is stored, and the session is marked as having a gap.

Accepted segments are written once to object storage under a retention lock: for 365 days, they can't be overwritten or deleted by anyone, including shellz.

Servers also sign every request to the shellz API (heartbeats, alerts, policy and key sync) with their server key, so a stolen network position can't impersonate a server.

## Decrypting in the browser

To play a recording, the browser:

1. gets the session's data key, either from shellz (standard mode) or by unwrapping it itself with the org private key unlocked by your passkey ([Perfect Secrecy](https://shellz.dev/docs/security/perfect-secrecy.md));
2. downloads the segments as ciphertext;
3. verifies signatures and the chain as above;
4. decrypts and decompresses each segment with the Web Crypto API.

Opening a recording requires a sign-in within the last 10 minutes, and each view is written to the audit log.

## Policies and key rotation

Each server follows a **policy**: whether recording is on, what happens when the spool fills up, and whether removal is allowed. Policies are signed by the org's Ed25519 key and carry a revision number. A server only applies a policy that is signed by your org, names that server (or its fleet), and has a higher revision than the one it has. A replayed or downgraded policy is rejected with a `policy-rejected` alert.

A new org key is signed by the previous one, forming a chain back to the key the server pinned at enrollment. A server only switches to a new key whose chain it can verify, and raises `org-key-rejected` otherwise.

## Releases

The installers and the agent verify every release against the pinned release key before installing it: an Ed25519 signature over the version, architecture, URL, SHA-256 hash and size. The agent refuses to install an older version than it runs.

## Offline verification

`shellz decrypt` verifies and decrypts recordings without the dashboard or network access: given a directory of segment files and either the org private key or one session's data key, it checks the chain (and server signatures with `--host-key`) and writes `.cast` and `.txt` files. See the [CLI reference](https://shellz.dev/docs/reference/cli.md#shellz-decrypt).

## Algorithms

| Purpose | Algorithm |
|---|---|
| Content encryption | AES-256-GCM, 96-bit random nonces |
| Key agreement | X25519 |
| Key derivation | HKDF-SHA256 |
| Signatures | Ed25519 |
| Hashes | SHA-256 |
| Compression | gzip |
| Transport | TLS 1.2 or later |
