Architecture

Sirr is a single Rust binary that handles encryption, storage, authentication, and automatic secret expiry. No sidecars, no external databases, no runtime dependencies.

Encryption flow

Every secret stored in Sirr is encrypted at rest using ChaCha20Poly1305. The encryption key is derived from your SIRR_MASTER_KEY using Argon2id, a memory-hard key derivation function.

Key derivation

On first startup, Sirr generates a sirr.salt file containing 32 cryptographically random bytes. This salt is stored alongside the database and must not be deleted. The master key is then derived as follows:

Key derivation

SIRR_MASTER_KEY (user-provided)
       |
       v
  Argon2id (64 MiB memory, 3 iterations, sirr.salt)
       |
       v
  32-byte derived encryption key

Per-record encryption

Each secret gets its own random 12-byte nonce, generated from a cryptographically secure random number generator. The secret value is then encrypted:

Per-record encryption

plaintext value + derived key + random 12-byte nonce
       |
       v
  ChaCha20Poly1305 encrypt
       |
       v
  (nonce || ciphertext || auth_tag) → stored in redb

Decryption reverses the process: the nonce is read from the stored record, and the derived key decrypts the ciphertext while verifying the authentication tag.

Storage model

Sirr uses redb, an embedded key-value database written in Rust. The entire database lives in a single file.

Files on disk

Sirr stores two files in its data directory:

  • sirr.db -- the redb database containing all encrypted secrets and metadata
  • sirr.salt -- 32 random bytes generated on first run, used for Argon2id key derivation

Both files are required. Losing sirr.salt means existing secrets cannot be decrypted, even with the correct master key.

Data directory defaults

The data directory is platform-specific unless overridden by SIRR_DATA_DIR:

PlatformPath
Linux~/.local/share/sirr/
macOS~/Library/Application Support/sirr/
Windows%APPDATA%\sirr\

Record structure

Each record in the database stores:

  • Name
    key
    Type
    string
    Description

    The secret identifier (e.g., DB_URL). Used as the redb key.

  • Name
    nonce
    Type
    [u8; 12]
    Description

    Random 12-byte nonce unique to this record.

  • Name
    ciphertext
    Type
    Vec<u8>
    Description

    The encrypted secret value with appended authentication tag.

  • Name
    created_at
    Type
    u64
    Description

    Unix timestamp of when the secret was created.

  • Name
    ttl
    Type
    Option<u64>
    Description

    Time-to-live in seconds from creation. None means no TTL.

  • Name
    max_reads
    Type
    Option<u32>
    Description

    Maximum number of reads allowed. None means unlimited.

  • Name
    reads
    Type
    u32
    Description

    Current read counter, incremented on each successful GET.

Request lifecycle

Every API request follows the same path through the server:

Request lifecycle

Client request
       |
       v
  Bearer token auth (constant-time comparison against SIRR_MASTER_KEY)
       |
       v
  Route handler (push / get / delete / list / prune)
       |
       v
  Encrypt or decrypt value using derived key
       |
       v
  redb read or write (ACID transaction)
       |
       v
  JSON response to client

Authentication uses constant-time comparison to prevent timing attacks against the master key. The same SIRR_MASTER_KEY that seeds encryption is used as the bearer token -- there is only one credential to manage.

Expiry model

Secrets in Sirr expire through two mechanisms, either of which can trigger deletion:

TTL (time-to-live)

Set a wallclock duration when pushing a secret. Once the TTL elapses, the secret is eligible for deletion. TTL is evaluated against the created_at timestamp and the current server time.

Max reads

Set a read counter limit when pushing a secret. Each successful GET increments the counter. When reads >= max_reads, the secret is deleted immediately after the response is sent.

Eviction strategy

Expired secrets are cleaned up through three mechanisms:

  1. Lazy eviction on read -- when a secret is requested, Sirr checks TTL and read count before returning it. If expired, the secret is deleted and a 404 is returned.
  2. Background sweep -- a periodic background task scans the database and removes expired secrets.
  3. Manual prune -- POST /prune triggers an immediate scan and removes all expired secrets. Returns the count of pruned records.

Design decisions

Why ChaCha20Poly1305

ChaCha20Poly1305 is an AEAD cipher that is safe to use in software-only environments. Unlike AES-GCM, it does not require hardware AES-NI instructions to resist timing side-channels. With 12-byte random nonces and the expected volume of secrets per instance, nonce collision probability is negligible.

Why Argon2id

Argon2id combines the side-channel resistance of Argon2i with the GPU/ASIC resistance of Argon2d. The 64 MiB memory cost ensures that brute-forcing the master key requires significant memory per attempt, making large-scale parallel attacks impractical.

Why redb

redb is a single-file, embedded, ACID-compliant database written in pure Rust. It requires no external processes, no TCP connections, and no configuration. This aligns with Sirr's goal of being a single binary with zero runtime dependencies. The entire data store is one file that can be backed up with a simple copy.

Was this page helpful?