Core Architecture
Persistence & SQLite DB
Encryption at rest, SQLite tables, KDF key derivation, and metadata caches.
Overview
Wiltkey implements strict encryption at rest to protect chat histories if a physical device is seized. Plaintext messages are never stored in the database. Instead, they are encrypted with a 256-bit AES master key derived on-the-fly from the user's PIN. When the app is locked or closed, the master key is purged from RAM, making the database undecryptable.
How it Works
-
Master Key Derivation (KDF):
When a user enters their PIN, the app derives the master key via a custom key derivation function (KDF) that hashes the concatenated PIN + Salt with 5000 iterations of SHA-256:
lib/core/state_auth.dart DART
static String deriveKey(String pin, String saltHex) { List<int> key = utf8.encode(pin + saltHex); for (int i = 0; i < 5000; i++) { key = sha256.convert(key).bytes; } return hexEncode(key); } -
AES-256-CBC Encryption at Rest:
Wiltkey uses AES-256 in Cipher Block Chaining (CBC) mode with a secure random 16-byte initialization vector (IV) prefixed to the ciphertext (format:
iv_base64:ciphertext_base64). The derived key encrypts the message payload before it is written to the SQLite column:text_encrypted_master = AES_Encrypt(plaintext, key=masterKeyHex) -
SQLite DB Schema:
The SQLite database (currently version 13) contains 5 tables. Key details:
contacts: Stores unified 1:1 and group profiles, offsets, slot information, and equipped avatar borders (DB v10).messages: Stores message IDs, timestamps, content type, raw OTP ciphertext (text_otp), local master-encrypted plaintext (text_encrypted_master), sidecar media paths for large bodies (DB v12), and pending on-demand remote file references (DB v13).group_info,group_lanes,group_profiles: Store shared-pad lane slot indices and peer arrival orders.group_profilesalso caches each member's broadcast profile (name, avatar, and equipped avatar-border id, DB v11) received over the full-meshgroup_member_profilechannel.- A member's equipped avatar-border id is a local-only cosmetic that rides the profile channels alongside the avatar: stored on
contacts.avatar_borderfor 1:1 peers (DB v10) andgroup_profiles.avatar_borderfor group members (DB v11). It never touches the OTP pad and renders for everyone (a peer's border always shows; ownership only gates whether you may equip it).
SQLite Schema Fields Reference
| Table | Field | Type | Description |
|---|---|---|---|
contacts |
is_archived |
INTEGER | Soft-nuke indicator (1 = archived read-only state). Added in DB version 2. |
contacts |
is_pinned |
INTEGER | Float to top of list flag. Added in DB version 3. |
messages |
text_otp |
TEXT | Base64 encoded ciphertext as received over the wire. |
messages |
text_encrypted_master |
TEXT | AES-256-CBC encrypted plaintext stashed locally. Null for unopened background messages. |
messages |
is_failed |
INTEGER | 1 when a message send failed (checked for auto-refunds on launch). Added in version 5. |
messages |
reactions |
TEXT | JSON {token: [reactorId, ...]} of emoji reactions, synced on the AES meta channel. Added in version 6. See Message Reactions. |
messages |
allow_save |
INTEGER | Sender opt-in letting the recipient save an image to their gallery (rides frame metadata dl, not the pad). Added in version 7. |
messages |
ephemeral / ttl_seconds |
INTEGER | Wilting-message config: ephemeral=1 marks a disappearing message, ttl_seconds its lifetime once opened. Rides the OTP envelope as eph/ttl. Added in version 8. See Wilting Messages. |
messages |
opened_at / expires_at |
INTEGER | Recipient-local wilt timing (epoch ms): stamped on first reveal; the message wilts at expires_at. Both null until opened. Added in version 8. |
messages |
wilted |
INTEGER | 1 once destroyed — text_otp + text_encrypted_master are blanked in place and offset scrambled, so no recoverable body remains. Added in version 8. |
messages |
wilted_by |
TEXT | JSON list of peers who confirmed they wilted their copy (sender-side "n/m wilted" tally), synced on the AES meta channel like reactions. Added in version 8. |
contacts / group_profiles |
avatar_border |
TEXT | Equipped cosmetic avatar-border identifier. Added in version 10 (contacts) and version 11 (group_profiles). |
messages |
media_path |
TEXT | Local filesystem path for offloading large encrypted message payloads as sidecar files, bypassing SQLite row size and CursorWindow memory limits. Added in version 12. |
messages |
remote_file_id / remote_size |
TEXT / INTEGER | Remote object storage identifier and payload size for tracking pending on-demand downloads waiting on the relay. Added in version 13. |
Gotchas & Edge Cases
⚠️ THE UNOPENED MESSAGE GAP
Messages arriving in the background while the device is locked exist only as
Messages arriving in the background while the device is locked exist only as
text_otp ciphertext (since the master key is not in RAM to encrypt them for at-rest storage). If the OTP pad is deleted or regenerated before the user unlocks and opens the chat, these messages become permanently undecryptable.
🧹 MESSAGE HISTORY RETENTION & PRUNING
Starting in v1.3.5, WiltKey allows users to configure automatic message history retention limits (e.g. keep last 100, 250, 500, or 1000 messages) or prune local history manually via
Security Invariant: Pruning removes message rows and unlinks sidecar media files on disk, but never alters or rolls back OTP pad write offsets, keystream generation seeds, or contact associations. Encryption state remains monotonically advancing and completely uninterrupted.
Starting in v1.3.5, WiltKey allows users to configure automatic message history retention limits (e.g. keep last 100, 250, 500, or 1000 messages) or prune local history manually via
pruneChatMessages() and deleteMessagesForChat().
Security Invariant: Pruning removes message rows and unlinks sidecar media files on disk, but never alters or rolls back OTP pad write offsets, keystream generation seeds, or contact associations. Encryption state remains monotonically advancing and completely uninterrupted.