Налаштування Keychain для безпечного збереження даних в iOS
Після оновлення додатка не знаходить токен авторизації — користувач знову видить екран входу. Або гірше: токен зберігся, але доступен іншому додатку того самого вендора без обмежень. Обидві ситуації — наслідок неправильно налаштованого Keychain.
Де саме ломається
Найчастіша помилка — зберігання токенів через UserDefaults. Дані потрапляють в Library/Preferences/*.plist, що входить в iCloud-бекап та доступен через інструменти витягу iTunes backup, як iBackup Viewer. Це не теоретична загроза.
Друга за частотою — використання Keychain без явного kSecAttrAccessible. За замовчуванням kSecAttrAccessibleWhenUnlocked, що розумно, але багато проектів не думають, чи потрібен доступ до даних при заблокованому екрані (для background tasks) або тільки коли пристрій розблокований і лише після першого разблокування після перезавантаження. Це принципово різні threat models.
Третя — відсутність kSecAttrAccessGroup при роботі в App Group. Якщо у вас основний додаток та віджет або розширення, і токен не розшарений явно, розширення не побачить Keychain-запис, хоча Bundle ID з тієї самої групи.
Правильне налаштування через Security framework
Базовий паттерн запису:
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app.auth",
kSecAttrAccount as String: "access_token",
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
kSecValueData as String: tokenData,
kSecAttrAccessGroup as String: "TEAMID.com.example.shared" // якщо потрібен App Group
]
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly — правильний вибір для більшості токенів: доступен після першого разблокування після перезавантаження, не синхронізується в iCloud, не переносится на інший пристрій. Якщо вам потрібна синхронізація між пристроями користувача (наприклад, пароль від заміток), тоді kSecAttrAccessibleWhenUnlocked з kSecAttrSynchronizable: kCFBooleanTrue. Але для auth-токенів синхронізація через iCloud Keychain небажана — компрометація одного пристрою компрометує всі.
Для читання з оновленням (upsert) — спочатку SecItemUpdate, при помилці errSecItemNotFound — SecItemAdd. Не робіть SecItemDelete + SecItemAdd — створює race condition у багатопоточному середовищі.
Біометрична захист через LocalAuthentication
Якщо дані потребують біометричного захисту перед доступом (ключи шифрування, приватні ключі):
let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.biometryCurrentSet, // .userPresence для fallback на пасскод
nil
)!
Флаг .biometryCurrentSet інвалідує запис при змінах біометрії (новий відбиток, перехід на Face ID) — це намисна поведінка для найтаємніших даних. Для менш критичних .biometryAny зберігає доступ після додавання нових відбитків.
Обгортка і тестованість
Прямі виклики SecItemAdd у production-коді роблять unit-тести неможливими без реального Keychain. Обгорніть у протокол:
protocol KeychainService {
func save(_ data: Data, for key: String) throws
func load(for key: String) throws -> Data
func delete(for key: String) throws
}
У тестах підставляйте InMemoryKeychainService. У production — реалізація через Security framework. Стандартний підхід у Clean Architecture для iOS.
Процес
Аудит поточного коду — шукаємо UserDefaults, NSKeyedArchiver, plaintext у файлах. Далі проектуємо KeychainService під конкретний набір збережених даних, реалізуємо з правильними kSecAttrAccessible, налаштовуємо App Group якщо потрібно розшарювання з розширеннями, покриваємо unit-тестами через мок. Окремо — перевірка поведінки при змінах біометрії та оновленні додатка.
Часові рамки — 1–3 дні. Проста заміна UserDefaults на Keychain для одного типу даних — ближче до дня. Повний аудит + рефакторинг + App Group + біометрія — до трьох.







