Реалізація PIN-коду для входу в мобільне приложення
PIN-код — локальний другий фактор: користувач один раз вводить повні облікові дані, потім розблокує приложення PIN-кодом. Це не аутентифікація на сервері — це розблокування локального сховища з credentials.
Ключове поняття: PIN неможна зберігати. Ні в якій формі. Навіть хеш PIN без salt — небезпечен (4–6 цифр, перебір всіх варіантів займає секунди).
Правильна криптографічна схема
PIN використовується для деривації ключа, яким шифрується реальний секрет (refresh token або симетричний ключ шифрування даних). Схема:
- Генеруємо випадковий
salt(16–32 байта,SecRandomCopyBytes/SecureRandom). - Виводимо ключ з PIN + salt через PBKDF2 (мінімум 100 000 ітерацій, SHA-256) або Argon2id.
- Зашифровуємо refresh token генерованим ключем (AES-256-GCM).
- Зберігаємо шифротекст + salt + IV в Keychain/EncryptedSharedPreferences.
- PIN ніде не зберігаємо.
// iOS — деривація ключа з PIN
func deriveKey(from pin: String, salt: Data) throws -> SymmetricKey {
let pinData = Data(pin.utf8)
var derivedKey = Data(count: 32)
let result = derivedKey.withUnsafeMutableBytes { derivedKeyPtr in
pinData.withUnsafeBytes { pinPtr in
salt.withUnsafeBytes { saltPtr in
CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
pinPtr.baseAddress, pinData.count,
saltPtr.baseAddress, salt.count,
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
100_000,
derivedKeyPtr.baseAddress, 32
)
}
}
}
guard result == kCCSuccess else { throw CryptoError.keyDerivationFailed }
return SymmetricKey(data: derivedKey)
}
Верифікація PIN при вводі: пробуємо розшифрувати AES-GCM з отриманим ключем. Якщо розшифровка вдалася (тег збігся) — PIN правильний. Якщо ні — неправильний. Ніяких isPinCorrect флагів у сховищі.
UI: кастомна клавіатура обов'язкова
Системна клавіатура для PIN — погана ідея з кількох причин:
- iOS та Android показують предиктивний ввід над клавіатурою — PIN може попасти в словник автокореції.
- Системна клавіатура має фіксований layout — немає рандомізації цифр.
- Треті особи теоретично можуть перехопити ввід через
InputMethodService(Android).
Робимо кастомну цифрову клавіатуру. В SwiftUI — LazyVGrid з кнопками, без UITextField. У Jetpack Compose — аналогічно через LazyVerticalGrid. Відображення введених цифр — заповнені/пусті кола, без тексту.
Рандомізація раскладки (shuffle digits) — опціонально для high-security приложень. Ускладнює shoulder surfing атаки.
Лічильник помилок та lockout
Після N невдалих спроб (зазвичай 3–5) — lockout. Варіанти:
- М'який: затримка між спробами, що зростає експоненціально (30 сек → 5 хв → 30 хв).
- Жорсткий: блокування PIN-входу, потрібен повний логін через credentials.
- Дуже жорсткий (enterprise): видалення даних приложення після 10 невдалих спроб.
Лічильник помилок зберігається в Keychain/EncryptedSharedPreferences — не в UserDefaults, інакше користувач може скинути лічильник видаленням/відновленням приложення з бекапу.
Зміна PIN
Старий PIN → розшифровуємо секрет → новий PIN → деривуємо новий ключ → шифруємо заново → зберігаємо з новим salt та IV. Атомарно: спочатку записуємо нові дані у тимчасовий ключ, перевіряємо що розшифровка працює, тільки потім видаляємо старі.
Біометрія + PIN
Біометрія — зручність, PIN — обов'язковий fallback. При lockout Face ID/Touch ID система вимагає passcode пристрою, а не PIN приложення. Це різні речі. PIN приложення повинен працювати незалежно від стану системної біометрії.
Архітектурно: LocalAuthService з методом unlock(), який пробує біометрію та при відмові/недоступності переключається на PIN-екран. Рішення про те, що показати першим, — конфігурація приложення або предпочтення користувача.
Терміни
Реалізація PIN з правильною криптографічною схемою, кастомною клавіатурою, лічильником помилок та біометричним fallback — 5–8 робочих днів.







