Tupi-Guarani for “small”

mirim

An embedded SQL database, encrypted at rest, with post-quantum sealed exports.

3,789 lines of safe Rust under #![forbid(unsafe_code)]. No RSA and no elliptic-curve cryptography in any file it writes, so there is nothing for a future quantum computer to unwind later.

ML-KEM-768 · FIPS 203 XChaCha20-Poly1305 at rest 477 KiB binary AGPL-3.0 / commercial
secure channel established
plaintext db
services
secrets
access_log
sealed export
a3f1 9c4e
7bd0 228a
1655 e27f
↑ ML-KEM-768 encapsulation ↑

key wrapped per recipient · outsiders cannot open it

Release 1.1.0 · 15 July 2026

Query ergonomics, and packages you can install.

No on-disk format changed. Every 1.0.0 vault, sealed export, log and manifest opens unchanged under 1.1.0, the new SQL is query-only, and the crypto stack did not move. This release is about the parts you touch: shaping results, loading a file, and getting a binary without a compiler.

What 1.1.0 adds
AreaChange
SQL ORDER BY col [ASC|DESC] (multi-key, stable), LIMIT n [OFFSET m], COUNT(*), and -- line plus /* */ block comments. All contextual keywords, so a column named order, count or limit still parses unquoted.
CLI .read <path> runs every statement in a .sql file. .schema prints the reconstructed CREATE TABLE statements.
Library Db::execute_script and mirim::split_statements load a multi-statement file with string-aware and comment-aware splitting. Db::schema_sql and Db::table_columns introspect the schema.
Samples Five realistic databases under samples/: a secrets vault, a CTF scoreboard, a device fleet, an audit trail, a password manager. CI loads all five on every push. cargo run --example gen_samples ./out materializes encrypted vaults and a sealed export.
Desktop mirim-gui, a small offline front end on eframe/egui. It opens through the durable encrypted engine and stays read-only until you allow writes.
Packaging .deb, .rpm and a Linux tarball, a SHA256SUMS manifest, and a detached ML-DSA-87 signature for every artifact.
Docs A beginner's guide, and a sourced comparison against SQLite, SQLCipher, DuckDB, libSQL/Turso, redb and sled.
Footprint The stripped CLI went from 436 KiB to 477 KiB. The growth is the query features. The crypto stack is unchanged.

Full history in the changelog, in Keep a Changelog format, with semantic versioning.

Download

mirim 1.1.0 for Linux x86-64.

Every artifact is listed in SHA256SUMS and carries a detached ML-DSA-87 signature that mirim produced with its own signing tool. Release builds are reproducible: the job builds twice and refuses to publish unless the two digests match.

Signed artifacts · git.securityops.co
PlatformFileSizeInstall with
Debian, Ubuntu mirim_1.1.0-1_amd64.deb 277 KiB sudo apt install ./mirim_1.1.0-1_amd64.deb
Fedora, RHEL, openSUSE mirim-1.1.0-1.x86_64.rpm 300 KiB sudo dnf install ./mirim-1.1.0-1.x86_64.rpm
Any Linux mirim-1.1.0-x86_64-linux.tar.gz 4.7 MiB tar xzf mirim-1.1.0-x86_64-linux.tar.gz
CLI, signing tool, GUI, the five samples, and the docs.
Desktop GUI only mirim-gui-1.1.0-x86_64-linux 9.5 MiB chmod +x mirim-gui-1.1.0-x86_64-linux
Source v1.1.0.tar.gz source cargo build --release · Rust 1.96.0, pinned in rust-toolchain.toml
Checksums SHA256SUMS 844 B sha256sum -c SHA256SUMS --ignore-missing
Signing key mirim-release.pub 2.5 KiB ML-DSA-87 public key. Also committed under keys/ in the repository.
verify, then install
# 1. checksums
$ sha256sum -c SHA256SUMS --ignore-missing
mirim_1.1.0-1_amd64.deb: OK

# 2. post-quantum signature (ML-DSA-87)
$ mirim-sign verify mirim_1.1.0-1_amd64.deb \
      mirim_1.1.0-1_amd64.deb.mf mirim-release.pub
ok

# 3. install
$ sudo apt install ./mirim_1.1.0-1_amd64.deb
$ mirim notes.mrm
or build it yourself
# Guix
$ guix shell rust rust:cargo gcc-toolchain -- \
      cargo build --release

# Debian / Ubuntu
$ sudo apt install curl build-essential
$ curl --proto '=https' --tlsv1.2 -sSf \
      https://sh.rustup.rs | sh
$ cargo build --release

# with the signing tool, or library only
$ cargo build --release --features cli,sign
$ cargo build --release --no-default-features
SHA-256 digests for every 1.1.0 artifact
c7be647a9bc4251c985db233cc79e934fa3d21226b6bc68f336df4c44d1737df  mirim-1.1.0-1.x86_64.rpm
06b1968f8f82fff743efc64b0724793d13a2913e5aa1ff1ad2feb19833dc7175  mirim-1.1.0-1.x86_64.rpm.mf
ec12a859015d57a4615f04aea673f67ab6a44292889d70081a2b5623f6ba9ca7  mirim-1.1.0-x86_64-linux.tar.gz
1cf660b778952cfce737910cb982d86ca970871415ce8aa5ac401c9070aaff8b  mirim-1.1.0-x86_64-linux.tar.gz.mf
4415a703c6a4616d13fc6a39b7d4c5e003c6d1afdc98e05f5df0a7b44db10a89  mirim-gui-1.1.0-x86_64-linux
cacc3174fbef141fcb1133419611c4d51216a436a860eb8f38fa35608d738180  mirim-gui-1.1.0-x86_64-linux.mf
9dc874c918a8d6811ca4d500df1912044503c4e1e1650c1862aabab501559648  mirim-release.pub
73232994a8106337ed30d83bab1fec36cd3415d1a5210fffbed00a907ad93b35  mirim_1.1.0-1_amd64.deb
716e2d940e52102664f3eb5d107bebb1e58d9e184f5946703e3c261f77cb5fad  mirim_1.1.0-1_amd64.deb.mf

macOS and Windows: build from source. The procedure is cargo build --release with the pinned toolchain, and nothing else.
Mirrors: the same bytes are on Codeberg and GitHub. SHA256SUMS is identical across all three, so verify against whichever you trust and fetch from whichever is fast.

What it does

Small data, kept private and durable.

mirim holds data that should stay in memory, encrypted on disk, and recoverable after a crash: keys, credentials, capability records, audit trails, configuration.

Encrypted at rest

Every vault is XChaCha20-Poly1305 over the snapshot. The key comes from Argon2id (RFC 9106, 64 MiB, 3 passes) or a 32-byte key you hold. The header is authenticated as associated data. Saves are atomic.

Post-quantum sealed exports

Seal a database to one recipient or to many with ML-KEM-768 (FIPS 203). Each recipient opens it with their own key. An outsider cannot open it at all.

Crash-safe durability

An encrypted write-ahead log, fsynced before a statement is acknowledged. An ok survives kill -9 and power loss, checked by a 60-iteration SIGKILL harness with zero acknowledged commits lost.

SQL with an index

CREATE, INSERT, SELECT, UPDATE, DELETE with parameter binding, plus ORDER BY, LIMIT and COUNT(*). PRIMARY KEY and UNIQUE columns are indexed, so duplicate checks and point lookups are O(1).

Rotation, and one writer

rotate_secret re-encrypts the vault and rebinds the log under a new secret in one atomic step. On Unix an advisory flock makes a second opener fail with Error::Locked rather than corrupt the log.

Embed from C

A C ABI (mirim-ffi) behind a single header, with every panic caught at the boundary. It is the one deliberate unsafe surface, and it runs under AddressSanitizer and UBSan on both sides.

Dialect

The whole SQL surface, on one screen.

Two types, no coercion, no surprises. What follows is not a summary of the language. It is the language.

cheat sheet
-- Line comments, and /* block comments */ (1.1.0).

CREATE TABLE users (
  id    INTEGER PRIMARY KEY,  -- unique, indexed
  email TEXT UNIQUE,          -- allows many NULLs
  age   INTEGER
);

-- One row per INSERT. Unlisted columns become NULL.
INSERT INTO users VALUES (1, 'ana@x.io', 30);
INSERT INTO users (id, email) VALUES (2, 'bia@x.io');

SELECT email, age FROM users
  WHERE age >= 18 AND email IS NOT NULL;
SELECT * FROM users ORDER BY age DESC;
SELECT id FROM users ORDER BY id LIMIT 10 OFFSET 20;
SELECT COUNT(*) FROM users WHERE age IS NULL;

-- Atomic per statement: a violation changes nothing.
UPDATE users SET age = 31 WHERE id = 1;
DELETE FROM users WHERE id = 2;

+ In the language

  • INTEGER (i64) and TEXT (UTF-8). Nothing else.
  • Single-column PRIMARY KEY and UNIQUE, each carrying an O(1) equality index.
  • WHERE with AND-chained comparisons and IS [NOT] NULL.
  • ORDER BY, multi-key and stable. NULL sorts first under ASC, last under DESC.
  • LIMIT, OFFSET and COUNT(*).
  • ? parameter binding. Bound values are data, never SQL.
  • Line and block comments.

- Deliberately absent

  • Joins and sub-queries.
  • GROUP BY, OR, and every aggregate except COUNT(*).
  • Transactions spanning statements. Each statement is atomic on its own.
  • Secondary indexes, and any query planner.
  • Type coercion. REAL, BLOB, DATE, BOOLEAN.
  • FOREIGN KEY, NOT NULL, DEFAULT, CHECK, composite keys.
  • Data larger than RAM. Durability is real, out-of-core storage is not.

Untrusted input goes in as a bound ? parameter, never as SQL text. Full walkthrough in the guide. Five worked schemas in samples/.

Measured

Every number here is reproducible.

No adjectives. Criterion medians on x86-64, in memory, against SQLite through rusqlite with the bundled library. Where mirim is slower, it says so.

13.4ms
Bulk insert, 10k rows with a PRIMARY KEY
2.5× faster than SQLite (33.6 ms)
1.42µs
Point SELECT by primary key, 10k rows
2.3× slower than SQLite (0.62 µs)
578µs
Durable commit, one statement, fsynced
fsync-bound, encryption costs nothing visible
477KiB
Stripped CLI, full crypto stack included
opt-level=z, LTO, x86-64 Linux
cargo bench --bench core
Workloadmirim 1.1.0SQLiteReading
durable commit, 1 statement, fsynced 578 µs782 µs SQLite in WAL mode with synchronous=FULL. Both are fsync-bound, so mirim's per-record encryption is invisible here.
bulk insert with PRIMARY KEY, 1k rows 1.23 ms3.40 ms Each insert's duplicate check is an O(1) index probe, not a row scan.
bulk insert with PRIMARY KEY, 10k rows 13.4 ms33.6 ms mirim's append-plus-hash build comes out ahead of a B-tree on this in-memory workload.
point SELECT by primary key, 10k rows 1.42 µs0.62 µs SQLite's rowid B-tree wins. mirim is within 2.3× of it, down from 33× slower when the lookup was still a scan.
checkpoint, 1k rows 4.18 msnone Folds the log into a fresh snapshot. No commensurable SQLite operation.

rustc 1.96.0, 30 samples over 3 s (durable groups 20). Storage was virtualized, so fsync figures especially will differ on real disks. Equality on a non-indexed column still scans O(rows), and DELETE rebuilds the affected table's index afterwards: asymptotically unchanged, a modest constant.

Compare

Where mirim actually differs.

Not raw SQL power. SQLite, DuckDB and libSQL outclass mirim there, by design. The difference is the security posture: mirim is the only one of these that derives its at-rest key with a memory-hard KDF by default, and the only one that offers post-quantum sealing and signing at all.

Embedded databases, side by side
Dimension mirim 1.1 SQLite SQLCipher DuckDB 1.4+ libSQL / Turso redb / sled
Kind Embedded SQLEmbedded SQLEmbedded SQL, encryptedEmbedded analytical SQLEmbedded SQL, SQLite forkEmbedded key-value
Implementation Safe Rust, forbid(unsafe_code)CC, SQLite plus extensionC++C and RustRust
Encrypted at rest Yes, default and only modeNo, SEE is a paid add-onYesYes, since 1.4 (Sept 2025)YesNo
At-rest cipher XChaCha20-Poly1305, whole snapshotnoneAES-256-CBC + HMAC-SHA512, per pageAES-256-GCM or CTR, per pageAEGIS, AES-GCM or ChaCha20-Poly1305, per pagenone
Passphrase KDF Argon2id, memory-hard (64 MiB, t=3)nonePBKDF2-HMAC-SHA512, 256k iterationsraw keyraw keynone
Post-quantum ML-KEM-768 sealed exports, ML-DSA-87 signed manifestsNoNoNoNoNo
Classical RSA or ECC in the file formats Nonen/an/an/an/an/a
Crash durability WAL, fsync before ack, SIGKILL-testedWAL or rollback journalSQLite'sWALWALYes, log or copy-on-write
SQL surface Minimal subsetFull SQLFull SQLRich analytical SQLFull SQLNone, key-value API
Data bigger than RAM NoYesYesYesYesYes
Concurrency Single writer, single processMulti-reader, single-writerSQLite'sSingle processMulti, with replicationMVCC (redb)
Footprint 477 KiB CLI, crypto includedunder 900 KiB librarySQLite plus cryptotens of MBseveral MBsmall
Reproducible build, SBOM, signed release All threenonononono
License AGPL-3.0-only or commercialPublic domainBSD-styleMITMITMIT / Apache-2.0

+ Choose mirim when

  • The dataset is small, sensitive and durable: credentials, an audit trail, a personal vault, a fleet inventory.
  • You want the at-rest key derived from a passphrase with a memory-hard KDF, by default rather than by configuration.
  • You need post-quantum sealed exports, or post-quantum signed artifacts.
  • You want to read the whole engine yourself and reproduce the build byte for byte.

- Choose something else when

  • You need real SQL, out-of-core data or wide language support: SQLite or libSQL, with SQLCipher or libSQL encryption for at-rest protection. Neither is post-quantum.
  • Your workload is analytical or columnar over larger data: DuckDB.
  • You do not need SQL at all: redb or sled, a fast embedded key-value store.
  • Your data outgrows memory. mirim's working set lives in RAM, and that is not a bug to be fixed later.

Figures are measured here, quoted from each vendor's own documentation, or plain statements of on-disk-format fact. The full analysis with every source is in docs/comparison.md.

Cryptography

Named primitives, with their standards.

No homemade ciphers, and no protocol invented in-house. Every primitive is a vetted implementation, exercised against the official test vectors.

Cryptography inventory
RolePrimitiveParametersStandard
Key encapsulationML-KEM-7681088-byte ciphertext, implicit rejectionFIPS 203
SignaturesML-DSA-874627-byte signature, sign featureFIPS 204
AEADXChaCha20-Poly130524-byte nonce, header bound as associated dataRFC 8439 and extension
Passphrase KDFArgon2id64 MiB, 3 passes, 4 lanesRFC 9106
Key derivationHKDF-SHA256domain-separated info string per formatRFC 5869
HashingSHA-256snapshot identity, manifest target digestFIPS 180-4
RandomnessOS CSPRNGvia getrandomplatform

ML-KEM decapsulation is implicit-rejection, so a wrong key and a tampered file are indistinguishable: both surface as one Error::Crypto with no detail. The suite pins official vectors, all machine-extracted from their sources: RFC 8439 §2.8.2 AEAD, NIST ACVP FIPS 203 (keyGen and encap/decap, including a modified-ciphertext implicit-rejection case), NIST ACVP FIPS 204 (keyGen, deterministic sigGen with context, and sigVer with modified-signature negatives), and the RFC 9106 §5.3 Argon2id vector.

On disk

Six formats. All frozen since 1.0.0.

These layouts will stay readable. A format-breaking change would be a new major version, and 1.1.0 did not touch a single byte of any of them.

File formats
ArtifactMagicVersionLayout
VaultMIRIM1\0\0v1 50-byte header (magic, version, KDF id, salt, nonce), then the AEAD ciphertext of the snapshot. File extension .mrm.
Sealed export, one recipientMIRIMSL1v1 1088-byte ML-KEM-768 ciphertext, 24-byte nonce, then the AEAD payload.
Sealed export, many recipientsMIRIMSL1v2 Payload encrypted once under a random key, which is then wrapped per recipient: a 1088-byte KEM ciphertext and a 48-byte wrapped key each. Any one of them opens it.
Write-ahead logMIRIMWL1v1 41-byte header carrying the SHA-256 of the vault it extends, then AEAD records with authenticated consecutive sequence numbers. A torn final record, the only damage an honest crash can produce, is truncated on open.
Signed manifestMIRIMMF1v1 4677 bytes: header, target kind, counter, the SHA-256 of the target, and an ML-DSA-87 signature over the first 50. File extension .mf.
Trust fileMIRIMTS1v1 49 bytes: the highest accepted counter and the hash of the last accepted target. This is the local trust root for rollback rejection.

Every header is bound as AEAD associated data, so a bit flip anywhere fails authentication and there is no partial decode of a tampered file. Keying and AAD bind each log record to the exact snapshot bytes it extends, so cross-vault replay, reordering and splicing all fail closed. Byte-level layouts are in the README.

Assurance

Checked more than once.

The same behaviour is verified by independent methods, so a bug has to evade all of them.

  • +Differential model. The engine against an independent oracle, over generated statement sequences.
  • +Property tests. Round-trip, replay and checkpoint invariants under proptest.
  • +Five fuzz targets. Wire decode, sealed-export open (v1 and v2), manifest verify, SQL text, and WAL replay. The wire decoder sits at 72% line coverage from the current corpus.
  • +Miri on the logic suites. ASan and UBSan on the C FFI, with both sides instrumented.
  • +Power cut. A 60-iteration SIGKILL harness. Zero acknowledged commits lost.
  • +Reproducible builds. Two builds, bit-identical, or the release does not ship.
  • +Supply chain. A committed CycloneDX 1.5 SBOM that CI fails on drift, plus cargo audit and cargo deny on every push.
secrets.mrm
$ mirim secrets.mrm
creating new vault: secrets.mrm
new passphrase:
repeat passphrase:
mirim 1.1.0 — .help for commands
mirim> .read samples/secrets_vault.sql
ok: ran 30 statement(s) from samples/secrets_vault.sql
mirim> SELECT COUNT(*) FROM access_log WHERE allowed = 0;
count(*)
--------
4
mirim> SELECT actor, ts FROM access_log
   ...> WHERE allowed = 0 ORDER BY ts DESC LIMIT 3;
actor        | ts
-------------------------
unknown-host | 1717301000
ex-employee  | 1717300800
svc-web      | 1717300500
mirim> 

Threat model

The non-goals are the half that matters.

A tool that says only what it defends is selling something. This is the shape of the model; SECURITY.md carries the full text, and it is worth reading before you rely on any of this.

+ What mirim protects

  • Offline disk theft and backup exposure. Someone holding the .mrm without the passphrase learns the file size, and that it is a mirim vault. Nothing else.
  • Tampering, header included. The 50-byte header is associated data. Any bit flip fails authentication, and there is no best-effort decode.
  • Passphrase brute force, within passphrase quality. Argon2id at 64 MiB and 3 passes raises the cost per guess. It does not rescue a weak passphrase.
  • Harvest now, decrypt later. No RSA, no elliptic curves, no classical Diffie-Hellman exists anywhere in either format.
  • Decryption-failure oracles. Wrong key, wrong recipient and tampered data all surface as the same error, with no detail.
  • Hostile vault files. The decoder bounds-checks every length before it allocates, and fails closed on every structural violation.

- What it does not protect against

  • A compromised host. Malware, a hostile root user or a debugger attached while the vault is open sees plaintext and can capture the passphrase. Swap and core dumps are not mitigated.
  • Rollback, only partially. The trust file makes it detectable given intact local trust state. An adversary who can rewrite that file can reset it.
  • Log truncation. Deleting the log, or tampering with its final record, is equivalent to rolling back to the last checkpoint plus a prefix.
  • Log metadata. Record count and record sizes are visible on disk and leak the number of mutations and the rough size of each. Records are not padded.
  • Ciphertext remanence after rotation. Rotation replaces the files. It does not shred old ciphertext from unallocated blocks, journals or backups.
  • Metadata generally. File size, existence, modification times and access patterns are all visible.
  • Locking off Unix. The advisory flock is a Unix path. Elsewhere, one writer is enforced by you.

Constant-time posture, reviewed site by site and recorded in SECURITY.md: no mirim-owned branch, comparison or table index depends on secret key material. Two deliberate timing signals remain and are documented, both with no attacker-controlled oracle. Report a vulnerability through the contact in SECURITY.md.

New · desktop

A front end that refuses by default.

mirim-gui is a small point-and-click way to open a vault, built on eframe and egui. It lives in its own crate so the core stays tiny and unsafe-free.

  • +Offline. No network code exists anywhere in the GUI. It touches the local vault file you open, and nothing else.
  • +Read-only until you opt in. Mutating statements are refused until you tick Allow writes, so a stray query cannot change data.
  • +The passphrase is zeroized the moment the vault is open, and is never written anywhere.
  • +Encrypted and durable. It opens through the same durable engine, so every acknowledged write is fsynced and the one-writer lock keeps a second instance out.
  • +Tables, a grid, and two buttons. Run SQL with Ctrl+Enter. One click to checkpoint, one to change the passphrase.
mirim-gui
# prebuilt, per release
$ chmod +x mirim-gui-1.1.0-x86_64-linux
$ ./mirim-gui-1.1.0-x86_64-linux

# or from source, in its own crate
$ cd gui && cargo build --release
   Compiling mirim-gui v1.1.0
    Finished `release` profile [optimized]
$ ./target/release/mirim-gui

License

Dual-licensed. Pick the one that fits.

Open by default under AGPL-3.0-only. A commercial license exists for the cases the AGPL's terms do not suit.

AGPL-3.0-only open source

Use, modify and distribute freely. The network-use clause (section 13) applies: a modified mirim offered as a network service must share its source with that service's users. Declared as license = "AGPL-3.0-only".

Commercial on request

Lifts the copyleft and network-use obligations under negotiated terms, for closed-source embedding or for running a modified service without publishing source. Contact Security Ops through securityops.co.

Full terms in LICENSE and LICENSING.md. Copyright (c) 2026 Cristian Cezar Moisés.

Stable · 1.1.0

Read the source. The formats are frozen.

The vault, log, sealed-export and manifest layouts are fixed and will stay readable. 3,789 lines is a weekend of reading, and you can reproduce the binary at the end of it.