trackslash
VAULT-58 P2

App lock password: persistent attempt counter and escalating delay

0
Sub-issue of VAULT-22 P2 Add an optional password to the app lock

Description

The part of VAULT-22 that doesn't depend on encryption storage. VAULT-46's unlock service hooks into it (see docs/on-device-encryption.md, "Unlocking and locking"), and so does VAULT-34/52's erase.

The change:

  • A wrong-attempt counter that survives relaunching the app. Keep it in the keychain (this device only), so force-quitting or reinstalling over the top doesn't reset it. The unlock service increments it before deriving the key, and resets it after a successful unlock.
  • Duress resets it too. A duress unlock resets the counter exactly as the real password does (MANIFESTO.md C2). The counter itself never knows which vault opened.
  • An escalating delay after repeated wrong attempts, based on iOS's passcode schedule:
    • no delay for the first few attempts, then minutes, then an hour
    • you choose the exact schedule, and explain it in the PR
    • Argon2 already costs about 0.5 s per try
  • The delay can't be skipped by relaunching or by setting the clock back.
  • A threshold hook that VAULT-34 can use later to erase after 10 wrong attempts in a row. Don't add the erase itself.

Rules:

  • Never log, print or measure anything about passwords, keys or duress.
  • The counter and the delay behave identically however many vaults exist.

Tests: unit tests with injected storage and clock covering counting, resetting, the delay schedule, persistence across a "relaunch", and a clock set backwards.

Linked issues

0

GitHub

0

No branches or pull requests linked.

Comments

1
Bradley

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.