DEV Community

Cover image for Ensuring App Integrity with Signature Verification in React Native
Ajmal Hasan
Ajmal Hasan

Posted on • Edited on

Ensuring App Integrity with Signature Verification in React Native

Introduction

In mobile app development, ensuring app security and integrity is crucial. One effective way to do this is by verifying the app's signature at runtime. In this blog, we’ll explore how to implement App Signature Verification in a React Native project using Kotlin for Android’s MainActivity.kt.

Why is App Signature Verification Important?

App signature verification helps prevent:

  • Unauthorized modifications to your app.
  • Reverse engineering and tampering.
  • Users running an unofficial version of your app.

By checking the app’s signature at launch, you can ensure only the intended release versions are used.

Creating Keystore Files

For apps that will be published on the Google Play Store, a Play Store keystore must be generated and used for signing the final release build.
Before verifying the app signature, a keystore file must be generated. The keystore file is required for signing the app during the release build process. To create a keystore file, run the following command:

keytool -genkey -v -keystore release.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias your_alias_name
Enter fullscreen mode Exit fullscreen mode

This command will generate a release.keystore file, which will be used to sign the app. Similarly you can do for debug.keystore or playstore.keystore also.

Extracting the App Signature

To extract the app signature for verification, navigate to the folder where the keystore file exists and run the following command:
To extract the app signature for verification, navigate to the folder where the keystore file exists and run the following command:

keytool -exportcert -alias your_alias_name -keystore release.keystore | openssl sha1 -binary | openssl base64
Enter fullscreen mode Exit fullscreen mode

Example Output:

gpOIDHFkKCtqPIAvhvsadBlDzIY=
Enter fullscreen mode Exit fullscreen mode

Add this to your .env file:

APP_SIGNATURE=gpOIDHFkKCtqPIAvhvsadBlDzIY=
Enter fullscreen mode Exit fullscreen mode

For setup, check the react-native-config documentation.

Alternatively, if you don't want to use the APP_SIGNATURE variable through the .env, you can use it directly in MainActivity.


Implementing App Signature Verification in Kotlin

Below is the Kotlin implementation to validate the app's signature at runtime:
MainActivity.kt

package com.my_app

import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import org.devio.rn.splashscreen.SplashScreen
import android.os.Bundle
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.util.Base64
import android.util.Log
import java.security.MessageDigest
import android.app.AlertDialog

class MainActivity : ReactActivity() {

    companion object {
        private const val VALID = 0  // Indicates a valid app signature
        private const val INVALID = 1 // Indicates an invalid or tampered signature
    }

    private var isAppCompromised = false // Flag to track whether the app is compromised

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        SplashScreen.show(this) // Show splash screen while verifying app signature

        val result = checkAppSignature(this) // Validate the app signature
        Log.d("LOG", "Signature Validation Result: $result")

        if (result != VALID) { // If the signature is invalid
            isAppCompromised = true
            showAlertDialog(
                "Developer Signature is tampered. Please sign in with the correct signature.",
                this
            ) // Show an alert to inform the user
        } else {
            SplashScreen.hide(this) // Hide splash screen only if the app signature is valid
        }
    }

    override fun getMainComponentName(): String {
        return if (isAppCompromised) "" else "my_app" // Prevents the app from running if compromised
    }

    override fun createReactActivityDelegate(): ReactActivityDelegate {
        return DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
    }

    /**
     * Checks if the app's signing certificate matches the expected signature.
     */
    private fun checkAppSignature(context: Context): Int {
        return try {
            // Get the app's package info and signing certificates based on the Android version
            val packageInfo: PackageInfo = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
                context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES)
            } else {
                @Suppress("DEPRECATION")
                context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
            }

            // Extract the signatures
            val signatures = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
                packageInfo.signingInfo.apkContentsSigners
            } else {
                @Suppress("DEPRECATION")
                packageInfo.signatures
            }

            for (signature in signatures) {
                // Generate SHA-1 hash of the signature
                val md: MessageDigest = MessageDigest.getInstance("SHA")
                md.update(signature.toByteArray())
                val currentSignature: String = Base64.encodeToString(md.digest(), Base64.DEFAULT).trim()

                Log.d("Current Signature", currentSignature) // Log the extracted signature
                Log.d("Expected Signature", BuildConfig.APP_SIGNATURE) // Log the expected signature

                // Compare the extracted signature with the expected one from BuildConfig
                if (BuildConfig.APP_SIGNATURE.equals(currentSignature, ignoreCase = true)) {
                    return VALID // Return valid if the signature matches
                }
            }
            INVALID // Return invalid if no matching signature is found
        } catch (e: Exception) {
            Log.e("SignatureCheck", "Exception in signature check", e) // Log any errors
            INVALID
        }
    }

    /**
     * Displays an alert dialog when the app signature is invalid.
     * Prevents the user from proceeding and forces them to exit.
     */
    private fun showAlertDialog(msg: String, context: Context) {
        runOnUiThread {
            val dialog = AlertDialog.Builder(context)
            dialog.setMessage(msg)
                .setTitle("Alert")
                .setCancelable(false) // Prevent user from dismissing the dialog
                .setPositiveButton("Exit") { _, _ -> finish() } // Exit the app when the button is clicked
                .create()
                .show()
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

How It Works

1) checkAppSignature(context):

  • Fetches the current app signature.
  • Compares it with the expected signature (BuildConfig.APP_SIGNATURE).
  • Returns VALID if they match, otherwise INVALID.

2) showAlertDialog(msg, context):

  • Displays an alert dialog if the app’s signature is tampered with.
  • Prevents the user from proceeding further.

Final Thoughts

App signature verification is an essential security measure to ensure that only authorized versions of your app run on users’ devices. Implementing this check prevents unauthorized modifications and enhances the security of your React Native application.

By integrating this approach, your app will remain protected from unauthorized alterations while maintaining user trust and integrity.


Top comments (0)