This lists every promise Vault makes about security and privacy: where it's made, the code that keeps it, and the
test that pins it. MANIFESTO.md sets the principles; this file says what's built.
A change that adds, changes or breaks a promise updates this file in the same pull request. IDs never change or get reused: a new promise takes the next free number, in the area it belongs to.
To report a security problem, see SECURITY.md.
- Promise is what Vault does, in plain words.
- Made in is where users or contributors are told it: the README, the manifesto (C1–C10), the in-app help
(FAQ pages), the changelog, the item editor or a settings screen, and
docs/on-device-encryption.md(the design doc). "This file" means only this file says it. - Kept by names the types that keep it, under
Vault/Sources. Search for them by name. - Pinned by names the test suite and test, under
Vault/Tests, or underVaultApp/VaultAppUITestsandVaultApp/VaultMacAppUITestsfor UI tests, orVaultApp/VaultMacAppTestsfor tests inside the running Mac app, or "none". A promise kept only by something being absent, like analytics, has no test. - Status is "Holds", or "Holds, with a limit" and the limit. The bigger limits are gathered in Accepted limits.
- Someone forcing you to unlock Vault, at a border, in a robbery or at home. This is the manifesto's core case. Face ID and the passcode don't stop them. The App Lock Password, a duress password, killphrases and hidden items are for them.
- Someone who picks up your unlocked iPhone, or sits down at your unlocked Mac. App Lock and locked items are for them.
- Someone with a copy of Vault's files, from a lost iPhone or Mac, or one of their backups. The App Lock Password encrypts the vault for them, and every backup Vault makes is encrypted. On the Mac, the password is always on.
- Other apps and other devices, through the clipboard, the keyboard, widgets, QuickType, Spotlight, AutoFill, and on the Mac, screen capture and the windows other apps can see.
It doesn't protect against a jailbroken or otherwise compromised iPhone or Mac, someone with an administrator's access to the Mac, iOS or macOS themselves, someone watching the screen, or a password that can be guessed.
| ID | Promise | Made in | Kept by | Pinned by | Status |
|---|---|---|---|---|---|
| G1 | Searching for an item's killphrase in Vault deletes the item straight away, without saying so. A failure looks the same as no match. | C2, README, item editor | VaultDataModel, PersistedLocalVaultStore, RecordVaultStore, EncryptedVaultStore |
VaultStoreContractTests deleteItemsMatchingKillphrase_deletesExactMatchOnly (every store); EncryptedVaultStoreTests deleteItemsMatchingKillphrase_returnsFalseWhenTheSaveFails. The SQLite store's failure path: none |
Holds, with a limit: how a killphrase matches (G2) |
| G2 | A killphrase is matched against the whole search as it's typed. Capitals count, spaces at either end don't, and only Vault's own search checks it, not AutoFill's. | item editor, FAQ (Duress Password) | KillphraseDigester, VaultDataModel |
KillphraseDigesterTests matches_isCaseSensitive; VaultStoreContractTests deleteItemsMatchingKillphrase_matchesQueryWithSurroundingWhitespace |
Holds, with a limit: see Accepted limits |
| G3 | Killphrases, search passphrases and locks are set item by item. There's no way to clear or turn them off all at once. | C1 | by absence (NewItemsSettingsSection covers new items only) |
none | Holds |
| G4 | Nothing logs, counts or measures killphrases, search passphrases or which vault is open. | C3, design doc | by absence: Vault has no logging or analytics (G71) | none | Holds |
| G5 | No screen shows which items have a killphrase. Widgets and Spotlight offer a code with a killphrase like any other. | C5 | VaultItemWidgetEligibility, SpotlightCode |
VaultItemWidgetEligibilityTests eligible_whenKillphraseIsSet; OTPWidgetLoadingTests suggestedEntities_includesACodeWithAKillphrase; WidgetCodeHandoffTests init_includesACodeWithAKillphrase; SpotlightCodeTests codes_showsACodeWithAKillphraseLikeAnyOther |
Holds |
| G6 | Once a killphrase or a delete removes an item, nothing on this device shows it was there, and there's no undo. | C6 | PersistedStoreScrubber, EncryptedVaultStore (whole-file rewrite) |
PersistedLocalVaultStoreResidueTests killphraseDeletion_leavesNoTraceInStoreFiles, delete_leavesNoTraceInStoreFiles, deletingAndEditing_leaveNoHistory |
Holds, with a limit: backups made before, including earlier auto-backups in their folder, still hold the item, and restoring one with its backup password brings it back |
| G7 | Killphrases and search passphrases are kept only as salted HMAC-SHA256 digests. Each vault has a keyring for each: its own random key, and keys it only matches against, those restored backups brought. With the App Lock Password on, and always on the Mac, a vault keeps its keyrings inside its encryption, so they go wherever the vault file goes, and a duress vault has its own from the start. The plain store's are in the keychain, never beside its digests, and so are a vault's while the password is off. Turning the password on or off moves them, in the same write as the vault's key, and never loses one. Phrases waiting to be rehashed after an upgrade are deleted once applied. | C3, C10, design doc, FAQ (Moving Between Devices) | KillphraseDigester, SearchPassphraseDigester, VaultPhraseKeyrings, OpenVaultPhraseKeyrings, DevicePhraseKeyrings, HMACKeyring, VaultEncryptionConverter, VaultPasswordChangeService, KillphraseRehashService, SearchPassphraseRehashService |
KillphraseDigesterTests makeDigest_usesDistinctSaltsAcrossCalls, makeDigest_usesTheDevicesOwnKey; HMACKeyringTests keysFromBackups_areKeptInTheKeychain, killphraseAndSearchPassphraseKeyrings_areSeparate; OpenVaultPhraseKeyringsTests encryptedVault_usesItsOwnKeyringsAndNeverTheKeychains, plainStore_keepsItsKeyringsInTheKeychain, vaultTheDeviceKeyOpens_usesTheKeyringsTurningThePasswordOffLeft; SlotFileKeyringsFixtureTests unlock_onADeviceWithNoneOfItsKeys_theVaultsPhrasesWork, vaults_shareNoKey; VaultEncryptionConverterTests encrypt_movesTheKeyringsIntoTheVaultThenDeletesThemFromTheKeychain; VaultPasswordChangeServiceTests turnOffPassword_movesTheKeyringsFromThePayloadToTheKeychain, turnOffThenOn_keepsEveryKeyAndLeavesNoneInTheKeychain; DuressVaultTests makeDuressVault_startsWithKeyringsOfItsOwn; KillphraseRehashServiceTests run_clearsPendingFileAfterAllEntriesWritten |
Holds, with a limit: see Accepted limits |
| G100 | An encrypted vault made before vaults kept their own keyrings takes a copy of the keychain's the first time it opens, with a new key of its own, and saves it in its payload before any of it is used. The keychain's stay, as another vault made then may not have opened yet, until an erase, or Delete All Data from a vault with its own, which destroys every other vault. If the copy can't be saved, the vault goes on using the keychain's and tries again the next time it reads its keys, so no key is lost. An app that knew only payloads without keyrings refuses one with them, rather than dropping them. | design doc | OpenVaultPhraseKeyrings, EncryptedVaultStore.updatePhraseKeyrings, EncryptedVaultPayload |
OpenVaultPhraseKeyringsTests vaultWithoutKeyrings_takesACopyOfTheKeychainsWithANewKeyOfItsOwn, vaultWithoutKeyrings_takingACopyAgain_changesNothing, vaultWithoutKeyrings_copyThatCantBeSaved_usesTheKeychainsAndTriesAgainLater, vaultWithoutKeyrings_phrasesSetBeforeItKeptItsOwn_stillWork, vaultsWithoutKeyrings_eachTakeACopyWithAKeyOfTheirOwn; SlotFileFixtureTests payload_isVersion1WithNoKeyrings; EncryptedVaultPayloadTests decode_refusesAVersionItDoesNotKnow |
Holds, with a limit: a vault that hasn't opened since the update still needs the keychain it was made with (see Accepted limits) |
| G8 | A hidden item only shows while the whole search is its passphrase. Capitals and the way accents were typed don't matter. | item editor | SearchPassphraseDigester, the stores' retrieve |
VaultStoreContractTests retrieveAll_doesNotReturnSearchOnlyItems, retrieveMatchingQuery_requiresExactPassphraseMatchCaseInsensitive; SearchPassphraseDigesterTests matches_handlesUnicodeNormalizationVariants |
Holds |
| G9 | Killphrases and search passphrases keep working after a backup is restored, on this iPhone or another, and after an erase: every backup carries the open vault's keys, and restoring one adds them to the open vault's keyrings, inside an encrypted vault, or in the keychain for the plain store. | README, FAQ (Moving Between Devices, Backup Security), changelog | VaultDataModel (export and import), OpenVaultPhraseKeyrings, HMACKeyring, EncryptedVaultDecoder |
BackupRoundTripTests pdfRoundTrip_phrasesMatchOnADeviceWithOtherKeys, pdfRoundTrip_withoutKeys_phrasesDoNotMatchOnADeviceWithOtherKeys; VaultDataModelTests import_addsTheBackupsKeysBeforeItsItems; OpenVaultPhraseKeyringsTests restore_putsTheBackupsKeysWithTheVaultsAndItsPhrasesWork, makeExport_carriesTheOpenVaultsKeysOnly; BackupCorpusRestoreTests pdf_onAnotherDevice_importsTheItemsWithTheirDigestsAsTheyWere |
Holds, with a limit: backups made before Vault carried the keys don't carry them, so their killphrases and search passphrases only work where the vault's keyrings still have the keys they were digested with |
| G10 | With the App Lock Password on, the vault file always has sixteen slots of equal size, and a slot with no vault looks like one with a vault. | FAQ (Duress Password), design doc | VaultSlotFile |
VaultSlotFileTests everySlotLooksRandomWhetherOrNotItHoldsAVault, everyFileIsTheSameLengthAndEveryBodyFillsItsSlot; DuressVaultTests duressVaultPayload_hasTheSameShapeAsAnEmptyRealVaults |
Holds |
| G11 | The real password, a duress password and a wrong one do the same work and finish at the same deadline. | FAQ (Duress Password), design doc | VaultUnlockService |
VaultUnlockServiceTests everyPassword_doesTheSameWorkAndReturnsAtTheDeadline, everyPassword_withADuressVaultMadeByTheStore_doesTheSameWorkAndReturnsAtTheDeadline |
Holds |
| G12 | A new duress password makes a new, empty vault with no backup settings. It replaces the vault's previous duress vault and writes only its own slot. | FAQ (Duress Password) | EncryptedVaultStore.makeDuressVault, VaultDuressSlots |
DuressVaultTests makeDuressVault_again_replacesThePreviousOne, makeDuressVault_leavesEveryOtherSlotAsItWas, makeDuressVault_startsWithNoBackupSettings |
Holds |
| G13 | Making duress vaults inside duress vaults never touches the real vault, up to eleven levels deep. | FAQ (Duress Password), design doc | VaultDuressSlots |
VaultDuressSlotsTests chainFromTheRealVault_neverTargetsItWithinElevenLevels, chainFromTheRealVault_canReachItAtTheTwelfthLevel; DuressVaultTests chainOfElevenDuressVaults_neverTouchesTheRealVault |
Holds, with a limit: see Accepted limits |
| G14 | Setting a password never shows whether another vault exists. A new duress password equal to the open vault's own is refused, and it's tried against no other slot. One that opens another vault is accepted without a word, and the vault made most recently wins, by the device's own stamp rather than its clock. | design doc | VaultPasswordChangeService, EncryptedVaultStore.makeDuressVault, VaultDeviceWrapStamper |
DuressVaultTests makeDuressVault_triesThePasswordAgainstNoSlot, makeDuressVault_withAnotherVaultsPassword_isAcceptedAndLeavesThatVault; VaultPasswordChangeServiceTests makeDuressVault_withTheCurrentPasswordAsTheNewOne_isRefusedOnceItsChecked; VaultUnlockServiceTests unlock_realPasswordReusedWithTheClockSetBack_stillOpensTheNewerVault |
Holds, with a limit: the vault that password used to open can't be opened any more. The FAQ says to use a password never used in Vault |
| G15 | Delete All Data, from any vault, destroys every vault on the iPhone and the open vault's backup password, and the killphrase and search passphrase keys in the keychain that only the destroyed vaults could need. | FAQ (Duress Password), changelog | RecordVaultStore.deleteVault, VaultDataModel.deleteVault, OpenVaultPhraseKeyrings.deleteKeyringsNoOtherVaultNeeds |
DuressVaultTests deleteVault_fromADuressVault_destroysTheRealVault, deleteVault_changesEveryOtherSlot_evenOnesThatHeldNothing; VaultDataModelTests deleteVault_removesTheBackupPassword; OpenVaultPhraseKeyringsTests deleteVault_fromAVaultWithItsOwnKeyrings_deletesTheKeychainsKeyrings |
Holds, with a limit: backups already saved aren't deleted (G63) |
| G16 | Changing or turning off the App Lock Password changes only the open vault's slot. | design doc | VaultPasswordChangeService |
VaultPasswordChangeServiceTests changePassword_leavesTheHeaderAndEveryOtherSlotByteForByte, turnOffThenOn_fromADuressVault_neverTouchesTheRealVault |
Holds |
| G17 | Each vault has its own backup password, backups and auto-backup folder, and never touches another vault's. | FAQ (Duress Password), design doc | OpenVaultBackupSettings, AutoBackupServiceImpl |
PerVaultAutoBackupTests eachVault_backsUpIntoItsOwnSettings; AutoBackupServiceImplTests twoVaultsBackingUpToOneFolder_neverTouchEachOthersFiles, seeding_neverHappensForAnEncryptedVault |
Holds, with a limit: two vaults backing up to one folder show there are two series, and the folder picker opens where it was last used |
| G18 | If you turn it on (it's off by default, and set for the device), ten wrong App Lock Passwords in a row, with no vault opened in between, erase every vault, its keys and its settings, without saying so. An erase that stops part way finishes at the next launch, and leaves no attempts lock file behind. | changelog, App Lock Password screen, FAQ (Duress Password) | VaultEraser, AppLockPasswordUnlocker, AppLockSettingsStore |
VaultEraserTests erase_fromALockedPasswordVault_leavesAFreshEmptyPlainStoreAndNoKeys, erase_crashingAtAnyStep_isFinishedByTheNextLaunch, erase_leavesNoAttemptsLockFile, erase_removesTheKillphraseAndSearchPassphraseKeyrings; AppLockPasswordUnlockerTests unlock_tenthWrongWithErasingOn_erasesBeforeAnswering; AppLockSettingsStoreTests erasesAfterFailedPasswords_isOffByDefault |
Holds, with a limit: backups you've saved, and iPhone backups, aren't erased (G63) |
| G19 | Any vault's password, a duress one included, starts the count of wrong passwords again, and with it the waits and the count towards an erase, and can turn erasing off for the device. | App Lock Password screen, FAQ (Duress Password), design doc | AppLockPasswordAttemptCounter, VaultUnlockService, VaultPasswordChangeService |
AppLockPasswordAttemptCounterTests reset_theNextWrongAttemptDoesntWait, countAttempt_rightAttemptsBetweenWrongOnes_neverWaitOrReachTheEraseThreshold; AppLockPasswordUnlockerTests unlock_wrongPasswordsEitherSideOfADuressPassword_startTheWaitsAgain; VaultUnlockServiceTests unlock_withAnyVaultsPassword_resetsTheCounter; VaultPasswordChangeServiceTests setErasesAfterFailedPasswords_fromADuressVault_setsItForTheDevice |
Holds, with a limit: for someone who knows a duress password, neither the waits nor the erase limit guessing on the device, only the key derivation (about a second a guess, or half a second on a file made before VAULT-128): four wrong guesses, the duress password, and four more, as often as they like |
| G20 | The tenth attempt in a row is only ever made at the lock screen, never from Settings or AutoFill, and no screen shows how many attempts are left. | design doc | VaultPasswordChangeService, AutofillVaultService, AppLockPasswordAttemptCounter |
VaultPasswordChangeServiceTests changeAndTurnOff_atTheLastAttempt_areOnlyAtTheLockScreen, makeDuressVault_atTheLastAttempt_isOnlyAtTheLockScreen; AutofillVaultServiceTests unlock_lastAttempt_isntTriedOrCounted. Attempts never shown: none (the locked state has no count) |
Holds |
| G21 | A right password whose attempt is thrown away, because the app locked or the task stopped, still resets the count of wrong passwords, so it doesn't count towards an erase. | design doc | VaultUnlockService, AppLockPasswordUnlocker |
AppLockPasswordUnlockerTests unlock_rightPasswordThrownAwayAtTheLastAttempt_thenOpensTheVault; VaultUnlockServiceTests unlock_cancelledWhileWaitingForTheDeadline_throwsTheAttemptAway |
Holds |
| G76 | Setting a duress password asks for the open vault's App Lock Password first, checked and counted as changing the password is. | App Lock Password screen, FAQ (Duress Password), changelog | AppLockPasswordFormViewModel, VaultPasswordChangeService.makeDuressVault(current:password:) |
AppLockPasswordFormViewModelTests setDuress_asksForTheCurrentPasswordAndANewOne, submit_setDuress_wrongCurrentPassword_saysSoAndMakesNothing; VaultPasswordChangeServiceTests makeDuressVault_checksTheCurrentPasswordAsAnAttempt, makeDuressVault_withAPasswordThatIsNotTheOpenVaults_isWrongAndMakesNothing |
Holds |
| G83 | The Restore page's choices follow the items the feed shows, so a vault holding only hidden items looks empty there, as it does in the feed. | changelog | VaultDataModel.hasVisibleItems |
VaultDataModelTests reloadItems_onlyItemsASearchShows_hasNoVisibleItems, reloadHasVisibleItems_whileASearchShowsHiddenItems_findsNone |
Holds |
| ID | Promise | Made in | Kept by | Pinned by | Status |
|---|---|---|---|---|---|
| G22 | App Lock is off by default. When it's on, Vault starts locked and locks again in the background, straight away or after 1, 5 or 15 minutes. | changelog | AppLockSettingsStore, AppLockDelay, AppLockService |
AppLockSettingsStoreTests isEnabled_isOffByDefault; AppLockServiceTests init_lockOn_startsLocked, delay_backOnceItsUp_locks; AppLockContainerTests sceneGoingToTheBackground_locksTheApp; AppLockTests (UI) test_launch_withAppLockOn_opensOnlyOnceDeviceAuthenticationPasses |
Holds |
| G23 | While App Lock is on, the app switcher and Control Center only see a cover with the vault door. | changelog | AppLockService.requiresPrivacyCover(in:), AppCover |
AppLockServiceTests requiresPrivacyCover_lockOn_whenNotActive, requiresPrivacyCover_lockOff_never; AppLockTests (UI) test_leavingTheApp_withRequireUnlockImmediately_coversTheVaultAndLocksIt |
Holds, with a limit: with App Lock off, the app switcher shows the screen as it was |
| G24 | Hide While Recording is on by default. A cover hides Vault, and the AutoFill sheet, while the screen is recorded, mirrored or shared. | changelog, Settings | AppCover, LocalSettingsState |
LocalSettingsTests hidesVaultWhileScreenCaptured_isOnByDefault; AppCoverTests required_screenCaptured_coversTheVault; VaultAutofillViewSnapshotTests screenCaptured_isCovered |
Holds, with a limit: screenshots aren't blocked |
| G25 | The App Lock Password is asked for after Face ID, Touch ID or the passcode. Face ID alone never opens a vault that has one, in the app or in AutoFill, and an iPhone with no passcode never gets as far as the password. | changelog, C4 | AppLockService, VaultAutofillViewModel |
VaultAutofillViewModelTests passwordOn_asksForThePasswordAfterDeviceAuthentication; AppLockPasswordUnlockTests noPasscode_withThePasswordSet_neverAsksForIt; AppLockTests (UI) test_appLockPassword_opensOnlyTheVaultItBelongsTo, test_lockScreen_onADeviceWithoutAPasscode_asksForOneAndNeverForThePassword |
Holds |
| G26 | Each attempt is counted before the key is derived, so quitting can't take one back. From the fifth wrong password in a row, each try waits 1, 5 or 15 minutes, then an hour, as iOS does. The count is kept for the device and shared with AutoFill, and not the widgets (G99). | changelog, FAQ (Duress Password) | AppLockPasswordAttemptCounter, AppLockPasswordAttemptStorage |
VaultUnlockServiceTests unlock_countsTheAttemptBeforeDerivingAndResetsItOnceUnlocked; AppLockPasswordAttemptCounterTests delay_followsTheIOSPasscodeSchedule, countAttempt_wrongAttemptsInARow_escalateTheDelay; AppLockPasswordAttemptKeychainStorageTests acquireExclusiveAccess_locksOutOtherProcessesMeanwhile, itemQuery_neverSyncs, init_sharesTheItemWithAutoFillOnly; AppLockViewCountdownTests passwordWait_endsWhenTheClockPassesIt |
Holds |
| G27 | An App Lock Password, a duress password and a new backup password need at least 8 characters, and not only numbers. | App Lock Password screen, FAQ (Backup Password), changelog, design doc | AppLockPasswordRules |
AppLockPasswordRulesTests problem_fewerThanEightCharacters_isTooShort, problem_onlyNumbers_isRejectedLikeAPIN; BackupKeyChangeViewModelTests newPassword_followsTheAppLockPasswordsRules, saveEnteredPassword_breakingTheRules_setsNothing |
Holds, with a limit: the rule stops a PIN, not a common word, so the screens say to use a long password. A backup password set before the rule keeps working |
| G28 | Changing or turning off the password needs the current one. Turning it off wraps the vault with a new device key, and turning it back on deletes that key and starts the count in a row again. | design doc | VaultPasswordChangeService |
VaultPasswordChangeServiceTests changePassword_withAWrongPassword_isWrongAndChangesNothing, turnOffPassword_wrapsTheVaultWithANewDeviceKey, turnOnPassword_resetsTheCountInARowFirstAndKeepsTheRecentCount |
Holds |
| G29 | With the password on, the iPhone locking locks the vault at once, and the AutoFill sheet too. Locking drops the vault's items, caches, search and keys. | design doc, changelog | AppLockService.deviceWillLock(), VaultAutofillViewModel.deviceWillLock(), VaultDataModel.purgeVaultContents(), VaultUnlockService.lock() |
AppLockPasswordVaultTests deviceWillLock_locksTheVaultAndForgetsWhatWasReadFromIt, relaunch_withTheVaultLeftOpen_startsLockedWhateverTheDelay; VaultAutofillViewModelTests passwordOn_deviceLocks_locksTheSheetAndTheVault; VaultDataModelPurgeTests purgeVaultContents_forgetsItemsTagsAndSearch; VaultUnlockServiceTests lock_locksTheSessionThenPurges |
Holds, with a limit: with a Require Unlock delay, a suspended Vault keeps the keys until it runs again, and decoded text can't be reliably zeroed (G40) |
| G30 | The App Lock Password can't be reset. The screen that sets it says so, and shows when you last saved a backup. | changelog | by design | none | Holds, with a limit: a forgotten password means erasing and restoring a backup (G9) |
| G31 | Restore, the Backup Password sheet and the Danger Zone ask for Face ID, Touch ID or the passcode, and say to set a passcode if there isn't one. | changelog | BackupRestoreViewModel, BackupKeyChangeViewModel, SettingsDangerViewModel |
BackupRestoreViewModelTests authenticate_deniedIfNotAuthenticated, init_withoutAPasscode_isUnavailable; SettingsDangerViewModelTests deleteEntireVault_requiresAuthenticationBeforeDeleting |
Holds, with a limit: someone forcing you can make you pass this (C4), so restoring also asks for the backup's password (G59) |
| G32 | A locked item needs Face ID, Touch ID or the passcode to view or copy it. QuickType never lists or fills a locked code. | item editor, changelog | VaultItemDetailView, VaultOTPAutofillStore, PasswordlessOTPAutofillStore, AutofillOTPCredentialResolver |
AutofillOTPCredentialResolverTests resolve_lockedItem_returnsUserInteractionRequired, resolve_lockedHOTPItem_returnsUserInteractionRequired; VaultOTPAutofillStoreImplTests syncAll_excludesLockedItems; PasswordlessOTPAutofillStoreTests sync_lockedCode_isNotSuggested |
Holds, with a limit: on an iPhone with no passcode, a locked item opens with a warning |
| G99 | The count of wrong App Lock Passwords, the wrap stamp and, on the Mac, erasing after failed passwords and the Require Unlock delay are kept in a keychain access group only Vault and its AutoFill extension have, on iOS and the Mac: not the widget extension, which can neither read nor change them. Earlier versions kept them in the App Group's access group, which the iOS widget extension has too, to read the vault. The app's launch and AutoFill's start move what's still there, adding a copy with the same bytes and accessibility and reading it back before deleting the original, so nothing is lost and the count is never lowered; until then, they're read from there. An erase deletes them from both. | changelog, design doc, docs/mac-app.md |
SharedKeychainItem, SharedKeychainItems, AppLockPasswordAttemptKeychainStorage, VaultWrapStampKeychainStorage, AppLockDeviceSettingsKeychainStorage, VaultApp.entitlements, VaultAppAutofill.entitlements, VaultMacApp.entitlements, VaultMacAutofill.entitlements |
SharedKeychainItemTests init_keepsTheItemInTheSharedGroupAndMovesItFromTheAppGroups, data_onlyInTheAppGroup_isReadFromThere, data_movedBetweenTheReads_isFound, move_fromTheAppGroup_keepsTheItemAsItWas, move_whenAnotherProcessAddsItMeanwhile_keepsTheirsAndDeletesTheAppGroups, move_failingAtAnyStep_keepsTheAppGroupsItemForTheNextMove, move_fromManyProcessesAtOnce_leavesOneCopyInTheSharedGroup, remove_failingInTheSharedGroup_neverLeavesTheOlderValue; AppLockPasswordAttemptKeychainStorageTests moveFromTheAppGroup_movesTheRecordHoldingTheLock, counter_afterTheRecordMoves_countsOnFromIt, counter_whenTheMoveFails_countsOnFromTheAppGroupsRecord; AppLockDeviceSettingsKeychainStorageTests settingsStore_withTheSettingsStillInTheAppGroup_readsThemNotTheDefaults; VaultEraserTests erase_deletesTheAttemptsAndWrapStampFromBothKeychainGroups; MacAppKeychainTests keychain_eachOfTheAppsGroups_holdsItems, keychain_theSameItemInTheAppGroupAndTheSharedGroup_isTwoItems; MacAppLaunchTests autofillExtension_hasTheAppGroupAndTheSharedKeychainGroup, widgetExtension_hasNoAppGroupAndOnlyTheWidgetsKeychainGroup; make validate's "iOS app entitlements" and "Mac app entitlements" checks |
Holds, with a limit: the device key stays in the App Group's group, where the iOS widget extension reads it (see Accepted limits), and an item stays there too until a launch can move it, which for the count and the stamp needs the device unlocked |
| ID | Promise | Made in | Kept by | Pinned by | Status |
|---|---|---|---|---|---|
| G33 | With the App Lock Password on, the vault is one file encrypted with AES-256-GCM. Only its 128-byte header can be read, and that says nothing about any vault. | design doc, changelog | VaultSlotFile, EncryptedVaultFile |
VaultSlotFileTests fileNeverContainsThePayloadThePasswordOrAnyKey, init_headersDifferOnlyInTheSalt; SlotFileFixtureTests password_opensOnlyItsVaultsSlot, wrongPassword_opensNoSlot |
Holds |
| G34 | Changing any byte of the file makes the vault fail to open. A header asking for out-of-range key derivation is refused before anything is derived. | design doc | VaultSlotFile, VaultSlotFileHeader |
VaultSlotFileTests changingAnyHeaderByteStopsTheSlotOpening, changingAnyNonceOrKeyBoxByteStopsTheSlotOpening, changingAnyBodyByteStopsThePayloadOpening, init_refusesParametersOutsideTheFormatsLimits, initBytes_refusesAMalformedHeader, init_acceptsUpTo128Passes |
Holds, with a limit: the test checks a sample of the body's bytes, not every one |
| G35 | Each vault fills its slot, so a slot doesn't show how much its vault holds. | design doc | VaultSlotFile |
VaultSlotFileTests everyFileIsTheSameLengthAndEveryBodyFillsItsSlot, slotSize_doublesUntilThePayloadFitsAndNeverShrinks |
Holds, with a limit: the slot size shows that some vault once outgrew the size before (over about 3,500 items) |
| G36 | Every save replaces the file in one verified step. A crash leaves the old vault or the new one, never a mix, and stray temporary copies are removed. | design doc | EncryptedVaultFile, SlotFileSystem, EncryptedVaultStore |
EncryptedVaultStoreTests save_failingAtAnyStepUpToTheRename_leavesTheFileAndTheStoreAsTheyWere, save_crashingAtAnyStep_leavesTheOldOrNewVaultForTheNextLaunch, open_removesTemporaryFilesACrashLeftBehind |
Holds |
| G37 | Setting the password deletes the unencrypted store, phrases waiting to be rehashed and set-aside copies, and only once the encrypted copy is verified and committed. | design doc | VaultEncryptionConverter, VaultStorageRecovery |
VaultEncryptionConverterTests encrypt_crashingAtAnyStep_leavesThePlainVaultOrTheEncryptedOneForTheNextLaunch, encrypt_withArchives_refusesUntilTheUserAgreesToDeleteThem; VaultEncryptionConverterSchemaTests encrypt_afterMigratingFromSchemaVersion_keepsEverythingTheVaultShows |
Holds |
| G38 | Without an App Lock Password, the vault is a SQLite store protected by iOS Data Protection, and it's in the iPhone's own backups. | design doc, README, FAQ (About Backups), privacy policy | PersistedLocalVaultStore |
none | Holds, with a limit: see Accepted limits |
| G39 | The unencrypted store is cleaned after deleting items, changing a killphrase or search passphrase, and at launch, so deleted content doesn't stay in its files. Each clean also deletes the history SwiftData keeps of past saves. | changelog, design doc | PersistedStoreScrubber, PersistedLocalVaultStore |
PersistedLocalVaultStoreResidueTests delete_leavesNoTraceInStoreFiles, changingKillphrase_leavesNoTraceOfOldDigest, scrubContentLeftByEarlierSessions_clearsWhatAnEarlierSessionLeft, deletingAndEditing_leaveNoHistory, scrubContentLeftByEarlierSessions_clearsHistory |
Holds, with a limit: after other edits, the old version stays in the files until Vault next opens |
| G40 | Keys, and the buffers the storage code owns, are wiped once used. | design doc | Argon2idKeyDeriver, SlotRandom.wipe |
Argon2idKeyDeriverTests hash_wipesTheWorkingMemoryBeforeFreeingIt, hash_wipesThePasswordBufferItIsGiven; VaultSlotFileTests compression_wipesTheScratchBufferBeforeFreeingIt |
Holds, with a limit: decoded items, and the password as typed, are Swift strings that can't be reliably zeroed, and no test sees the key box and body buffers freed |
| G41 | Recovery phrases are always encrypted and locked, stay masked until tapped, hide when Vault is in the background or the screen is captured, and can't be copied, from the item or the editor. | changelog, FAQ (About Recovery Phrases) | RecoveryPhraseDetailEdits, RecoveryPhraseDetailViewModel, RecoveryPhraseDetailView, EditedTextClipboard |
RecoveryPhraseStorageTests encryptedVault_refusesPlaintextPhrase; RecoveryPhraseDetailViewModelTests areWordsRevealed_existingItemStartsMasked; RecoveryPhraseDetailViewSnapshotTests hiddenWhileInactive, hiddenWhileCaptured; EditedTextClipboardTests recoveryPhraseWord_copiesNothing |
Holds, with a limit: screenshots aren't blocked |
| G102 | Private keys are always encrypted with a password of their own and always locked, whatever their stored lock state: one can't be saved without a password, and every store, backup and transfer refuses one that isn't encrypted. A device without a passcode can't add or open one. | changelog, FAQ (About Private Keys), item editor | PrivateKeyDetailEdits, PrivateKeyDetailViewModel, VaultDataModelEditorAdapter, PersistedVaultItemEncoder, VaultBackupItemEncoder, VaultDetailCreateView |
PrivateKeyStorageTests store_insertPlaintextKey_throwsAndStoresNothing, store_updateWithPlaintextKey_throwsAndKeepsTheEncryptedKey (every store), persistedItem_encode_refusesPlaintextKey, backupItem_encode_refusesPlaintextKey, encryptedVault_refusesPlaintextKey, editor_withoutAPasswordOrKey_refusesToSave, store_endToEnd_createdKeyCanBeDecryptedWithItsPassword; PrivateKeyDetailEditsTests lockState_isAlwaysLocked, isComplete_securityNeedsAPassword; PrivateKeyDetailViewModelTests isLocked_existingItemAlwaysStartsLocked, allowsUnlockWithoutDeviceAuthentication_isFalse; PrivateKeyDetailViewSnapshotTests lockedStateNoAuthentication; EncryptedItemFixtureTests (encrypted-private-key-v1.json) |
Holds, with a limit: the title isn't encrypted (G42) |
| G103 | A private key stays masked until tapped, on its page and in its editor, hides when Vault is in the background or the screen is captured, and can't be copied, from the item or the editor. It's never shown as a QR code. | changelog, FAQ (About Private Keys) | PrivateKeyDetailViewModel, PrivateKeyDetailView, PrivateKeyTextView, PrivateKeyKeyStep, PrivateKeyTextEditor, EditedTextClipboard |
PrivateKeyDetailViewModelTests isKeyRevealed_existingItemStartsMasked, lock_masksARevealedKey; PrivateKeyDetailViewSnapshotTests wif_masked, editMode_existingKeyMasked, hiddenWhileInactive, hiddenWhileCaptured; EditedTextClipboardTests privateKey_copiesNothing; SecretTextInputTraitsTests privateKeyTextEditor_keepsKeyboardFromLearningAndNeverHyphenates. No QR code: by absence |
Holds, with a limit: screenshots aren't blocked |
| G104 | Vault only reads a private key's own text to say what it is, and to check its checksum and range: it never works out a public key or an address from it, signs or decrypts with it, or sends it anywhere. A key that doesn't check is still saved as entered, with a warning. | FAQ (About Private Keys), item editor | PrivateKeyValidator, ArmoredPrivateKeys, Base58Check, Bech32, PrivateKeyDetailEdits |
PrivateKeyValidatorTests; PrivateKeyEncodingTests; PrivateKeyDetailEditsTests isComplete_contentNeedsAKey, makePrivateKey_keepsTheWhitespaceInsideOtherText. That nothing is derived: by absence (Vault has no secp256k1 code) |
Holds |
| G42 | An item encrypted with its own password is encrypted with AES-GCM, and the password is never stored. | FAQ (Item Encryption) | VaultItemEncryptor, VaultItemDecryptor |
VaultItemEncryptorTests encryptAndDecrypt; EncryptedItemFixtureTests decrypt_withItsPassword_givesTheItemItWasMadeFrom, decrypt_withAWrongPassword_fails; RecoveryPhraseDetailViewModelTests saveChanges_keepsNewKeyAndDropsPasswordAfterSaving |
Holds, with a limit: the item's title, description and tags aren't encrypted (a recovery phrase's or a private key's description is), and an item encrypted before v2 keeps v1's key derivation, lighter than the App Lock Password's, until it's opened (see Accepted limits) |
| G43 | A PDF backup's file exists only while its share sheet is open. | changelog | BackupPDFTemporaryFiles |
BackupGeneratedPDFViewModelTests shareSheetFinished_cancelledStaysUnsavedLogsNothingAndDeletesFile; BackupPDFTemporaryFilesTests deleteAll_deletesOnlyBackupPDFs |
Holds, with a limit: if Vault closes with the sheet open, the file stays until the Backups page next opens |
| G80 | When Vault can't open its storage, it never deletes a file that might be the only copy of a vault, the extensions never open a store that might not be the vault, and the failure screen says to keep Vault installed. | design doc, changelog | VaultStorageRecovery, VaultStorageStateFile, VaultStoreFailureView |
VaultStorageRecoveryTests recover_plainWithAnEncryptedFileButNoPlainStore_deletesNothing, recover_noStateFileWithAnEncryptedFileAndNoItemsInThePlainStore_deletesNothing, extensions_noStateFileWithAnEncryptedFile_isUnavailable; VaultRootPlainStoreTests plainStoreRecoveryMode_inAnExtension_onlyOpens; VaultStoreFailureViewSnapshotTests layout |
Holds |
| ID | Promise | Made in | Kept by | Pinned by | Status |
|---|---|---|---|---|---|
| G44 | While App Lock or the App Lock Password is on, widgets show Vault as locked, and a widget's settings list no codes. | changelog, Settings | WidgetVaultLoader |
OTPWidgetLoadingTests providerTimeline_appLockOn_isLockedWithoutReadingTheVault, entityQuery_vaultEncrypted_listsNothing, suggestedEntities_appLockOn_listNothing |
Holds, with a limit: a widget added before keeps its code's name and account in iOS's widget settings until you remove it |
| G45 | Widgets never show locked or hidden codes. On the Lock Screen and in StandBy they hide their codes, sites and accounts until the iPhone is unlocked, and copying a code from a widget asks for the iPhone to be unlocked first. | changelog | VaultItemWidgetEligibility, OTPWidgetProvider, OTPWidgetCodeActions |
VaultItemWidgetEligibilityTests ineligible_whenLocked, ineligible_whenSearchableLevelIsOnlyPassphrase; OTPWidgetLockScreenPrivacyTests redacted_showsNoDigits, copyTOTPCode_asksForTheIPhoneToBeUnlocked, incrementAndCopyHOTPCode_asksForTheIPhoneToBeUnlocked |
Holds, with a limit: with App Lock off, a Home Screen widget shows a live code and its name |
| G46 | While the App Lock Password is on, QuickType's list is emptied and never written. | changelog, design doc | PasswordlessOTPAutofillStore, AutofillOTPCredentialResolver |
PasswordlessOTPAutofillStoreTests syncAll_passwordNeeded_emptiesTheStoreAndSavesNothing, sync_passwordTurnedOnWhileWriting_emptiesTheStoreAgain; AutofillOTPCredentialResolverTests resolve_passwordNeeded_returnsUserInteractionRequiredWithoutReadingTheVault |
Holds |
| G47 | QuickType never lists hidden or locked codes, or codes with Show in QuickType off. New codes start with it off. | item editor, changelog | VaultOTPAutofillStore, LocalSettingsState |
VaultOTPAutofillStoreImplTests syncAll_hidesItemsWithRequiresSearchPassphrase, syncAll_excludesLockedItems, syncAll_excludesItemsWithShowInQuickTypeFalse; LocalSettingsTests newItems_startUnlockedAndOutOfQuickTypeByDefault |
Holds, with a limit: with App Lock on and no password, QuickType lists the site and account of codes offered in it, and asks you to unlock before filling one |
| G48 | With the App Lock Password on, AutoFill asks for it in its own sheet, checks there's memory to derive the key, and never erases. | design doc | AutofillVaultService, VaultAutofillViewModel |
VaultAutofillViewModelTests prepareToUnlock_passwordOnWithoutTheMemory_needsTheApp; AutofillVaultServiceTests unlock_notEnoughMemory_countsNothingAndSaysSo, unlock_lastAttempt_isntTriedOrCounted |
Holds |
| G49 | Show in Spotlight is off by default and works only while App Lock is off. Spotlight gets site names only, never account names or locked or hidden codes, and turning App Lock on empties it. | changelog, Settings | SpotlightCode, SpotlightCodeIndexer, VaultRoot |
LocalSettingsTests showsCodesInSpotlight_isOffByDefault; SpotlightCodeTests codes_isEmptyWhileAppLockIsOn, codes_neverShowsALockedCode, codes_neverShowsAHiddenCode. Turning the setting off with App Lock: none |
Holds |
| G50 | Everything copied from Vault is marked concealed, cleared after 1 minute by default, and stays on this device unless Universal Clipboard allows that kind of text. | changelog, C7 | Pasteboard, ConcealedPasteboard, LocalSettingsState |
LocalSettingsTests pasteTimeToLive_defaultsToOneMinute, universalClipboard_isOffForEveryContentTypeByDefault; PasteboardTests copy_isLocalOnlyByDefault, copyText_detailStaysOnThisDeviceWhateverTheSettings; WidgetPasteboardTests copyOTP_alwaysStaysOnThisDevice |
Holds, with a limit: choosing Never keeps copies on the clipboard |
| G51 | Cut and Copy go through Vault's clipboard. Text being edited offers only Cut, Copy, Paste, Select, Select All and Delete, and can't be dragged into another app. Dragging a code out of the feed gives only its ID. | changelog | EditedTextClipboard, SelectableTextView, VaultItem+Transferable |
EditedTextClipboardTests editMenu_offersOnlyEditingActions, textDrag_isOff, vaultItem_draggedOffersItsIDAndNoText; SelectableTextTests init_cantBeDraggedOrRewritten |
Holds |
| G52 | Apple's keyboard learns nothing typed into Vault: every field turns off autocorrection and Writing Tools. | changelog | SecretTextInput |
SecretTextInputDeclarationTests everyTextInputDeclaresSecretTextInput; SecretTextInputTraitsTests textField_keepsKeyboardFromLearning |
Holds, with a limit: a third-party keyboard sees what's typed outside password fields |
| G53 | A PDF backup's share sheet has no Copy or Markup. | changelog | BackupGeneratedPDFView |
BackupPDFShareSheetTests backupShareSheet_excludesCopyAndMarkup |
Holds |
| ID | Promise | Made in | Kept by | Pinned by | Status |
|---|---|---|---|---|---|
| G54 | Every export, a PDF backup, an auto-backup or a transfer, is the whole vault encrypted with its backup password. There's no unencrypted export. | README, FAQ (About Backups) | EncryptedVaultEncoder |
BackupRoundTripTests pdfRoundTrip_wrongKey_failsDecrypt; TransferQRCodeRestoreTests transfer_scannedShuffledWithRepeatsAndAnotherTransfersCode_restoresTheVault; no unencrypted path: none |
Holds |
| G55 | A backup is encrypted with AES-GCM, with a new IV every time, under a key from a fixed, slow key derivation (the secure one in release builds). A backup password set or changed since v2 derives with Argon2id over 512 MiB, with 32 passes and 4 lanes. One set before keeps its key, which the app carries on to that Argon2id the first time it loads it, in the background: the new key, its derivation and salt replace the old together or not at all, only if no password has been set or removed since, and backups made with it need both derivations. Only the app does it, never an extension. | FAQ (Backup Security), design doc, changelog | DerivedEncryptionKey, VaultKeyDeriverFactory, VaultBackupEncryptor, BackupPasswordUpgrader, VaultDataModel.loadBackupPassword(), BackupPasswordStoreImpl, OpenVaultBackupSettings, VaultRoot |
VaultKeyDeriverParameterPinTests backupSecureV1_pinsExactKDFChain, backupSecureV2_pinsExactKDFChain, backupSecureV1ThenV2_pinsExactKDFChain; VaultKeyDeriverRealParameterTests backupSecureV1ThenV2_carryingOnAStoredKey_derivesTheKnownAnswer; VaultKeyDeriverTests continueDerivation_fromAStoredV1Key_isTheKeyThePasswordRecreates; VaultDataModelBackupPasswordUpgradeTests loadBackupPassword_v1Key_storesItCarriedOnToV2AndBacksUpWithIt, loadBackupPassword_whileUpgrading_backupsUseTheOldKey, storeBackupPassword_whileUpgrading_keepsTheNewPassword, upgrade_failingToStore_leavesTheKey, loadBackupPassword_withoutAnUpgrader_keepsTheKey; BackupPasswordStoreImplTests upgradePassword_replacesTheKeyAndKeepsWhenItWasSet, upgradePassword_stoppingWithThePasswordRemoved_nextLoadRestoresTheNewKey; OpenVaultBackupSettingsTests unlocked_upgradePassword_afterAnotherPasswordWasSet_keepsThatPassword; VaultEncryptorTests encrypt_expectedDataReturnedUsingAESGCM; BackupFixtureTests encrypt_withTheFixturesRandomness_writesExactlyTheFixture; BackupCorpusRestoreTests backup_restoresToExactlyTheVaultItWasMadeOf (auto-backup-keygen-v2.json, auto-backup-keygen-v1-then-v2.json); the release choice and that extensions never upgrade: none |
Holds, with a limit: a backup's salt stays the same while its password does, backups made before the key moved on keep v1's derivation, and deriving needs 512 MiB in the app (see Accepted limits) |
| G56 | Killphrases and search passphrases are in a backup only as digests, and their keys only inside the encryption. | C10, FAQ (Backup Security), design doc | VaultBackupItemEncoder, VaultDataModel.makeExport, VaultBackupEncryptor |
VaultDataModelTests makeExport_carriesEveryKeyOnTheKeyrings; EncryptedVaultDecoderTests decryptAndDecode_carriesTheKillphraseAndSearchPassphraseKeys; BackupRoundTripTests pdfRoundTrip_merge_preservesDuressMetadata; a scan of the backup's bytes: none |
Holds, with a limit: anyone who can decrypt a backup can test guesses at its phrases (see Accepted limits) |
| G57 | A saved backup, a PDF or an auto-backup, is padded inside the encryption to a fixed size: 32 KB, then doubling. A transfer is padded by a random amount. | changelog, design doc | EncryptedVaultEncoder, VaultBackupEncryptor |
EncryptedVaultEncoderTests encryptAndEncode_savedBackupsOfDifferentVaults_areTheSameSize, encryptAndEncode_forATransfer_isPaddedByARandomAmount; VaultBackupEncryptorTests encodeFillingFixedSize_landsWithinTheToleranceEveryTime |
Holds, with a limit: a PDF's file size also varies with the hint and the paper size |
| G58 | A PDF backup shows in plain text only that it's a Vault backup, when it was made, its page and QR code counts, and a password hint if you write one. The hint starts empty. | changelog, FAQ (About Backups) | VaultExportDataBlockGenerator, VaultExportDataBlockHeaderGenerator, BackupCreatePDFViewModel |
BackupCreatePDFViewModelTests init_withoutAHint_startsEmpty |
Holds |
| G59 | Vault keeps the backup key so it can make backups without asking. Restoring always asks for the backup's own password, even on this iPhone. The key isn't synced to other devices. | FAQ (Backup Password), changelog | BackupPasswordStoreImpl, SecureStorageImpl, BackupImportFlowViewModel |
BackupImportFlowViewModelTests handleImportFromEncryptedVault_alwaysAsksForThePassword, handleImportFromPDF_asksForThePassword; BackupPasswordStoreImplTests setPassword_setsDataEncodedCorrectly |
Holds |
| G60 | An Import & Override that fails part way leaves the vault as it was. | changelog | every store's importAndOverrideVault |
VaultStoreContractTests importAndOverrideVault_failingPartWayLeavesVaultUntouched |
Holds |
| G61 | Backups keep each code's QuickType choice and each note's preview, and backups made by earlier versions restore. | changelog, README | VaultBackupItemEncoder, VaultBackupItemDecoder |
BackupRoundTripTests pdfRoundTrip_preservesQuickTypeAndPreviewMode; VaultBackupItemDecoderTests decode_backupWithoutQuickTypeOrPreviewModeRestoresAsBefore; BackupFixtureTests decrypt_withThePassword_givesTheBackupItWasMadeFrom; BackupCorpusRestoreTests backup_restoresToExactlyTheVaultItWasMadeOf |
Holds, with a limit: a backup from before those choices were kept restores with QuickType on and full previews, and older backups don't carry what killphrases need (G9) |
| G62 | Auto-backup never replaces a file, only deletes files it wrote itself, always keeps the newest, and writes nothing if the vault changes while it's making a backup. Choosing a shorter time to keep backups asks first. | changelog, FAQ (About Backups) | AutoBackupServiceImpl, iCloudDriveProvider, AutoBackupViewModel |
AutoBackupServiceImplTests cleanupOldBackups_deletesOnlyTheFilesThisVaultWrote, cleanupOldBackups_keepsTheOnlyBackupHoweverOld, forceBackup_whenTheVaultChangesWhileItIsMade_writesNothing; AutoBackupProviderCoverageTests iCloudDriveProvider_write_neverReplacesAFile; AutoBackupViewModelTests needsConfirmation_onlyForAShorterTime |
Holds, with a limit: earlier auto-backups stay in the folder until they're older than the time you keep them for, with items as they were then, and each file's name shows when it was made |
| G63 | Erasing and Delete All Data don't touch backups you've saved, or the iPhone's own backups. | FAQ (Duress Password), App Lock Password screen | by design | none | Holds |
| G77 | Importing a backup, or scanning a code, checks each QR code's details, and each code's period and counter, before using them. | changelog | DataShardDecoder, VaultBackupItemDecoder, OTPAuthURIDecoder |
DataShardDecoderTests addShardData_throwsIfTotalIsOutOfRange, addShardData_throwsIfNumberIsOutsideTotal; VaultBackupItemDecoderTests decodeOTP_failsToDecodeTOTPWithAPeriodTheVaultCantUse, decodeOTP_failsToDecodeHOTPWithACounterTheVaultCantStore; OTPAuthURIDecoderTests decodeType_throwsErrorForZeroPeriod; BackupImportMalformedInputTests codesWithAPositionOutsideTheirTotal_areInvalidAndLeaveNothingScanned, pdf_fuzzed_neverCrashes |
Holds |
| G78 | A backup is refused if it would decompress to more than 64 MiB. | this file | BoundedLZMADecompression |
BoundedLZMADecompressionTests decompress_pastTheLimit_throws, maximumLength_is64MiB |
Holds |
| G79 | "Last backup" counts only backups that were saved, a PDF or an auto-backup, not a transfer. | changelog | BackupEventLogger, DeviceTransferExportViewModel |
BackupEventLoggerImplTests lastBackupEvent_transferLoggedByAnEarlierVersion_isNotABackup; DeviceTransferExportViewModelTests generateShards_isNotLoggedAsABackup |
Holds |
| ID | Promise | Made in | Kept by | Pinned by | Status |
|---|---|---|---|---|---|
| G64 | The App Lock Password's key comes from Argon2id (the unmodified reference code) with 64 MiB of memory, and the 1, 2 or 4 lanes and 3–128 passes that fit the most work in about a second on the device that sets it. A file keeps them for life, so one made before VAULT-128 keeps its half second, one lane and 3–32 passes. The unlock deadline is 1.5 times the derivation, only ever raised, counting the work of every lane, and never more than 5 seconds. Backup and item passwords use the same Argon2id with more memory and always four lanes, and only in the app, which can spare it (G55, G68). | design doc | Argon2idKeyDeriver, AppLockKeyDerivation, AppLockKeyDerivationCalibrator, VaultUnlockService, LiveVaultUnlockWork, CArgon2 |
Argon2idKeyDeriverTests hash_matchesRFC9106TestVector, key_matchesReferenceKnownAnswers, key_over64MiB_matchesTheKeyDerivedOneLaneAtATime, hash_withTheAppLockPasswordsMemory_holdsJustThatMuch; AppLockKeyDerivationTests constants_matchTheDesign, calibrate_choosesTheLanesThatFitTheMostPasses, calibrate_lanesThatFitTheCeilingEach_choosesTheQuickest, calibrate_neverChoosesFewerThanThreePasses, calibrate_neverChoosesMoreThan128Passes; AppLockProductionKeyDerivationTests password_withTheShippedKeyDerivation_opensTheVaultAndRefusesAWrongOne; VaultUnlockServiceTests unlock_raisesTheDeadlineNoFurtherThanTheMaximum, unlock_timesTheWorkForTheFilesLanes; LiveVaultUnlockWorkTests cpuTime_ofADerivationWithFourLanes_countsTheirShareOfTheWork; SlotFileFixtureTests unlock_onADeviceThatCalibratesDifferently_opensTheVault |
Holds |
| G65 | A password gives the same key however its accents were typed (Unicode NFC). | design doc | VaultSlotFileHeader, VaultPasswordChangeService |
VaultSlotFileTests passwordKey_isTheSameHoweverThePasswordsAccentsAreComposed; VaultUnlockServiceTests unlock_derivesFromTheComposedFormOfThePassword; SlotFileFixtureTests duressPasswordWithTheAccentDecomposed_opensTheDuressVault |
Holds |
| G66 | Each slot's key is wrapped with HKDF-SHA256 of the password's key, the slot's nonce and its kind. Each vault has its own random data key, the header is bound to every slot, and changing a password gives the vault a new data key. | design doc | VaultSlotRootKey, VaultSlotFile |
VaultSlotFileTests wrapKey_isHKDFOfTheRootKeyWithTheSlotNonceAndKindLabel, rekey_leavesNoKeyThatOpensBothTheOldAndNewBodies; SlotFileFixtureTests storedSlotsInEachOthersPlaces_openNoSlot; VaultPasswordChangeServiceTests changePassword_rotatesTheVaultsDataKey |
Holds |
| G67 | The key derivations for backups and items, and the stored formats, never change once released. A stronger derivation is a new signature, beside the old (v2 in VAULT-128), and a signature this version doesn't know says it needs a newer version of Vault. | Vault/README.md, design doc |
VaultKeyDeriver, the golden fixtures |
VaultKeyDeriverParameterPinTests backupSecureV1_pinsExactKDFChain, itemSecureV1_pinsExactKDFChain, backupSecureV2_pinsExactKDFChain, backupSecureV1ThenV2_pinsExactKDFChain, itemSecureV2_pinsExactKDFChain, signatureIDs_areStable, upgrades_moveEachV1DerivationToItsV2; VaultKeyDeriverRealParameterTests; VaultKeyDeriverTests Item_Secure_v1_recreatesExpectedKeyForSamePasswordAndSalt, tryFromString_aSignatureThisVersionDoesntKnow_saysToUpdateVault; PBKDF2KeyDeriverTests key_withEachV1DeriversParameters_matchesTheKeyCryptoSwiftDerived; BackupKeyDecryptorViewModelTests attemptDecryption_backupFromANewerVersion_saysToUpdateVault; SlotFileFixtureTests header_isTheOneTheFileWasMadeWith; BackupFixtureTests encryptedVault_namesItsVersionDerivationAndSalt; EncryptedItemFixtureTests storedItem_isTheItemTheFixtureWasMadeFrom; PrivateKeyEncryptableTests encryptedContainer_encodedFormat; BackupCorpusRestoreTests backup_isPaddedAsItsFormatWas |
Holds |
| G68 | An item's own password derives its key with Argon2id over 256 MiB, with 4 passes and 4 lanes, whenever it's set or changed. An item encrypted before that, with scrypt then PBKDF2-SHA384, is encrypted again with it the next time its password opens it in the app, from that password: the same bytes under a new key, salt and IV, with nothing else about the item changed, its dates included, and only if it's still the item that was opened. If anything fails it opens as before, and nothing is said or logged. | design doc, FAQ (Item Encryption), changelog | VaultKeyDeriver (Item.Secure.v1, Item.Secure.v2), EncryptedItemDetailViewModel, VaultItemReencryptor, VaultStoreEncryptedItemReplacer |
VaultKeyDeriverParameterPinTests itemSecureV1_pinsExactKDFChain, itemSecureV2_pinsExactKDFChain; VaultKeyDeriverRealParameterTests itemSecureV2_derivesTheKnownAnswer; EncryptedItemDetailViewModelUpgradeTests startDecryption_v1Item_savesItEncryptedWithV2AndOpensWithTheNewKey, startDecryption_itemNoLongerTheOneOpened_opensWithTheOldKey, startDecryption_savingFails_opensWithTheOldKeySilently, startDecryption_v1RecoveryPhrase_isStoredWithV2AndNothingElseChanged; VaultItemReencryptorTests reencrypt_holdsExactlyWhatTheItemHeldUnderTheNewKey; VaultStoreContractTests replaceEncryptedItem_replacesOnlyTheEncryptionAndKeepsTheDates, replaceEncryptedItem_whenItsNotTheOldOneAnyMore_leavesIt; EncryptedItemFixtureTests noteKeygenV2_isTheV1NoteWithOnlyItsEncryptionChanged |
Holds, with a limit: an item keeps v1's lighter derivation until it's opened, and in every copy made before (see Accepted limits) |
| G81 | AES-GCM decryption needs the full 16-byte authentication tag. | this file | AESGCMDecryptor |
AESGCMDecryptorTests decrypt_throwsErrorIfTagIsNotFullLength, decrypt_throwsErrorIfTagIsNotFullLengthForEmptyMessage; VaultDecryptorTests decrypt_emptyDataWithoutItsTagFails |
Holds |
| G82 | Keys and a code's secret never appear in their descriptions, so nothing printed or dumped shows them. | this file | KeyData, DerivedEncryptionKey, OTPAuthSecret |
KeyDataTests description_leavesOutTheBytes, mirror_hasNoChildren; DerivedEncryptionKeyTests description_leavesOutTheKey; OTPAuthSecretTests description_leavesOutTheSecret |
Holds |
| ID | Promise | Made in | Kept by | Pinned by | Status |
|---|---|---|---|---|---|
| G69 | Vault has no servers, no accounts and no sync. | README, FAQ (Moving Between Devices), privacy policy | by absence | none | Holds, with a limit: the iPhone's own backups include Vault's data (G38), and auto-backup writes to a folder you choose, such as one in iCloud Drive |
| G70 | Vault makes no network requests. Notes never load images, and a link opens only when you tap it. | README, privacy policy, changelog | NeverLoadedMarkdownImageProvider |
MarkdownImagesNeverLoadTests everyMarkdownViewNeverLoadsImages, inlineImages_areNeverLoaded; other network use: none |
Holds |
| G71 | Vault has no analytics, telemetry or logs. | C3, privacy policy | by absence, and the no_logging lint rule, which fails the build on system logging |
the lint rule, run by make lint |
Holds |
| G72 | Vault does nothing in the background: auto-backup runs only on changes made in the app. | design doc | by absence (no BGTaskScheduler) |
none | Holds |
| G73 | Every dependency is pinned to an exact version, and Package.resolved pins its commit. |
Vault/Package.swift |
Vault/Package.swift, Vault/Package.resolved |
none | Holds, with a limit: the screenshot framer, used only for marketing screenshots, follows its main branch |
| G74 | No prebuilt binary code ships in the app. | README | Vault/Package.swift |
none | Holds, with a limit: see Accepted limits |
| G75 | The source is public, and anyone can build the app from it. | README, Settings (Open Source) | the public repository | none | Holds |
| G101 | The iPhone, iPad and Mac apps, and their AutoFill and widget extensions, run with Apple's Enhanced Security: pointer authentication (arm64e), a hardened process with a hardened heap, checked allocations that also check pointer arithmetic for overflow, read-only dyld state, and version 2 of the enhanced security and platform restrictions. So a memory-safety bug in Vault, or in a library it uses, is much harder to exploit. Every target and Swift package is built with pointer authentication, and the iOS targets also with the arm64e.x1 slice, so pointer arithmetic is checked for overflow on iPhones with FEAT_CPA2 (A20 Pro and later). |
changelog, docs/mac-app.md |
ENABLE_ENHANCED_SECURITY, ENABLE_POINTER_AUTHENTICATION and ENABLE_HARDWARE_CHECKED_POINTER_ARITHMETIC_SLICE in the project, WorkspaceSettings.xcsettings, VaultApp.entitlements, VaultMacApp.entitlements, VaultAppAutofill.entitlements, VaultWidgetsExtension.entitlements, VaultMacAutofill.entitlements, VaultMacWidgets.entitlements |
make validate's "iOS app entitlements" and "Mac app entitlements" checks, which fail unless each app and extension has every one of these entitlements with its value |
Holds |
The Mac app (docs/mac-app.md) keeps every promise above that its shared code keeps, which is most of them: duress,
the vault file, the cryptography, backups and transfer, and offline and supply chain hold unchanged, through the same
code. The rows below are its own.
Some rows above have a Mac version here:
- G22, G29: the Mac locks when another app comes to the front rather than in the background, and at once when the screen locks or the Mac sleeps (G86).
- G24: windows are kept out of every capture, rather than covered once capture is noticed (G91).
- G25, G31: Touch ID, an Apple Watch or the Mac's login password, rather than Face ID or the passcode (G85, G88, G93, G95).
- G41: a recovery phrase's words also hide while Vault isn't the active app (G88).
- G102, G103: a private key needs Touch ID or the Mac's password as well as its own password, and also hides while Vault isn't the active app (G105).
- G44, G45: widgets show Vault locked whenever it's locked, rather than whenever the password is on, and show the codes the unlocked app hands them otherwise, never a locked or hidden one. There's no Lock Screen or StandBy, and copying from a widget asks for the Mac to be unlocked (G98).
- G46: the identity store is always empty, as the password is always on (G95).
- G50, G51, G52: the Mac's clipboard, fields and keyboard (G87, G89).
- G18, G22: erasing after failed passwords and the Require Unlock delay are kept in the keychain, not the App Group's defaults (G96).
Some don't apply on the Mac:
- G16 and G28, where they're about turning the App Lock Password off, which the Mac doesn't offer (G85). Changing it holds as they say.
- G23: there's no app switcher snapshot to cover (
docs/mac-app.md, "Locking"). - G38, G39: there's no unencrypted SQLite store; before the password is set, the vault is only in memory (G85).
- G43, G53: there's no temporary file or share sheet for a PDF backup (G93).
- G49: there's no Spotlight.
| ID | Promise | Made in | Kept by | Pinned by | Status |
|---|---|---|---|---|---|
| G84 | The Mac app is sandboxed in its own App Group, with the hardened runtime, and has only the entitlements its design lists: no network, no keychain access group but its own, the App Group's, the one it shares with AutoFill (G99) and its widgets', its AutoFill extension none but the App Group's and the one it shares with the app, and its widget extension no App Group, which would also be a keychain access group, and no keychain access group but the widgets', so neither extension can read the app's keychain items, nor the other's, no iCloud, no Apple Events, and no exceptions to the sandbox or the hardened runtime. | docs/mac-app.md |
VaultMacApp.entitlements, VaultMacAutofill.entitlements, VaultMacWidgets.entitlements |
make validate's "Mac app entitlements" check; MacAppKeychainTests keychain_defaultGroup_isTheAppsOwn, keychain_eachOfTheAppsGroups_holdsItems; MacAppLaunchTests app_hasNoWayInFromOutside, autofillExtension_providesOneTimeCodesOnly, autofillExtension_hasTheAppGroupAndTheSharedKeychainGroup, widgetExtension_isOnlyWidgets, widgetExtension_hasNoAppGroupAndOnlyTheWidgetsKeychainGroup, appIntents_areOnlyTheWidgetsAndNoneIsOfferedOutsideIt, appGroupContainer_isTheTeamPrefixedOne, launch_runsInTheAppSandbox |
Holds |
| G85 | On the Mac, the App Lock Password is always set: the first launch asks for it before anything else, with the same rules and warning as iOS (G27, G30), after Touch ID or the Mac's password, and nothing can skip it or turn it off. Until it's set, the vault is an empty store in memory, so no vault is ever written that isn't encrypted. An erase goes back to the first launch. | docs/mac-app.md, first launch |
VaultMacRootView, VaultMacFirstLaunchView, VaultMacRoot, VaultStorageRecovery |
MacAppLockTests (UI) test_firstLaunch_setsThePasswordThenLocksAndUnlocksWithIt, test_firstLaunch_withAShortPassword_cantBeSet; VaultStorageRecoveryTests recover_whileEncryptingAnInMemoryStore_deletesTheEncryptedFilesAndGoesBackToPlain, recover_inMemoryPlainStoreWithAnEncryptedFileButNoJournal_deletesNothing; VaultEraserTests erase_withThePlainStoreInMemory_asOnTheMac_leavesNoStoreOnDisk |
Holds |
| G86 | The Mac locks at once, whatever the Require Unlock delay, when the screen locks, the screen saver starts, the displays or the Mac sleep, or another account takes over the screen. It locks when another app comes to the front, straight away or once the delay has passed with Vault still behind (Vault's own Touch ID or password prompt isn't another app, and an ordinary app coming to the front while it's up is), from Lock Vault, when its main window closes, whatever else is open, and when it quits. Locking clears what Vault copied, closes any Open, Save or Print panel, and forgets the sidebar's selection. Settings shows only that Vault is locked, and Help and About hold nothing from the vault. | docs/mac-app.md |
VaultMacLockTriggers, AppLockService.lockNow(), VaultMacAppDelegate, VaultMacLockedWindowGate |
VaultMacLockTriggersTests distributedNotification_locksAtOnceWhateverTheDelay, workspaceNotification_locksAtOnceWhateverTheDelay, leavingTheApp_withADelay_locksOnceItsPassedWhileStillAway, comingBackWithinTheDelay_staysUnlocked; AppLockServiceTests lockNow_locksWhateverTheDelay; MacAppLockTests (UI) test_firstLaunch_setsThePasswordThenLocksAndUnlocksWithIt; VaultMacLockTriggersTests closingTheMainWindow_locksAtOnceWhateverTheDelay, closingAnotherWindow_leavesVaultUnlocked, unlocking_underItsOwnPrompt_isNotLeaving, promptWhileUnlocked_withRequireUnlockImmediately_staysUnlocked, anotherAppComingToTheFront_whileItsOwnPromptIsUp_isLeaving, leavingForAnotherApp_whileItsOwnPromptIsUp_isLeaving; MacWindowPrivacyTests test_locking_leavesNoWindowShowingAnythingFromTheVault |
Holds, with a limit: within a Require Unlock delay, Vault's window stays on screen as it was left, behind other apps |
| G87 | Everything the Mac app copies, or its widget copies, is marked concealed and transient, so clipboard managers neither show nor keep it, stays on this Mac unless Universal Clipboard allows that kind of text (a widget's copy always stays), and is cleared after the Clear Clipboard time, 1 minute by default, or when Vault locks or quits, unless something else was copied since. | docs/mac-app.md |
VaultMacPasteboard, NSPasteboardSystemPasteboard, VaultMacAppDelegate, ConcealedPasteboard, VaultMacWidgetCopies |
VaultMacPasteboardTests copy_marksTheTextConcealedAndTransient, copy_isLocalOnlyByDefault, copy_withUniversalClipboardAllowedForCodes_canReachOtherDevicesButDetailsStillCant, copy_isClearedAfterTheClearClipboardTime, clear_onlyWhileNothingElseHasBeenCopied, clearNow_clearsWhatVaultCopied, adoptWidgetCopy_isClearedOnceItsTimeIsUp, adoptWidgetCopy_isClearedWhenVaultLocks; VaultMacWidgetCopiesTests aWidgetsCopy_isTakenOverByVaultsClipboardOnceItsHeardOf, becomingActive_takesOverACopyNotHeardOf, takeOver_aCopyNotHeardOf_isClearedWhenVaultLocks, takeOver_somethingCopiedSince_isLeftAlone; WidgetCopiedCodeStoreTests leave_keepsTheNoteThenTellsTheApp; OTPWidgetMacTests copy_isConcealedAndTransient, copyOTP_staysOnThisMacAndLeavesTheAppANote |
Holds, with a limit: choosing Never keeps copies on the clipboard, and whether macOS's own clipboard history keeps a concealed copy is checked by hand |
| G88 | On the Mac, a locked item shows nothing of itself until Touch ID or the Mac's password passes, and its code isn't copied until they do. A recovery phrase needs Touch ID or the Mac's password as well as its own password, whatever its stored lock state, and again once Vault stops being the active app; its words stay masked until clicked, hide again while Vault isn't the active app, and can't be selected or copied. Searching finds hidden items and fires killphrases exactly as on iOS (G1, G2, G8), and the list marks no item that has either (C5). | docs/mac-app.md |
VaultMacItemDetailView, VaultMacAuthenticationGate, VaultMacCopier, VaultMacEncryptedItemDetailView, VaultMacRecoveryPhraseDetailView, VaultMacItemRow, VaultDataModel |
VaultMacCopierTests copy_lockedCode_copiesOnlyOnceTheUserHasAuthenticated; VaultMacEncryptedItemDetailViewTests requiresDeviceAuthentication_recoveryPhrase_always; VaultMacFeedSnapshotTests lockedPage_showsNothingOfTheItem, recoveryPhrase_wordsMasked, list; VaultMacFeedModelTests reloadItems_itemNoLongerListed_closesItsPage, load_thenSearchingAKillphrase_deletesItsItemQuietly, load_thenSearchingASearchPassphrase_showsItsHiddenItemOnly |
Holds |
| G89 | On the Mac, every text field edits with Vault's own field editor, or is a secure field: nothing typed is spell or grammar checked, completed, predicted, substituted or offered to Writing Tools or the Services menu, and Secure Keyboard Entry is on while a field has focus and Vault is the active app, so apps watching the keyboard see nothing. It's off while another app is in front, as AppKit's secure fields have it, and on again once Vault is back with the field still focused. A single-line field keeps no undo history, a note's is its own and forgotten as its editor closes, and Look Up shows nothing. Cut and Copy go through Vault's clipboard (G87), a field's menu has only the plain text actions, and text can't be dragged out. | docs/mac-app.md |
VaultMacFieldEditor, VaultMacSecureInput, VaultMacTextField, VaultMacTextEditor, VaultMacSearchField, VaultMacSecureField, secretTextInput() |
VaultMacFieldEditorTests init_turnsOffEverythingThatLearnsFromWhatsTyped, copy_copiesTheSelectionThroughVaultsClipboardOnly, services_getNothing, menu_offersOnlyThePlainTextActions, field_whileEditing_learnsNothingAndKeepsNoUndoHistory, lookUp_offersNothing, noteEditor_undoHistory_isItsOwnAndForgottenAsItGoes, focus_turnsOnSecureKeyboardEntryUntilItsLost, anotherAppInFront_turnsOffSecureKeyboardEntryUntilVaultIsBack; VaultMacSecureInputTests anotherAppInFront_turnsItOffThoughTheFieldKeepsFocus, vaultBackInFront_withTheFieldStillFocused_turnsItOnAgain, focus_whileAnotherAppIsInFront_waitsForVault; VaultMacTextInputDeclarationTests noSwiftUITextFieldsInTheMacApp; SecretTextInputDeclarationTests everyTextInputDeclaresSecretTextInput |
Holds |
| G90 | On the Mac, a new code is checked as on iOS, whether it's typed in or read from an image (G77). The Mac app has no camera support and no camera entitlement. A recovery phrase is always encrypted with a password of its own and locked (G41), and its words are typed into secure fields. A note's password, and a recovery phrase's, only counts once it's typed the same twice, and is never stored (G42). Deleting an item asks first, and can't be undone (C6). | docs/mac-app.md |
VaultMacCodeEditor, VaultMacQRCodeImageReader, VaultMacApp.entitlements, VaultMacNoteEditor, VaultMacRecoveryPhraseEditor, VaultMacNewPassword, VaultMacMainView |
VaultMacEditorTests codeScanned_otpauthURI_isTheCode, codeScanned_anythingElse_isNothing, newPassword_onlyCountsOnceConfirmed; VaultMacEditorSnapshotTests newRecoveryPhrase, editRecoveryPhrase; MacItemEditingTests |
Holds |
| G105 | On the Mac, a private key needs Touch ID or the Mac's password as well as its own password, whatever its stored lock state, and again once Vault stops being the active app. On its page and in its editor it's masked until clicked, hides again while Vault isn't the active app, and can't be selected or copied: its editor's field copies nothing, and Cut deletes nothing. It's always encrypted with a password of its own, which only counts once it's typed the same twice (G102). | docs/mac-app.md |
VaultMacEncryptedItemDetailView, VaultMacAuthenticationGate, VaultMacPrivateKeyDetailView, VaultMacPrivateKeyEditor, VaultMacFieldEditor, VaultMacTextEditor |
VaultMacEncryptedItemDetailViewTests requiresDeviceAuthentication_privateKey_always; VaultMacFieldEditorTests copiesNothing_copyAndCutDoNothing; VaultMacFeedSnapshotTests privateKey_keyMasked; VaultMacEditorSnapshotTests newPrivateKey, editPrivateKey |
Holds |
| G91 | On the Mac, with Hide While Recording on, the default, no Vault window, sheet or alert can be captured: each one's sharingType is .none, so it's left out of screenshots, screen recordings, screen sharing and AirPlay. This is the Mac's version of G24. |
docs/mac-app.md |
VaultMacWindowPrivacy |
VaultMacWindowPrivacyTests apply_withHideWhileRecording_keepsTheWindowOutOfEveryCapture, windowUpdating_makesItPrivate, turningHideWhileRecordingOff_reachesAWindowAsItUpdates; MacAppLaunchTests launch_everyWindowIsHiddenFromCaptureAndNeverRestored |
Holds, with a limit: it depends on macOS honouring sharingType. A debug build launched for the App Store screenshots, which shows only the demo vault, lets its windows be captured |
| G92 | On the Mac, a window shows nothing from the vault once it's locked, in Mission Control, App Exposé and the Dock as well, and minimising a window locks Vault first and hides its contents until it's back. No window is restored, so macOS never saves what one showed. A window's title is only ever "Vault", "About Vault", "Vault Help" or the Settings window's tab, never an item's name. Nothing is offered to Handoff, Quick Look, the Share menu or the Services menu. | docs/mac-app.md |
VaultMacWindowPrivacy, VaultMacScene, VaultMacRootView, VaultMacLockedWindowGate |
VaultMacWindowPrivacyTests apply_windowIsNeverRestoredOrATab, minimisingAWindow_locksVaultFirst, minimisingAWindow_hidesItsContentsUntilItsBack; VaultMacWindowTitleDeclarationTests; VaultMacWindowTests title_isOnlyEverTheAppsName; MacAppLaunchTests app_hasNoWayInFromOutside; MacWindowPrivacyTests |
Holds, with a limit: apps allowed to control the Mac can read its windows |
| G93 | On the Mac, every backup and transfer follows the iOS rules, through the same code (G54–G62): Keep a Backup saves the encrypted, padded PDF only where the user chooses in the save panel, or prints it, with no temporary file of Vault's or share sheet, and is logged as a backup once it's saved or printed. Every panel starts in Documents, never the folder another vault used. Move to Another Device saves nothing, and its codes stop and are forgotten when its page closes, even while they're still being made. Auto-Backup writes only into the folder the user chose, kept with a security-scoped bookmark. Restore asks for Touch ID or the Mac's password first, and always for the backup's own password (G31, G59). | docs/mac-app.md |
VaultMacKeepABackupPage, VaultMacBackupPDFSaver, VaultMacTransferPage, DeviceTransferExportViewModel.stop(), VaultMacAutoBackupPage, iCloudDriveProvider, VaultMacRestorePage |
VaultMacBackupTests saver_save_writesThePDFAndLogsItOnce, saver_printOnPaper_logsOnlyOnceItsPrinted, roundTrip_backupSavedOnTheMac_restoresIntoAnotherVault, restore_backupMadeOnAnIPhone_restoresOnTheMac; DeviceTransferExportViewModelTests stop_endsTheTransferAndForgetsItsCodes, stop_whileGenerating_showsNothingOnceItFinishes; AutoBackupProviderCoverageTests iCloudDriveProvider_configure_keepsTheFolderAcrossARelaunch, iCloudDriveProvider_onTheMac_bookmarksAreSecurityScoped; BackupCorpusRestoreTests (the corpus, pdf-made-on-mac.pdf included, restored on iOS and the Mac) |
Holds, with a limit: the print panel's PDF menu can open or send a backup, as macOS offers for anything printed |
| G94 | On the Mac, every setting starts at iOS's default (C7): Hide While Recording on, Universal Clipboard off for codes and notes, the clipboard cleared after 1 minute, Require Unlock at Immediately, and erasing after failed passwords off. The Mac's own Keep Out of Time Machine starts on (G97). Changing the App Lock Password, setting a duress password or turning erasing on or off needs the current one, and Delete All Data asks first and then for Touch ID or the Mac's password (G15, G18). There's no way to turn the password off, and a launch argument can't set any of Vault's settings. | docs/mac-app.md |
VaultMacSettingsView, VaultMacAppLockPasswordForm, VaultMacDeleteAllDataSheet, LocalSettingsState, AppLockSettingsStore, VaultMacSettingsArguments |
VaultMacSettingsTests defaults_areiOSs, security, changePasswordForm; LocalSettingsTests; AppLockPasswordFormViewModelTests; SettingsDangerViewModelTests; VaultMacSettingsArgumentsTests removeVaultSettings_takesVaultsSettingsOutOfTheArgumentsOnly |
Holds |
| G95 | On the Mac, AutoFill fills one-time codes under the iOS AutoFill rules: its sheet asks for Touch ID or the Mac's password, then the App Lock Password (G25, G48); its attempts are counted with the app's and it never makes the last one before an erase (G20); it locks when the Mac locks, when the user goes to another app (its own Touch ID or password prompt isn't one), and at the end of every request (G29); it offers time-based codes only, never a locked or hidden one, and its search never checks killphrases or copies (G2, G47); Hide While Recording keeps it out of captures as far as macOS honours its window's sharingType (G24). Nothing is filled without the sheet, and each request empties the system's list of codes. |
docs/mac-app.md |
VaultMacCredentialProviderViewController, VaultMacAutofillModel, VaultMacAutofillRoot, AutofillVaultService, VaultMacAutofillLockObserver |
VaultMacAutofillModelTests passwordSet_asksForThePasswordAfterTouchIDOrTheMacsPassword, theMacLocking_locksTheSheetAndTheVault, userLeaving_locksTheSheetAndTheVault, passwordNotSetYet_sendsTheUserToVault, endRequest_locksTheVault, codes_neverOfferALockedOrHiddenCode, search_neverFiresAKillphrase, vaultNotSetUp_sendsTheUserToVault; AutofillVaultServiceTests unlock_lastAttempt_isntTriedOrCounted; VaultMacAutofillLockObserverTests; RELEASE.md's Mac checks |
Holds, with a limit: hiding the sheet from capture depends on macOS, and is checked by hand |
| G96 | On the Mac, erasing after failed passwords and the Require Unlock delay are kept in the keychain, on this Mac only, in the access group only Vault and its AutoFill extension have (G99): only they can change them, not something else allowed into the App Group's container. What an earlier version kept in the App Group's defaults moves there the first time they're read, and the defaults are never read for them again. If the keychain can't be read, the delay is Immediately, Settings shows erasing as on, and a password attempt that depends on erasing isn't made. An erase keeps them, with erasing off. | docs/mac-app.md |
AppLockSettingsStore, AppLockDeviceSettingsKeychainStorage, AppLockPasswordUnlocker, AppLockService.setDelay(_:), VaultEraser |
AppLockSettingsStoreTests deviceSettings_firstRead_moveWhatTheDefaultsHeld, deviceSettings_firstReadWithNothingToMove_storeTheDefaultValues, deviceSettings_onceStored_ignoreWhatTheDefaultsSay, deviceSettings_storedFirstByAnotherProcess_areReadAsTheyStoredThem, deviceSettings_unreadable_failClosed, deviceSettings_cantBeStored_setThrowsAndLeavesThemAsTheyWere; AppLockDeviceSettingsKeychainStorageTests init_sharesTheItemWithAutoFillOnly, itemQuery_neverSyncs, attributes_keepTheItemOnThisDeviceOnceItsBeenUnlocked, settingsStore_withTheSettingsStillInTheAppGroup_readsThemNotTheDefaults; AppLockPasswordUnlockerTests unlock_eraseDueWithErasingUnreadable_throwsWithoutErasingOrTrying, unlock_tenthWrongWithErasingUnreadable_throwsWithoutErasing; AppLockServiceTests setDelay_cantBeStored_staysTheSame; EncryptedVaultPasswordServiceTests setPassword_whenErasingCantBeTurnedOff_setsNoPassword; VaultEraserTests (VaultEraseHarness.keptKeychainItems), erase_whenErasingCantBeTurnedOff_stillErasesEveryVault; EncryptedVaultPasswordServiceTests unlock_tenthWrongPasswordWithErasingOn_whenErasingCantBeTurnedOff_stillErasesAndANewPasswordStartsWithItOff. That the Mac app reads them from the keychain: none |
Holds, with a limit: they stay on this Mac, so a vault moved to another Mac with Migration Assistant starts with erasing off and Require Unlock at Immediately |
| G97 | On the Mac, Keep Out of Time Machine, in Settings, keeps Vault's App Group container, with the vault file, out of Time Machine's backups. It's on by default, for vaults made before it too (C7): every launch sets the container's own exclusion unless the user has turned it off, so it covers every file in it, however often a save replaces the vault file. Turning it off is kept with the settings in G96, where only Vault can change it, and if that can't be read, the vault is kept out. | docs/mac-app.md, FAQ (About Backups), Settings |
VaultStorageBackupExclusion, AppLockSettingsStore, VaultMacRoot.setup(), VaultMacSecuritySettings |
VaultStorageBackupExclusionTests apply_byDefault_keepsTheDirectoryOutOfBackups, optOut_survivesARelaunch, apply_choiceUnreadable_keepsTheDirectoryOutOfBackups, setExcluded_off_choiceCantBeStored_throwsAndKeepsTheDirectoryOut, deviceSettings_storedWithoutTheChoice_keepTheVaultOut; AppLockSettingsStoreTests includesVaultInBackups_isOffByDefault; VaultMacSettingsTests defaults_areiOSs; that Time Machine honours it: none |
Holds, with a limit: Time Machine's local snapshots, and backups made while it was off, still hold the vault (see Accepted limits) |
| G98 | On the Mac, widgets show codes only while Vault is unlocked, and never open the vault. While it's unlocked, the app hands the widget extension the next five minutes of each time-based code a widget may show: never a secret, a locked or hidden code, or a counter-based code. They're kept in a keychain item only the app and the widget extension can read, readable only while the Mac is unlocked, on this Mac only, and the widget extension has no App Group, so it reads nothing else of Vault's. Any lock takes them back at once, deleting them or, failing that, replacing them with none, and trying again until it can; so do quitting and every launch. Codes worked out before a lock are never handed over after it, and a widget never shows a code past its five minutes. While Vault's locked, widgets show it locked, and a widget's settings list no codes. Copying from a widget asks for the Mac to be unlocked, and is cleared as Vault's own copies are (G87). Widgets have no links, and their actions aren't offered to Shortcuts or Spotlight. | docs/mac-app.md |
VaultMacWidgetCodes, WidgetCodeHandoff, WidgetCodeHandoffStore, WidgetKeychainItem, OTPWidgetProvider, OTPWidgetSource, OTPWidgetLink, VaultMacWidgets.entitlements |
VaultMacWidgetCodesTests update_whileUnlocked_handsOverTheCodesWidgetsMayShowAndReloadsThem, withdraw_takesTheCodesBackAtOnceAndReloads, update_whileLocked_handsNothingOverAndReadsNothing, locking_whileTheCodesAreWorkedOut_handsNothingOver, whileUnlocked_handsTheCodesOverAgainBeforeTheyRunOut, withdraw_failing_triesAgainUntilItCan, handingOverAgain_stopsTryingToTakeTheOldCodesBack, withdrawLeftovers_takesBackWhatAnEarlierRunLeft; WidgetCodeHandoffTests encoded_neverHoldsTheSecret, init_leavesOutWhatWidgetsMayNotShow, init_leavesOutCounterBasedCodes, code_isThePeriodsCodeUntilTheHandoffExpires; WidgetCodeHandoffStoreTests withdraw_codesNotDeleted_leavesNoneInTheirPlace, withdraw_codesNeitherDeletedNorReplaced_throws, codes_areReadableOnlyWhileUnlockedAndOnlyOnThisDevice, items_areInTheWidgetsOwnGroup; OTPWidgetHandoffTests timeline_showsEachCodeInTurnThenLocked, timeline_withNoHandoff_isLocked, entityQuery_withNoHandoff_listsNothing, copy_onceTheCodesAreTakenBack_copiesNothing; OTPWidgetMacTests source_isTheCodesTheAppHandsOver, links_areNone, actions_areNotOfferedToShortcutsOrSpotlight, copy_asksForTheMacToBeUnlocked; MacAppLaunchTests widgetExtension_hasNoAppGroupAndOnlyTheWidgetsKeychainGroup, appIntents_areOnlyTheWidgetsAndNoneIsOfferedOutsideIt; make validate's "Mac app entitlements" check |
Holds, with a limit: see Accepted limits |
These are known and accepted. Each is said where users will read it.
- Without an App Lock Password, the vault isn't encrypted by Vault. It's a SQLite store that iOS Data Protection covers only while the iPhone is locked. Anyone who can read it sees every item, and which items are hidden or have a killphrase. It's in the iPhone's own backups to iCloud or a computer, as other apps' data is, and without Advanced Data Protection Apple can read an iCloud Backup. Setting an App Lock Password encrypts the vault on the iPhone and in those backups. (G38)
- Killphrases match as you type. Every change to the search is checked against the whole search, capitals counting and spaces at either end ignored. So a killphrase fires on the way to typing a longer search that starts with it. Only Vault's own search checks killphrases, not AutoFill's. (G1, G2)
- Backups carry the keys that check phrases. That's what keeps killphrases and search passphrases working once a backup is restored. It also means anyone who can decrypt a backup can test guesses at its phrases, so a phrase shouldn't be a password used anywhere else. (G7, G56)
- Backups with the same password share a salt. A backup's key derivation salt is chosen when its backup password is set, and stays the same until the password changes. So, without decrypting anything, backups made with one password can be told apart from those made with another. (G55)
- Items and backup passwords from before v2 keep their lighter key derivation until they're used. An item encrypted before v2 moves to v2's Argon2id only when its password next opens it, and a backup password set before v2 only when a Backups page next loads it. Until then, and in every copy made before, such as an earlier backup, an iPhone backup or a backup PDF, a guess at the password costs only v1's derivation: for an item, scrypt and PBKDF2, much less than the App Lock Password's Argon2id. A backup password carried on to v2 keeps its salt, so an older backup made with it can be attacked at v1's cost, and what that finds opens the newer backups too. Give encrypted items long passwords, such as a few random words, and open them once after updating. (G42, G55, G68)
- Backup and item passwords need a lot of memory to derive. v2's Argon2id takes 512 MiB for a backup and 256 MiB for an item, so it only ever runs in the app. On an iPhone short of free memory, iOS could end the app part way through a restore, or while it moves a backup key on, which then stays as it was. Nothing is lost, and trying again with fewer apps open works. (G55, G68)
- On iOS, the widget extension can read the device key. While the App Lock Password is off, the widgets show codes from the vault, as they do when there's never been a password, and the device key is what opens it then. So it stays in the App Group's keychain access group, which the widget extension has. The count of wrong passwords and the wrap stamp are kept where only Vault and its AutoFill extension can read them. With the password on, there's no device key. (G28, G44, G99)
- Some build tools are prebuilt binaries. mockolo, SwiftFormat and SwiftLint are downloaded as binaries pinned by checksum. They run only while building. mockolo writes the mocks that are compiled into the package's modules. (G74)
- Duress vaults nest safely eleven deep. A duress vault can have its own, and so on. For eleven levels, none can touch the real vault. Beyond that, each new duress vault has up to a one in five chance of taking the real vault's place. (G13)
- A duress password starts the waits again. Any vault's password resets the count of wrong passwords, so someone who knows a duress password can make four wrong guesses at another vault's password, open the duress vault, and do it again, as often as they like. Neither the waits nor erasing after 10 slow them down on the iPhone, only the key derivation, about a second a guess (half a second on a vault file made before VAULT-128). A long App Lock Password is what keeps the real vault safe. (G19)
- On the Mac, something running as you, with your consent, can copy the vault file. macOS keeps other apps out of Vault's App Group unless you allow them, or give them Full Disk Access. Anything you do can copy the vault file and try passwords on it elsewhere, as with a copy of an iPhone's files: a long App Lock Password is what protects it. It could also change the settings kept beside the vault that anyone at the unlocked Vault can change, such as Hide While Recording, but not erasing after failed passwords or the Require Unlock delay, which are in the keychain (G96). (G85, G94)
- On the Mac, Time Machine's local snapshots keep earlier copies of the vault. They're of the whole disk, so they hold the vault file as it was, and so do Time Machine's backups made before Vault kept the vault out of them, or while Keep Out of Time Machine was off. An item a killphrase deleted, or a vault Delete All Data or an erase removed, can come back from one with the App Lock Password it had then. (G6, G15, G18, G63, G97)
- A vault that hasn't opened since vaults kept their own keyrings still needs the keychain. Before, every vault on a device checked its phrases with the same keys, in the keychain. An encrypted vault takes its copy the first time it opens, and a vault can't tell whether there are others, so the keychain keeps them. A vault moved to another Mac, or restored onto another iPhone, before it has opened once loses them, and so does a duress vault that hasn't been opened. Open each vault once before moving it, or move with a Vault backup, which carries them. Every vault that took a copy also has those keys to match against, so a backup of a duress vault made then carries the keys the real vault's phrases from then were digested with, as it always did. (G7, G100)
- Without an App Lock Password, killphrases and search passphrases don't move with the vault. Their keys stay in the keychain, never beside the digests in a store that isn't encrypted, and so do a vault's while the password is off. Move with a Vault backup, which carries them. (G7)
- On the Mac, windows macOS draws for Vault aren't kept out of captures. The Open panel's preview, an input method's candidate window, Hover Text and Zoom, and the Touch ID prompt are macOS's, not Vault's. (G91)
- On the Mac, the print panel can send a backup on. Its PDF menu opens the backup in Preview, or sends it in Mail or Messages, as it does for anything printed. Only Print and Save as PDF count as a backup. (G93)
- On the Mac, apps you've allowed to control it can read Vault. An app with Accessibility access, in Privacy & Security, can read what Vault's windows show, and what's typed into its fields, while they show it, as a screen reader must. Secure Keyboard Entry only keeps keystrokes from apps watching the keyboard. (G89, G92)
- On the Mac, a widget can show codes for a few minutes after Vault locks. Vault takes its widgets' codes back the moment it locks and asks macOS to reload them, but if macOS holds the reload back, or Vault stops without locking, as when it crashes, or the codes can be neither deleted nor replaced as the Mac locks and it's unlocked again before they can, a widget goes on showing the codes it was given until they run out: five minutes at most, and never a stale code. Widgets are drawn by macOS, so Hide While Recording doesn't keep them out of captures. A widget's settings keep the site and account of the code it was set up with until it's removed, and with a duress vault open, a widget set up for another vault's code shows it unavailable, as for a code that's been deleted. Don't put a code you'd hide from someone forcing you to unlock Vault on a widget. (G98)
- On the Mac, hiding windows from capture depends on macOS. Vault asks macOS to leave its windows out of every
capture, and it's checked by hand on each version of macOS (
RELEASE.md), but Vault can't make it do so. That's most uncertain for the AutoFill sheet, which macOS shows inside the app it fills. (G91, G95)