.mydt is the on-disk representation used by the Secure Files tool. Each
source file becomes one .mydt object with a random physical name; the
original name, folder, size, timestamps and contents are encrypted inside it.
The format is deliberately small so that a CLI or SDK can implement it in a few
dozen lines. Reference implementation and CLI: the mydt crate in
crates/mydt (the desktop app uses it unchanged).
- Stored representation is opaque to filesystem indexers, thumbnailers, OCR and any reader that only sees the bytes on disk.
- A single
.mydtfile is self-contained: it can be decrypted with just the password, without any database, index or sidecar. - Authenticated: any modification of any byte is detected.
- Not a container for many files, not streaming, not deniable, not a defense against a compromised machine while the vault is unlocked.
Objects are named <id>.mydt where id is 16 cryptographically random bytes
rendered as 32 lowercase hex characters (e.g. 83a91c2f….mydt). The name
carries no information about the content. Writers create <id>.mydt.tmp,
fsync, then rename it over the final name; readers delete stale .mydt.tmp
files. Nothing else in the storage folder is touched.
All integers are unsigned 32-bit little-endian.
| Offset | Length | Field |
|---|---|---|
| 0 | 4 | Magic MYDT (ASCII) |
| 4 | 1 | Version, 0x01 |
| 5 | 16 | Argon2 salt |
| 21 | 4 | Argon2 m_cost (KiB) |
| 25 | 4 | Argon2 t_cost |
| 29 | 4 | Argon2 p_cost |
| 33 | 24 | dek_nonce |
| 57 | 48 | Wrapped DEK: 32-byte key + 16-byte tag |
| 105 | 24 | meta_nonce |
| 129 | 4 | meta_len — metadata ciphertext length including its 16-byte tag |
| 133 | N | Metadata ciphertext (N = meta_len) |
| 133 + N | 24 | payload_nonce |
| 157 + N | rest | Payload ciphertext + 16-byte tag, to end of file |
The fixed header is 133 bytes. meta_len must be in [16, 65536].
- KDF: Argon2id, version 0x13, parameters from the header, 32-byte output.
The desktop app writes
m_cost = 65536(64 MiB),t_cost = 3,p_cost = 1. Input is the UTF-8 master password; the result is the KEK. - AEAD: XChaCha20-Poly1305 (24-byte nonces, 16-byte tags). Nonces are random per write; the 192-bit nonce space makes random nonces safe.
- Keys: each file has its own random 32-byte DEK. The KEK only ever encrypts DEKs, so rotating the password requires rewrapping one 48-byte block per file, not re-encrypting payloads.
KEK = Argon2id(password, salt, m_cost, t_cost, p_cost)
wrapped_dek = XChaCha20Poly1305(KEK, dek_nonce, DEK, aad = bytes[0..33])
meta_ct = XChaCha20Poly1305(DEK, meta_nonce, meta_json, aad = bytes[0..133])
payload_ct = XChaCha20Poly1305(DEK, payload_nonce, plaintext, aad = bytes[0..133])
The associated data binds the KDF parameters to the DEK wrap, and the whole
fixed header (including meta_len) to both the metadata and the payload.
Swapping headers, nonces or lengths between files therefore fails
authentication.
meta_json is a UTF-8 JSON object:
{ "name": "secrets.env", "dir": "proj/config", "size": 1432,
"mtime": 1755820000000, "importedAt": 1755820123456 }name— original file name, no path separators.dir— logical folder as/-separated segments,""for the root. Folders are derived from this field; there are no folder objects.size— plaintext length in bytes.mtime,importedAt— epoch milliseconds.
Unknown fields must be ignored by readers so the object can grow without a
version bump. MIME type is not stored; readers derive it from name.
- Check magic and version; reject anything else.
- Read the KDF block, derive (or reuse a cached) KEK. All files in one storage folder share the same salt, so the KEK is derived once per unlock and a listing never runs Argon2.
- Unwrap the DEK with
aad = bytes[0..33]. Failure means wrong password, foreign salt, or tampering — readers should not distinguish beyond that. - Decrypt
meta_ctwithaad = bytes[0..133]. Listing stops here: only133 + meta_lenbytes need to be read from disk. - Decrypt the payload with the same AAD.
A reader must treat every length field as untrusted and bound what it reads
(MAX_META_BYTES = 64 KiB, payload cap 20 MiB in the desktop app).
Every write — import, rename, move, replace — produces a fresh DEK and fresh nonces and rewrites the whole object atomically. Rewriting in place is never done.
- Existence of the folder, number of objects, approximate plaintext size
(ciphertext is plaintext + 157 +
meta_lenbytes), filesystem timestamps. - KDF parameters and salt (public by design).
- The storage folder path, stored in the app's SQLCipher database.
Nothing else: names, extensions, folder structure, MIME types and contents are all inside the AEAD envelope.
The version byte is bumped only for incompatible layout changes. Readers must reject unknown versions; writers must never downgrade an object. KDF parameters are per file, so they can be changed without a version bump.
Protects stored data at rest against anything that can only read the storage folder: indexers, backups, other apps, a copied drive. Does not protect against malware or a privileged process on a machine where the vault is unlocked, against screen capture during preview, or against a keylogger. See the PRD's security section for the full table.