Secure communication is the backbone of modern financial app development. HTTPS, powered by TLS (Transport Layer Security), ensures that the data exchanged between your app and its server stays protected from unauthorized access. In this blog, we’ll explore HTTPS communication on Android, focusing on TLS 1.3—the latest and most secure version of the protocol. We’ll guide you through the process of implementing secure communication in Kotlin, simplifying each step with clear, jargon-free explanations.
What is HTTPS and TLS?
HTTPS HTTPS (Hypertext Transfer Protocol Secure) is an upgrade to HTTP, designed to secure the communication between web clients and servers. It uses TLS (Transport Layer Security) to encrypt the data, protecting it from interception during transmission. This is especially important for safeguarding sensitive details like passwords, payment information, or personal data.
TLS TLS is a cryptographic protocol that offers three core protections:
Encryption: Ensures that data remains confidential and cannot be accessed by unauthorized parties.
Authentication: Confirms that the server is legitimate and, optionally, verifies the client’s identity.
Integrity: Guarantees that the data hasn’t been modified during transmission.
TLS 1.3 TLS 1.3, the latest version of the protocol, brings several key enhancements:
Improved Handshake Performance: Reduces the time needed to establish a secure connection.
Stronger Encryption: Implements more robust encryption methods for better security.
HTTPS As the secure version of HTTP, HTTPS uses TLS to encrypt the data exchanged between the app and the server. In the context of financial applications, HTTPS offers:
Confidentiality: Safeguards sensitive information like user credentials and transaction data from being intercepted.
Data Integrity: Ensures the information sent and received is unchanged during transit.
Server Authentication: Verifies the authenticity of the server, helping protect against fraud and man-in-the-middle attacks.
TLS 1.3 TLS 1.3, released in 2018, brings numerous advantages over previous versions:
Stronger Security: Phases out older, vulnerable protocols such as RSA key exchange, making the connection more secure.
Faster Handshakes: Simplifies the connection process, improving speed and reducing delay.
Forward Secrecy: Even if an attacker gains access to a server’s private key, past communication remains secure.
For financial apps, using TLS 1.3 is essential to ensure both robust security and a smooth, responsive user experience.
Setting Up HTTPS in Android Apps
Android natively supports HTTPS, but to make sure your app works with TLS 1.3, you’ll need to configure a few settings and understand the requirements.
Prerequisites
Make sure your app is targeting Android 10 (API level 29) or higher, as this version comes with native support for TLS 1.3.
Install a valid SSL certificate on the server hosting your APIs to establish secure communication.
Step-by-Step Implementation
Kotlin
// Use the latest version in the future.implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.google.code.gson:gson:2.12.0")
We’ll utilize OkHttp for handling HTTPS requests, as it offers a lightweight and efficient solution.
Creating a Secure HTTP Client
To enable HTTPS with TLS 1.3, configure OkHttp’s OkHttpClient. This client will handle secure communication with your backend.
connectTimeout: The maximum duration allowed for establishing a connection.
readTimeout: The maximum time allowed to wait for data after the connection is established.
writeTimeout: The maximum time allowed to wait while sending data to the server.
With Android 10 and higher versions supporting TLS 1.3 natively, no extra configuration is needed for the protocol. The OkHttp client automatically negotiates the highest version it supports.
For older Android versions, ensure that the device is using the latest system libraries, or incorporate third-party TLS solutions such as Conscrypt to enable support for newer TLS protocols like TLS 1.2 or TLS 1.3.
Making Secure HTTPS Requests
Once the client is ready, use it to make API requests.
Request Building: Defines the target URL and HTTP method (GET in this case).
Response Handling: Reads and parses the server’s response. Always handle errors to ensure reliability.
Key Points About TLS 1.3
Backward Compatibility: TLS 1.3 supports backward compatibility with older versions (like TLS 1.2) by negotiating the highest mutually supported version.
Performance: TLS 1.3 reduces round-trip times (RTTs) during handshakes, resulting in faster connection establishment, making it ideal for mobile apps.
Security: TLS 1.3 deprecates weak cryptographic algorithms and only supports modern, secure encryption methods, ensuring enhanced security.
Testing HTTPS Communication
Use Postman: Test your API endpoints, ensuring valid certificates are used and checking SSL/TLS connection aspects like certificate trust and hostname verification.
Validate Pinning: Validate certificate pinning by changing the server’s certificate and ensuring that the client rejects untrusted connections. Ensure both server and client-side pinning implementations are correctly configured.
Check TLS Version: Check the TLS version using tools like Wireshark, which can capture network traffic and verify if TLS 1.3 is being used, or use OpenSSL for command-line verification.
Best Practices for Secure HTTPS Communication
Use Strong Encryption: Always enable the latest TLS protocols (preferably TLS 1.2 or 1.3) and ensure strong cipher suites are used for secure communication.
Avoid Hardcoding Keys: Avoid hardcoding keys and sensitive data in your source code; use secure storage mechanisms like Android Keystore, EncryptedSharedPreferences, or secure servers to store such information.
Monitor Dependencies: Monitor and regularly update libraries (e.g., OkHttp, Retrofit) to patch vulnerabilities. Use tools like Dependabot to stay up to date with security updates.
Implement Error Handling: Implement robust error handling to manage network issues gracefully without exposing sensitive information. Provide meaningful feedback to users without revealing implementation details or errors.
Conclusion
Secure HTTPS (TLS 1.3) communication isn’t just a best practice — it’s a must for financial Android apps. With Kotlin and powerful tools like OkHttp, you can easily implement top-tier security without the hassle. By following these steps, you’ll not only protect sensitive data but also earn your users’ trust every step of the way.
Let’s secure the financial world, one app at a time..!
In today’s digital age, ensuring secure communication and data integrity is essential, especially when handling sensitive information in financial Android applications. User data like credit card numbers, bank account details, and personal identifiers must be safeguarded to prevent unauthorized access. One effective technology for achieving this level of security is JOSE (JSON Object Signing and Encryption).
JOSE provides a standardized approach for securely signing, encrypting, and verifying JSON data, making it a valuable tool for securing APIs and data transmissions. By using JOSE, developers can ensure the authenticity, integrity, and confidentiality of the data being exchanged.
In this article, we will introduce you to the core concepts behind JOSE, demonstrate its significance in securing financial Android applications, and walk you through the implementation process using Kotlin, complete with practical code examples. By the end of this guide, you’ll understand how JOSE encryption plays a crucial role in protecting sensitive data.
What is JOSE?
JOSE is a suite of standards defined by the IETF that provides a structured approach to securing JSON data. It is ideal for modern applications that rely heavily on APIs for communication and is commonly used in APIs, mobile/web applications, and microservices. It includes:
JWS (JSON Web Signature): Ensures data integrity and authenticity by signing JSON objects.
JWE (JSON Web Encryption): Secures the data by encrypting it.
JWK (JSON Web Key): A format for representing cryptographic keys.
JWA (JSON Web Algorithms): Defines algorithms used for signing and encryption.
JWT (JSON Web Token): A compact representation often used for claims (data) and identity.
In financial applications, JOSE is crucial for:
Data Confidentiality: Encrypt sensitive data like transactions or user credentials.
Data Integrity: Ensure the data has not been tampered with.
Authentication: Verify the identity of users or systems through signatures.
Why Use JOSE in Financial Android Apps?
Regulatory Compliance: Many financial standards like PCI-DSS demand secure data transmission and storage.
End-to-End Encryption: JOSE ensures secure communication between the client (Android app) and the server.
Enhanced User Trust: Users trust apps that prioritize their security and privacy.
How JOSE Works: A Simplified Flow
Signing Data with JWS:
The app generates a digital signature for the JSON data using a private key.
The recipient verifies the signature using the corresponding public key.
Encrypting Data with JWE:
JSON data is encrypted using a symmetric or asymmetric encryption algorithm.
Only the intended recipient can decrypt the data using their private key.
Sending the Encrypted and Signed Data:
The app sends the JWE or JWS to the server over a secure channel (e.g., HTTPS).
JOSE Structure
The JOSE framework operates through a JSON-based object divided into three major parts:
Header: Metadata specifying encryption/signing algorithms and key information.
Payload: The actual data to be signed/encrypted.
Signature/Encryption: The cryptographic output, which is either a signature or encrypted content.
For encrypted data, a typical JWE looks like this:
First, we’ll generate an RSA key pair for signing and verification. This key pair consists of a private key (used for signing) and a public key (used for verification). For data encryption, we’ll also generate a separate symmetric AES key, which will be used to encrypt the sensitive data itself.
RSA Algorithm: RSA is an asymmetric encryption technique that uses two distinct keys: a private key and a public key. The private key is employed for signing data and decrypting messages, while the public key is used for verifying signatures and encrypting messages.
KeyPair: A KeyPair consists of the private and public keys. The KeyPairGenerator is responsible for generating this pair. In the implementation:
Private Key: The RSAPrivateKey is used for decryption and signing data.
Public Key: The RSAPublicKey is used for encryption and verifying signatures.
Key Size: A 2048-bit key size is widely used, offering a good balance between security and performance. For higher security, you can opt for larger key sizes, such as 3072 or 4096 bits, based on your specific needs.
Signing JSON Data with JWS
Here, we’ll sign some financial data.
Kotlin
import com.nimbusds.jose.*import com.nimbusds.jose.crypto.RSASSASignerimport com.nimbusds.jwt.SignedJWTimport java.security.interfaces.RSAPrivateKeyimport java.util.Date// Dummy financial data exampledataclassFinancialData(val accountNumber: String,val amount: Double,val transactionId: String)funsignData(financialData: FinancialData, privateKey: RSAPrivateKey): String {// Convert the financial data object to a JSON stringvaldata = """ { "accountNumber": "${financialData.accountNumber}", "amount": ${financialData.amount}, "transactionId": "${financialData.transactionId}" } """// Create a payload with the financial dataval payload = Payload(data)// Create a JWS header with RS256 algorithmval header = JWSHeader.Builder(JWSAlgorithm.RS256).build()// Create a JWS objectval jwsObject = JWSObject(header, payload)// Sign the JWS object using the RSASSASignerval signer = RSASSASigner(privateKey) jwsObject.sign(signer)// Return the serialized JWS (compact format)return jwsObject.serialize()}funmain() {// Just example - RSAPrivateKey (for demonstration purposes, this key would normally be loaded from a secure store)val privateKey: RSAPrivateKey = TODO("Load the private key here")// Create some dummy financial dataval financialData = FinancialData( accountNumber = "1234567890", amount = 2500.75, transactionId = "TXN987654321" )// Sign the financial dataval signedData = signData(financialData, privateKey)// Output the signed dataprintln("Signed JWT: $signedData")}
Here,
Dummy Financial Data
We created a simple FinancialData data class with fields like accountNumber, amount, and transactionId to represent a financial transaction.
This FinancialData object is then converted into a JSON string that will be the payload of the JWT.
Payload Creation
The data string is a JSON representation of the FinancialData. This string is passed to the Payload constructor to create the JWT payload.
Signing
The RSASSASigner uses the provided private key to sign the JWT, ensuring the integrity and authenticity of the financial data.
RSASSASigner is used to generate digital signatures using the RSA Signature Scheme with Appendix (SSA), where the signature contains a hash of the message but not the message itself. It separates the signature from the original message, ensuring the signature proves authenticity without altering the message.
Serialization
The final signed JWT is serialized into a compact format (a URL-safe string) using the serialize() method.
Note :- In real-world scenarios, the RSAPrivateKey would typically be securely loaded from a file, key store, or environment variable. Also, you can customize the fields or structure of the FinancialData class to suit your specific use case.
Encrypting Data with JWE
Let’s move on and encrypt the data.
Kotlin
import com.nimbusds.jose.crypto.RSAEncrypterimport com.nimbusds.jose.EncryptionMethodimport com.nimbusds.jose.JWEHeaderimport com.nimbusds.jose.JWEObjectimport com.nimbusds.jose.Payloadimport java.security.interfaces.RSAPublicKeyfunencryptData(data: String, publicKey: RSAPublicKey): String {// Create the payload from the input dataval payload = Payload(data)// Build the JWE header with RSA-OAEP-256 for key encryption // and AES-GCM 256 for data encryptionval header = JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM).build()// Initialize the JWE object with the header and payloadval jweObject = JWEObject(header, payload)// Encrypt the JWE object using the RSA public keyval encrypter = RSAEncrypter(publicKey) jweObject.encrypt(encrypter)// Return the serialized JWE (in compact format) for transmissionreturn jweObject.serialize()}
In this process,
Payload: The Payload is created from the provided data (a string), which will be encrypted.
JWE Header: The JWEHeader specifies the encryption algorithms:
RSA_OAEP_256 is used for securely encrypting the symmetric key. This algorithm encrypts the symmetric key used for payload encryption. The RSA public key is employed in this step, ensuring that only the recipient with the private key can decrypt the symmetric key.
A256GCM (AES GCM with a 256-bit key) is used for encrypting the payload. The data is encrypted using AES with a 256-bit key in Galois/Counter Mode (GCM), ensuring both confidentiality and integrity.
JWE Object: This is the combination of the encrypted symmetric key and the encrypted payload, and is represented as a JWE token that can be securely transmitted.
RSAEncrypter: The RSAEncrypter is responsible for encrypting the symmetric key using the RSA public key.
Serialization: After encryption, the JWE object is serialized into a compact string format, making it ready for secure transmission.
Important Point to Note About JWT,
JWT: A JWT is a compact, URL-safe token format that can represent either a JWS (JSON Web Signature) or JWE (JSON Web Encryption).
When JWT is used as a JWS, it means the payload is signed (i.e., the data is authenticated, but not encrypted).
When JWT is used as a JWE, it means the payload is encrypted.
Verifying and Decrypting
On the recipient’s end, verify the signature and decrypt the data.
Kotlin
import com.nimbusds.jose.JWSObjectimport com.nimbusds.jose.crypto.RSASSAVerifierimport java.security.interfaces.RSAPublicKeyfunverifySignature(jws: String, publicKey: RSAPublicKey): Boolean {returntry {// Parse the JWS string into a JWSObjectval jwsObject = JWSObject.parse(jws)// Create a verifier using the public RSA keyval verifier = RSASSAVerifier(publicKey)// Verify the signature of the JWS object and return the result jwsObject.verify(verifier) } catch (e: Exception) {// Optionally log the exception for debuggingprintln("Error verifying signature: ${e.message}")false }}
Error Handling: The try-catch block ensures that any exception (e.g., parsing error, invalid JWS format, verification failure) is caught.
JWSObject.parse(jws): This parses the provided JWS string into a JWSObject. If the string is malformed or invalid, it will throw an exception, which is handled in the catch block.
RSASSAVerifier(publicKey): This creates a verifier using the provided RSAPublicKey, and the verify method is used to validate the signature. It returns true if the signature is valid, otherwise false.
Decrypting Data
Kotlin
import com.nimbusds.jose.JWEObjectimport com.nimbusds.jose.crypto.RSADecrypterimport java.security.interfaces.RSAPrivateKeyfundecryptData(jwe: String, privateKey: RSAPrivateKey): String {returntry {// Parse the JWE string into a JWEObjectval jweObject = JWEObject.parse(jwe)// Create a decrypter using the RSA private keyval decrypter = RSADecrypter(privateKey)// Decrypt the JWE object jweObject.decrypt(decrypter)// Return the decrypted payload as a UTF-8 string jweObject.payload.toStringUTF8() } catch (exception: Exception) {// Handle any errors (e.g., invalid JWE format, decryption issues)println("Error during decryption: ${exception.message}")"" }}
Here, when returning the decrypted payload, instead of calling toString(), you should use .toStringUTF8() if the payload is encoded in UTF-8. This ensures proper handling of the byte content. Additionally, if an exception occurs during the decryption process, the function currently returns an empty string. Depending on your needs, you might consider returning null, rethrowing the exception, or handling the error in another way that suits your application.
Best Practices
Use Strong Keys: Ensure RSA keys are at least 2048 bits, with 3072 or 4096 bits recommended for long-term security.
Secure Key Storage: Store private keys securely using Android’s Keystore system to prevent unauthorized access.
Regular Key Rotation: Periodically update keys to reduce the risk of long-term exposure, ensuring old keys are securely discarded.
Combine with HTTPS: Use HTTPS to encrypt data in transit and ensure secure communication, and apply encryption at the application layer for sensitive data at rest.
Implementing JOSE for Security in Financial APIs and Beyond
When integrating with financial APIs, secure data transmission is essential. Using JOSE (JSON Object Signing and Encryption) helps you meet security standards. By leveraging JOSE for signing and encrypting data, you can align with widely adopted industry protocols, such as:
OAuth 2.0 Tokens: Commonly use JWTs, which may be signed or unsigned, to facilitate secure authentication and communication.
Banking APIs: For example, Open Banking and PSD2 (Payment Services Directive 2) APIs, which often rely on OAuth 2.0 for secure access and data exchange, with JWTs providing a secure mechanism for identity verification.
In addition to financial applications, JOSE can be applied to various industries where security is paramount. Here are some real-world use cases:
Secure API Tokens: Sign JWT tokens for integrity and encrypt them to ensure confidentiality during transmission.
Payment Gateways: Encrypt sensitive payment information, such as credit card details, to protect against data breaches.
Healthcare Apps: Encrypt and securely transfer patient data between devices and servers, ensuring compliance with regulations such as HIPAA.
Conclusion
JOSE encryption is a powerful tool for securing financial data in Android apps. By using standards like JWS for signing and JWE for encryption, you can ensure the confidentiality, integrity, and authenticity of your data. The Kotlin code examples provided here offer a practical starting point for implementing JOSE in your applications.
With the increasing prevalence of online transactions, adopting JOSE is no longer just a best practice—it’s a necessity. Implement it today to strengthen your app’s defenses against cyber threats. Remember, security isn’t just a feature; it’s a responsibility. By embracing these standards, you’ll build trust and ensure compliance in your financial Android apps.
Message replay protection is a critical security feature, especially for mobile apps handling sensitive operations like financial transactions. It ensures that each message exchanged between the client (your app) and the server is unique and valid, preventing attackers from reusing intercepted messages to perform malicious actions.
If you’re building or maintaining an Android app, this is one security measure you don’t want to overlook. Let’s dive into what message replay protection is, why it’s essential, and how you can implement it effectively in your Android application using Kotlin.
What Is Message Replay Protection?
These days, most of us rely on wallet apps or banking apps for payments. In fact, many people in india— including me — barely carry any cash anymore! 😊 But here’s the thing: as we use these financial apps for transactions, we often overlook a potential risk. Imagine this: a malicious actor (like a hacker) intercepts a network request from your app that authorizes a fund transfer. If your app doesn’t have replay protection, the hacker could simply resend that same intercepted request and execute the transfer again — without you even knowing. You wouldn’t realize it until you notice how much money is gone. Scary, right?
Message replay protection prevents attackers from reusing old or intercepted messages to perform unauthorized actions. It works by using things like timestamps, random numbers (nonces), or sequence numbers to make each message unique. With replay protection in place, the server can spot the repeated message and reject it, keeping the communication secure.
Why Is It Important?
In the world of Android apps—particularly finance, e-commerce, or any domain dealing with sensitive data—security breaches can result in financial loss, legal troubles, and damaged user trust. Implementing message replay protection:
Safeguards transactions and sensitive operations.
Ensures compliance with industry standards like PCI DSS (Payment Card Industry Data Security Standard).
Bolsters your app’s reputation for security and reliability.
How Message Replay Protection Works
Message replay protection ensures that every message sent during communication is unique and cannot be reused by an attacker. Here’s how it typically works:
Nonces (Numbers Used Once): Unique identifiers, such as timestamps or random numbers, are attached to messages.
Server Validation: The server checks whether the nonce has been used before.
Rejection of Duplicates: If the same nonce is detected, the server rejects the message, thwarting the replay attempt.
Message Replay Protection Implementation
The core idea behind replay protection is to use unique identifiers and timestamps for every request. Here’s the typical flow:
Generate a unique nonce (number used once) for each message.
Include the nonce and a timestamp in the request payload.
Use a cryptographic hash to sign the request, ensuring the data isn’t tampered with.
On the server side:
Validate the nonce to ensure it hasn’t been used before.
Check the timestamp to confirm the message isn’t too old.
Verify the cryptographic signature.
If any of these validations fail, the server rejects the request.
Without further delay, let’s implement message replay protection, step by step.
Using a Unique Request Identifier (Nonce)
A nonce (number used once) ensures every request is unique. The server validates this identifier to prevent duplicate processing.
On the server, we validate the nonce and timestamp.
Server-side Validation Steps
Nonce Validation
Maintain a record of used nonces.
Reject requests with duplicate nonces.
Timestamp Validation
Calculate the time difference between the server time and the request timestamp.
Reject requests older than a predefined threshold (e.g., 5 or 10 minutes).
Kotlin
funisRequestValid(request: SecureRequest, usedNonces: MutableSet<String>, timeThreshold: Long = 5 * 60 * 1000): Boolean {// Check if nonce is already usedif (usedNonces.contains(request.nonce)) {returnfalse }// Check if timestamp is within the allowed rangeval currentTime = System.currentTimeMillis()if ((currentTime - request.timestamp) > timeThreshold) {returnfalse }// Add nonce to used list after successful validation usedNonces.add(request.nonce)returntrue}
Here,
usedNonces: A set that keeps track of nonces already used.
timeThreshold: Maximum allowed time difference (e.g., 5 minutes).
If the nonce is already used or the timestamp is invalid, the request is rejected.
Secure Communication with HMAC
To further enhance security, sign the request using HMAC (Hash-based Message Authentication Code). This ensures that the request data cannot be tampered with.
Replay the same request multiple times and ensure the server rejects duplicates.
Use Secure Channels
Always use HTTPS to prevent eavesdropping.
Keep Secrets Safe
Store API keys and secret keys securely (e.g., Android’s Keystore).
Log Suspicious Activity
Maintain logs for failed attempts to analyze potential attack patterns.
Conclusion
Securing your app isn’t just about writing good code—it’s about understanding and anticipating threats. Message replay attacks are a real danger, but with strategies like unique nonces, timestamps, and cryptographic validation, you can stay one step ahead.
By following the steps above, you’re not just protecting your users—you’re building trust and setting a standard for security in your apps.
Stay vigilant, keep learning, and code securely..!
In today’s connected world, securing app communication is a top priority for Android developers. Whenever your app exchanges data with a server, there’s a risk that attackers could intercept and alter this information. A reliable way to guard against this is by using certificate pinning. This security measure helps protect your app from Man-in-the-Middle (MITM) attacks, which aim to intercept the communication between your app and its server.
In this guide, we’ll explore what certificate pinning involves, why it’s an essential security practice for Android apps, and how to set it up in Kotlin. By the end, you’ll be equipped with the knowledge to implement certificate pinning in your projects and bolster your app’s security. Let’s get started!
What is Certificate Pinning?
Certificate pinning is a security measure that ensures our app only trusts specific SSL/TLS certificates for a given domain, instead of relying solely on certificates issued by Certificate Authorities (CAs). This guarantees that our app communicates securely with the intended server and not with a fake or malicious one.
Wait, wait—what exactly are SSL/TLS certificates, and who are Certificate Authorities (CAs)? These terms might sound familiar, but let’s break them down in simple terms. To make things clearer, let’s first look at the basic concept of a secure connection, using an everyday example.
Imagine you’re visiting a coffee shop. You trust the barista because you’ve been coming there for years. But how do you know you’re talking to the right person when they serve you your coffee? You check their name tag — it’s a simple form of identification. In the digital world, this “name tag” is an SSL/TLS certificate, which proves the identity of the server you’re connecting to.
Now, who gives the barista their name tag? In the digital world, that’s the role of Certificate Authorities (CAs). These trusted entities issue and verify certificates, ensuring that the server you’re connecting to is actually the one it claims to be.
Once you understand this basic “handshake” concept, you’ll see how certificate pinning adds an additional layer of security — ensuring your app always talks to the right server, using only a specific certificate, and not a potential imposter.
What Makes Certificate Pinning Essential?
Certificate Pinning is a security technique that binds or “pins” your app to a specific server certificate. Instead of trusting any certificate signed by a recognized Certificate Authority (CA), the app is set up to accept only a specific certificate or public key. This means that if a CA is compromised or a fraudulent certificate is used, your app will detect the mismatch and reject the connection.
When your app communicates with a server, it typically relies on HTTPS to encrypt and secure the data exchange. While HTTPS provides strong protection, it still has vulnerabilities—like the risk of malicious CAs issuing fake certificates that attackers could use to intercept sensitive data. Certificate Pinning protects against this by validating only the exact certificate your app should trust, making it much harder for attackers to impersonate your server.
By default, Android apps trust a broad set of CAs, which means that if any of these is compromised, a malicious actor could intercept the app-server communication. By using Certificate Pinning, your app trusts only specific certificates, reducing the risk of Man-in-the-Middle (MITM) attacks and keeping your data exchanges more secure.
Implementing Certificate Pinning in Android
Let’s dive into how to implement certificate pinning in an Android app using Kotlin. We’ll go through a step-by-step approach to setting up and verifying certificates with the OkHttp library, a popular HTTP client library for Android.
Identify the Server Certificate
Before implementing Certificate Pinning, you need the certificate’s public key or SHA-256 hash. You can use tools like browsers or OpenSSL to extract this information.
Bash
openssls_client-connectgoogle.com:443-showcerts
Or use a browser,
Add OkHttp Dependency
Nest, add OkHttp and OkHttp-TLS dependency in your build.gradle file to manage network operations with certificate pinning support.
Now, let’s configure certificate pinning using the SHA-256 fingerprint obtained earlier.
Kotlin
import okhttp3.OkHttpClientimport okhttp3.Requestimport okhttp3.CertificatePinnerfuncreatePinnedOkHttpClient(): OkHttpClient {// Define the certificate pin for your serverval certificatePinner = CertificatePinner.Builder() .add("yourserver.com", "sha256/YourCertificateSHA256FingerprintHere") .build()// Configure OkHttpClient with certificate pinningreturn OkHttpClient.Builder() .certificatePinner(certificatePinner) .build()}funmakeSecureRequest() {val client = createPinnedOkHttpClient()val request = Request.Builder() .url("https://yourserver.com/api/endpoint") .build()// Make the secure request client.newCall(request).execute().use { response ->if (response.isSuccessful) {println("Response: ${response.body?.string()}") } else {println("Failed to fetch data: ${response.code}") } }}
Here,
CertificatePinner Configuration:
First, we create an instance of CertificatePinner.
We use the add() method to define a specific certificate pin for our server. Replace "yourserver.com" with your actual server’s domain and "sha256/YourCertificateSHA256FingerprintHere" with your SHA-256 certificate fingerprint. This fingerprint serves as the unique identifier that OkHttp will use to verify the server’s authenticity.
OkHttpClient Configuration:
Next, we configure an OkHttpClient instance and set the certificatePinner to it. This ensures that any requests made through this client will only trust the pinned certificate. If the server presents a certificate that doesn’t match the pinned one, OkHttp will throw an exception and reject the connection.
Making a Secure Request:
Finally, we create a request with the secure OkHttpClient instance configured with our certificate pinner. This request attempts to connect to https://yourserver.com/api/endpoint.
Upon receiving the response, we check if it’s successful. If the certificate matches, data is retrieved; if it doesn’t match, an exception is raised, preventing insecure data exchanges.
Handling Multiple Pinning with Backup Certificates
What happens if your server’s certificate is updated or rotated? This is where backup pinning comes into play. By pinning multiple certificates or public keys, you allow your app to connect even if one certificate changes.
This ensures that if your certificate rotates, the app will still trust the new certificate as long as its public key hash is pinned.
Dynamically Pinning Certificates
In some scenarios, it might be necessary to pin certificates dynamically, particularly when working with multiple environments or during development. You can achieve this by fetching the certificate hash at runtime.
Here, the correct pin is selected based on the environment, giving you flexibility across various stages of development and deployment.
Challenges and Considerations
While certificate pinning is a powerful tool for securing your app, there are a few challenges and considerations to keep in mind:
Updating pins: If the server’s certificate needs to be changed (for example, when the certificate expires), we’ll need to update the pinned certificate in the app and release a new version. This means we must ensure the certificate is updated regularly and we have a good process in place for deploying new app versions.
Risk of breakage: If the pinning is too strict, we might face situations where legitimate changes to the server’s certificate (e.g., switching to a different CA) could break the connection. This is why it’s important to monitor certificate changes and have an update strategy.
Backup mechanism: We can implement a backup mechanism to allow updates to the certificate pin during runtime, giving us flexibility without forcing users to update the app every time a pin change occurs.
Best Practices
Here are a few best practices to ensure we’re using certificate pinning effectively:
1. Pin multiple certificates: It’s a good idea to pin more than one certificate or public key. This gives us flexibility in case of certificate rotation or renewal without breaking the app’s functionality.
2. Handle certificate expiry gracefully: Plan for certificate expiration by regularly rotating certificates and testing your app with updated pins before they expire.
3. Hardcoding Pins: Avoid hardcoding pins in your app for security reasons. If the app is decompiled, attackers can retrieve the pinned certificate hash. Consider dynamically fetching pins or using obfuscation techniques to secure your app.
4. Managing Multiple Environments: As demonstrated earlier, dynamically switching pins based on environments (development, staging, production) is crucial. Be careful not to expose development pins in production environments.
5. Monitor and audit pins: Regularly audit your pinned certificates to ensure they’re up-to-date and match the server’s current certificates. You can also use logging to track failed pin validation attempts.
6. Fallback to normal SSL checks: In cases where pinning fails, allow the app to fall back to the standard SSL/TLS verification to avoid completely blocking the user.
Conclusion
Certificate pinning is a vital security practice for any Android app that exchanges sensitive information over the network. By enforcing a strict check on the server’s certificate, you can significantly reduce the risk of MITM attacks.
In this guide, we explored how to set up certificate pinning using Kotlin and OkHttp. Implementing certificate pinning might seem a bit complex at first, but it’s a worthy investment in the long run. It gives your users confidence that their data is secure, especially in an age where data breaches are increasingly common. Give certificate pinning a try in your Android project to keep your app secure and trustworthy!
In Android app development, data security is a top priority, especially when working with sensitive information. Protecting user data from unauthorized access and misuse is a responsibility we can’t overlook. In this guide, I’ll walk you through practical methods to secure data in Android apps, and show you how to implement these techniques effectively in your projects.
We’ll start with Local Session Timeouts to limit data exposure during inactivity, then move on to Disabling App Data Backup to keep sensitive data out of potentially insecure backups. Next, we’ll discuss Protecting Configuration Data to secure app settings, In-Memory Sensitive Data Holding to prevent unintentional leaks, and finally, Secure Input for PIN Entry to guard against interception. Each section is crafted to help you build more robust and secure Android apps. Let’s dive in!
Data Security
Data security is all about keeping user information safe, whether it’s stored (at rest) or moving from one place to another (in transit). This means using encryption to protect data, securely storing it, and handling sensitive information with extra care.
Local Session Timeout
A local session timeout is a security feature that helps keep user data safe by tracking inactivity. If a user hasn’t interacted with the app for a set amount of time, the app will automatically log them out. This feature is especially important in financial apps, where protecting sensitive information is a top priority.
In financial apps, leaving a session open can be a serious security risk. If someone else picks up the user’s phone, they could access the app and potentially perform unauthorized actions. By adding a session timeout, we:
Reduce the risk of unauthorized access,
Safeguard sensitive financial data, and
Ensure compliance with security standards in the financial industry.
To implement a local session timeout in Kotlin, we can use a CountDownTimer that resets each time the user interacts with the app.
Kotlin
constval TIMEOUT_DURATION = 5 * 60 * 1000L// 5 minutes in millisecondsclassSessionManager(privateval context: Context) {privatevar timer: CountDownTimer? = null// Start or restart the inactivity timerfunstartSessionTimeout() { timer?.cancel() // cancel any existing timer timer = object : CountDownTimer(TIMEOUT_DURATION, 1000L) {overridefunonTick(millisUntilFinished: Long) {// Optionally, add logging or other feedback here }overridefunonFinish() {onSessionTimeout() } }.start() }// Reset the timer on user interactionfunresetSessionTimeout() {startSessionTimeout() }// Handle session timeout (e.g., log the user out)privatefunonSessionTimeout() {// Example action: Redirect to login screen context.startActivity(Intent(context, LoginActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK }) }// Cancel the timer when the session endsfunendSession() { timer?.cancel() }}classMainActivity : AppCompatActivity() {privatelateinitvar sessionManager: SessionManageroverridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main) sessionManager = SessionManager(this)// Start the session timer when the activity is created sessionManager.startSessionTimeout() }overridefunonUserInteraction() {super.onUserInteraction()// Reset the session timeout on any user interaction sessionManager.resetSessionTimeout() }overridefunonDestroy() {super.onDestroy()// End the session when the activity is destroyed sessionManager.endSession() }}
startSessionTimeout(): Starts a countdown timer that will log the user out after the set duration.
onUserInteraction(): Resets the timer whenever the user interacts with the app to prevent unintended logouts.
Disabling App Data Backup
When building financial apps, security should always be a top priority. Sensitive information like banking credentials, personal details, and financial records must be protected at all costs. One often-overlooked security measure is disabling app data backups. While Android’s automatic cloud backup feature is convenient, it can expose sensitive data if not managed properly.
By default, Android automatically backs up an app’s data to Google Drive, including SharedPreferences, files, and other persistent data. This process is controlled by the android:allowBackup attribute in the app’s AndroidManifest.xml. By setting this attribute to false, the app ensures its data is not backed up, which is essential for securing financial apps and other apps that handle sensitive information.
XML
<applicationandroid:name=".FinancialApp"android:allowBackup="false"android:fullBackupContent="false" ... ><!-- other configurations --></application>
android:allowBackup=”false”: Prevents Android from backing up any data from this app.
android:fullBackupContent=”false”: Ensures that no full data backup occurs, even if the device supports full data backups.
While both allowBackup="false" and fullBackupContent="false" significantly reduce the chances of unauthorized backups and data exposure, they do not provide 100% protection, especially on rooted or compromised devices. That’s why, in financial apps, we check if the device is rooted and implement additional tampering checks to enhance security.
Configuration Data Protection
Sensitive configuration data, like API keys or access tokens, shouldn’t be hardcoded directly into the app. Instead, it’s safer to encrypt them or store them securely in the Android Keystore, which serves as a secure container for cryptographic keys. Hardcoding sensitive information exposes it to potential attackers, who can easily extract it from the app’s binary. In contrast, the Android Keystore provides tamper-resistant storage, ensuring that your sensitive data remains protected.
Encrypted SharedPreferences
SharedPreferences is commonly used to store small data values in Android, but the issue with standard SharedPreferences is that it saves data in plain text, which is vulnerable if the device is compromised. For sensitive data like API keys or user credentials, it’s best to use EncryptedSharedPreferences, which ensures your data is encrypted and stored securely. Let’s take a look at how to implement this.
Kotlin
import androidx.security.crypto.EncryptedSharedPreferencesimport androidx.security.crypto.MasterKeysfungetSecureSharedPreferences(context: Context): SharedPreferences {val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)return EncryptedSharedPreferences.create("secure_preferences", // Name of the preferences file masterKeyAlias, // The master key for encryption context, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM )}funsaveConfigData(context: Context, apiKey: String) {val sharedPreferences = getSecureSharedPreferences(context)with(sharedPreferences.edit()) {putString("api_key", apiKey)apply() // Save the data securely }}fungetConfigData(context: Context): String? {val sharedPreferences = getSecureSharedPreferences(context)return sharedPreferences.getString("api_key", null) // Retrieve the secure data}
Here,
MasterKeys.getOrCreate() creates a master key using AES-256 encryption. This key is used to encrypt the data.
EncryptedSharedPreferences.create() initializes the EncryptedSharedPreferences instance with the specified encryption schemes for both the keys and values.
putString() securely saves sensitive data like API keys, while getString() retrieves the encrypted value.
Encrypting API Keys and Tokens
Hardcoding API keys and tokens directly into your app’s code can create serious security vulnerabilities. If someone decompiles your app or gains unauthorized access, these sensitive credentials could be exposed. Instead, it’s safer to store them in an encrypted format and decrypt them only when needed during runtime.
Here’s how you can use AES encryption in Kotlin to securely handle your API keys and tokens.
Kotlin
import javax.crypto.Cipherimport javax.crypto.KeyGeneratorimport javax.crypto.SecretKeyimport javax.crypto.spec.GCMParameterSpecimport android.util.Base64// Encrypting a string with AESfunencryptData(plainText: String, secretKey: SecretKey): String {val cipher = Cipher.getInstance("AES/GCM/NoPadding") cipher.init(Cipher.ENCRYPT_MODE, secretKey)val iv = cipher.ivval encryptedData = cipher.doFinal(plainText.toByteArray())val ivAndEncryptedData = iv + encryptedDatareturn Base64.encodeToString(ivAndEncryptedData, Base64.DEFAULT)}// Decrypting the encrypted stringfundecryptData(encryptedText: String, secretKey: SecretKey): String {val ivAndEncryptedData = Base64.decode(encryptedText, Base64.DEFAULT)val iv = ivAndEncryptedData.sliceArray(0 until 12) // Extract the 12-byte IVval encryptedData = ivAndEncryptedData.sliceArray(12 until ivAndEncryptedData.size)val cipher = Cipher.getInstance("AES/GCM/NoPadding")val gcmParameterSpec = GCMParameterSpec(128, iv) // 128-bit authentication tag length cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec)val decryptedData = cipher.doFinal(encryptedData)returnString(decryptedData)}// Generate Secret Key for AESfungenerateSecretKey(): SecretKey {val keyGenerator = KeyGenerator.getInstance("AES") keyGenerator.init(256) // AES 256-bit encryptionreturn keyGenerator.generateKey()}
AES/GCM/NoPadding: This mode provides strong encryption and also ensures no unnecessary padding is added, keeping the data size as small as possible.
Initialization Vector (IV): The IV is crucial for ensuring that even if the same data is encrypted multiple times, the output will differ. It’s stored alongside the encrypted data and is required for decryption.
generateSecretKey(): This method creates a 256-bit AES key, which can be used for both encryption and decryption. To further enhance security, you can store this key in the Android Keystore.
Android Keystore for Secure Key Management
Storing encryption keys directly in the app can leave them vulnerable to attacks. To avoid this, we can use the Android Keystore system, which securely stores keys either in hardware or a secure enclave, ensuring that only the app has access to them. This adds a significant layer of protection, especially for sensitive data.
Here’s how you can generate and securely manage keys using the Keystore:
Kotlin
import android.security.keystore.KeyGenParameterSpecimport android.security.keystore.KeyPropertiesimport java.security.KeyStoreimport javax.crypto.KeyGeneratorimport javax.crypto.SecretKey// Generate and store a key in Android KeystorefuncreateKey() {val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")val keyGenParameterSpec = KeyGenParameterSpec.Builder("SecureKeyAlias", KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ).setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .build() keyGenerator.init(keyGenParameterSpec) keyGenerator.generateKey()}// Retrieve the secret key from KeystorefungetSecretKey(): SecretKey? {val keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null)return keyStore.getKey("SecureKeyAlias", null) as SecretKey?}
KeyGenParameterSpec.Builder: This part sets the encryption requirements, such as the encryption block mode and padding. In this case, we’re using AES with GCM mode, which is both secure and efficient.
createKey(): This function creates a new AES encryption key and securely stores it in the Keystore with the alias SecureKeyAlias. The key is only accessible to the app, making it safe from potential leaks.
getSecretKey(): This function retrieves the stored key from the Keystore when needed for encryption or decryption. The key is never exposed in the code, adding an extra layer of security.
Secure In-Memory Sensitive Data Holding
When your app processes sensitive information like user session tokens, PINs, or account numbers, this data is temporarily stored in memory. If this information is kept in memory for too long, it becomes vulnerable to unauthorized access—especially in rooted or debug-enabled environments where attackers could potentially retrieve it from other applications. Financial apps are particularly at risk because they handle highly sensitive data, so securing session tokens, PINs, and account numbers in memory is essential for protecting user privacy and minimizing exposure to attacks.
Best Practices for Securing In-Memory Data in Android
To keep session tokens, PINs, account numbers, and other sensitive data safe in memory, consider these three core principles:
Minimal Data Exposure: Only keep sensitive data in memory for as long as absolutely necessary, and clear it promptly once it’s no longer needed.
Kotlin
funperformSensitiveOperation() {val sensitiveData = fetchSensitiveData() // Example: fetching from secure storagetry {// Use the sensitive data within a limited scopeprocessSensitiveData(sensitiveData) } finally {// Clear sensitive data once it's no longer needed sensitiveData.clear() }}
Data Clearing: Ensure that sensitive data is swiftly and thoroughly cleared from memory when it’s no longer required. We can use ByteArray and clear the data immediately after use.
Kotlin
classSensitiveDataHandler {funprocessSensitiveData(data: ByteArray) {try {// Process the sensitive data securely } finally {data.fill(0) // Clear data from memory immediately } }}
Obfuscation: Make it difficult for attackers to make sense of session tokens, PINs, or account numbers if they gain access to memory.
Secure Input for PIN Entry
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 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.
To securely capture PINs, use Android’s secure input types, and avoid storing PINs in plain text. Always hash sensitive data and use Base64 encoding before encrypting and storing it.
Kotlin
import android.content.Contextimport android.text.InputTypeimport android.widget.EditTextimport androidx.security.crypto.EncryptedSharedPreferencesimport androidx.security.crypto.MasterKeysimport java.security.MessageDigestimport java.util.*classSecurePinManager(context: Context) {privateval masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)privateval encryptedPrefs = EncryptedSharedPreferences.create("secure_prefs", masterKeyAlias, context, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM )funsetupPinInputField(editText: EditText) { editText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD }funsavePin(pin: String) {val hashedPin = hashPin(pin) // Hash the PIN before saving encryptedPrefs.edit().putString("user_pin", hashedPin).apply() }funverifyPin(inputPin: String): Boolean {val storedHashedPin = encryptedPrefs.getString("user_pin", null)val inputHashedPin = hashPin(inputPin) // Hash the input before comparisonreturn storedHashedPin == inputHashedPin }// Hashes the PIN using SHA-256privatefunhashPin(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 }}
Here,
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.
Conclusion
Securing sensitive data in Android requires a combination of best coding practices and taking advantage of built-in security features. Here’s a quick recap of the key points we covered:
Local Session Timeout: Automatically log users out after a period of inactivity to reduce the risk of unauthorized access.
Disabling App Data Backup: Prevent backups of sensitive data, ensuring that it doesn’t get exposed in case of a backup breach.
Configuration Data Protection: Encrypt configuration data to keep it safe from unauthorized access.
Secure In-Memory Data Holding: Only store sensitive data in memory when absolutely necessary, and make sure it’s cleared once it’s no longer needed.
Secure Input for PIN Entry: Use secure input types and enable accessibility settings to protect PIN entries from being exposed.
By implementing these simple practices, you can create a more secure and trustworthy Android app. For apps handling sensitive data, these steps are crucial to ensuring that user information stays private and safe!
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.
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.
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.
Kotlin
classSecurePinEntryView@JvmOverloadsconstructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) {privateval pinDigits = mutableListOf<EditText>() // Holds the EditTexts for each PIN digitprivateval maxPinLength = 4// Number of PIN digitsinit { orientation = HORIZONTALsetupPinFields() }privatefunsetupPinFields() {for (i in0 until maxPinLength) {val digitField = createDigitField() pinDigits.add(digitField)addView(digitField) } }privatefuncreateDigitField(): EditText {returnEditText(context).apply { inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORDsetBackgroundColor(Color.TRANSPARENT) filters = arrayOf(InputFilter.LengthFilter(1)) isCursorVisible = false textAlignment = View.TEXT_ALIGNMENT_CENTER layoutParams = LayoutParams(0, LayoutParams.MATCH_PARENT, 1f) // Distribute space evenlysetOnFocusChangeListener { _, hasFocus ->if (hasFocus && text.isEmpty()) {this.text.clear() // Clear text only if empty to avoid accidental deletion } } } }// Collects the PIN entered by the userfungetPin(): String {return pinDigits.joinToString("") { it.text.toString() } }// Clears the entered PINfunclearPin() { 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 customized EditText 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.
Kotlin
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 stepif (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() }}privatefunverifyPin(pin: String): Boolean {// Replace this with actual PIN verification logicreturn 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.
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.
Kotlin
import android.content.Contextimport android.text.InputTypeimport android.widget.EditTextimport androidx.security.crypto.EncryptedSharedPreferencesimport androidx.security.crypto.MasterKeysimport java.security.MessageDigestimport java.util.*classSecurePinManager(context: Context) {privateval masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)privateval encryptedPrefs = EncryptedSharedPreferences.create("secure_prefs", masterKeyAlias, context, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM )funsetupPinInputField(editText: EditText) { editText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD }funsavePin(pin: String) {val hashedPin = hashPin(pin) // Hash the PIN before saving encryptedPrefs.edit().putString("user_pin", hashedPin).apply() }funverifyPin(inputPin: String): Boolean {val storedHashedPin = encryptedPrefs.getString("user_pin", null)val inputHashedPin = hashPin(inputPin) // Hash the input before comparisonreturn storedHashedPin == inputHashedPin }// Hashes the PIN using SHA-256privatefunhashPin(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.
Kotlin
// BAD: Never log sensitive dataLog.d("PinEntry", "Entered PIN: $enteredPin") // GOOD: No sensitive data in logsLog.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..!
Data security is vital for financial apps. Sensitive information — whether it’s login details or payment data — needs to be protected at all times, not just when it’s stored, but also while it’s in memory. Although encryption for stored data is widely practiced, securing data in memory is sometimes underestimated. Yet, keeping data safe while it’s actively being used is equally important. In this blog, we’ll walk through best practices for handling in-memory data securely on Android. With Kotlin code, I’ll show you practical ways to protect sensitive information while it’s in use.
Why Secure In-Memory Sensitive Data Holding Matters
When your app handles sensitive information—such as user session tokens, PINs, or account numbers—it temporarily stores this data in memory during processing. If this data remains in memory longer than necessary, it becomes vulnerable to unauthorized access, particularly in environments that are rooted or debug-enabled. Attackers in these environments can potentially access memory and retrieve sensitive information from other applications. Financial apps are particularly at risk due to the highly sensitive nature of the data they handle. However, any app that stores sensitive information, such as health apps or apps handling personally identifiable information (PII), should prioritize securing in-memory data to protect user privacy and minimize the risk of exposure to attacks.
Best Practices for Securing In-Memory Data in Android
To safeguard session tokens, PINs, account numbers, and other sensitive data in memory, consider the following best practices:
Minimal Data Exposure Keep sensitive data in memory only for as long as necessary. Ensure that it is cleared promptly once it is no longer required. This minimizes the window of opportunity for attackers to access it.
Encryption Always encrypt sensitive data before storing it in memory. This ensures that even if attackers manage to access the memory, they won’t be able to interpret the data without the encryption key. Using Android’s Keystore system is highly recommended for secure encryption key management.
Obfuscation Make it more difficult for attackers to make sense of session tokens, PINs, or account numbers in memory. While obfuscation alone is not a secure measure, it adds an additional layer of protection by making the data harder to interpret if accessed.
Data Clearing Securely clear sensitive data from memory as soon as it is no longer needed. Ensure that memory is overwritten securely to prevent recovery using memory analysis tools. Consider using techniques like memcpy or similar secure memory handling functions to wipe sensitive data completely from memory.
Secure Memory Handling Consider using Android’s secure storage mechanisms like EncryptedSharedPreferences or Android Keystore to manage sensitive information securely. These tools provide a higher level of protection for storing encryption keys or sensitive data that needs to be accessed in memory.
Anti-Debugging and Root Detection Implement anti-debugging and root detection techniques to prevent attackers from using debugging tools to extract data from the app. While these techniques are not foolproof, they add an additional layer of defense.
Secure Coding Practices Avoid accidentally logging or exposing sensitive data in crash reports, logs, or other outputs. Ensure that sensitive information is handled securely throughout the app’s lifecycle.
Implementing Secure In-Memory Data Handling in Kotlin
Minimize Data Exposure
To protect sensitive data, only keep it in memory for as long as absolutely necessary. Avoid storing it in global or static variables where it might remain accessible longer than needed. Here’s how we can handle this securely in Kotlin
Kotlin
funperformSensitiveOperation() {val sensitiveData = fetchSensitiveData() // Example: fetching from secure storagetry {// Use the sensitive data within a limited scopeprocessSensitiveData(sensitiveData) } finally {// Clear sensitive data once it's no longer needed sensitiveData.clear() }}
Here,
sensitiveData is fetched and used only within the scope of the function.
Once the operation is complete, the data is cleared, minimizing its exposure time and keeping it safe.
By ensuring sensitive data only lives in memory for the shortest time possible, you reduce the risk of unauthorized access.
Use ‘CharArray’ or ‘ByteArray’ Instead of a String
In both Java and Kotlin, String is immutable, meaning once it’s created, it cannot be modified. While this makes strings reliable for certain use cases, it can also be a security risk when dealing with sensitive data because the string cannot be cleared from memory once it’s no longer needed. On the other hand, CharArray is mutable, so we can modify its contents, making it a safer choice for sensitive information like passwords.
Once you’re done using sensitive data stored in a CharArray, it’s important to clear it immediately to prevent it from lingering in memory. You can do this by setting all characters to \u0000 (the null character)
Kotlin
funclearPassword(password: CharArray) {for (i in password.indices) { password[i] = '\u0000' }}
Calling clearPassword(password) right after you’re finished using the password ensures that no sensitive data is left exposed in memory longer than necessary, reducing the risk of unauthorized access.
The same applies to ByteArray. Since ByteArray is mutable, we can modify or clear its contents after use, providing a more secure way to handle sensitive data in memory.
Kotlin
classSensitiveDataHandler {privatevar sensitiveData: ByteArray? = null// Store sensitive data securely as a ByteArrayfunstoreData(data: String) { sensitiveData = data.toByteArray() // Convert String data to ByteArray }// Clear the sensitive data by overwriting it with zerosfunclearData() { sensitiveData?.fill(0) // Overwrite with zeros sensitiveData = null// Set the reference to null }}
This approach helps ensure that sensitive information is overwritten and doesn’t remain in memory, mitigating security risks if memory is compromised.
Encrypting Sensitive Data in Memory
Even with minimal exposure and proper data clearing, it’s a good practice to add an extra layer of security by encrypting sensitive data while it’s in memory. This helps protect the data in case of an attack or a breach. In Kotlin, you can use the Cipher class to easily encrypt and decrypt data on the fly.
Here’s how you can implement an encryption utility using AES-GCM for secure encryption and decryption
Kotlin
import javax.crypto.Cipherimport javax.crypto.KeyGeneratorimport javax.crypto.SecretKeyimport javax.crypto.spec.GCMParameterSpecobjectMemoryEncryptionUtil {privateconstval TRANSFORMATION = "AES/GCM/NoPadding"privateconstval TAG_LENGTH_BIT = 128privateval secretKey: SecretKeyinit {val keyGen = KeyGenerator.getInstance("AES") keyGen.init(256) // 256-bit AES encryption secretKey = keyGen.generateKey() // Generate a secret key }// Encrypt the data and return the initialization vector (IV) and encrypted datafunencrypt(data: ByteArray): Pair<ByteArray, ByteArray> {val cipher = Cipher.getInstance(TRANSFORMATION) cipher.init(Cipher.ENCRYPT_MODE, secretKey)val iv = cipher.iv // Get the initialization vectorval encryptedData = cipher.doFinal(data) // Encrypt the datareturnPair(iv, encryptedData) }// Decrypt the data using the IV and encrypted datafundecrypt(iv: ByteArray, data: ByteArray): ByteArray {val cipher = Cipher.getInstance(TRANSFORMATION)val spec = GCMParameterSpec(TAG_LENGTH_BIT, iv) // Specify the GCM parameter cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) // Initialize the cipher for decryptionreturn cipher.doFinal(data) // Decrypt and return the data }}
AES-GCM (Galois/Counter Mode): This encryption mode offers both confidentiality and integrity, ensuring that the encrypted data cannot be tampered with without detection.
encrypt(): This method encrypts the given ByteArray and returns a pair consisting of the initialization vector (IV) and the encrypted data.
decrypt(): This method takes the IV and encrypted data, decrypts it, and returns the original data in its unencrypted form.
Now, let’s see how to use this encryption utility with sensitive data
Kotlin
val sensitiveData = "SensitiveData".toByteArray()val (iv, encryptedData) = MemoryEncryptionUtil.encrypt(sensitiveData)// Decrypt the data later when neededval decryptedData = MemoryEncryptionUtil.decrypt(iv, encryptedData)
Here,
Sensitive data is first converted to a ByteArray.
It is then encrypted using the encrypt() function, which returns both the IV and the encrypted data.
When you need to access the original data, you can use the decrypt() function with the IV and encrypted data to safely decrypt it.
By implementing encryption in-memory, you provide an additional layer of protection for sensitive data, ensuring that even if an attacker gains access to the memory, the data remains unreadable.
Using Secure Storage for Persistent Data
When it comes to storing sensitive data beyond the session, it’s essential to use secure storage mechanisms to ensure that the data remains protected even after the app is closed or the device is restarted. Android’s Keystore System is specifically designed for this purpose, providing a secure place to store cryptographic keys and sensitive information like passwords and tokens.
In below code, we’ll use the Keystore to securely generate a key and then encrypt/decrypt sensitive data stored in a persistent manner.
Kotlin
import android.security.keystore.KeyPropertiesimport android.security.keystore.KeyGenParameterSpecimport java.security.KeyStoreimport javax.crypto.KeyGeneratorimport javax.crypto.Cipherimport javax.crypto.SecretKeyimport javax.crypto.spec.GCMParameterSpecobjectSecureStorageUtil {privateconstval KEY_ALIAS = "SecureKeyAlias"// Alias for the keyprivateconstval TRANSFORMATION = "AES/GCM/NoPadding"// Encryption mode and paddingprivateconstval TAG_LENGTH_BIT = 128// Tag length for GCM// Function to retrieve or generate the encryption keyprivatefungetKey(): SecretKey {val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }// Check if the key already existsif (!keyStore.containsAlias(KEY_ALIAS)) {val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")val spec = KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) // Set GCM block mode .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) // No padding .build() keyGen.init(spec) keyGen.generateKey() // Generate the key and store it securely }// Return the secret key from the keystorereturn (keyStore.getEntry(KEY_ALIAS, null) as KeyStore.SecretKeyEntry).secretKey }// Function to encrypt data using the Keystore keyfunencrypt(data: ByteArray): Pair<ByteArray, ByteArray> {val cipher = Cipher.getInstance(TRANSFORMATION) cipher.init(Cipher.ENCRYPT_MODE, getKey()) // Initialize cipher with encryption keyval iv = cipher.iv // Get the initialization vector (IV)val encryptedData = cipher.doFinal(data) // Encrypt the datareturnPair(iv, encryptedData) // Return both IV and encrypted data }// Function to decrypt data using the Keystore keyfundecrypt(iv: ByteArray, encryptedData: ByteArray): ByteArray {val cipher = Cipher.getInstance(TRANSFORMATION) cipher.init(Cipher.DECRYPT_MODE, getKey(), GCMParameterSpec(TAG_LENGTH_BIT, iv)) // Initialize cipher for decryptionreturn cipher.doFinal(encryptedData) // Decrypt and return the data }}
Key Generation: If the key doesn’t already exist in the Keystore, it is generated using KeyGenerator with the AES algorithm and stored in the AndroidKeyStore. This ensures the key never leaves the secure hardware and is not accessible by unauthorized parties.
AES-GCM Encryption: We use AES in Galois/Counter Mode (GCM), which provides both encryption and integrity protection. The Cipher class is used to perform encryption and decryption operations with the key stored in the Keystore.
Data Encryption & Decryption
encrypt(): Encrypts the input data and returns both the initialization vector (IV) and encrypted data. The IV is crucial for decrypting the data later.
decrypt(): Uses the stored key to decrypt the data by passing the IV and encrypted data.
Here’s how we can use SecureStorageUtil to securely store and retrieve encrypted data, again, similar to the encryption utility mentioned above.
Kotlin
val sensitiveData = "SensitiveData".toByteArray()// Encrypt the dataval (iv, encryptedData) = SecureStorageUtil.encrypt(sensitiveData)// Decrypt the data later when neededval decryptedData = SecureStorageUtil.decrypt(iv, encryptedData)
But, why do we use Keystore?
The Android Keystore System ensures that the cryptographic keys are stored securely in hardware-backed storage (if available), making them less vulnerable to attacks such as device rooting or debugging. By using Keystore for key management, you reduce the risk of exposing sensitive data, even if the app is compromised.
This approach helps ensure that sensitive data is encrypted both at rest (when stored) and in transit (while being processed), providing an additional layer of security for your financial app’s sensitive information.
Conclusion
By implementing these practices, you can significantly mitigate the risk of sensitive data being exposed in memory:
Minimize data exposure by limiting its time in memory.
Use mutable types like CharArray or ByteArray and clear them after use.
Encrypt sensitive data while in memory.
Store persistent data securely using Android’s Keystore system.
These measures ensure that even if an attacker gains access to memory or persistent storage, they won’t be able to easily retrieve sensitive information.
Protecting sensitive data in financial apps is essential to prevent security breaches that could put user information, app configurations, and financial transactions at risk. In this blog, I’ll walk you through how to secure configuration data in financial apps. We’ll begin by looking at common vulnerabilities, then move on to practical solutions like encryption and secure storage practices. Along the way, I’ll break things down step by step to help you apply these strategies with ease. Let’s dive in and make your financial app more secure!
Why Configuration Data Protection Needed?
Configuration data is essential in financial apps as it often contains API keys, URLs, and settings that control how the app behaves. In financial apps, this data is particularly sensitive. If it’s not properly secured, attackers could exploit vulnerabilities, bypass authentication, steal financial data, or even manipulate transactions.
Core Techniques to Protect Configuration Data
Here are a few core techniques to keep your configuration data secure:
Using Encrypted SharedPreferences for Sensitive Data
Encrypting API Keys and Tokens
Using Android Keystore for Secure Key Management
Network Security Configuration for Secure Data Transmission
Let’s dive into each of these, starting with Encrypted SharedPreferences.
Using Encrypted SharedPreferences for Sensitive Data
SharedPreferences is commonly used in Android to store small pieces of data, like user settings or app preferences. However, the downside is that standard SharedPreferences stores data in plain text, which can easily be accessed if the device is compromised. This is a significant security risk, especially when dealing with sensitive information like API keys.
To secure sensitive data, we can use EncryptedSharedPreferences. It encrypts the data, ensuring that even if someone gains access to the storage, they won’t be able to read the sensitive information.
Here’s how you can use EncryptedSharedPreferences:
Kotlin
import androidx.security.crypto.EncryptedSharedPreferencesimport androidx.security.crypto.MasterKeysfungetSecureSharedPreferences(context: Context): SharedPreferences {val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)return EncryptedSharedPreferences.create("secure_preferences", // Name of the preferences file masterKeyAlias, // The master key for encryption context, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM )}funsaveConfigData(context: Context, apiKey: String) {val sharedPreferences = getSecureSharedPreferences(context)with(sharedPreferences.edit()) {putString("api_key", apiKey)apply() // Save the data securely }}fungetConfigData(context: Context): String? {val sharedPreferences = getSecureSharedPreferences(context)return sharedPreferences.getString("api_key", null) // Retrieve the secure data}
Here,
MasterKeys.getOrCreate() creates a master key using AES-256 encryption. This key is used to encrypt the data.
EncryptedSharedPreferences.create() initializes the EncryptedSharedPreferences instance with the specified encryption schemes for both the keys and values.
putString() securely saves sensitive data like API keys, while getString() retrieves the encrypted value.
By using EncryptedSharedPreferences, we ensure that even if someone gains unauthorized access to the device’s storage, the data remains encrypted and safe. This is a simple yet powerful way to protect sensitive configuration data in your financial app.
Encrypting API Keys and Tokens
Hardcoding API keys and tokens directly into your app’s code can create serious security vulnerabilities. If someone decompiles your app or gains unauthorized access, these sensitive credentials could be exposed. Instead, it’s safer to store them in an encrypted format and decrypt them only when needed during runtime.
Here’s how you can use AES encryption in Kotlin to securely handle your API keys and tokens.
Kotlin
import javax.crypto.Cipherimport javax.crypto.KeyGeneratorimport javax.crypto.SecretKeyimport javax.crypto.spec.GCMParameterSpecimport android.util.Base64// Encrypting a string with AESfunencryptData(plainText: String, secretKey: SecretKey): String {val cipher = Cipher.getInstance("AES/GCM/NoPadding") cipher.init(Cipher.ENCRYPT_MODE, secretKey)val iv = cipher.ivval encryptedData = cipher.doFinal(plainText.toByteArray())val ivAndEncryptedData = iv + encryptedDatareturn Base64.encodeToString(ivAndEncryptedData, Base64.DEFAULT)}// Decrypting the encrypted stringfundecryptData(encryptedText: String, secretKey: SecretKey): String {val ivAndEncryptedData = Base64.decode(encryptedText, Base64.DEFAULT)val iv = ivAndEncryptedData.sliceArray(0 until 12) // Extract the 12-byte IVval encryptedData = ivAndEncryptedData.sliceArray(12 until ivAndEncryptedData.size)val cipher = Cipher.getInstance("AES/GCM/NoPadding")val gcmParameterSpec = GCMParameterSpec(128, iv) // 128-bit authentication tag length cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec)val decryptedData = cipher.doFinal(encryptedData)returnString(decryptedData)}// Generate Secret Key for AESfungenerateSecretKey(): SecretKey {val keyGenerator = KeyGenerator.getInstance("AES") keyGenerator.init(256) // AES 256-bit encryptionreturn keyGenerator.generateKey()}
AES/GCM/NoPadding: This mode provides strong encryption and also ensures no unnecessary padding is added, keeping the data size as small as possible.
Initialization Vector (IV): The IV is crucial for ensuring that even if the same data is encrypted multiple times, the output will differ. It’s stored alongside the encrypted data and is required for decryption.
generateSecretKey(): This method creates a 256-bit AES key, which can be used for both encryption and decryption. To further enhance security, you can store this key in the Android Keystore.
By using AES encryption to handle your API keys and tokens, you’re adding an extra layer of security to prevent unauthorized access to your financial app’s sensitive data. This approach ensures that your sensitive information remains secure, even if the device is compromised.
Btw, I know you might be wondering about one term. Any guesses..? Without further delay, let’s take a look!
What is an IV (Initialization Vector)?
An Initialization Vector (IV) is a random value used in cryptographic algorithms, such as AES, to ensure that each encryption operation produces unique results — even when encrypting the same data multiple times with the same key.
Why is IV Important?
Prevents Repeated Patterns: Without an IV, encrypting the same data with the same key would always result in the same ciphertext. This predictability is a security risk. The IV ensures that even when encrypting identical data, the output (ciphertext) will be different each time, making it harder for attackers to detect patterns or deduce information.
Enhances Security: In encryption modes like AES-CBC (Cipher Block Chaining) or AES-GCM (Galois/Counter Mode), the IV plays a crucial role by adding randomness to the encryption process. This added randomness strengthens the encryption, making it more resistant to attacks.
Must Be Unique: The IV doesn’t need to be kept secret, but it must be unique for each encryption operation. Reusing the same IV with the same key for different data introduces vulnerabilities. When an IV is reused, attackers may be able to spot patterns or exploit weaknesses in the encryption.
How Does the IV Work?
During Encryption: The IV is combined with the plaintext and encryption key to create the ciphertext. Its role is to introduce randomness, ensuring that even identical plaintexts will produce different ciphertexts when encrypted.
During Decryption: To decrypt the data properly, the same IV used during encryption must be provided. It’s typically sent alongside the ciphertext, ensuring the receiver can use it during decryption to recover the original data.
Storing and Transmitting the IV
The IV itself doesn’t need to be kept secret, but it must be made available to the receiver. Usually, it’s transmitted along with the encrypted data, either as a prefix or in a predefined format. This ensures that the IV can be used during decryption. However, even though the IV isn’t secret, it still must be securely transmitted to ensure proper decryption.
Let’s get back to our discussion on App Configuration Data Protection and see how we can use Android Keystore for secure key management.
Using Android Keystore for Secure Key Management
Storing encryption keys directly in the app can leave them vulnerable to attacks. To avoid this, we can use the Android Keystore system, which securely stores keys either in hardware or a secure enclave, ensuring that only the app has access to them. This adds a significant layer of protection, especially for sensitive data.
Here’s how you can generate and securely manage keys using the Keystore:
Kotlin
import android.security.keystore.KeyGenParameterSpecimport android.security.keystore.KeyPropertiesimport java.security.KeyStoreimport javax.crypto.KeyGeneratorimport javax.crypto.SecretKey// Generate and store a key in Android KeystorefuncreateKey() {val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")val keyGenParameterSpec = KeyGenParameterSpec.Builder("SecureKeyAlias", KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ).setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .build() keyGenerator.init(keyGenParameterSpec) keyGenerator.generateKey()}// Retrieve the secret key from KeystorefungetSecretKey(): SecretKey? {val keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null)return keyStore.getKey("SecureKeyAlias", null) as SecretKey?}
KeyGenParameterSpec.Builder: This part sets the encryption requirements, such as the encryption block mode and padding. In this case, we’re using AES with GCM mode, which is both secure and efficient.
createKey(): This function creates a new AES encryption key and securely stores it in the Keystore with the alias SecureKeyAlias. The key is only accessible to the app, making it safe from potential leaks.
getSecretKey(): This function retrieves the stored key from the Keystore when needed for encryption or decryption. The key is never exposed in the code, adding an extra layer of security.
By using the Android Keystore, we avoid the risk of exposing sensitive keys within the app, ensuring a higher level of security for encryption operations.
Network Security Configuration for Secure Data Transmission
In financial apps, securely transmitting sensitive data over HTTPS is critical to prevent man-in-the-middle attacks. If not properly configured, cleartext traffic (HTTP) can expose this data to unauthorized access. To ensure your app uses HTTPS and blocks any unencrypted traffic, you can define network security policies using a configuration file.
Here’s how to enforce HTTPS using a network security configuration in your app
Create a network security configuration file (network_security_config.xml) in the res/xml folder:
cleartextTrafficPermitted="false": This setting ensures that the app only allows encrypted HTTPS traffic and blocks any HTTP (cleartext) traffic, preventing sensitive data from being exposed.
<domain> tag: You specify trusted domains (like yourapi.com) that the security settings apply to, including all of its subdomains (by setting includeSubdomains="true").
By adding this configuration, you’re ensuring that your financial app’s data transmissions remain secure, guarding against potential security threats.
Conclusion
Securing app configuration data in financial apps is essential for protecting sensitive user information and maintaining trust. By implementing practices like using EncryptedSharedPreferences, encrypting sensitive values, storing keys in the Android Keystore, and enforcing HTTPS, you can significantly reduce the risk of data breaches and vulnerabilities.
These steps will help ensure that your financial Android app handles sensitive data securely, giving users the peace of mind they need when using your app.
When building financial apps, security should always be a top priority. Sensitive information like banking credentials, personal details, and financial records must be protected at all costs. One often-overlooked security measure is disabling app data backups. While Android’s automatic cloud backup feature is convenient, it can expose sensitive data if not managed properly. In this guide, we’ll walk through the steps to disable app data backup in Android apps, ensuring that user data remains secure.
Why Disable App Data Backup in Financial Apps?
While app data backup can be a convenient feature for many apps, it poses risks for apps that handle sensitive financial data. Here’s why it’s crucial to disable it:
Protecting Sensitive Data: If a device is compromised or when users switch to a new device, any backed-up data could be exposed during restoration, which is a major security concern.
Ensuring Compliance: Many financial institutions have strict data security requirements, and allowing data to be backed up to external storage might violate these regulations.
Reducing Risk: Disabling backups prevents data from being stored on potentially less secure platforms or devices, keeping it safe from unauthorized access.
Before disabling the backup, let’s first take a moment to see which files are usually being backed up.
What Gets Backed Up
By default, Auto Backup includes files from most directories assigned to your app by the system, such as:
Shared Preferences Files
Internal Storage Files: Files saved to your app’s internal storage and accessed via getFilesDir() or getDir(String, int)
Database Files: Files in the directory returned by getDatabasePath(String), including those created using SQLiteOpenHelper
External Storage Files: Files located in the directory returned by getExternalFilesDir(String)
Auto Backup doesn’t include files stored in certain directories, such as:
Cache Directory: Files saved in getCacheDir(), getCodeCacheDir(), and getNoBackupFilesDir() These files are temporary and are intentionally excluded from backup to avoid unnecessary storage and syncing.
You can customize the backup process by specifying which files should be included or excluded from Auto Backup, giving you greater control over the data your app manages.
How DisablingApp Data Backup Works in Android
By default, Android automatically backs up an app’s data to Google Drive, including SharedPreferences, files, and other persistent data. This process is controlled by the android:allowBackup attribute in the app’s AndroidManifest.xml. By setting this attribute to false, the app ensures its data is not backed up, which is essential for securing financial apps and other apps that handle sensitive information.
XML
<applicationandroid:name=".FinancialApp"android:allowBackup="false"android:fullBackupContent="false" ... ><!-- other configurations --></application>
allowBackup="false":
This attribute prevents Android from automatically backing up the app’s data to Google Drive or any other backup service. This includes both user-initiated and system-initiated backups. Setting allowBackup="false" effectively disables the Android backup mechanism for the app, reducing the risk of unauthorized access to app data through backups.
Important Note: While this setting prevents automatic backups through Android’s system, it does not guarantee complete protection. Devices with root access or custom ROMs can bypass this setting and potentially access app data or perform backups using alternative methods.
fullBackupContent="false":
This attribute ensures that the app’s data is excluded from full device backups, regardless of the allowBackup setting. When set to false, it prevents the app’s data from being included in any full-device backup (such as Google’s full-device backup feature) even if allowBackup is set to true.
Important Note: This attribute prevents the app’s data from being included in standard full backups, but it does not protect against all possible data extraction methods. Devices with root access or custom ROMs may still be able to access the app’s data through other means, such as direct file system access.
While both allowBackup="false" and fullBackupContent="false" significantly reduce the chances of unauthorized backups and data exposure, they do not provide 100% protection, especially on rooted or compromised devices. That’s why, in financial apps, we check if the device is rooted and implement additional tampering checks to enhance security.
Securing Sensitive Data Locally
Disabling backups is only part of the equation in securing sensitive data. It’s also crucial to protect locally stored information. Jetpack’s Security library provides tools like EncryptedSharedPreferences and EncryptedFile in Kotlin, which ensure that data stored on the device remains encrypted. These components integrate seamlessly with Android’s architecture and provide strong encryption, making them excellent choices for securely handling sensitive data in financial or personal apps.
MasterKeys: Creates or retrieves a master key that is used to encrypt the shared preferences.
EncryptedSharedPreferences: Securely stores shared preferences with AES encryption, which is suitable for sensitive data.
PrefKeyEncryptionScheme & PrefValueEncryptionScheme: These schemes ensure both keys and values in SharedPreferences are encrypted, providing additional security.
Securely Storing Files with EncryptedFile
Sometimes, an app may need to store files, such as transaction records or receipts. Using EncryptedFile can help ensure these files are securely stored.
EncryptedFile: Provides AES256_GCM encryption to ensure that files are securely stored on disk.
FileEncryptionScheme: Specifies the encryption scheme to use for file security, which includes AES encryption with a secure HKDF key derivation function.
Additional Security Considerations
Use ProGuard: Obfuscate your app’s code to make it much harder for attackers to reverse-engineer.
Implement Strong User Authentication: For accessing sensitive areas of the app, use secure authentication methods like biometrics or PINs.
Clear Sensitive Data on Logout: Ensure that all stored sensitive data is cleared when the user logs out or exits.
Here’s a quick example of a function to clear sensitive data from EncryptedSharedPreferences:
This function retrieves the EncryptedSharedPreferences instance and clears all saved data, ensuring that no sensitive information remains stored in the app.
Testing Backup Disabling and Data Security
To ensure everything is working as expected, it’s crucial to test that app data isn’t backed up and that sensitive data remains secure:
Backup Testing: After setting up the backup restriction, install the app, add some data, and try to back it up through device settings or ADB. Check to confirm that none of the app’s data is backed up.
Encryption Verification: Attempt to access shared preferences or files outside the app’s context, such as by using a file manager or rooted device. This helps verify that sensitive data remains encrypted and unreadable, confirming the security setup is effective.
Testing these areas ensures that data protection features are robust and that user data remains secure, especially for apps managing sensitive information.
Conclusion
Disabling app data backup in Android apps, especially financial ones, is essential for protecting user data and complying with strict security requirements. By making a few adjustments in the AndroidManifest and following secure data storage practices, you can help ensure that your app’s sensitive data remains safe from unauthorized backups and access. Implementing these steps and following security best practices will help you build a more secure financial app that safeguards your users’ valuable data.
In financial Android apps, setting up local session timeouts is essential to prevent unauthorized access if a user leaves the app unattended. With a session timeout, the app automatically logs the user out after a certain period of inactivity, adding a layer of security to protect sensitive data.
In this blog, I’ll walk you through:
What a local session timeout is
Why session timeouts are crucial for financial apps
How to implement a session timeout in Kotlin with step-by-step code
Best practices for managing session timeouts effectively
Let’s dive into how you can secure your app and enhance user trust by setting up session timeouts the right way.
What is a Local Session Timeout?
A local session timeout is a security feature that helps keep user data safe by tracking inactivity. If a user hasn’t interacted with the app for a set amount of time, the app will automatically log them out. This feature is especially important in financial apps, where protecting sensitive information is a top priority.
Why Local Session Timeout is Important for Financial Apps
In financial apps, leaving a session open can be a serious security risk. If someone else picks up the user’s phone, they could access the app and potentially perform unauthorized actions. By adding a session timeout, we:
Reduce the risk of unauthorized access,
Safeguard sensitive financial data, and
Ensure compliance with security standards in the financial industry.
How to Set Up a Local Session Timeout
Here’s how to add a local session timeout feature to an Android app using Kotlin. We’ll take it step-by-step:
Define the Inactivity Timeout Duration — Decide how long the app should remain active without user interaction.
Track User Activity — Monitor interactions like touches, scrolls, or button presses to keep track of activity.
Reset the Timer — Each time the user interacts with the app, reset the timer to give them more active time.
Handle the Timeout — If no activity is detected within the specified time, log the user out automatically.
Step-by-Step Implementation in Kotlin
Step 1: Set Up Constants
First, let’s define a constant for our timeout duration. For example, we might want a timeout of 5 minutes.
Next, let’s create a SessionManager class to handle the session tracking and timeout. This class will manage a timer that resets every time the user interacts with the app.
Kotlin
classSessionManager(privateval context: Context) {privatevar timer: CountDownTimer? = null// Start or restart the inactivity timerfunstartSessionTimeout() { timer?.cancel() // cancel any existing timer timer = object : CountDownTimer(TIMEOUT_DURATION, 1000L) {overridefunonTick(millisUntilFinished: Long) {// Optionally, add logging or other feedback here }overridefunonFinish() {onSessionTimeout() } }.start() }// Reset the timer on user interactionfunresetSessionTimeout() {startSessionTimeout() }// Handle session timeout (e.g., log the user out)privatefunonSessionTimeout() {// Example action: Redirect to login screen context.startActivity(Intent(context, LoginActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK }) }// Cancel the timer when the session endsfunendSession() { timer?.cancel() }}
startSessionTimeout: Starts or restarts a countdown timer. If there’s no activity, onFinish() calls onSessionTimeout().
resetSessionTimeout: Resets the timer whenever the user interacts with the app.
onSessionTimeout: This function defines what happens when the timer expires. Here, we’re redirecting the user to the login screen.
endSession: Cancels the timer when the session ends, helping save resources.
Step 3: Integrate Session Timeout in the Main Activity
In your main activity, you’ll initialize SessionManager and handle user interactions to keep the timer updated.
Kotlin
classMainActivity : AppCompatActivity() {privatelateinitvar sessionManager: SessionManageroverridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main) sessionManager = SessionManager(this)// Start the session timer when the activity is created sessionManager.startSessionTimeout() }overridefunonUserInteraction() {super.onUserInteraction()// Reset the session timeout on any user interaction sessionManager.resetSessionTimeout() }overridefunonDestroy() {super.onDestroy()// End the session when the activity is destroyed sessionManager.endSession() }}
onUserInteraction: This built-in method is called whenever the user interacts with the app (touch, scroll, etc.). We’re using it to reset the session timeout.
onDestroy: Stops the timer if the activity is destroyed, which helps save resources.
Step 4: Add Login Handling (Optional)
Redirecting the user to the login screen upon timeout adds an extra layer of protection for sensitive data. Assuming you have a LoginActivity set up, the SessionManager class will send users there if their session times out.
Best Practices for Session Timeout in Financial Apps
Choose a Practical Timeout Duration: For financial apps, a timeout of 5 to 10 minutes of inactivity is generally a good choice. It strikes the right balance between keeping data secure and not being too disruptive for the user.
Notify the User Before Logging Out: Many apps show a quick warning dialog just before logging out. This gives users a chance to stay logged in by interacting with the app, making the experience smoother and reducing unexpected logouts.
Handle Background State Changes Carefully: If the user switches to another app or the app moves to the background, consider starting the timeout timer or even logging out immediately. This reduces the risk of leaving sensitive data open if the app isn’t actively being used.
Conclusion
Implementing session timeouts in financial apps is essential for protecting user data. I’ve shared how using Kotlin and Android’s CountDownTimer makes it simple to set up a reliable timeout system. By choosing a practical timeout duration, notifying users before logout, and handling background state changes, we can ensure that our apps are both secure and user-friendly.
As developers, it’s our job to safeguard sensitive information while making sure the app remains intuitive. With these steps in place, you’ll be able to create a financial app that balances both security and a smooth user experience. Keep iterating and refining—this approach will help you build a stronger, safer app over time.