Imagine a user is logging into their banking app while grabbing coffee in a crowded cafe. They quickly type in their PIN, maybe not noticing someone glancing over their shoulder — or that a vulnerability in the app could put their data at risk. That’s exactly why Secure Input for PIN Entry is so important, especially in financial apps where a PIN is more than just a few numbers; it’s a gateway to sensitive information.
In this guide, we’ll build a secure PIN entry system for Android. I’ll walk you through the Kotlin code step-by-step, along with key security tips. So stay tuned..!
Why is Secure PIN Entry So Important?
In financial apps, PINs are often the key to user authentication, making secure PIN entry essential. Here’s why it matters:
- Prevent Shoulder Surfing: Stop others from sneaking a peek at the PIN in crowded or public spaces.
- Block Unauthorized Access: Strengthen PIN handling to eliminate weak security points.
- Protect Against Data Leaks: Safeguard sensitive data by avoiding insecure storage or logging practices.
Best Practices for Secure Input for PIN Entry
- Use a Custom View for Input Masking
- Default Android input views may not be secure enough, as they’re designed for generic inputs. Creating a custom view for PIN entry adds control over how data is handled and stored.
- Minimize PIN Storage Duration
- Store PIN data in memory only as long as needed. Clear it from memory once used.
- Use Secure Storage for Sensitive Data
- Don’t store the PIN itself; instead, use tokens or session IDs post-authentication.
- Disable Screenshots
- Prevent screenshots and screen recording to avoid capturing the PIN on-screen.
- Avoid Logging Sensitive Data
- Ensure that the PIN isn’t logged or displayed in the logcat.
- Use Obfuscation Techniques
- Obfuscate sensitive parts of the codebase to make reverse engineering harder.
Implementing Secure PIN Entry in Kotlin
Alright, without wasting time, let’s jump into the Kotlin code and start building our secure PIN entry feature.
Disable Screenshots
Preventing screenshots and screen recording ensures that no sensitive data gets captured visually.
In your Activity
class, disable screenshots by adding this line in the onCreate
method.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
setContentView(R.layout.activity_pin_entry)
}
The FLAG_SECURE
flag prevents the app from taking screenshots or recording the screen when the PIN entry screen is open.
Create a Custom PIN Entry View
A custom PIN entry view allows us to control how each input character behaves and ensures that data is stored only in memory for the duration needed.
Create a SecurePinEntryView
class that extends LinearLayout
.
class SecurePinEntryView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
private val pinDigits = mutableListOf<EditText>() // Holds the EditTexts for each PIN digit
private val maxPinLength = 4 // Number of PIN digits
init {
orientation = HORIZONTAL
setupPinFields()
}
private fun setupPinFields() {
for (i in 0 until maxPinLength) {
val digitField = createDigitField()
pinDigits.add(digitField)
addView(digitField)
}
}
private fun createDigitField(): EditText {
return EditText(context).apply {
inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
setBackgroundColor(Color.TRANSPARENT)
filters = arrayOf(InputFilter.LengthFilter(1))
isCursorVisible = false
textAlignment = View.TEXT_ALIGNMENT_CENTER
layoutParams = LayoutParams(0, LayoutParams.MATCH_PARENT, 1f) // Distribute space evenly
setOnFocusChangeListener { _, hasFocus ->
if (hasFocus && text.isEmpty()) {
this.text.clear() // Clear text only if empty to avoid accidental deletion
}
}
}
}
// Collects the PIN entered by the user
fun getPin(): String {
return pinDigits.joinToString("") { it.text.toString() }
}
// Clears the entered PIN
fun clearPin() {
pinDigits.forEach { it.text.clear() }
}
}
Here,
- Orientation & Style: We set the orientation to
HORIZONTAL
to align the PIN digits in a row.createDigitField()
: Creates a customizedEditText
for each PIN digit. InputType.TYPE_NUMBER_VARIATION_PASSWORD
hides the PIN visually by displaying dots.- Each digit field is limited to one character using
InputFilter.LengthFilter(1)
. isCursorVisible
is set to false to remove the blinking cursor, which makes it harder for onlookers to see which digit is currently being entered.
Handle PIN Submission
Once the user enters the PIN, we’ll verify it securely. Here’s an example of how to collect and handle the PIN securely.
val securePinEntryView = findViewById<SecurePinEntryView>(R.id.securePinEntryView)
val submitButton = findViewById<Button>(R.id.submitButton)
submitButton.setOnClickListener {
val enteredPin = securePinEntryView.getPin()
// Verify the PIN here or pass it to the next step
if (verifyPin(enteredPin)) {
// Handle successful PIN entry
Toast.makeText(this, "PIN verified!", Toast.LENGTH_SHORT).show()
} else {
// Clear PIN for incorrect attempts
securePinEntryView.clearPin()
Toast.makeText(this, "Incorrect PIN. Try again.", Toast.LENGTH_SHORT).show()
}
}
private fun verifyPin(pin: String): Boolean {
// Replace this with actual PIN verification logic
return pin == "1234" // Example PIN for demonstration
}
In this code,
getPin()
: Collects the entered PIN from the custom view.verifyPin()
: Checks if the entered PIN matches the stored PIN. In a real application, use a secure method to validate the PIN.
Clear PIN on Incorrect Attempts
Clearing the PIN on incorrect attempts prevents attackers from guessing and observing patterns.
private fun handleIncorrectPin() {
securePinEntryView.clearPin() // Clears the entered PIN
Toast.makeText(this, "Incorrect PIN. Try again.", Toast.LENGTH_SHORT).show()
}
Hash, Encode, Encrypt, and Store
Always hash sensitive data and use Base64 encoding before encrypting and storing it.
Since we haven’t added any PIN security logic yet, let’s take the next step and organize it into its own class, following the Single Responsibility Principle. This way, we’ll keep things clean and put the logic in the SecurePinManager
class.
import android.content.Context
import android.text.InputType
import android.widget.EditText
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import java.security.MessageDigest
import java.util.*
class SecurePinManager(context: Context) {
private val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
private val encryptedPrefs = EncryptedSharedPreferences.create(
"secure_prefs",
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun setupPinInputField(editText: EditText) {
editText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
}
fun savePin(pin: String) {
val hashedPin = hashPin(pin) // Hash the PIN before saving
encryptedPrefs.edit().putString("user_pin", hashedPin).apply()
}
fun verifyPin(inputPin: String): Boolean {
val storedHashedPin = encryptedPrefs.getString("user_pin", null)
val inputHashedPin = hashPin(inputPin) // Hash the input before comparison
return storedHashedPin == inputHashedPin
}
// Hashes the PIN using SHA-256
private fun hashPin(pin: String): String {
val digest = MessageDigest.getInstance("SHA-256")
val hashedBytes = digest.digest(pin.toByteArray())
return Base64.getEncoder().encodeToString(hashedBytes) // Encode the hashed bytes in Base64
}
}
- PIN Hashing: The PIN is now hashed using SHA-256 before saving and comparing. This adds a layer of security by ensuring the raw PIN is never stored.
- Base64 Encoding: The hashed PIN is encoded using Base64 to store it as a string in
EncryptedSharedPreferences
.
Prevent Sensitive Data from Being Logged
Avoid logging the entered PIN or sensitive information.
// BAD: Never log sensitive data
Log.d("PinEntry", "Entered PIN: $enteredPin")
// GOOD: No sensitive data in logs
Log.d("PinEntry", "PIN entry attempt")
Additional Key Security Tips
- Limit Accessibility During PIN Entry: Restrict accessibility features like screen readers or magnification during PIN entry to prevent accidental exposure of sensitive information.
- Enable Biometric Authentication: Consider using biometric authentication (e.g., fingerprint, face recognition) for an extra layer of security, alongside the PIN.
- Encrypt Sensitive Data: While PINs themselves shouldn’t be stored directly, always hash sensitive data and use Base64 encoding before encrypting and storing it.
- Regularly Clear Sensitive Data: If you’re using data holders like
LiveData
or other similar components, ensure that sensitive data is cleared when it is no longer needed. Properly manage the lifecycle of such data to avoid unintentional retention in memory or storage.
Conclusion
Building a secure PIN entry system isn’t just about protecting data—it’s about earning user trust and safeguarding their sensitive information. With Kotlin’s secure handling features, you can create a seamless, safe experience for your users.
By following these steps, you’re not only securing their PINs but also ensuring their data is treated with the highest level of care in your financial app. Let’s keep security at the forefront and provide users the peace of mind they deserve..!