Реализация 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 система требует пасскод устройства, а не PIN приложения. Это разные вещи. PIN приложения должен работать независимо от состояния системной биометрии.
Архитектурно: LocalAuthService с методом unlock(), который пробует биометрию и при отказе/недоступности переключается на PIN-экран. Решение о том, что показать первым, — конфигурация приложения или предпочтение пользователя.
Сроки
Реализация PIN с правильной криптографической схемой, кастомной клавиатурой, счётчиком ошибок и биометрическим fallback — 5–8 рабочих дней.







