Реалізація 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 — його можна скинути видаленням даних програми без рута, а 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 та тестування граничних випадків — до трьох днів.







