Skip to content

berbyte/cryptlite

Repository files navigation

cryptlite

A small Go library for field-level encrypted persistence on top of SQLite.

Encrypted blobs cannot be queried. Extract the fields you need into normal SQLite columns; encrypt only the raw payload.

What it does

  • AES-256-GCM encryption of arbitrary byte slices or JSON payloads
  • OS keychain-backed master key storage (macOS Keychain, Windows Credential Manager, Linux Secret Service)
  • Per-encryption random nonce — no nonce reuse
  • Key versioning and rotation — old rows always decryptable
  • Optional associated data (AAD) to bind ciphertext to a specific row context
  • Schema helpers for the encrypted column pattern

What it does not do

  • SQLCipher or full database file encryption
  • Encrypted JSON querying or SQL rewriting
  • ORM integration
  • CGO dependency (uses modernc.org/sqlite)
  • Cloud KMS
  • Compression

Installation

go get github.com/berbyte/cryptlite

Quickstart

store, err := cryptlite.Open(cryptlite.Config{
    DBPath: "app.db",
    Keychain: cryptlite.KeychainConfig{
        Service: "myapp",
        Account: "default",
    },
})
if err != nil {
    return err
}
defer store.Close()

ctx := context.Background()

// Encrypt a JSON blob.
blob, err := store.EncryptJSON(ctx, rawJSON)

// Decrypt it later.
plaintext, err := store.DecryptJSON(ctx, *blob)

// Use AAD to bind ciphertext to a specific row.
blob, err = store.Encrypt(ctx, rawJSON, []byte("mytable/myjson/rowid-123"))
plaintext, err = store.Decrypt(ctx, *blob, []byte("mytable/myjson/rowid-123"))

Schema pattern

Extract queryable fields into normal columns. Encrypt the raw payload.

CREATE TABLE hook_invocations (
    id                            TEXT    PRIMARY KEY,
    received_at                   INTEGER NOT NULL,
    branch                        TEXT,
    stage                         TEXT    NOT NULL,
    tool_name                     TEXT,
    -- encrypted blob columns:
    raw_input_json_ciphertext     BLOB    NOT NULL,
    raw_input_json_nonce          BLOB    NOT NULL,
    raw_input_json_key_version    INTEGER NOT NULL
);

CREATE INDEX idx_hook_invocations_stage ON hook_invocations(stage, received_at);

Insert flow:

  1. Receive raw JSON.
  2. Parse and extract fields needed for WHERE/JOIN/ORDER BY.
  3. Store extracted fields as normal columns.
  4. store.Encrypt(ctx, rawJSON, aad) → get *EncryptedBlob.
  5. Store Ciphertext, Nonce, KeyVersion columns.

The sqlite.EncryptedColumns helper generates the column definitions:

// Returns: "body_ciphertext BLOB NOT NULL, body_nonce BLOB NOT NULL, body_key_version INTEGER NOT NULL"
cols := sqlite.EncryptedColumns("body")

Keychain behavior

On first Open, if no key exists in the keychain, the library generates a random 256-bit key and stores it. On subsequent opens it loads the existing key. Keys are versioned — v1, v2, etc.

// Rotate to a new key. Old rows remain decryptable.
newVersion, err := store.RotateKey(ctx)

Testing without the OS keychain

Pass a memory keyring for tests:

store, err := cryptlite.OpenWithKeyring(cfg, keyring.NewMemory())

Threat model summary

cryptlite protects encrypted blobs from offline inspection of the SQLite file. It does not protect against a compromised running process, malware on the machine, memory dumps, or a local user with live access. See docs/threat-model.md for details.

Connection handling

Every connection opened by cryptlite is configured with two SQLite pragmas:

  • journal_mode=WAL — Write-Ahead Logging. Multiple readers never block a writer; the writer never blocks readers. Persistent: set once, survives close/reopen.
  • busy_timeout=5000 — When a write lock is unavailable, SQLite retries for up to 5 seconds before returning SQLITE_BUSY. Per-connection: applied via the DSN so every pooled connection gets it.

These are embedded in the file: URI DSN passed to the driver, which guarantees they apply to every new connection regardless of pool size.

Store lifecycle

flowchart TD
    A[Open / OpenWithKeyring] --> B["sql.Open — file: URI with _pragma params"]
    B --> C[PRAGMA journal_mode=WAL]
    C --> D["PRAGMA busy_timeout=5000ms"]
    D --> E["migrate: CREATE TABLE IF NOT EXISTS encsqlite_keys"]
    E --> F{active key in table?}
    F -- yes --> G[loadKeyVersion from OS keychain]
    F -- no  --> H["bootstrapKey: generate + store v1"]
    G --> I[Store ready]
    H --> I
    I --> J["EncryptJSON / DecryptJSON"]
    J --> K[Close]
Loading

Concurrent multi-process access

sequenceDiagram
    participant P1 as process 1
    participant P2 as process 2
    participant DB as SQLite WAL file

    P1->>DB: Open (WAL mode, busy_timeout=5s)
    P2->>DB: Open (WAL mode, busy_timeout=5s)
    P1->>DB: INSERT — write lock acquired
    P2->>DB: INSERT — SQLITE_BUSY → retry loop
    DB-->>P1: OK
    P1->>DB: Close
    DB-->>P2: lock released, write proceeds
    P2->>DB: INSERT
    DB-->>P2: OK
    P2->>DB: Close
Loading

License

MIT

About

Encrypted blob storage for SQLite with OS keychain-backed keys and extracted query metadata.

Resources

License

Security policy

Stars

Watchers

Forks

Contributors