uttrflow / uttrflow/uttrflow-swift

Retention trusts the wall clock with no guard: a dictation dated in the future outlives the promise, and one read at a clock that jumped ahead deletes the whole history

Open
#613 0 comments 0 reactions 0 assignees View on GitHub
area:general bug P2
Dominant language
Swift
Stars
4
Forks
17
Avg merge
3h 32m
Merged PRs (30d)
277

Description

## What happens

Every store decides "deleted after N days" from the record's stored date against `Date()` at the time of reading, and applies the answer to the disk on read:

- History: `DictationRecord.survives(days:now:)` is `when + days * 86_400 > now` (`Sources/UttrflowHistory/DictationRecord.swift:54-57`), and `DictationHistoryStore.records(keeping:)` rewrites the file whenever that drops a record (`Sources/UttrflowHistory/DictationHistoryStore.swift:38-43`). The main window calls it on every refresh (`Sources/Uttrflow/AppDelegate.swift:1369`).
- Clipboard: `ClipboardStore.survives` is the same arithmetic on `copiedAt` (`Sources/UttrflowClipboard/ClipboardStore.swift:374-382`), applied on read by `clips(keeping:)`.
- Recordings: `RecordingStore.waiting(now:)` keeps a file while `now.timeIntervalSince(creationDate) < 24 h` and deletes the rest (`Sources/UttrflowAudio/RecordingStore.swift:103-107`).

Two consequences, both measured:

1. **Clock set back.** A dictation made while the clock read a year ahead is kept for a year plus the window after the clock is corrected. A negative age also passes the recordings check, so a recording dated ahead is never pruned until the clock catches up.
2. **Clock jumps ahead once.** A single refresh while the clock is wrong deletes every record older than the window from disk. When the clock comes back, the history is gone.

## Why it matters

The retention setting is a privacy promise ("deleted after N days"), and #2 is irreversible data loss triggered by something outside the app: a manual date change, a Mac whose clock is wrong after a dead battery before the network syncs, a test of another app. The first is the promise quietly not kept for clips that can include a secret.

## How to reproduce (headless)

A throwaway test in `Tests/UttrflowHistoryTests/DictationHistoryStoreTests.swift` with its `Sandbox`:

- Seed `[record dated epoch + 365 days, record dated epoch − 1 day]`, read with `Retention(days: 7, now: epoch + 30 days)`: the future-dated record is still returned.
- Seed `[record dated epoch − 1 day]`, read once with `Retention(days: 7, now: epoch + 400 days)`, then read with `Retention(days: 7, now: epoch)`: the result is `[]` and the file is gone.

Both on main at 26d7bc1.

## Acceptance criteria

- A record dated after `now` (beyond a small tolerance) is treated as dated `now` for retention, in all three stores, so it cannot outlive the window.
- Reading does not permanently delete on the strength of a clock that moved far forward in one step; for example the read filters what is shown but leaves the disk tidy-up to a write, or refuses to tidy when `now` is implausibly far past the newest record.
- Tests cover both directions for history, clips and recordings.

Contributor guide

Open the contributing guide

Research direction

Start with the retention paths named in Sources/UttrflowHistory/DictationRecord.swift, DictationHistoryStore.swift, Sources/UttrflowClipboard/ClipboardStore.swift, and Sources/UttrflowAudio/RecordingStore.swift, then run the Sandbox tests in Tests/UttrflowHistoryTests/DictationHistoryStoreTests.swift. Trace how reads filter and rewrite stored data. Done means future dates cannot outlive retention, clock jumps do not cause irreversible read-time deletion, and tests cover both directions for all three stores.

Written by the indexing model from the issue text.

Assessment

Tech stack
macos, swift
Domain
desktop, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
56/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.