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.

License

MIT

About

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

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Contributors