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.
secrets
access_log
7bd0 228a
1655 e27f
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.
| Area | Change |
|---|---|
| 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.
| Platform | File | Size | Install 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.gzCLI, 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. |
# 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
# 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.
-- 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 KEYandUNIQUE, each carrying an O(1) equality index. WHEREwith AND-chained comparisons andIS [NOT] NULL.ORDER BY, multi-key and stable. NULL sorts first under ASC, last under DESC.LIMIT,OFFSETandCOUNT(*).?parameter binding. Bound values are data, never SQL.- Line and block comments.
- Deliberately absent
- Joins and sub-queries.
GROUP BY,OR, and every aggregate exceptCOUNT(*).- 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.
| Workload | mirim 1.1.0 | SQLite | Reading |
|---|---|---|---|
| durable commit, 1 statement, fsynced | 578 µs | 782 µ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 ms | 3.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 ms | 33.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 µs | 0.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 ms | none | 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.
| Dimension | mirim 1.1 | SQLite | SQLCipher | DuckDB 1.4+ | libSQL / Turso | redb / sled |
|---|---|---|---|---|---|---|
| Kind | Embedded SQL | Embedded SQL | Embedded SQL, encrypted | Embedded analytical SQL | Embedded SQL, SQLite fork | Embedded key-value |
| Implementation | Safe Rust, forbid(unsafe_code) | C | C, SQLite plus extension | C++ | C and Rust | Rust |
| Encrypted at rest | Yes, default and only mode | No, SEE is a paid add-on | Yes | Yes, since 1.4 (Sept 2025) | Yes | No |
| At-rest cipher | XChaCha20-Poly1305, whole snapshot | none | AES-256-CBC + HMAC-SHA512, per page | AES-256-GCM or CTR, per page | AEGIS, AES-GCM or ChaCha20-Poly1305, per page | none |
| Passphrase KDF | Argon2id, memory-hard (64 MiB, t=3) | none | PBKDF2-HMAC-SHA512, 256k iterations | raw key | raw key | none |
| Post-quantum | ML-KEM-768 sealed exports, ML-DSA-87 signed manifests | No | No | No | No | No |
| Classical RSA or ECC in the file formats | None | n/a | n/a | n/a | n/a | n/a |
| Crash durability | WAL, fsync before ack, SIGKILL-tested | WAL or rollback journal | SQLite's | WAL | WAL | Yes, log or copy-on-write |
| SQL surface | Minimal subset | Full SQL | Full SQL | Rich analytical SQL | Full SQL | None, key-value API |
| Data bigger than RAM | No | Yes | Yes | Yes | Yes | Yes |
| Concurrency | Single writer, single process | Multi-reader, single-writer | SQLite's | Single process | Multi, with replication | MVCC (redb) |
| Footprint | 477 KiB CLI, crypto included | under 900 KiB library | SQLite plus crypto | tens of MB | several MB | small |
| Reproducible build, SBOM, signed release | All three | no | no | no | no | no |
| License | AGPL-3.0-only or commercial | Public domain | BSD-style | MIT | MIT | MIT / 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.
| Role | Primitive | Parameters | Standard |
|---|---|---|---|
| Key encapsulation | ML-KEM-768 | 1088-byte ciphertext, implicit rejection | FIPS 203 |
| Signatures | ML-DSA-87 | 4627-byte signature, sign feature | FIPS 204 |
| AEAD | XChaCha20-Poly1305 | 24-byte nonce, header bound as associated data | RFC 8439 and extension |
| Passphrase KDF | Argon2id | 64 MiB, 3 passes, 4 lanes | RFC 9106 |
| Key derivation | HKDF-SHA256 | domain-separated info string per format | RFC 5869 |
| Hashing | SHA-256 | snapshot identity, manifest target digest | FIPS 180-4 |
| Randomness | OS CSPRNG | via getrandom | platform |
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.
| Artifact | Magic | Version | Layout |
|---|---|---|---|
| Vault | MIRIM1\0\0 | v1 | 50-byte header (magic, version, KDF id, salt, nonce), then the AEAD ciphertext of the snapshot. File extension .mrm. |
| Sealed export, one recipient | MIRIMSL1 | v1 | 1088-byte ML-KEM-768 ciphertext, 24-byte nonce, then the AEAD payload. |
| Sealed export, many recipients | MIRIMSL1 | v2 | 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 log | MIRIMWL1 | v1 | 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 manifest | MIRIMMF1 | v1 | 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 file | MIRIMTS1 | v1 | 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 auditandcargo denyon every push.
$ 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
.mrmwithout 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
flockis 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.
# 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.