Documentation
Everything needed to add mdc-lite to an app: what it is, how to install it, and exactly what its encryption guarantees do and don't cover.
Installation
mdc-lite ships as a native library per platform, plus one shared C header (mdc_lite.h). There's no package manager integration yet — add the library and header to your project directly.
| Platform | What you add |
|---|---|
| Rust | Add the crate as a path or git dependency and use mdc_lite::LiteStore; directly — no FFI needed. |
| Swift (iOS/watchOS) | Combine the per-architecture .a files into an XCFramework, add mdc_lite.h as its bridging header. See Building → iOS & watchOS. |
| Kotlin (Android/Wear OS) | Drop each libmdc_lite.so into app/src/main/jniLibs/<abi>/, declare the five functions via external fun + System.loadLibrary("mdc_lite"). |
| C/C++ | Link against the platform's static or dynamic library and #include "mdc_lite.h" directly. |
Quickstart
The entire API is five operations. Opening a store is cheap — it just resolves a directory path and holds your key in memory, so there's no connection pool or background thread to manage.
use mdc_lite::LiteStore;
let key: [u8; 32] = /* from platform secure storage - see "Platform key custody" */;
let store = LiteStore::open("/path/to/store/dir", key)?;
store.put("diary/2026-08-24", b"went for a run, felt good")?;
let value = store.get("diary/2026-08-24")?;
store.exists("diary/2026-08-24"); // -> bool
let all_keys = store.list_keys()?; // -> Vec<String>
store.delete("diary/2026-08-24")?;
Through the C ABI, the same five calls:
#include "mdc_lite.h"
MdcLiteStore *store = mdc_lite_open("/path/to/store", key_ptr);
mdc_lite_put(store, "diary/2026-08-24", value_ptr, value_len);
uint8_t *out_ptr; size_t out_len;
mdc_lite_get(store, "diary/2026-08-24", &out_ptr, &out_len);
// ... read out_ptr[0..out_len) ...
mdc_lite_free_buffer(out_ptr, out_len); // always release what mdc_lite_get returns
mdc_lite_delete(store, "diary/2026-08-24");
mdc_lite_close(store);
API reference
| Rust | C | Behavior |
|---|---|---|
LiteStore::open(dir, key) | mdc_lite_open | Creates the store directory if needed. Cheap — no I/O beyond that until the first operation. |
.put(key, value) | mdc_lite_put | Encrypts and writes, overwriting any existing entry for that key. Writes via a temp file + rename, so a kill mid-write never leaves a corrupted entry. |
.get(key) | mdc_lite_get | Decrypts and returns the value. Fails loud (a distinct error, never silently wrong bytes) if the key is wrong or the file was tampered with. |
.exists(key) | mdc_lite_exists | A file-existence check — doesn't decrypt anything. |
.delete(key) | mdc_lite_delete | Idempotent — deleting a key that isn't there is not an error. |
.list_keys() | — | Every logical key currently stored. Skips any entry it can't decrypt rather than failing the whole call — see Security model. |
On-disk format
One file per entry, named by a keyed BLAKE3 hash of the logical key (hex-encoded) — so the filename itself never reveals what you stored. The encrypted record is:
[ 24-byte nonce ][ ciphertext ][ 16-byte auth tag ]
The plaintext sealed inside that ciphertext is:
[ u16 LE key_len ][ key_bytes ][ value_bytes ]
That record is then DNA-encoded before it's written — 2 bits per base, the same mapping MDC Platform's archive tier uses:
00 → A 01 → C 10 → G 11 → T
So the file that actually lands on disk is ACGT text, not raw binary. This wraps the ciphertext, never plaintext — the 2-bit mapping above is public (you're reading it), so encoding plaintext directly would only be obfuscation; encryption is what actually makes an entry unreadable without the key. get() reverses both steps: decode the base sequence back to the encrypted record, then decrypt.
The logical key travels inside the encrypted payload rather than the filename — that's what lets list_keys() recover key names after decrypting, while a raw directory listing on its own reveals nothing (now literally just base-letter text either way).
Breaking change from v1.1.0 and earlier: those versions wrote raw binary, not DNA-encoded text. A v1.1.0 store is not readable by v1.2.0+, and vice versa — there's no migration path for this prototype-stage format change.
Security model
Every value and every key name is encrypted with XChaCha20-Poly1305 — a 256-bit-key, 192-bit-nonce authenticated cipher. A fresh random nonce is generated for every write; XChaCha20's 192-bit nonce space makes that safe with no persistent counter to manage, unlike AES-GCM's narrower 96-bit nonce. The result is then DNA-encoded (see On-disk format) — that step is for storage consistency with MDC Platform, not additional protection; the encryption above is what actually secures an entry.
This library never generates, stores, or has any opinion about the key's custody. Losing it is equivalent to losing the data — there's no recovery path, by design. See Platform key custody for how the key itself should actually be obtained and stored.
On "quantum encryption"
Real quantum key distribution (QKD) transmits photons over dedicated fiber or free-space links between two fixed endpoints using specialized hardware — it's a network-link technology, not something deployable inside an app.
What actually matters for data sitting on a device is symmetric key strength. Grover's algorithm — the relevant quantum speedup — only halves an n-bit key's effective strength, so 256 bits (what XChaCha20-Poly1305 and AES-256 both use) stays enormous. Quantum computers meaningfully threaten asymmetric crypto instead (RSA/ECC, via Shor's algorithm), which this library doesn't use anywhere — there's no key exchange happening here, since this is a local, standalone store.
Two deliberately different failure modes
get(key) is a specific lookup — if it can't be decrypted (wrong key, tampering, corruption), that's meaningful signal, so it fails loud. list_keys() is an enumeration — letting one bad or foreign file hide every other valid key would be a worse outcome on a device where you generally want the app to keep working, so it silently skips anything it can't decrypt.
Platform key custody
Not part of this crate by design — real key security on a phone or watch comes from the platform's own secure hardware. These sketches are illustrative starting points, not copy-paste-ready code.
iOS / watchOS — Keychain
import Security
func getOrCreateStoreKey() -> Data {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "mdc-lite-store-key",
kSecReturnData as String: true,
]
var item: AnyObject?
if SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess {
return item as! Data
}
var keyBytes = Data(count: 32)
_ = keyBytes.withUnsafeMutableBytes {
SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!)
}
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "mdc-lite-store-key",
kSecValueData as String: keyBytes,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
]
SecItemAdd(addQuery as CFDictionary, nil)
return keyBytes
}
Android / Wear OS — Keystore-backed EncryptedSharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
fun getOrCreateStoreKey(context: Context): ByteArray {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val prefs = EncryptedSharedPreferences.create(
context, "mdc_lite_key_prefs", masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
prefs.getString("store_key", null)?.let { return Base64.decode(it, Base64.NO_WRAP) }
val key = ByteArray(32).also { SecureRandom().nextBytes(it) }
prefs.edit().putString("store_key", Base64.encodeToString(key, Base64.NO_WRAP)).apply()
return key
}
Building for iOS & watchOS
rustup target add aarch64-apple-ios aarch64-apple-ios-sim \
aarch64-apple-watchos aarch64-apple-watchos-sim
cargo build --release --target aarch64-apple-ios # device
cargo build --release --target aarch64-apple-ios-sim # Apple Silicon simulator
cargo build --release --target aarch64-apple-watchos # watch device
cargo build --release --target aarch64-apple-watchos-sim # watch simulator
watchOS targets are Rust Tier 3 (less mature than iOS's) — some toolchain versions need -Z build-std on nightly to build core/alloc for them. Combine the resulting .a files into an XCFramework with xcodebuild -create-xcframework, and use mdc_lite.h as its bridging header.
Building for Windows
Verified in this project's own build environment — no Visual Studio required, just mingw-w64:
rustup target add x86_64-pc-windows-gnu
brew install mingw-w64 # or your platform's mingw-w64 package
cargo build --release --target x86_64-pc-windows-gnu
Produces mdc_lite.dll plus libmdc_lite.dll.a (the import library to link against). It's a standard C ABI DLL — loadable from an MSVC-built consumer too, since the PE/C-ABI boundary doesn't care which compiler produced it.
Building for Android & Wear OS
Verified end-to-end for all three ABIs below using the real Android NDK:
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
brew install --cask android-ndk
export ANDROID_NDK_HOME="/opt/homebrew/share/android-ndk"
NDK_BIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin"
# API level 24 (Android 7.0+) - change the version suffix for a
# different minSdkVersion.
export CC_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang"
export AR_aarch64_linux_android="$NDK_BIN/llvm-ar"
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_BIN/aarch64-linux-android24-clang"
export CC_armv7_linux_androideabi="$NDK_BIN/armv7a-linux-androideabi24-clang"
export AR_armv7_linux_androideabi="$NDK_BIN/llvm-ar"
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="$NDK_BIN/armv7a-linux-androideabi24-clang"
export CC_x86_64_linux_android="$NDK_BIN/x86_64-linux-android24-clang"
export AR_x86_64_linux_android="$NDK_BIN/llvm-ar"
export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="$NDK_BIN/x86_64-linux-android24-clang"
cargo build --release --target aarch64-linux-android # arm64-v8a
cargo build --release --target armv7-linux-androideabi # armeabi-v7a
cargo build --release --target x86_64-linux-android # emulator
CC_*/AR_*, and following those instructions exactly reproduces a real build failure, not a working one. CC_*/AR_* fixes the BLAKE3 dependency's own C build script (ToolNotFound without them). CARGO_TARGET_<TRIPLE>_LINKER is a separate variable cargo itself reads - without it, cargo's default linker resolution for these targets can fall through to whatever plain clang/ld is on PATH (on macOS, Apple's own linker), which fails with a confusing ld: unknown options: --version-script=... error since Apple's linker doesn't understand the ELF-target flags rustc passes it.Copy each resulting .so into jniLibs/<abi>/ — Wear OS runs on Android, so the same outputs work for a watch face the same way they do for a phone app.
What this deliberately is not
- No query language, no schema, no filters — a raw encrypted key-value store. Layer your own indexing on top if you need one.
- No compaction or space reclamation beyond deleting a file on
delete()— no background process on a battery/resource-constrained device. - No multi-device sync, and no network code at all.
- No built-in key rotation — re-
put()everything under a newLiteStoreopened with a new key, then delete the old store directory.