Done in https://github.com/badbundle/vault-app/pull/640 (merged as 4294e922).
The counter: AppLockPasswordAttemptCounter, an actor in VaultFeed/AppLock, has three calls.
countAttempt()is called by the unlock service before it derives the key. It saves the count first. It returns.counted(reachesEraseThreshold:)or.delayed(remaining). If the keychain can't be read or written it throws, which fails closed.reset()is called when any password opens a vault, a duress one included (C2). It should also be called when a password is first set, because the keychain can outlive a deleted app.remainingDelay()is what the lock screen shows. The lock screen never shows how many attempts are left.
Storage: one keychain item holds the count and the time, updated in place. It's WhenUnlockedThisDeviceOnly, not synced, and in the App Group access group, so AutoFill counts against it too.
Schedule: iOS's passcode delays.
| Wrong attempts in a row | Delay |
|---|---|
| 1–4 | none |
| 5 | 1 min |
| 6 | 5 min |
| 7–8 | 15 min |
| 9 or more | 1 hour |
Clock: the delay is timed with ContinuousClock, which counts time since boot, so changing the date either way does nothing. After a restart, the delay starts again in full, and that's saved. A delay can only count less time than really passed, never more.
Threshold hook: eraseThreshold = 10, for VAULT-34. An attempt interrupted by force-quitting stays counted.
Tests:
AppLockPasswordAttemptCounterTests, with injected storage and clock: counting, reset, the schedule, a relaunch, the clock going backwards, the threshold, and storage failures.- Keychain query and attribute tests.
- Checked in the simulator: the count survived a relaunch and a reinstall over the top.