talsec / Free-RASP-ReactNative

React Native plugin for improving app security and threat monitoring on Android and iOS mobile devices.
https://github.com/talsec/Free-RASP-Community
MIT License
77 stars 8 forks source link

java.lang.NoClassDefFoundError: Failed resolution of: Lcom/freeraspreactnative/FreeraspReactNativePackage; #67

Open powilliam opened 3 weeks ago

powilliam commented 3 weeks ago

Describe the bug Started right after upgrading from expo SDK 48 to SDK 51, only in Android and in the release build.

Already tried:

My workaround until now was to wrap the android client myself into a bridge, but that's not ideal as I just don't want to maintain it while there's an official react native library.

tompsota commented 3 weeks ago

Hello @powilliam,

Thank you for reporting the bug. I've seen a couple of issues recently that were caused by R8 in newer versions of RN, and this one seems to be very similar. I'll try to reproduce it tomorrow.

By the way, would you mind sharing your modified bridge with us? It could save me some time debugging the issue and the fix could be potentially released sooner.

Thanks, Tomas from Talsec

powilliam commented 3 weeks ago

opa bão? @tompsota,

that's interesting to hear. I can try different/lower versions of the R8 to see if this issue would stop. btw I totally can share the bridge:

TLDR: it's literally the same implementation as freerasp-react-native/android, just with some syntax differences in the threat handler and the bridge itself, so I can just keep using the library in the react native side without having to do something crazy for the iOS

// [react-native.config.js]
module.exports = {
  dependencies: {
    'freerasp-react-native': {
      platforms: {
        android: null,
      },
    },
  },
}
// [Threat.kt]
package mine

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableArray

sealed class Threat(val value: Int) {
  data object AppIntegrity : Threat((10000..999999999).random())
  data object PrivilegedAccess : Threat((10000..999999999).random())
  data object Debug : Threat((10000..999999999).random())
  data object Hooks : Threat((10000..999999999).random())
  data object Passcode : Threat((10000..999999999).random())
  data object Simulator : Threat((10000..999999999).random())
  data object SecureHardwareNotAvailable : Threat((10000..999999999).random())
  data object SystemVPN : Threat((10000..999999999).random())
  data object DeviceBinding : Threat((10000..999999999).random())
  data object UnofficialStore : Threat((10000..999999999).random())
  data object ObfuscationIssues : Threat((10000..999999999).random())
  data object DevMode : Threat((10000..999999999).random())

  companion object {
    internal fun getThreatValues(): WritableArray {
      return Arguments.fromList(
        listOf(
          AppIntegrity.value,
          PrivilegedAccess.value,
          Debug.value,
          Hooks.value,
          Passcode.value,
          Simulator.value,
          SecureHardwareNotAvailable.value,
          SystemVPN.value,
          DeviceBinding.value,
          UnofficialStore.value,
          ObfuscationIssues.value,
          DevMode.value,
        )
      )
    }
  }
}
// [FreeraspThreatHandler.kt]

package mine

import com.aheaditec.talsec_security.security.api.ThreatListener

class FreeraspThreatHandler(
  val onHandle: (Threat) -> Unit
) : ThreatListener.ThreatDetected,
  ThreatListener.DeviceState {
  override fun onRootDetected() = onHandle(Threat.PrivilegedAccess)
  override fun onDebuggerDetected() = onHandle(Threat.Debug)
  override fun onEmulatorDetected() = onHandle(Threat.Simulator)
  override fun onTamperDetected() = onHandle(Threat.AppIntegrity)
  override fun onUntrustedInstallationSourceDetected() = onHandle(Threat.UnofficialStore)
  override fun onHookDetected() = onHandle(Threat.Hooks)
  override fun onDeviceBindingDetected() = onHandle(Threat.DeviceBinding)
  override fun onObfuscationIssuesDetected() = onHandle(Threat.ObfuscationIssues)
  override fun onUnlockedDeviceDetected() = onHandle(Threat.Passcode)
  override fun onHardwareBackedKeystoreNotAvailableDetected() = onHandle(Threat.SecureHardwareNotAvailable)
  override fun onDeveloperModeDetected() = onHandle(Threat.DevMode)
  override fun onSystemVPNDetected() = onHandle(Threat.SystemVPN)
}
// [FreeraspNativeModule.kt]

package mine

import com.aheaditec.talsec_security.security.api.Talsec
import com.aheaditec.talsec_security.security.api.TalsecConfig
import com.aheaditec.talsec_security.security.api.ThreatListener
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableArray
import com.facebook.react.modules.core.DeviceEventManagerModule

class FreeraspNativeModule(
  private val reactContext: ReactApplicationContext
) : ReactContextBaseJavaModule(reactContext) {
  override fun getName() = NAME

  private val handler = FreeraspThreatHandler { threat ->
    notifyListeners(threat)
  }

  private val listener = ThreatListener(handler)

  @ReactMethod
  fun addListener(eventName: String) {
  }

  @ReactMethod
  fun removeListeners(count: Int) {
  }

  @ReactMethod
  fun talsecStart(options: ReadableMap, promise: Promise) {
    try {
      val config = options.config()
      listener.registerListener(reactContext)
      Talsec.start(reactContext, config)
      promise.resolve("freeRASP started")
    } catch (exception: Exception) {
      val params = Arguments.createMap().apply {
        putString("message", exception.message)
      }

      promise.reject("initializationError", params)
    }
  }

  @ReactMethod
  fun getThreatIdentifiers(promise: Promise) {
    promise.resolve(Threat.getThreatValues())
  }

  @ReactMethod
  fun getThreatChannelData(promise: Promise) {
    val channelData: WritableArray = Arguments.createArray().apply {
      pushString(THREAT_CHANNEL_NAME)
      pushString(THREAT_CHANNEL_KEY)
    }

    promise.resolve(channelData)
  }

  @ReactMethod
  fun onInvalidCallback() {
    android.os.Process.killProcess(android.os.Process.myPid())
  }

  private fun notifyListeners(threat: Threat) {
    val params = Arguments.createMap().apply {
      putInt(THREAT_CHANNEL_KEY, threat.value)
    }

    reactContext
      .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
      .emit(THREAT_CHANNEL_NAME, params)
  }

  companion object {
    const val NAME = "FreeraspReactNative"

    val THREAT_CHANNEL_NAME = (10000..999999999).random()
      .toString()

    val THREAT_CHANNEL_KEY = (10000..999999999).random()
      .toString()
  }
}

private fun ReadableMap.config(): TalsecConfig {
  val androidConfig = getMap("androidConfig")
  val packageName = androidConfig?.getString("packageName")!!
  val certificateHashes = mutableListOf<String>()
  val hashes = androidConfig.getArray("certificateHashes")!!

  for (i in 0 until hashes.size()) {
    @Suppress("UNNECESSARY_SAFE_CALL")
    hashes.getString(i)?.let {
      certificateHashes.add(it)
    }
  }

  val watcherMail = getString("watcherMail")
  val alternativeStores = mutableListOf<String>()

  if (androidConfig.hasKey("supportedAlternativeStores")) {
    val stores = androidConfig.getArray("supportedAlternativeStores")!!

    for (i in 0 until stores.size()) {
      @Suppress("UNNECESSARY_SAFE_CALL")
      stores.getString(i)?.let {
        alternativeStores.add(it)
      }
    }
  }

  var isProd = true
  if (hasKey("isProd")) {
    isProd = getBoolean("isProd")
  }

  return TalsecConfig(
    packageName,
    certificateHashes.toTypedArray(),
    watcherMail,
    alternativeStores.toTypedArray(),
    isProd
  )
}
tompsota commented 3 weeks ago

Hi @powilliam,

thanks for sharing the bridge.

Did you modify also FreeraspReactNativePackage class in any way? Because that's the package which is somehow missing, based on the error message you get.

I tried to set up project with Expo 51, but did not get the error in release build. So I suspect that maybe something went wrong during the upgrade of Expo. Did you try to reinstall the plugin (i.e. remove it and add again)? If not, please try it.

Cheers, Tomas from Talsec