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 contains 5 tables. Key details:
contacts: Stores unified 1:1 and group profiles, offsets, and slot information.messages: Stores message IDs, timestamps, content type, raw OTP ciphertext (text_otp), and local master-encrypted plaintext (text_encrypted_master).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) 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. |
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.