[iOS / libtacky / Disappearing Messages] Expired messages remain stored and readable through Edit History #19

Open
opened 2026-11-04 20:56:46 +00:00 by asemeniv · 1 comment
Member

[iOS / libtacky / Disappearing Messages] Expired messages remain stored and readable through Edit History

Severity: Major
Issue ID: EPH-01, EPH-02, EPH-APP
Date: November 4, 2026

Summary

AnotherIM's Disappearing Messages feature does not permanently delete expired message content from the local database.

Although messages disappear from the chat after their expiration timer, their original text remains accessible through libtacky's message revisions API, which the iOS application uses for Info → Edit history.

This affects both:

  • Sent messages: Original text remains after expiration.
  • Received messages: Read messages retain their original text after expiration.

The application promises:

New messages here disappear for everyone once the time is up after they are read.

However, libtacky defaults to preserve_history = 1, which retains expired message content as revision history. The iOS application does not override this setting.

Anyone with access to an unlocked device may therefore be able to recover supposedly disappeared conversations.

Testing confirmed:

  • Expired messages are retracted but their original text remains in local revision history.
  • The behavior affects both sent and received messages.
  • Setting preserve_history = 0 removes expired message content in the tested scenario.
  • Build 94 does not explicitly configure preserve_history.

Important: Local database retention was reproduced in automated tests. Actual visibility through the iOS Edit History interface has not yet been verified on a physical device.

Affected components:

  • iOS: AppEnvironment.swift, MessageCell.swift, ChatDetailsView.swift
  • libtacky: tacky/xmpp/modules/ephemeral.tcl, messagestore.tcl

Related issues: RT-20, RT-21 (deleted messages retained in local history).

Environment

Item Details
Application anotherim-swift 8f62071, branch feat/ui-redesign, build 94
libtacky 2f42a2f2 / v1.2.0-beta2
Test server ejabberd 26.09, anotherim-server b9e675d
Client main@example.local, libtacky application profile
Peer peer2@example.local, OMEMO-enabled contact
Message expiration 5 seconds
Configuration preserve_history unset (default: 1)
Verification Level 1 cloud-based test and source-code review

Preconditions

  1. main and peer2 are contacts with OMEMO enabled.
  2. Enable a 5-second disappearing-message timer using ephemeral set_timer, matching the application's Disappearing Messages feature.
  3. Leave preserve_history at its default value.

Steps to Reproduce

Run:

SWIFT_SRC=<anotherim-swift-build-94> SERVER_SRC=<anotherim-server> bash setup/run_all.sh run_ephemeral

  1. EPH-01 — Sent message: Have main send EPH01-<run>-own secret.
  2. Wait 9 seconds, then inspect message rows, message revisions, and messagestore archived.
  3. EPH-02 — Received message: Have peer2 send EPH02-<run>-their secret.
  4. Mark the received message as read using mark_own_read.
  5. Wait 9 seconds and inspect the same storage APIs.
  6. EPH-APP: Check whether build 94 configures preserve_history.
  7. EPH-OFF control: Repeat the sent-message scenario in a new session with preserve_history 0.

Expected Behavior

Once the expiration timer completes:

  • The message content should be removed from local storage.
  • The original text should not be recoverable through Edit History or chat search.
  • Both sent and received expired messages should be discarded.

XEP-0466 v0.1.0 §3.2 requires expired messages to be discarded, while §6 specifies best-effort removal from memory and disk.

This expectation also follows the application's Disappearing Messages description.

Actual Behavior

Run d09e839c (evidence/run_ephemeral_1.out, evidence/events_main_d09e839c) confirmed the following.

EPH-01 — Sent message

The message expired approximately 5 seconds after sending and was retracted from the active chat view.

However, after 9 seconds, message revisions still returned:

{timestamp 1793801924797082 kind original body {EPH01-d09e839c-own secret} attachments {} encryption omemo}
{timestamp 1793801929797082 kind retract body {} attachments {} encryption {}}

The original message body remains intact in the revision history.

The iOS application uses this API for Info → Edit history (MessageCell.swift:155, 237-246).

EPH-02 — Received message

After the received message was marked as read, it expired approximately 5 seconds later.

The active message was retracted, but its original text (EPH02-…-their secret) remained accessible through message revisions.

EPH-APP — Application configuration

Build 94 does not configure preserve_history in the application code.

The relevant libtacky settings are initialized in AppEnvironment.swift:404-411, leaving the default history-preservation behavior enabled.

EPH-OFF — Control test

Repeating the scenario with:

preserve_history 0

removed the expired message content from the inspected storage APIs.

This confirms that the retention is caused by the history-preservation configuration rather than a failure of the expiration timer itself.

Reproduction Rate

2/2 successful automated runs:

  • d09e839c
  • 8f7ef5be

Both runs confirmed expiration after approximately 5 seconds and continued availability of original message text through message revisions.

Root Cause

libtacky intentionally retains retracted message content when preserve_history is enabled.

  1. ephemeral.tcl:9-10, 88-99 documents that expired messages are retracted while their original content remains in history when preserve_history is enabled.
  2. messagestore.tcl:2033 sets preserve_history to 1 by default.
  3. messagestore.tcl:1250-1255 applies retraction while retaining the message row (kept 1).
  4. The iOS application does not override the default configuration (AppEnvironment.swift:404-411).
  5. MessageCell.swift:155, 237-246 exposes revision history through Info → Edit history, without an identified exception for expired messages.

Consequently, expiration hides the active message body but does not erase its original content from local storage.

A related discrepancy exists in expiration timing: libtacky expires sent messages based on their sending timestamp, while the UI description suggests expiration after reading.

Suggested Fix

Preferred approach: Disable history preservation in the iOS libtacky configuration.

Set:

preserve_history = "0"

in AppEnvironment alongside the other libtacky settings.

The EPH-OFF control test confirmed that this removes expired message content from the inspected local storage APIs.

This change also affects message retractions and cleared conversation history (RT-19a, RT-20, RT-21), where equivalent removal behavior was previously confirmed by RT-OFF tests.

Alternative: If preserving revision history is intentional, prevent expired and deleted message content from being displayed through Edit History and revise the UI description accordingly.

However, hiding revision history in the UI alone would not remove sensitive content from the local database.

Verification After Fix

  1. Enable Disappearing Messages in an OMEMO chat between two iPhones.
  2. Exchange messages in both directions and mark received messages as read.
  3. Wait for the expiration timer.
  4. Verify that expired messages disappear from the chat.
  5. Inspect Info → Edit history and confirm that original text cannot be recovered.
  6. Search the conversation for unique text from expired messages.
  7. Verify that message revisions and local storage no longer contain expired message bodies.
  8. Repeat after restarting the application.
  9. Confirm that ordinary message editing and history behavior remain consistent with the intended retention policy.
  10. Verify whether sent-message expiration timing matches the UI description.

Evidence and Status

Evidence:

  • evidence/run_ephemeral_1.out
  • evidence/run_ephemeral_2.out
  • evidence/ephemeral_<run>.json
  • evidence/events_main_<run>
  • evidence/stanzas_ephemeral_<run>.log
  • evidence/ephemeral.tcl_1-30.txt
  • evidence/ephemeral.tcl_85-100.txt
  • evidence/messagestore.tcl_1248-1256_2030-2034.txt
  • evidence/ChatDetailsView.swift_412-426.txt
  • evidence/MessageCell.swift_155-158_237-248.txt
  • evidence/AppEnvironment.swift_400-412.txt

Automated test:

autotests/metadata-audit/checks/run_ephemeral.py

Test report:

results/2026-11-04_invite_ephemeral.md, section 3.

Complete evidence archive:

_claude_tmp/evidence_invite_ephemeral_2026-11-04.tgz

Verified:

  • Original message text remains in revision history after expiration.
  • Both sent and received messages are affected.
  • Reproduction in two automated runs.
  • preserve_history 0 removes expired content in the control test.
  • Relevant source code in anotherim-swift 8f62071 and libtacky 2f42a2f2.
  • Relevant requirements in XEP-0466 §3.2 and §6.

Not verified:

  • Whether Edit History actually displays expired message text on a physical iPhone.
  • Whether chat search exposes expired content in the iOS application.
  • Whether expired messages remain in server-side MAM archives.
  • End-to-end behavior on physical devices running build 94.
# [iOS / libtacky / Disappearing Messages] Expired messages remain stored and readable through Edit History **Severity:** Major **Issue ID:** EPH-01, EPH-02, EPH-APP **Date:** November 4, 2026 ## Summary AnotherIM's **Disappearing Messages** feature does not permanently delete expired message content from the local database. Although messages disappear from the chat after their expiration timer, their original text remains accessible through libtacky's `message revisions` API, which the iOS application uses for **Info → Edit history**. This affects both: - **Sent messages:** Original text remains after expiration. - **Received messages:** Read messages retain their original text after expiration. The application promises: > New messages here disappear for everyone once the time is up after they are read. However, libtacky defaults to `preserve_history = 1`, which retains expired message content as revision history. The iOS application does not override this setting. Anyone with access to an unlocked device may therefore be able to recover supposedly disappeared conversations. **Testing confirmed:** - Expired messages are retracted but their original text remains in local revision history. - The behavior affects both sent and received messages. - Setting `preserve_history = 0` removes expired message content in the tested scenario. - Build 94 does not explicitly configure `preserve_history`. **Important:** Local database retention was reproduced in automated tests. Actual visibility through the iOS Edit History interface has not yet been verified on a physical device. **Affected components:** - iOS: `AppEnvironment.swift`, `MessageCell.swift`, `ChatDetailsView.swift` - libtacky: `tacky/xmpp/modules/ephemeral.tcl`, `messagestore.tcl` Related issues: RT-20, RT-21 (deleted messages retained in local history). ## Environment | Item | Details | |---|---| | Application | anotherim-swift `8f62071`, branch `feat/ui-redesign`, build 94 | | libtacky | `2f42a2f2` / v1.2.0-beta2 | | Test server | ejabberd 26.09, anotherim-server `b9e675d` | | Client | `main@example.local`, libtacky application profile | | Peer | `peer2@example.local`, OMEMO-enabled contact | | Message expiration | 5 seconds | | Configuration | `preserve_history` unset (default: `1`) | | Verification | Level 1 cloud-based test and source-code review | ## Preconditions 1. `main` and `peer2` are contacts with OMEMO enabled. 2. Enable a 5-second disappearing-message timer using `ephemeral set_timer`, matching the application's **Disappearing Messages** feature. 3. Leave `preserve_history` at its default value. ## Steps to Reproduce Run: `SWIFT_SRC=<anotherim-swift-build-94> SERVER_SRC=<anotherim-server> bash setup/run_all.sh run_ephemeral` 1. **EPH-01 — Sent message:** Have `main` send `EPH01-<run>-own secret`. 2. Wait 9 seconds, then inspect `message rows`, `message revisions`, and `messagestore archived`. 3. **EPH-02 — Received message:** Have `peer2` send `EPH02-<run>-their secret`. 4. Mark the received message as read using `mark_own_read`. 5. Wait 9 seconds and inspect the same storage APIs. 6. **EPH-APP:** Check whether build 94 configures `preserve_history`. 7. **EPH-OFF control:** Repeat the sent-message scenario in a new session with `preserve_history 0`. ## Expected Behavior Once the expiration timer completes: - The message content should be removed from local storage. - The original text should not be recoverable through Edit History or chat search. - Both sent and received expired messages should be discarded. XEP-0466 v0.1.0 §3.2 requires expired messages to be discarded, while §6 specifies best-effort removal from memory and disk. This expectation also follows the application's Disappearing Messages description. ## Actual Behavior Run `d09e839c` (`evidence/run_ephemeral_1.out`, `evidence/events_main_d09e839c`) confirmed the following. **EPH-01 — Sent message** The message expired approximately 5 seconds after sending and was retracted from the active chat view. However, after 9 seconds, `message revisions` still returned: ```text {timestamp 1793801924797082 kind original body {EPH01-d09e839c-own secret} attachments {} encryption omemo} {timestamp 1793801929797082 kind retract body {} attachments {} encryption {}} ``` The original message body remains intact in the revision history. The iOS application uses this API for **Info → Edit history** (`MessageCell.swift:155, 237-246`). **EPH-02 — Received message** After the received message was marked as read, it expired approximately 5 seconds later. The active message was retracted, but its original text (`EPH02-…-their secret`) remained accessible through `message revisions`. **EPH-APP — Application configuration** Build 94 does not configure `preserve_history` in the application code. The relevant libtacky settings are initialized in `AppEnvironment.swift:404-411`, leaving the default history-preservation behavior enabled. **EPH-OFF — Control test** Repeating the scenario with: `preserve_history 0` removed the expired message content from the inspected storage APIs. This confirms that the retention is caused by the history-preservation configuration rather than a failure of the expiration timer itself. ## Reproduction Rate **2/2 successful automated runs:** - `d09e839c` - `8f7ef5be` Both runs confirmed expiration after approximately 5 seconds and continued availability of original message text through `message revisions`. ## Root Cause libtacky intentionally retains retracted message content when `preserve_history` is enabled. 1. `ephemeral.tcl:9-10, 88-99` documents that expired messages are retracted while their original content remains in history when `preserve_history` is enabled. 2. `messagestore.tcl:2033` sets `preserve_history` to `1` by default. 3. `messagestore.tcl:1250-1255` applies retraction while retaining the message row (`kept 1`). 4. The iOS application does not override the default configuration (`AppEnvironment.swift:404-411`). 5. `MessageCell.swift:155, 237-246` exposes revision history through **Info → Edit history**, without an identified exception for expired messages. Consequently, expiration hides the active message body but does not erase its original content from local storage. A related discrepancy exists in expiration timing: libtacky expires sent messages based on their sending timestamp, while the UI description suggests expiration after reading. ## Suggested Fix **Preferred approach: Disable history preservation in the iOS libtacky configuration.** Set: `preserve_history = "0"` in `AppEnvironment` alongside the other libtacky settings. The EPH-OFF control test confirmed that this removes expired message content from the inspected local storage APIs. This change also affects message retractions and cleared conversation history (RT-19a, RT-20, RT-21), where equivalent removal behavior was previously confirmed by RT-OFF tests. **Alternative:** If preserving revision history is intentional, prevent expired and deleted message content from being displayed through Edit History and revise the UI description accordingly. However, hiding revision history in the UI alone would not remove sensitive content from the local database. ## Verification After Fix 1. Enable Disappearing Messages in an OMEMO chat between two iPhones. 2. Exchange messages in both directions and mark received messages as read. 3. Wait for the expiration timer. 4. Verify that expired messages disappear from the chat. 5. Inspect **Info → Edit history** and confirm that original text cannot be recovered. 6. Search the conversation for unique text from expired messages. 7. Verify that `message revisions` and local storage no longer contain expired message bodies. 8. Repeat after restarting the application. 9. Confirm that ordinary message editing and history behavior remain consistent with the intended retention policy. 10. Verify whether sent-message expiration timing matches the UI description. ## Evidence and Status Evidence: - `evidence/run_ephemeral_1.out` - `evidence/run_ephemeral_2.out` - `evidence/ephemeral_<run>.json` - `evidence/events_main_<run>` - `evidence/stanzas_ephemeral_<run>.log` - `evidence/ephemeral.tcl_1-30.txt` - `evidence/ephemeral.tcl_85-100.txt` - `evidence/messagestore.tcl_1248-1256_2030-2034.txt` - `evidence/ChatDetailsView.swift_412-426.txt` - `evidence/MessageCell.swift_155-158_237-248.txt` - `evidence/AppEnvironment.swift_400-412.txt` Automated test: `autotests/metadata-audit/checks/run_ephemeral.py` Test report: `results/2026-11-04_invite_ephemeral.md`, section 3. Complete evidence archive: `_claude_tmp/evidence_invite_ephemeral_2026-11-04.tgz` **Verified:** - Original message text remains in revision history after expiration. - Both sent and received messages are affected. - Reproduction in two automated runs. - `preserve_history 0` removes expired content in the control test. - Relevant source code in anotherim-swift `8f62071` and libtacky `2f42a2f2`. - Relevant requirements in XEP-0466 §3.2 and §6. **Not verified:** - Whether Edit History actually displays expired message text on a physical iPhone. - Whether chat search exposes expired content in the iOS application. - Whether expired messages remain in server-side MAM archives. - End-to-end behavior on physical devices running build 94.
Contributor
  • #19: Fixed in 4832765 (product decision): preserve_history = 0. Edits, deletions and expired messages keep nothing; the first launch after the update clears what earlier builds kept.
- #19: Fixed in 4832765 (product decision): preserve_history = 0. Edits, deletions and expired messages keep nothing; the first launch after the update clears what earlier builds kept.
Sign in to join this conversation.
No labels
done
fixed
returned
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
another-im/anotherim-swift#19
No description provided.