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

  1. 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);
    }
  2. 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)
  3. 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_profiles also caches each member's broadcast profile (name, avatar, and equipped avatar-border id) received over the full-mesh group_member_profile channel.
    • A member's equipped avatar-border id is a local-only cosmetic that rides the profile channels alongside the avatar: stored on contacts.avatar_border for 1:1 peers (DB v10) and group_profiles.avatar_border for 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 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.