cryptlite

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 19, 2026 License: MIT Imports: 14 Imported by: 0

README

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]
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

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrKeyNotFound           = errors.New("encryption key not found")
	ErrInvalidCiphertext     = errors.New("invalid ciphertext")
	ErrUnsupportedKeyVersion = errors.New("unsupported key version")
	ErrInvalidConfig         = errors.New("invalid config")
	// ErrKeyAlreadyExists is returned when storeKeyVersion would overwrite an
	// existing keychain entry for the same service/account. Overwriting silently
	// would orphan every row already encrypted under the existing key.
	ErrKeyAlreadyExists = errors.New("encryption key already exists in keychain")
	// ErrInconsistentKeyState is returned when the database has no active-key
	// metadata but the keychain already holds a key for the default version.
	// This signals the db metadata and keychain have diverged (e.g. a restored
	// or partially-migrated database) — minting a fresh key here would silently
	// overwrite the real one and orphan everything encrypted under it.
	ErrInconsistentKeyState = errors.New("encryption key metadata is inconsistent with keychain")
)

Functions

This section is empty.

Types

type Config

type Config struct {
	DBPath   string
	Keychain KeychainConfig
}

Config is the top-level configuration for opening a Store.

type EncryptedBlob

type EncryptedBlob struct {
	Ciphertext []byte
	Nonce      []byte
	KeyVersion int
}

EncryptedBlob holds the output of a single encryption operation.

type KeychainConfig

type KeychainConfig struct {
	Service string
	Account string
}

KeychainConfig identifies the OS keychain entry used to store the master key.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the main entry point for encrypted SQLite access.

func Open

func Open(cfg Config) (*Store, error)

Open opens (or creates) the SQLite database and loads the active encryption key.

func OpenWithKeyring

func OpenWithKeyring(cfg Config, kr keyring.Keyring) (*Store, error)

OpenWithKeyring opens the store using the provided Keyring — intended for tests.

func (*Store) Close

func (s *Store) Close() error

Close closes the underlying database connection.

func (*Store) DB

func (s *Store) DB() *sql.DB

DB returns the raw *sql.DB for application queries.

func (*Store) Decrypt

func (s *Store) Decrypt(_ context.Context, blob EncryptedBlob, aad []byte) ([]byte, error)

Decrypt decrypts a blob using the key version stored in the blob.

func (*Store) DecryptJSON

func (s *Store) DecryptJSON(ctx context.Context, blob EncryptedBlob) ([]byte, error)

DecryptJSON is a convenience wrapper for JSON payloads with no AAD.

func (*Store) Encrypt

func (s *Store) Encrypt(_ context.Context, plaintext, aad []byte) (*EncryptedBlob, error)

Encrypt encrypts plaintext using the active key and the provided AAD.

func (*Store) EncryptJSON

func (s *Store) EncryptJSON(ctx context.Context, rawJSON []byte) (*EncryptedBlob, error)

EncryptJSON is a convenience wrapper for JSON payloads with no AAD.

func (*Store) RotateKey

func (s *Store) RotateKey(_ context.Context) (int, error)

RotateKey creates a new key version in the keychain and marks it active. Old keys remain available for reads.

Directories

Path Synopsis
cmd
encsqlite command
encsqlite is a CLI helper for managing a cryptlite-backed SQLite database.
encsqlite is a CLI helper for managing a cryptlite-backed SQLite database.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL