
Secure Payment Transaction TapToPay Android
Over the past few months, Iβve been deeply involved in the development of an Android payment application, where security has been a fundamental pillar of the entire process.
Working on this project has given me invaluable insights into the intricate world of securing Android applications.
The protection levels on this Android Payment App, can be devided inΒ Compilation Layer, Runtime Layer.
For free complete read access, please continue with this link :Β https://medium.com/@sofienrahmouni/securing-android-behind-a-few-seconds-of-payment-transaction-bf6817119d51?source=friends_link&sk=0117880616ac48dde2590f0b8138d766
1. Protecting Your App from Reverse Engineering & Debugging
- Anti-Developer-Options: Developer options activation can be the first door for an attacker to start so we should check it :
fun isDeveloperOptionsEnabled(context: Context): Boolean { return try { Settings.Global.getInt( context.contentResolver, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED ) == 1 } catch (e: Exception) { Log.e("DeveloperOptions", "Error checking Developer Options", e) false } }
- Anti-Debugging: Detect debuggers at runtime usingΒ
isDebuggerConnected()
Β :
Here is snippet code used to detect if the application was build on debug mode and when the debbuger attached:
// Check debbugable flag val isDebuggable =(context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
// Check debugger is connected at runtime import android.os.Debug.isDebuggerConnected val isDebuggerConnected= isDebuggerConnected()
The debug check also can be done at the system level using native methodptrace()
Β :
bool = ptrace(PTRACE_TRACEME, 0, 0, 0) == -1;
- Anti-Root Detection: As known in the Android world, the key to every attack vector starts with obtaining privileged access as an admin or root user. Therefore, we must protect our application by implementing root detection :
object RootDetectionUtil { fun isRooted(): Boolean { // Check known binary root file paths val paths = listOf( "/data/local", "/data/local/bin", "/data/local/xbin", "/sbin", "/su/bin", "/system", "/system/app", "/system/bin", "/system/bin/.ext", "/system/bin/failsafe", "/system/sd/xbin", "/system/usr/we-need-root", "/system/xbin" ) val binaries = listOf( "magisk", "su", "su2", "superuser.apk", "Superuser.apk", "su-backup", ".su", "busybox", "busybox.apk" ) for (path in paths) { for (bin in binaries) { val filePath = File("$path/$bin") if (filePath.exists()) { Log.e("RootDetection", "Smartphone rooted ($filePath)") return true } } } // Check for the existence of the "su" binary try { if (Runtime.getRuntime().exec(arrayOf("/system/xbin/which", "su")) .inputStream.bufferedReader().readLine() != null ) { return true } } catch (e: Exception) { e.printStackTrace() } // Check if AOSP ROM was signed with release keys else means isRooted if (android.os.Build.TAGS?.contains("test-keys") == true) { return true } Log.d("RootDetection", "Smartphone not rooted") return false } }
- Obfuscation: To make the reverse engineering process more difficult, Android offers open-source obfuscation tools such asΒ ProGuardΒ and R8, as well asΒ more advanced paid options likeΒ DexGuard, to make decompilation harder.
- Anti-Tampering: On the other hand, Google offers an interesting solution to verify app integrity using theΒ Google Play Integrity API. It is based on a verdict attestation service, making it a more reliable solution compared to local environment checks. However, this solution is subject to limitations, as Google provides only 10,000 free tokens per day.
Here is an example ofΒ decoded response play integrity verdictΒ (The real world response is encrypted & encoded) :
{ "requestDetails": { "requestPackageName": "com.payment.app", "nonce": "b64_encoded_nonce", "timestampMillis": 1710000000000 }, "appIntegrity": { "appRecognitionVerdict": "PLAY_RECOGNIZED", "packageName": "com.payment.app", "certificateSha256Digest": ["b64_encoded_cert_hash"] }, "deviceIntegrity": { "deviceRecognitionVerdict": ["MEETS_DEVICE_INTEGRITY"] }, "accountDetails": { "appLicensingVerdict": "LICENSED" } }
Continuing withΒ Anti-tamperingΒ protection, we should also implement APK signature verification. It is recommended to perform this check on a remote server by sending the current certificate signature and comparing it on the server side.
Here is below an example showing the mechanism to get signature application certificate :
object SignatureUtil { private val TAG = SignatureUtil::class.java.simpleName /** ** Get application signature to be used later in the check anti-tampering ** on Server side, in case have is different than setted in the BE means ** the application was changed/tampered. **/ fun getApkSigners(context: Context): List<Signature>? { return try { if (context .packageManager .getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES) .signingInfo?.hasMultipleSigners() == true ) { context .packageManager .getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES) .signingInfo?.apkContentsSigners ?.toList() } else { context .packageManager .getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES) .signingInfo?.signingCertificateHistory ?.toList() } } catch (e: PackageManager.NameNotFoundException) { Log.e(TAG, "Cannot read APK content signers", e) null } } }
And finally, here is new anti-tampering protection addition was added in Android 10, theΒ useEmbeddedDexΒ flag which is really simple to be used.
Here is an example how to use :
- In manifest xml file, it should putted on it like this :
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.payement.app" xmlns:tools="http://schemas.android.com/tools"> <application android:useEmbeddedDex="true" tools:targetApi="Q"> .... </application> </manifest>
- In theΒ build gradle :
packagingOptions { dex { useLegacyPackaging = false } } // In case, bad behavior when genarting signed aab, we can use this aaptOptions // aaptOptions { // noCompress 'dex' // }
2. Securing Execution Environment
- Anti-Emulation: We should detect virtual environments VM detection Here is below method which check the multiple layers emulation environnements :
object EmulatorDetection { private val QEMU_DRIVERS = arrayOf("goldfish") private val GENY_FILES = arrayOf( "/dev/socket/genyd", "/dev/socket/baseband_genyd" ) private val PIPES = arrayOf( "/dev/socket/qemud", "/dev/qemu_pipe" ) private val X86_FILES = arrayOf( "ueventd.android_x86.rc", "x86.prop", "ueventd.ttVM_x86.rc", "init.ttVM_x86.rc", "fstab.ttVM_x86", "fstab.vbox86", "init.vbox86.rc", "ueventd.vbox86.rc" ) private val ANDY_FILES = arrayOf( "fstab.andy", "ueventd.andy.rc" ) private val NOX_FILES = arrayOf( "fstab.nox", "init.nox.rc", "ueventd.nox.rc" ) fun isRunningInEmulator(context: Context): Boolean { return when { checkBasic() -> true checkAdvanced() -> true checkPackageName(context) -> true else -> false } } private fun checkBasic(): Boolean { var rating = 0 if (Build.PRODUCT == "sdk_x86_64" || Build.PRODUCT == "sdk_google_phone_x86" || Build.PRODUCT == "sdk_google_phone_x86_64" || Build.PRODUCT == "sdk_google_phone_arm64" || Build.PRODUCT == "vbox86p") { rating++ } if (Build.MANUFACTURER == "unknown") { rating++ } if (Build.BRAND == "generic" || Build.BRAND.equals("android", ignoreCase = true) || Build.BRAND == "generic_arm64" || Build.BRAND == "generic_x86" || Build.BRAND == "generic_x86_64" ) { rating++ } if (Build.DEVICE == "generic" || Build.DEVICE == "generic_arm64" || Build.DEVICE == "generic_x86" || Build.DEVICE == "generic_x86_64" || Build.DEVICE == "vbox86p") { rating++ } if (Build.MODEL == "sdk" || Build.MODEL == "Android SDK built for arm64" || Build.MODEL == "Android SDK built for armv7" || Build.MODEL == "Android SDK built for x86" || Build.MODEL == "Android SDK built for x86_64") { rating++ } if (Build.HARDWARE == "ranchu") { rating++ } if (Build.FINGERPRINT.contains("sdk_google_phone_arm64") || Build.FINGERPRINT.contains("sdk_google_phone_armv7") ) { rating++ } var result = (Build.FINGERPRINT.startsWith("generic") || Build.MODEL.contains("google_sdk") || Build.MODEL.lowercase().contains("droid4x") || Build.MODEL.contains("Emulator") || Build.MODEL.contains("Android SDK built for x86") || Build.MANUFACTURER.contains("Genymotion") || Build.HARDWARE == "goldfish" || Build.HARDWARE == "vbox86" || Build.PRODUCT == "sdk" || Build.PRODUCT.startsWith( "google_sdk") || Build.PRODUCT == "sdk_x86" || Build.PRODUCT == "vbox86p" || Build.BOARD.lowercase() .contains("nox") || Build.BOOTLOADER.lowercase().contains("nox") || Build.HARDWARE.lowercase().contains("nox") || Build.PRODUCT.lowercase().contains("nox") || Build.SERIAL.lowercase().contains("nox") || Build.HOST.contains("Droid4x-BuildStation") || Build.MANUFACTURER.startsWith("iToolsAVM") || Build.DEVICE.startsWith("iToolsAVM") || Build.MODEL.startsWith("iToolsAVM") || Build.BRAND.startsWith("generic") || Build.HARDWARE.startsWith("vbox86")) if (result) return true result = result or (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) if (result) return true result = result or ("google_sdk" == Build.PRODUCT) return if (result) true else rating >= 2 } private fun checkQEmuDrivers(): Boolean { for (drivers_file in arrayOf(File("/proc/tty/drivers"), File("/proc/cpuinfo"))) { if (drivers_file.exists() && drivers_file.canRead()) { val data = ByteArray(1024) try { val `is`: InputStream = FileInputStream(drivers_file) `is`.read(data) `is`.close() } catch (exception: Exception) { exception.printStackTrace() } val driverData = String(data) for (known_qemu_driver in QEMU_DRIVERS) { if (driverData.contains(known_qemu_driver)) { return true } } } } return false } private fun checkAdvanced(): Boolean { return (checkFiles(GENY_FILES) || checkFiles(ANDY_FILES) || checkFiles(NOX_FILES) || checkQEmuDrivers() || checkFiles(PIPES) || checkFiles(X86_FILES)) } private fun checkFiles(targets: Array<String>): Boolean { for (pipe in targets) { val qemuFile = File(pipe) if (qemuFile.exists()) { return true } } return false } fun checkPackageName(context: Context): Boolean { val packageManager = context.packageManager val intent = Intent(Intent.ACTION_MAIN, null) intent.addCategory(Intent.CATEGORY_LAUNCHER) val availableActivities = packageManager.queryIntentActivities(intent, 0) for (resolveInfo in availableActivities) { if (resolveInfo.activityInfo.packageName.startsWith("com.bluestacks.")) { return true } } val packages = packageManager .getInstalledApplications(PackageManager.GET_META_DATA) for (packageInfo in packages) { val packageName = packageInfo.packageName if (packageName.startsWith("com.vphone.")) { return true } else if (packageName.startsWith("com.bignox.")) { return true } else if (packageName.startsWith("com.nox.mopen.app")) { return true } else if (packageName.startsWith("me.haima.")) { return true } else if (packageName.startsWith("com.bluestacks.")) { return true } else if (packageName.startsWith("cn.itools.") && Build.PRODUCT.startsWith("iToolsAVM")) { return true } else if (packageName.startsWith("com.kop.")) { return true } else if (packageName.startsWith("com.kaopu.")) { return true } else if (packageName.startsWith("com.microvirt.")) { return true } else if (packageName == "com.google.android.launcher.layouts.genymotion") { return true } } val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val runningServices = manager.getRunningServices(30) for (serviceInfo in runningServices) { val serviceName = serviceInfo.service.className if (serviceName.startsWith("com.bluestacks.")) { return true } } return false } }
- Check Runtime Environment: During runtime, it is essential to validate environment variables, such as running processes, file access, and installed applications, as well as system properties, to detect any anomalies. This check should beΒ performed outside the Android environment, on the backend side. Therefore, before executing any transaction-related actions, the current environment information should be sent for verification.
Below is a code snippet to retrieve all the necessary information.
import android.system.ErrnoException import android.system.Os import android.system.OsConstants import android.util.Log import java.io.BufferedReader object SecureEnvironmentCheckerInfo { private const val TAG = "SecureEnvironmentCheckerInfo" // The verified paths are : // "/data/local/", // "/data/local/bin/", // "/data/local/xbin/", // "/sbin/", // "/system/bin/", // "/system/bin/.ext/", // "/system/bin/failsafe/", // "/system/sd/xbin/", // "/system/usr/we-need-root/", // "/system/xbin/", // "/system/etc/", // "/data/", // "/dev/" fun getFileAccesses(paths: Array<String>): HashMap<String, Boolean> { Log.d(TAG, "Checking file accesses using system calls") val fileAccesses = HashMap<String, Boolean>() for (path in paths) { fileAccesses[path] = try { Os.access(path, OsConstants.F_OK) } catch (e: ErrnoException) { false } } Log.d(TAG, "Checked ${fileAccesses.size} file accesses") return fileAccesses } fun getSystemProperties(): HashMap<String, String>? { Log.d(TAG, "Check system properties using property service") val systemCommand = "getprop" val content = getOutput(systemCommand, event) ?: return null val systemProperties = SystemPropertyParser.parse(content) if (systemProperties != null) { Log.d(TAG, "Retrieve${systemProperties.size} system properties") } else { Log.e(TAG, "Retrieve system properties from '$systemCommand' output") } return systemProperties } fun getInstalledPackages(): Array<String>? { Log.d(TAG, "Retrieve installed packages using package manager with user 0") val systemCommand = "pm list packages --user 0" val content = getOutput(systemCommand) ?: return null val installedPackages = PackageParser.parse(content) Log.d(TAG, "Retrieved ${installedPackages.size} installed packages") return installedPackages } private fun getOutput(systemCommand: String): String? { var process: Process? = null return try { process = Runtime.getRuntime().exec(systemCommand) val output = process!!.inputStream.bufferedReader().use(BufferedReader::readText) val exitValue = process.waitFor() if (exitValue == 0) { output } else { Log.e(TAG, "'$systemCommand' command terminated with exit value $exitValue") null } } catch (e: Exception) { Log.e(TAG, "Error occurred while executing '$systemCommand' command: $e") null } finally { process?.destroy() if (process?.isAlive == true) { process.destroyForcibly() } } } }
- Anti-Sideloading: Restrict APK installation from untrusted sources, we recommand to download the application from Google play by checking the vendor application :
object SideLoadingDetector { private const val AUTHORIZED_APP_VENDOR = "com.android.vending" private fun getInstallerPackageName(context: Context) = context.packageManager .getInstallSourceInfo(context.packageName) .installingPackageName fun isAppSideLoaded(context: Context): Boolean { return try { getInstallerPackageName(context) } catch (e: IllegalArgumentException) { Log.e(TAG, "Cannot access to app installer", e) null } ?.also { Log.d(TAG, "Package installer is: $it") } ?.takeIf { it == AUTHORIZED_APP_VENDOR } ?.let { false } ?: (true).also { Log.e(TAG, "App side loaded") } } } }
- Anti-Multiwindow: Prevent multi-window mode to avoid overlay attacks :
fun MultiWindowModeDetector(activity: Activity): Boolean = activity.isInMultiWindowMode
3. Preventing Data Leakage & Unauthorized Access
In payment applications, data leakage and unauthorized access present significant security risks, potentially exposing sensitive user information such as Payment Card details (PAN, CVV, etc.). To mitigate these security vulnerabilities, it is crucial to implement the following protective measures:
- Log Protection: Avoid logging sensitive data usingΒ
Log.d()
; use structured logging withΒTimberβ¦
In best case, we should remove any logs using as example proguard rules (in case, we need logs for monotring, we must encrypt it before send it to backend) :
# Remove Android logging calls -assumenosideeffects class android.util.Log { public static boolean isLoggable(java.lang.String, int); public static int v(...); public static int d(...); public static int i(...); public static int w(...); public static int e(...); public static int println(int, java.lang.String, java.lang.String); public static java.lang.String getStackTraceString(java.lang.Throwable); }
- Secure Database & PreferenceΒ : We can useΒ SQLCipherΒ for encrypted database storage or save encrypted data directly in your DB and for shared pref, we recammand to use this library from GoogleΒ EncryptedSharedPreferences.
- Secure Binding Services: When using services, it is essential to restrict AIDL exposure by properly configuring export and enable flags. Additionally, applying signature-based permissions ensures that only trusted applications with the correct signature can interact with the service, enhancing security:
Here is below snippet code to how secure Android Service :
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <permission android:name="com.payment.app.permission.BIND_SERVICE" android:protectionLevel="signature" /> <application android:name="com.payment.app.Application" android:icon="@mipmap/ic_launcher" android:label="@string/app_name"> <activity android:name="com.payment.app.MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name="com.payment.app.service.CoreService" android:exported="true" android:enabled="true" android:permission="com.payment.app.BIND_SERVICE" > <intent-filter> <action android:name="com.payment.app.action.BIND_SERVICE" /> </intent-filter> </service> </application> </manifest>
- Anti-Screenshot & Overlay Detection: Android purpose multiple mechanisme to protect fromΒ overlay attacksΒ : UseΒ
FLAG_SECURE
Β to block screenshots and to block overlays using this flag permissionΒ android.permission.HIDE_OVERLAY_WINDOWS(introduced in Android 12)
. For oldest Android version, we can implement a custom solution to inject event in random touch and intercept it.
Here is fully example of Overlay Detector class :
import android.annotation.SuppressLint import android.app.Activity import android.app.Instrumentation import android.graphics.Point import android.os.Build import android.os.SystemClock import android.util.Log import android.view.MotionEvent import android.view.View import android.view.WindowManager import kotlinx.coroutines.* import kotlin.math.absoluteValue class OverlayDetection( screenSize: Point, private var onOverlayDetected: (() -> Unit)? = null ) { companion object { private val TAG = OverlayDetection::class.java.simpleName const val INJECTED_STATE = -35 private const val OFFSET_PERCENT = 10 private const val RETRY_DELAY_INJECT_EVENT = 1500L //1.5 seconds private fun getFlagsWindow() = MotionEvent.FLAG_WINDOW_IS_OBSCURED or MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED } private var lastCallNotCaught = false private var overlayDetectionRunning = true private val scope = CoroutineScope(Dispatchers.IO) private val instrumentation = Instrumentation() private var view: SecureViewContainer? = null private var activity: Activity? = null private val offset: Point = Point( (screenSize.x * OFFSET_PERCENT) / 100, (screenSize.y * OFFSET_PERCENT) / 100 ) private val range = Point( screenSize.x - (2 * offset.x), screenSize.y - (2 * offset.y), ) private var job: Job? = null fun setViewToProtect(view: SecureViewContainer) { Log.d(TAG, "OverlayDetection: protect view $view") this.view = view activity = view.context as Activity this.view?.setOnTouchListener { _: View?, event: MotionEvent -> onOverlayEventDetected(event) false } this.view?.setOnOverlayDetectionListener { onOverlayEventDetected(event) } } private fun hasSecureFlag(): Boolean = activity?.window?.attributes?.flags?.let { (it and WindowManager.LayoutParams.FLAG_SECURE) != 0 } ?: false private fun injectEvent() { if (lastCallNotCaught) { onOverlayDetected.invoke() } else { injectTouchEvent() } } private fun injectTouchEvent() = randomPoint().also { point -> if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { if (point.value != null) { Log.d(TAG, "Inject event at ${point.value}") lastCallNotCaught = true try { touch(point.value, MotionEvent.ACTION_DOWN) touch(point.value, MotionEvent.ACTION_UP) } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) onOverlayDetected.invoke() } } else { Log.e(TAG, "Random point can't be generated for inject event") onOverlayDetected.invoke() } } else { Log.w(TAG, "ANDROID 13: Touch injection deactivated at $point") } } private fun touch(point: Point, event: Int) { instrumentation.sendPointerSync( MotionEvent.obtain( SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), event, point.x.toFloat(), point.y.toFloat(), INJECTED_STATE ) ) } private fun randomPoint(): Rand.Result<Point> { val xRand = Rand.nextInt() val x = xRand.value ?: return Rand.Result(xRand.nativeResult!!) val yRand = Rand.nextInt() val y = yRand.value ?: return Rand.Result(yRand.nativeResult!!) val point = Point((x.absoluteValue % range.x) + offset.x, (y.absoluteValue % range.y) + offset.y) return Rand.Result(point) } fun start() { stop() overlayDetectionRunning = true lastCallNotCaught = false Log.d(TAG, "Start overlay protection") job = scope.launch { delay(1000L) view?.requestFocus() while (overlayDetectionRunning) { injectEvent() delay(RETRY_DELAY_INJECT_EVENT) } } } fun stop() { overlayDetectionRunning = false lastCallNotCaught = false Log.d(TAG, "Stop overlay protection") job?.cancel() job = null } fun forbidMultiWindowMode(activity: Activity): Boolean = activity.isInMultiWindowMode fun onOverlayEventDetected(event: MotionEvent) { Log.d(TAG, "Received motion event: state(${event.metaState}) flags(${event.flags})") when { !hasSecureFlag() -> onOverlayEventDetected(event) event.metaState != INJECTED_STATE -> Unit event.flags == MotionEvent.ACTION_MOVE -> Unit // Workaround to avoid false/positive toast overlay detection (event.flags and getFlagsWindow()) != 0 -> onOverlayDetected.invoke() } lastCallNotCaught = false } }
import android.content.Context import android.util.AttributeSet import android.util.Log import android.view.MotionEvent import androidx.constraintlayout.widget.ConstraintLayout class SecureViewContainer @JvmOverloads constructor( context: Context, attributeSet: AttributeSet? = null, defStyle: Int = 0, ) : ConstraintLayout( context, attributeSet, defStyle ) { companion object { private val TAG = SecureViewContainer::class.java.simpleName } private var onOverlayDetectionListener: (() -> Unit)? = null fun setOnOverlayDetectionListener(onOverlayDetectionListener: () -> Unit) { this.onOverlayDetectionListener = onOverlayDetectionListener } override fun onFilterTouchEventForSecurity(ev: MotionEvent): Boolean { val checkIsObscured = ev.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0 val checkIsPartiallyObscured = ev.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0 Log.d(TAG, "onFilterTouchEventForSecurity: " + "isObscured: $checkIsObscured, isPartiallyObscured: $checkIsPartiallyObscured") if (checkIsObscured || checkIsPartiallyObscured) { onOverlayDetectionListener?.invoke() } return super.onFilterTouchEventForSecurity(ev) } }
<com.payment.app.SecureViewContainer xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <androidx.appcompat.widget.AppCompatImageView android:id="@+id/icon" android:layout_width="200dp" android:layout_height="200dp" android:src="@drawable/icon" /> </com.payment.app.SecureViewContainer>
- Anti-Camera & Microphone Abuse: Restrict background access using Android 10+ APIs and put some mechanism detection when camera is open and used by another threat application, here is an example to handle using the camera by another application:
import android.content.Context import android.hardware.camera2.CameraManager import android.util.Log class CameraDetector(context: Context, private var onCameraDetected: (() -> Unit)? = null) { companion object { private val TAG = CameraDetector::class.java.simpleName } private val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager private val cameraAvailabilityCallback = object : CameraManager.AvailabilityCallback() { override fun onCameraUnavailable(cameraId: String) { super.onCameraUnavailable(cameraId) onCameraDetected.invoke() } } fun start() { try { manager.registerAvailabilityCallback(cameraAvailabilityCallback, null) } catch (e: Exception) { Log.e(TAG, "Cannot register availability camera callback", e) } } fun stop() { try { manager.unregisterAvailabilityCallback(cameraAvailabilityCallback) } catch (e: Exception) { Log.e(TAG, "Cannot unregister availability camera callback", e) } } }
- Anti-Accessibility Exploits: When Android introduced the Accessibility API, it was intended for beneficial use cases. However, over time, it has been exploited and considered a potential backdoor. To mitigate this risk and prevent unauthorized access to on-screen information via the Accessibility API.
Here is an example of how to implement proper protections.:
import android.graphics.Rect import android.view.View import android.view.accessibility.AccessibilityNodeInfo import androidx.core.view.ViewCompat class SecuredAccessibilityDelegateHelper(val view: View) : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) { super.onInitializeAccessibilityNodeInfo(host, info) view.importantForAccessibility = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS info.isEnabled = false info.setBoundsInScreen(Rect(0, 0, 0, 0)) info.text = "" } }
class SecuredText(context: Context, attrs: AttributeSet) : AppCompatTextView(context, attrs) { init { accessibilityDelegate = SecuredAccessibilityDelegateHelper(this) } }
<com.payement.app.SecuredText android:id="@+id/text" android:layout_width="wrap_content"xxx android:layout_height="30dp" android:gravity="center" android:textColor="@color/white" android:textSize="15sp" android:textStyle="bold" />
4. Strengthening Authentication & Secure Communication
- Biometric Authentication: Starting Android 6.0 theΒ Biometric APIΒ exist for strong user authentication likeΒ fingerprint,Β face, orΒ irisΒ which be used to verify the identity of the user before doing any transaction.
- Secure Connection (TLS/SSL): Communication must occur over a secure channel using protocols such as TLS/SSL. It is highly recommended to enforce TLS 1.2 or higher (preferably TLS 1.3 if supported by the server). Additionally, implementingΒ certificate pinningΒ is mandatory to prevent man-in-the-middle attacks. Below is the process for certificate enrollment :

Android Certificate Enrollment
- HTTPS/SSL Security: To prevent Man-in-the-Middle (MITM) attacks, Android 7 (API 24) introduced theΒ Network Security Config, allowing developers to customize network security settings within their applications. Below is an example demonstrating how to configure it properly at manifest file:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="co.payement.app"> <application android:networkSecurityConfig="@xml/network_security_config"> ... </application> </manifest
At network security config file :
<?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config> <domain includeSubdomains="true">payment-domain.com</domain> <pin-set> <pin digest="SHA-256">ZEDJEEKKEKLEE+fibTqbIsWNR/X7CWNVW+CEEA=</pin> <pin digest="SHA-256">BFDBDFDFVDccvVFVVcfdesdsdvsdsdvsd=</pin> </pin-set> </domain-config> </network-security-config>
- Secure Tokens (JWE, UUID): In some cases of Android authentication processes, tokenization is required. There are different types of tokens, such asΒ JWT, JWS, JWE, and JWK. For security reasons, we recommend using the most suitable and secure type based on the applicationβs needs asΒ encrypted token JWE.
- API Key Protection: Every connection that requires API keys should ensure they are stored securely, either as aΒ Gradle propertyΒ or anΒ environment variable. Google provides an interesting plugin,Β Secrets Gradle Plugin, to help manage sensitive data securely. Additionally, it is recommended toΒ restrict API key usageΒ by specifying theΒ allowed package IDΒ in the API provider settings..
5. Secure Cryptography & Key Management
- Secure Cryptography: In the programming world,Β cryptographyΒ is a vast and complex topic that cannot be fully covered in this article. However, there are key recommendations fromΒ Android security guidelinesΒ and well-known security organizations such asΒ NIST (USA), ECRYPT (EU), and CRYPTREC (Japan)Β for selecting the appropriate cryptographic algorithms and best practices :
FromΒ Android recommandationΒ :

Recommended cryptography algorithm by Android
FromΒ NISTΒ :

Recommended cryptography algorithm by NIST
FromΒ ECRYPT

Recommended cryptography algorithm by ECRYPT
FromΒ CRYPTREC

Recommended cryptography algorithm by CRYPTREC
and finally, to store the encrypted locally, itβsΒ requiredΒ to generate it by the AndroidΒ Keystore APIΒ usingΒ Hardware-backed Keystore
- Key Rotation: Programming security is time-sensitive, and in this context, cryptographic keys must be regularly updated to mitigate potential risks. It is recommended not to exceed a 90-day lifespan for secure keys to ensure optimal protection.
- Use Secure Random: When generating secure keys, one crucial parameter that must not be ignored is the random field. To generate it, it should come from trusted entropy sources such as theΒ SecureRandomΒ API. Therefore, we must avoid using any other sources for generating random values.
6. Enhancing System Security & Updates
- Secure Elements & TEE (Trusted Execution Environment): As said in the last point,Β Android KeystoreΒ should be generated onΒ Trusted Execution Environment (TEE)Β storage backed inΒ StrongBoxΒ introduced in Android 9 (for lowest version, we can useΒ software white boxΒ but itβs must be respect the security standard).
- GMS Security Updates: Google provides GMS (Google Mobile Services), which checks for any security updates. To verify this, we should check the currently installed version of Google Play Services. Below is the minimum version that should be present on the device :
object GooglePlayServicesChecker { private val TAG = GooglePlayServices::class.java.simpleName private const val MINIMUM_VERSION = 201516018 private val availability = GoogleApiAvailability.getInstance() fun isSupported(context: Context): Boolean = (availability.isGooglePlayServicesAvailable( context, MINIMUM_VERSION ) == ConnectionResult.SUCCESS) .also { Log.d(TAG, "Google Play Services availability: $it") try { ProviderInstaller.installIfNeeded(context) Log.d(TAG, "Google Play Services update ProviderInstaller installIfNeeded") } catch (e: GooglePlayServicesRepairableException) { Log.e(TAG, "An error has occurred", e) // Indicates that Google Play services is out of date, disabled, etc. // Prompt the user to install/update/enable Google Play services. GoogleApiAvailability.getInstance() .showErrorNotification(context, e.connectionStatusCode) Log.e(TAG, "Error occurred during check supported GooglePlayServices :", e) } catch (e: GooglePlayServicesNotAvailableException) { // Indicates a non-recoverable error; the ProviderInstaller is not able // to install an up-to-date Provider. // Note: As the min version 201516018 is higher than which required 11925000 (extracted from // ProviderInstaller class) so no need to block the user by out-dated google play service popup. Log.e(TAG, "An error has occurred", e) } } .also { Log.d(TAG, "Google Play Services version: " + getVersion(context)) } private fun getVersion(context: Context): Long? = try { PackageInfoCompat.getLongVersionCode( context.packageManager.getPackageInfo( GoogleApiAvailability.GOOGLE_PLAY_SERVICES_PACKAGE, 0 ) ).also { Log.d(TAG, "Google Play Services version: $it") } } catch (e: PackageManager.NameNotFoundException) { Log.e(TAG, "Google Play Services package not found", e) null } }
- Backup Security: In Android, to retrieve your preferences after reinstalling an application, we use the backup mechanism. However, this represents a security issue, which is why it has been deprecated starting from Android 12 and may be removed in future versions. We recommend disabling auto-backups for sensitive data by settingΒ
android:allowBackup="false"
. If there is a need to use backups, make sure not to store any sensitive data. - CVE PatchingΒ & Dependency Updates: In Android, we have a higher probability to use third party library which can represent a vulenrability risk, so before integrating any library, we must check the origin , the maintainers community, the latest support date .. and after that we should regularly update and check for CVEs and finally use only Stable version.
- Android Security Support patch :Β Every version of Android has a security lifecycle and especially end of life security support so we the minmum Android version must be respect following this example table taken from this referenceΒ https://endoflife.date/androidΒ :

7. Advanced Security Techniques
- Secure OpenGL: In some context , we need toΒ drawΒ some sensitive information in the screen as displaying the random PIN so to protect rendering data to be read illegaly from the RAM memory, Android purpose this flagΒ EGL_PROTECTED_CONTENT_EXTΒ used to protect the rendering data context.
Here is below this intersting example code to create secured OpenGL context in Android :
import android.content.Context import android.opengl.EGL14.* import android.opengl.GLSurfaceView import android.util.Log import com.visa.BuildConfig import javax.microedition.khronos.egl.* /** * A simple GLSurfaceView sub-class that demonstrate how to perform * OpenGL ES 2.0 rendering into a GL Surface. Note the following important * details: * * - The class must use a custom context factory to enable 2.0 rendering. * See ContextFactory class definition below. * * - The class must use a custom EGLConfigChooser to be able to select * an EGLConfig that supports 2.0. This is done by providing a config * specification to eglChooseConfig() that has the attribute * EGL10.ELG_RENDERABLE_TYPE containing the EGL_OPENGL_ES2_BIT flag * set. See ConfigChooser class definition below. * * - The class must select the surface's format, then choose an EGLConfig * that matches it exactly (with regards to red/green/blue/alpha channels * bit depths). Failure to do so would result in an EGL_BAD_MATCH error. * * - The class use some OpenGL protection extension if it's exist on the Android Device otherwise * the used context is standard. * About this mechanism of this protected extension EGL_PROTECTED_CONTENT_EXT = 0x32C0 : * * The attribute EGL_PROTECTED_CONTENT_EXT can be applied to EGL contexts, * EGL surfaces and EGLImages. If the attribute EGL_PROTECTED_CONTENT_EXT * is set to EGL_TRUE by the application, then the newly created EGL object * is said to be protected. A protected context is required to allow the * GPU to operate on protected resources, including protected surfaces and * protected EGLImages. * * GPU operations are grouped into pipeline stages. Pipeline stages can be * defined to be protected or not protected. Each stage defines * restrictions on whether it can read or write protected and unprotected * resources, as follows: * When a GPU stage is protected, it: * - Can read from protected resources * - Can read from unprotected resources * - Can write to protected resources * - Can NOT write to unprotected resources * * When a GPU stage is not protected, it: * - Can NOT read from protected resources * - Can read from unprotected resources * - Can NOT write to protected resources * - Can write to unprotected resources * */ internal class GL2JNIView( context: Context?, ) : GLSurfaceView(context) { companion object { private val TAG = GL2JNIView::class.java.simpleName private const val EGL_CONTEXT_CLIENT_ATTR_VERSION = 0x3098 private const val EGL_CONTEXT_CLIENT_VALUE_VERSION = 2 private const val EGL_EXTENSION_PROTECTED_CONTENT_NAME = "EGL_EXT_protected_content" private const val EGL_EXT_PROTECTED_CONTENT = 0x32C0 } init { val isSecureExtensionSupported = isSecureExtensionSupported() Log.d(TAG, "OpenGl Secure extension supported : $isSecureExtensionSupported") if (BuildConfig.DEBUG) { debugFlags = DEBUG_CHECK_GL_ERROR or DEBUG_LOG_GL_CALLS } setEGLContextFactory(ContextFactory(isSecureExtensionSupported)) setEGLWindowSurfaceFactory(WindowSurfaceFactory(isSecureExtensionSupported)) } internal inner class ContextFactory(private val secureContext: Boolean) : EGLContextFactory { override fun createContext(egl: EGL10, display: EGLDisplay, config: EGLConfig): EGLContext { val attrList = if (secureContext) { intArrayOf(EGL_CONTEXT_CLIENT_ATTR_VERSION, EGL_CONTEXT_CLIENT_VALUE_VERSION, EGL_EXT_PROTECTED_CONTENT, EGL_TRUE, EGL10.EGL_NONE) } else { intArrayOf(EGL_CONTEXT_CLIENT_VERSION, EGL_CONTEXT_CLIENT_VALUE_VERSION, EGL10.EGL_NONE) } val context = egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, attrList) if (context == EGL10.EGL_NO_CONTEXT) { Log.e(TAG, "Error creating EGL context.") } checkEglError("eglCreateContext") return context } override fun destroyContext(egl: EGL10, display: EGLDisplay, context: EGLContext) { if (!egl.eglDestroyContext(display, context)) { Log.e("DefaultContextFactory", "display: $display context: $context") } } } internal inner class WindowSurfaceFactory(private val secureWindowSurface: Boolean) : EGLWindowSurfaceFactory { override fun createWindowSurface( egl: EGL10, display: EGLDisplay, config: EGLConfig, nativeWindow: Any, ): EGLSurface? { var result: EGLSurface? = null try { val attrList = if (secureWindowSurface) { intArrayOf(EGL_EXT_PROTECTED_CONTENT, EGL_TRUE, EGL10.EGL_NONE) } else { intArrayOf(EGL10.EGL_NONE, EGL10.EGL_NONE, EGL10.EGL_NONE) } result = egl.eglCreateWindowSurface(display, config, nativeWindow, attrList) checkEglError("eglCreateWindowSurface") } catch (e: IllegalArgumentException) { Log.e(TAG, "eglCreateWindowSurface", e) } return result } override fun destroySurface(egl: EGL10, display: EGLDisplay, surface: EGLSurface) { egl.eglDestroySurface(display, surface) } } private fun isSecureExtensionSupported(): Boolean { val display: android.opengl.EGLDisplay? = eglGetDisplay(EGL_DEFAULT_DISPLAY) val extensions = eglQueryString(display, EGL10.EGL_EXTENSIONS) return extensions != null && extensions.contains(EGL_EXTENSION_PROTECTED_CONTENT_NAME) } private fun checkEglError(methodName: String) { val eglResult = eglGetError() if (eglResult == EGL_SUCCESS) { val message = "$methodName succeeded without error (EGL_SUCCESS)" Log.d(TAG, message) } else { val errorHexString = "0x${Integer.toHexString(eglResult)}" val errorDesc = eglResultDesc(eglResult) val message = "$methodName: EGL error $errorHexString encountered. $errorDesc" Log.e(TAG, message) } } // https://registry.khronos.org/EGL/sdk/docs/man/html/eglGetError.xhtml private fun eglResultDesc(eglResult: Int) = when (eglResult) { EGL_SUCCESS -> "The last function succeeded without error." EGL_NOT_INITIALIZED -> "EGL is not initialized, or could not be initialized, for the specified EGL display connection." EGL_BAD_ACCESS -> "EGL cannot access a requested resource (for example a context is bound in another thread)." EGL_BAD_ALLOC -> "EGL failed to allocate resources for the requested operation." EGL_BAD_ATTRIBUTE -> "An unrecognized attribute or attribute value was passed in the attribute list." EGL_BAD_CONTEXT -> "An EGLContext argument does not name a valid EGL rendering context." EGL_BAD_CONFIG -> "An EGLConfig argument does not name a valid EGL frame buffer configuration." EGL_BAD_CURRENT_SURFACE -> "The current surface of the calling thread is a window, pixel buffer or pixmap that is no longer valid." EGL_BAD_DISPLAY -> "An EGLDisplay argument does not name a valid EGL display connection." EGL_BAD_SURFACE -> "An EGLSurface argument does not name a valid surface (window, pixel buffer or pixmap) configured for GL rendering." EGL_BAD_MATCH -> "Arguments are inconsistent (for example, a valid context requires buffers not supplied by a valid surface)." EGL_BAD_PARAMETER -> "One or more argument values are invalid." EGL_BAD_NATIVE_PIXMAP -> "A NativePixmapType argument does not refer to a valid native pixmap." EGL_BAD_NATIVE_WINDOW -> "A NativeWindowType argument does not refer to a valid native window." EGL_CONTEXT_LOST -> "A power management event has occurred. The application must destroy all contexts and reinitialise OpenGL ES state and objects to continue rendering." else -> "" } }
- Multiprocess Security: To avoid a global disaster in the event that reading data from memory is compromised, we recommend defining a multiprocess mechanism usingΒ
isolatedProcess. This helps to prevent unauthorized access by ensuring that each process operates in a separate environment, making it more difficult for one process to access the memory or data of another..
As example, introduce an isolted process service container :
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.payement.app" xmlns:tools="http://schemas.android.com/tools"> <application> <service android:name="com.payement.app.service.IsoltedProcessService" android:process=":isoltedProcessProcess" /> </application> </manifest>
- Bonus:Β Here is some useful resources to be up to date by security world of Android:Β android_securecoding_en.pdfΒ ,Β security-best-practices,Β www-project-mobile-app-security.
Job Offers
Conclusion
By reading this article, you can get an idea of how a simple payment transaction, which takes only a few seconds, goes through a comprehensive security workflow. Also, keep in mind that there is no final version of security β itβs crucial to continuously stay aligned and updated with the evolving security landscape of Android.
And finally remember, in every update of your security layer, donβt forget this quote:
βDonβt let security kill your business, try to find the right balance.β
For any question & suggestion, Letβs discuss !
This article was previously published on proandroiddev.com.