[Huawei Developers] muraliameakula:
Hi everyone,

In this article, I will talk about the security SDK development in a single code base structure for Huawei Safety Detect and Google Safety Net services, which will make your applications more secure. Thanks to this SDK, Huawei Safety Detect service with HMS (Huawei Mobile Services) and stages with GMS (Google Mobile Services) can be run compatible with Google Safety Net package for 2 platforms.

Within the scope of the SDK, we will include the following features;

User Detect: With this feature, you can make your application more secure by checking whether the users using our application are fake users. This feature is a very important and frequently used feature for banks and many e-commerce applications.

Root Detection: With this feature, it allows you to make the application more secure by checking whether the device running the application is a rooted device. It is critically important, especially for applications in the banking industry.

Huawei Safety Detect Service​

Huawei Safety Detect service, as I mentioned at the beginning of my article, is a security service that allows you to make your applications more secure and protect against security threats.You can find detailed information about Huawei Safety Detect service here.

Google Safety Net Service​

Safety Net is a service that provides a set of services and APIs that help protect your app against security threats, including bad URLs, potentially harmful apps, and rogue users.You can find detailed information about the Google Safety Net service here.

Adding a Module

First, let’s create a new module where we will do all the improvements. Let’s create a new module by choosing File -> New -> New Module -> Android Library and name it safety. After this step, we need to add dependencies to the build.gradle file of our module.

build.gradle(safety)

implementation 'com.huawei.hms:safetydetect:5.0.5.302'
implementation 'com.google.android.gms:play-services safetynet:17.0.0'

After creating a new module and adding the necessary dependencies, we can now start SDK development.

First of all, we create our interface, which contains the functions that we will use jointly for both platforms (Google-Huawei). Next, we will create the Device class, which will allow us to find the Mapper class and the mobile service type installed on the device.

interface SafetyService {
fun userDetect(appKey : String,callback: SafetyServiceCallback<SafetyServiceResponse>)
fun rootDetection(appKey: String,callback: SafetyRootDetectionCallback<RootDetectionResponse> )
interface SafetyServiceCallback<T>{
fun onSuccessUserDetect(result: T? = null)
fun onFailUserDetect(e: java.lang.Exception)
}
interface SafetyRootDetectionCallback<T>{
fun onSuccessRootDetect(result: T? = null)
fun onFailRootDetect(e: java.lang.Exception)
}
object Factory {
fun create(context: Context): SafetyService {
return when (Device.getMobileServiceType(context)) {
MobileServiceType.GMS -> {
GoogleSafetyServiceImpl(context)
}
MobileServiceType.HMS -> {
HuaweiSafetyServiceImpl(context)
}
else -> {
throw Exception("Unknown service")
}
}
}
}
}

As seen above, we create methods and callbacks that will perform both user detect and root detection. Then, in the create method, we check the service availability on the device from the Device class and work the Google or Huawei SafetyServiceImpl classes.

enum class MobileServiceType {
HMS,
GMS,
NON
}
object Device {
/**
* Mobile services availability of devices
*
* @return Device mobile service type enum
*/
fun getMobileServiceType(
context: Context,
firstPriority: MobileServiceType? = null
): MobileServiceType {
val gms: Boolean = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(context) == com.google.android.gms.common.ConnectionResult.SUCCESS
val hms: Boolean = HuaweiApiAvailability.getInstance()
.isHuaweiMobileServicesAvailable(context) == com.huawei.hms.api.ConnectionResult.SUCCESS
return if (gms && hms) {
firstPriority ?: MobileServiceType.HMS
} else if (gms) {
MobileServiceType.GMS
} else if (hms) {
MobileServiceType.HMS
} else {
MobileServiceType.NON
}
}

After these operations, we must create the Mapper class in order to parse the objects that we send and receive from the services as parameters, and the other classes we need to define.

abstract class Mapper<I, O> {
abstract fun map(from: I): O
}

After this step, we must define separate Mapper classes for Google and Huawei. For example, as a result of user detect operation, responseToken object is returned to us as Google Safety Net API and Huawei Safety Detect service return parameter. Thanks to our mapper class, we will be able to parse it into our response class, which we will create when it returns from Google or Huawei service.

class GoogleSafetyMapper: Mapper<SafetyNetApi.RecaptchaTokenResponse, SafetyServiceResponse>() {
override fun map(from: SafetyNetApi.RecaptchaTokenResponse): SafetyServiceResponse = SafetyServiceResponse(
responseToken = from.tokenResult
)
}

class HuaweiSafetyMapper : Mapper<UserDetectResponse, SafetyServiceResponse>() {
override fun map(from: UserDetectResponse): SafetyServiceResponse = SafetyServiceResponse(
responseToken = from.responseToken
)
}

As seen in the codes above, Huawei Safety Detect service returns UserDetectResponse and Google Safety Net service returns RecaptchaTokenResponse objects for the result of user detect operation. We will return the SafetyServiceResponse object in our SDK.

data class SafetyServiceResponse(
val responseToken: String
)

We should do the same for the root detection feature. We will create our RootDetectionResponse class, which will enable us to parse objects returned from Google or Huawei service by creating mapper classes for root detection.

data class RootDetectionResponse(
val apkDigestSha256: String,
val apkPackageName: String,
val basicIntegrity: Boolean,
val nonce: String,
val timestampMs: Long
)

Next we need to create our mapper classes for Google and Huawei. SafetyNet and Safety Detect services return Json object as response parameter. Here, instead of sending a json object, we will use the parsed version of the json object in our SDK.

class GoogleRootDetectMapper : Mapper<JSONObject,RootDetectionResponse>() {
override fun map(from: JSONObject): RootDetectionResponse = RootDetectionResponse(
apkDigestSha256 = from.getString("apkDigestSha256"),
apkPackageName = from.getString("apkPackageName"),
basicIntegrity = from.getBoolean("basicIntegrity"),
nonce = from.getString("nonce"),
timestampMs = from.getLong("timestampMs")
)
}

class HuaweiRootDetectMapper : Mapper<JSONObject, RootDetectionResponse>(){
override fun map(from: JSONObject): RootDetectionResponse = RootDetectionResponse(
apkDigestSha256 = from.getString("apkDigestSha256"),
apkPackageName = from.getString("apkPackageName"),
basicIntegrity = from.getBoolean("basicIntegrity"),
nonce = from.getString("nonce"),
timestampMs = from.getLong("timestampMs")
)
}

After all these steps, we will now create our SafetyServiceImpl classes, which we will implement our interface and add functionality to our functions. It must be created separately for both Google and Huawei.

class GoogleSafetyServiceImpl(private val context: Context): SafetyService {
private val mapper: Mapper<SafetyNetApi.RecaptchaTokenResponse, SafetyServiceResponse> = GoogleSafetyMapper()
private val rootDetectMapper: Mapper<JSONObject, RootDetectionResponse> = GoogleRootDetectMapper()
override fun userDetect(appKey: String,callback: SafetyService.SafetyServiceCallback<SafetyServiceResponse>){
/**
* App key value is the SITE_API_KEY value in Google Mobile Services.
*/
SafetyNet.getClient(context).verifyWithRecaptcha(appKey)
.addOnSuccessListener(){
val responseToken = it.tokenResult
if(responseToken.isNotEmpty()){
callback.onSuccessUserDetect(mapper.map(it))
}
}.addOnFailureListener(){
callback.onFailUserDetect(it)
}
}
override fun rootDetection(
appKey: String,
callback: SafetyService.SafetyRootDetectionCallback<RootDetectionResponse>
){
val nonce = ByteArray(24)
try {
val random: SecureRandom = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SecureRandom.getInstanceStrong()
} else {
SecureRandom.getInstance("SHA1PRNG")
}
random.nextBytes(nonce)
} catch (e: NoSuchAlgorithmException) {
Log.e(TAG, e.message!!)
}
SafetyNet.getClient(context).attest(nonce, appKey)
.addOnSuccessListener{ result ->
val jwsStr = result.jwsResult
val jwsSplit = jwsStr.split(".").toTypedArray()
val jwsPayloadStr = jwsSplit[1]
val payloadDetail = String(Base64.decode(jwsPayloadStr.toByteArray(StandardCharsets.UTF_8), Base64.URL_SAFE), StandardCharsets.UTF_8)
val jsonObject = JSONObject(payloadDetail)
callback.onSuccessRootDetect(rootDetectMapper.map(jsonObject))
}.addOnFailureListener{ e->
callback.onFailRootDetect(e)
}
}
}

As can be seen in our SafetyServiceImpl class created for the Google side above, functionality has been added to the user detection and root detection methods by implementing the methods we created in the interface. In cases where there is onSuccess() in user detect and root detection processes, we transfer the response returned from the SafetyNet API to our own response class with mapper, thanks to the callbacks we created in our interface. As a result of this process, objects returned from services are transferred to our response class that we created in our SDK.

The important issue here is that the appKey value is different in Google and Huawei services. It corresponds to the SITE_API_KEY value on the Google side. The SITE_API_KEY value needs to be generated by the reCAPTCHA API Console. Thanks to this console, you can prevent risky login attempts in your application, etc. You can track many metrics.

For the Huawei side, we should also create our Huawei Safety ServiceImpl class.

class HuaweiSafetyServiceImpl(private val context: Context): SafetyService {
private val mapper: Mapper<UserDetectResponse, SafetyServiceResponse> = HuaweiSafetyMapper()
private val rootDetectMapper: Mapper<JSONObject, RootDetectionResponse> = HuaweiRootDetectMapper()
val TAG = "CommonMobileServicesSafetySDK"
/**
App key value is the app_id value in Huawei Mobile Services.
*/
override fun userDetect(
appKey: String,
callback: SafetyService.SafetyServiceCallback<SafetyServiceResponse>
){
val client = SafetyDetect.getClient(context)
client.userDetection(appKey).addOnSuccessListener {
val responseToken = it.responseToken
if(responseToken.isNotEmpty()){
callback.onSuccessUserDetect(mapper.map(it))
}
}.addOnFailureListener {
callback.onFailUserDetect(it)
}
}
@SuppressLint("LongLogTag")
override fun rootDetection(
appKey: String,
callback: SafetyService.SafetyRootDetectionCallback<RootDetectionResponse>
) {
val nonce = ByteArray(24)
try {
val random: SecureRandom = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SecureRandom.getInstanceStrong()
} else {
SecureRandom.getInstance("SHA1PRNG")
}
random.nextBytes(nonce)
} catch (e: NoSuchAlgorithmException) {
Log.e(TAG, e.message!!)
}
SafetyDetect.getClient(context)
.sysIntegrity(nonce, appKey)
.addOnSuccessListener { result ->
val jwsStr = result.result
val jwsSplit = jwsStr.split(".").toTypedArray()
val jwsPayloadStr = jwsSplit[1]
val payloadDetail = String(Base64.decode(jwsPayloadStr.toByteArray(StandardCharsets.UTF_8), Base64.URL_SAFE), StandardCharsets.UTF_8)
val jsonObject = JSONObject(payloadDetail)
callback.onSuccessRootDetect(rootDetectMapper.map(jsonObject))
}
.addOnFailureListener { e ->
callback.onFailRootDetect(e)
}
}
}

In the Huawei Safety Detect service, the appKey value corresponds to the appId value.

On the root detection side, the SITE_API_KEY value is created from the Google API Console. In the Huawei Safety service, it also corresponds to the appId value.

After all these steps, we have completed the developments on the SDK side. After implementing the SDK in a different project so that we can test it, you can use it as follows.

private var safetyService = SafetyService.Factory.create(requireContext())
appKey = if(Device.getMobileServiceType(requireContext())== MobileServiceType.GMS){
this.getString(R.string.google_site_api_key)
}
else{
this.getString(R.string.app_id)
}
safetyService?.userDetect(appKey, object : SafetyService.SafetyServiceCallback<SafetyServiceResponse> {
override fun onFailUserDetect(e: Exception) {
Toast.makeText(requireContext(), e.toString(), Toast.LENGTH_SHORT).show()
}
override fun onSuccessUserDetect(result: SafetyServiceResponse?) {
viewModel.signInWithEmail(email, password)
}
})
safetyService?.rootDetection(appKey, object : SafetyService.SafetyRootDetectionCallback<RootDetectionResponse> {
override fun onFailRootDetect(e: Exception) {
Toast.makeText(applicationContext,e.toString(),Toast.LENGTH_SHORT).show()
}
override fun onSuccessRootDetect(result: RootDetectionResponse?) {
if(result!= null){
if(result.basicIntegrity){
showSecurityAlertMessage(getString(R.string.root_device_info),"Info",true)
}
else{
showSecurityAlertMessage(getString(R.string.no_root_device_error),"Security Warning",false)
}
}
}
})

As seen in the example code above, we set our appKey value according to the service availability on the device, thanks to the Device class we created in the SDK. Here, if the device is GMS, we set the API_KEY values that we created from Google reCaptcha and Api Console, and set the appId value to the appKey value if it is HMS. We can easily use the user detect and root detection features by calling the methods we will use in the next step, thanks to the interface we created in our SDK.

You can find screenshots of user detect and root detect features of a different application using the SDK.

Tips and Tricks

During SDK development, all common methods should be handled by interfaces.

App key value is app id for Huawei services, SITE_API_KEY value generated from Google Api Console for Google services.

Conclusion

In this article, I tried to explain how the services used for security in Google and Huawei services can be developed by making them compatible with both GMS and HMS devices and combining them under a single SDK. I hope it was a useful article for everyone. Thank you for taking the time to read.

References

Google Safety Net API

Huawei Safety Detect Service

Original Source