Реализация PIN-кода для подтверждения транзакций мобильного криптокошелька
PIN-код — fallback к биометрии и самостоятельный механизм защиты транзакций. Главная ошибка реализации: хранить PIN в открытом виде или сравнивать его строковое представление без хэширования. Вторая по частоте — не ограничивать количество попыток.
Как хранить PIN правильно
PIN никогда не хранится как есть. Минимальный приемлемый вариант — PBKDF2-HMAC-SHA256 с уникальной солью (32 байта из CSPRNG) и количеством итераций от 100 000. Лучше — bcrypt или Argon2id, но последний требует стороннюю библиотеку на iOS.
На iOS: PBKDF2 через CCKeyDerivationPBKDF из CommonCrypto:
func deriveKey(from pin: String, salt: Data, iterations: UInt32 = 200_000) -> Data {
var derivedKey = Data(count: 32)
let pinData = pin.data(using: .utf8)!
derivedKey.withUnsafeMutableBytes { derivedPtr in
pinData.withUnsafeBytes { pinPtr in
salt.withUnsafeBytes { saltPtr in
CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
pinPtr.baseAddress, pinData.count,
saltPtr.baseAddress, salt.count,
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
iterations,
derivedPtr.baseAddress, 32
)
}
}
}
return derivedKey
}
Соль + хэш хранятся в Keychain с kSecAttrAccessibleWhenUnlockedThisDeviceOnly. PIN из памяти обнуляется сразу после деривации.
Ограничение попыток и блокировка
После 3 неудачных попыток — задержка (например, 30 секунд). После 5 — более долгая. После 10 — полная блокировка кошелька с требованием восстановления через seed-фразу. Счётчик попыток хранится в Keychain (не UserDefaults — его можно сбросить удалением данных приложения без root, а Keychain — нельзя).
Атаку через удаление и переустановку приложения можно частично нейтрализовать: на iOS Keychain с kSecAttrAccessibleWhenUnlockedThisDeviceOnly без kSecAttrSynchronizable переживает переустановку. На Android данные KeyStore при удалении приложения уничтожаются — там блокировку нужно хранить через EncryptedSharedPreferences в отдельном contentProvider или через бэкенд.
UI: кастомный numpad
Системная цифровая клавиатура удобна, но делает невозможным визуальный контроль — не понятно, сколько цифр введено. Кастомный numpad (6 кружков + 10 цифр + backspace) — стандарт для криптокошельков. Без haptic feedback на каждый тап — ощущение хуже.
PIN-поле никогда не должно предлагать автозаполнение из iCloud Keychain или менеджера паролей — textContentType = .none + autocorrectionType = .no + keyboardType = .numberPad на скрытом TextField под кастомным numpad.
Сроки — 1–3 дня. PBKDF2 + UI numpad + ограничение попыток — день-полтора. Добавление механизмов anti-bypass и тестирование граничных случаев — до трёх дней.







