Messaging Mechanics

1-on-1 Lanes & Byte-Borrowing

Send pipelines, optimistic bubbles, failed message refunds, and disjoint keystream borrowing.

Overview

Wiltkey 1-on-1 conversations are structured as a pair of unidirectional lanes sliced out of a single shared keystream.pad file. The initiator of the chat writes outgoing messages to the lower half (offset 0 to size/2) and reads incoming messages from the upper half (offset size/2 to size). The responder writes and reads from the inverse segments.

Because these partitions are rigid, if one user sends significantly more data than the other, their sending lane will deplete (wilt) first, even if the other lane has megabytes of unused capacity. To solve this, Wiltkey implements keystream byte-borrowing, allowing a sender to claim disjoint fragments of the peer's unused sending space.

Keystream Partition Model

Initiator Sending Lane (0 - 5MB) Responder Sending Lane (5MB - 10MB) outgoingOffset outgoingMaxOffset (Ceiling) LENT RANGE

The Send Pipeline

  1. Capacity & Tier Size Check: The client checks remaining keystream bytes and validates file payload size against the user's tier (5 MB for free accounts, 50 MB for WiltKey Plus). If sending an image, the user can select compression quality (low/medium/high) or choose Uncompressed to transmit full-resolution original bytes untouched. If the primary sending lane has insufficient bytes (< 74 bytes), the client scans additionalSlots for a borrowed range. If none exists, it triggers a requestBorrow call and fails the send.
  2. Synchronous Pointer Reservation: To prevent race conditions where a rapid sequence of taps triggers multiple messages consuming the same offset, the client advances the offset pointer synchronously in RAM before starting any async operations.
  3. Optimistic Placeholder Bubble: A message bubble is added instantly to the UI window with isPending = true. Large message bodies are automatically written as local sidecar files (messages.media_path) to keep the SQLite row small and prevent memory crashes on image-heavy histories.
  4. XOR, Socket Transmission & On-Demand Delivery: The client performs the XOR operation on the plaintext bytes and saves the message record. For small text messages, ciphertext envelopes stream directly over WebSocket. For large media files (≥ 500 KB), the relay holds the object and delivers a lightweight FILE_OFFER frame. Recipients tap the message in chat to initiate download (sending REQUEST_FILE → receiving FILE_TOKEN → streaming via HTTP GET /api/v1/file), and the relay deletes the file only after a FILE_RECEIVED acknowledgment is returned.

Failed Message Refund

When a message send fails (or if the user deletes a pending/failed bubble), Wiltkey recovers the key bytes. If the deleted message's offset matches the tail pointer (i.e. offset + payloadBytes == outgoingOffset), it rolls the pointer back to the message's offset. This prevents gaps in the keystream so that capacity is not permanently lost due to simple connectivity issues.

Byte-Borrowing Mechanics

If a sender's space drops below 500 bytes (proactive top-up) or 74 bytes (hard block), a borrow_request is sent to the peer. The peer divides their own remaining unused sending lane in half:

grant = unused / 2
The peer lowers their sending ceiling (outgoingMaxOffset -= grant) and sends a borrow_grant containing the range [start, end). The borrower registers this as a triple [start, ptr=start, end] in additionalSlots, expanding their sending capacity.

Delivery Reconciliation & Resync

Each delivered message normally fires a one-shot delivery_receipt back to the sender (single check → double check). That receipt is queued on the relay for 24h if the sender is offline — so a sender that stays closed past the TTL can be left showing a permanent single check even though the peer received the message. Symmetrically, the receiver only auto-pulls missing history when a newer message arrives to expose the gap (offset > incomingOffset_requestChatResync); a gap with nothing after it stays unfilled.

syncOneOnOneChat() reconciles both directions on demand. It is wired to a sync button in the chat header (always tappable; its icon switches from sync to the sync_problem glyph — attention via shape, not colour, so it reads on both the dark and light themes — when a sent message sits on a single check while a later one already double-checked) and runs automatically in two more cases when that stuck-delivery (or inbound-gap) signal is present: once on chat open, and again whenever a peer's message lands while the app is open (maybeAutoReconcileOnPeerMessage, fired from the inbound path). The peer's arrival proves they're reachable, so it's the ideal moment to answer our pending delivery_check — the user no longer has to tap Sync. It's guarded so it only fires when there's actually something to reconcile, and a per-chat in-flight lock coalesces a burst of arrivals into a single sync, so it's never a loop or relay spam.

  • Inbound pull: requests a resync for the whole range past our contiguous incomingOffset, recovering any messages we never received.
  • Outbound check: sends a delivery_check listing our undelivered sent messages ({id, offset}). The peer answers delivery_check_response with confirmed (offsets it holds → we flip those to delivered) and missing (offsets it lacks → we resend them the normal way, which yields a fresh receipt).

Both delivery_check frames carry only metadata (ids/offsets); resends reuse the already-encrypted OTP ciphertext, so nothing is exposed in plaintext to the relay.

ℹ️ CONVERGES ON FOREGROUND, NOT ON SIMULTANEOUS PRESENCE
The peer does not need to be online at the moment you trigger a sync. The relay store-and-forwards delivery_check and its response with a 24h TTL, and the background isolate buffers these frames but cannot act on them (no master key). The check is therefore answered — and the response applied — when each side next brings the app to the foreground, where the buffer is drained (on unlock or resume). Net effect: reconciliation converges on the next foreground of each side, not on both apps being open at the same time. See Notifications & Background Modes.

Message Editing & Deletion (Zero Keystream Reuse)

Because one-time pad keystream material cannot be re-encrypted or overwritten without catastrophic cryptographic failure, Wiltkey implements editing and soft-deletion via forward references:

  • Edit Operations: When a sender edits a message, the new text is encrypted into the next available pad offset. The envelope carries edit_target_id referencing the original message. In SQLite, the original message's edited_by_id column is updated with the new ID. When rendering history, the client automatically displays the latest edited content in the original visual bubble.
  • Soft Deletion: Deleting a message dispatches an edit payload replacing the body with "[Message removed]" and flags it as deleted. For media and voice notes, deletion sends an immediate remote and local cache wipe.

Emergency Time Wilt Chats (Destructive Remote Handshake)

When two contacts have exhausted their physical one-time pad budget and cannot meet for BLE recharging, either party can initiate an Emergency Chat from the contact profile screen:

  1. Destructive Session Replacement: Initiating an emergency chat purges the local depleted or archived 1:1 chat pad and instantiates a 24-hour Time Wilt stream cipher session.
  2. Handshake Frame: The initiator dispatches an encrypted emergency_chat control frame carrying the new session's parameters and ephemeral key.
  3. Pending State Lock: On the dashboard, the newly generated chat tile is rendered in a locked/pending state (disabled for sending) until the peer responds.
  4. Peer Acknowledgment: When the peer receives emergency_chat, their local app clears the old chat, mirrors the Time Wilt parameters, and returns an emergency_chat_ack control frame. Both sides unlock the chat simultaneously.

Smart Gap Auditing & Clean Points

When messages are refunded or dropped due to network interruptions, a permanent offset gap can occur in the message sequence. Rather than leaving the chat stuck with a persistent warning icon, Wiltkey employs intelligent gap verification:

  • Continuous Gap Auditing (auditAndSync1on1Gaps): The client scans contiguous message offset ranges across the incoming lane $[0, \text{incomingOffset})$. When an offset gap is detected, a targeted chat_resync_request is sent for that specific missing range.
  • Clean Gap Cache (cleanGapAuditCache): When the peer returns a chat_resync_response with no payload (confirming the gap was an abandoned/refunded send that never produced ciphertext), the range is cached as verified clean. The warning sync indicator automatically clears without requiring full pad burns.

Dynamic Voice Recording & Hands-Free Lock

Voice messages are encrypted directly into one-time pad keystream material (or on-demand stream cipher for Time Wilt chats):

  • Dynamic Duration Budgeting: The app dynamically calculates the maximum allowable recording duration before starting, based on available contact buffer bytes, relay payload envelope limits (5MB free, 50MB Plus), and the active audio quality bitrate (Lo-fi 12kbps, Voice 24kbps, Clear 32kbps).
  • Auto-Stop Buffer Guard: Recording automatically finishes 1 second before overrunning the chat's remaining byte budget.
  • Hands-Free Slide-to-Lock: Dragging the mic button upwards (ΔY < -45px) latches into locked mode with tactile haptic feedback, revealing a dedicated cancel/trash button on the left and a prominent Send button on the right.
  • Slide-to-Cancel: Dragging left (ΔX < -55px) arms the cancel trigger, discarding the recording on release.

Key Files & Symbols

File Path Symbol Name Description
lib/core/state_chats.dart sendMessage() Handles offset checks, reserves offsets, creates pending bubbles, and transmits envelopes.
lib/core/state_chats.dart auditAndSync1on1Gaps() Scans SQLite for missing offset gaps and verifies unrecorded pad holes.
lib/features/chat/…/voice_recording_mixin.dart VoiceRecordingMixin Encapsulates dynamic budget caps, slide-to-lock, slide-to-cancel gestures, and HUD.
lib/core/state_chats.dart editMessage() Encrypts replacement text into advancing pad offset with edit_target_id forward link.
lib/core/state_contacts.dart startEmergencyChat() Destructively creates an ephemeral Time Wilt session and sends emergency_chat handshake.
lib/core/state_chats.dart clearFailedMessage() Wipes a failed bubble and rolls back pointers to refund keystream.
lib/core/state_chats.dart syncOneOnOneChat() Manual/one-shot reconciliation: pulls missing inbound history and sends a delivery_check for undelivered outbound messages. Backs the header sync button.
lib/core/state_chats.dart maybeAutoReconcileOnPeerMessage() Fires syncOneOnOneChat automatically when a peer message arrives and there's an inbound gap or stuck-undelivered send. Guarded (only when needed) and per-chat in-flight-locked, so a burst of arrivals coalesces into one sync.
lib/core/state_inbound.dart _handleDeliveryCheck() / _handleDeliveryCheckResponse() Peer confirms/flags which listed messages it holds; sender marks confirmed ones delivered and resends the missing ones.
lib/core/state_borrow.dart AppStateBorrow Extension coordinating byte-borrowing requests, grants, and offset mapping.
lib/core/state_borrow.dart handleBorrowRequest() Calculates grant size, shrinks local outgoing ceiling, and transmits borrow_grant envelope.

Gotchas & Edge Cases

⚠️ NO OUT-OF-ORDER REFUNDS
Refunds only work if the message being cleared is at the head of the sending boundary. If a user deletes an older failed message while newer messages have already advanced the pointer, the refund cannot roll back the pointer (to avoid double-allocation), resulting in a dead hole in the pad.