Automatic OTP/SMS Detection Integration Android

Shubham Agrawal
4 min readJan 13, 2021
Auto-Detect OTP messages

Privacy concerns have always been an issue for Android users. Before Android 6 Marshmallow devices, accessing almost anything was possible from user's devices which makes users privacy violated and vulnerable. But since Android released request application permission before actually using it made it secure in some manners.

Two of the major dangerous level permissions i.e., SMS and Location permissions are most of the privacy concerning permissions. And now it's almost impossible for new and old apps having these permissions to be published without having a very strong use-case.

SMS / OTP detection requires SMS permissions from user. And its a common use case for apps to auto-detect OTP while logging in through mobile verification. But since READ_SMS and RCEIVE_SMS are not so much easier to get published on the play store, Google launched SMS Verification APIs which does the task in-place.

So now let's dive into the coding.

STEP 1: Add the dependencies inside build.gradle.

implementation 'com.google.android.gms:play-services-auth:17.0.0'
implementation 'com.google.android.gms:play-services-auth-api-phone:17.4.0'

STEP 2: Start the SMS Retriever

private void startSMSRetrieverClient() {
SmsRetrieverClient client = SmsRetriever.getClient(this);
Task<Void> task = client.startSmsRetriever();
task.addOnSuccessListener(aVoid -> {
// Successfully started retriever, expect broadcast intent
// ...
});
task.addOnFailureListener(e -> {
// Failed to start retriever, inspect Exception for more details
// ...
});
}

STEP 3: Send the phone number to your server to trigger the OTP message. Below given the format of the message. At the end of the message, the hash key string has to be there.

Your ExampleApp code is: 123ABC78FA+9qCX9VSu

Note: See STEP 8 on how to generate the hash keys.

STEP 4: Receive the verification message using a Broadcast Receiver.

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

import com.google.android.gms.auth.api.phone.SmsRetriever;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.common.api.Status;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* BroadcastReceiver to wait for SMS messages. This can be registered either
* in the AndroidManifest or at runtime. Should filter Intents on
* SmsRetriever.SMS_RETRIEVED_ACTION.
*/
public class MySMSBroadcastReceiver extends BroadcastReceiver {

private OTPReceiveListener otpReceiveListener;

public MySMSBroadcastReceiver() {
}

public void init(OTPReceiveListener otpReceiveListener) {
this.otpReceiveListener = otpReceiveListener;
}

@Override
public void onReceive(Context context, Intent intent) {
if (SmsRetriever.SMS_RETRIEVED_ACTION.equals(intent.getAction())) {
Bundle extras = intent.getExtras();
if (extras != null) {
Status status = (Status) extras.get(SmsRetriever.EXTRA_STATUS);
if (status != null)
switch (status.getStatusCode()) {
case CommonStatusCodes.SUCCESS:
// Get SMS message contents
String message = (String) extras.get(SmsRetriever.EXTRA_SMS_MESSAGE);
if (message != null) {
Pattern pattern = Pattern.compile("(\\d{4})");
// \d is for a digit
// {} is the number of digits here 4.
Matcher matcher = pattern.matcher(message);
String val = "";
if (matcher.find()) {
val = matcher.group(0); // 4 digit number
if (this.otpReceiveListener != null)
this.otpReceiveListener.onOTPReceived(val);
} else {
if (this.otpReceiveListener != null)
this.otpReceiveListener.onOTPReceived(null);
}
}
break;
case CommonStatusCodes.TIMEOUT:
if (this.otpReceiveListener != null)
this.otpReceiveListener.onOTPTimeOut();
break;
}
}
}
}

interface OTPReceiveListener {
void onOTPReceived(String otp);

void onOTPTimeOut();
}
}

STEP 5: Register the receiver inside Android Manifest.

<receiver android:name=".MySMSBroadcastReceiver" android:exported="true"
android:permission="com.google.android.gms.auth.api.phone.permission.SEND"
>
<intent-filter>
<action android:name="com.google.android.gms.auth.api.phone.SMS_RETRIEVED"/>
</intent-filter>
</receiver>

STEP 6: Or Register using Context.registerReceiver inside your Activity/Fragment inside onCreate().

@Override
public void onCreate() {

startSMSRetrieverClient(); // Already implemented above.
MySMSBroadcastReceiver mySMSBroadcastReceiver = new MySMSBroadcastReceiver();
activity.registerReceiver(mySMSBroadcastReceiver, new IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION));
mySMSBroadcastReceiver.init(new MySMSBroadcastReceiver.OTPReceiveListener() {
@Override
public void onOTPReceived(String otp) {
// OTP Received
}

@Override
public void onOTPTimeOut() {

}
});
}

STEP 7: Unregister the receiver inside onDestroy() if registered inside onCreate().

@Override
public void onDestroy() {
super.onDestroy();
if (mySMSBroadcastReceiver != null)
activity.unregisterReceiver(mySMSBroadcastReceiver);
}

STEP 8: The most important part now to get the app signature so that the app auto-verify the SMS message received. I am using AppSignatureHelper class to generate hash secret keys for different build variants. (release, debug, and play store release version).

import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.util.Base64;
import android.util.Log;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;

public class AppSignatureHelper extends ContextWrapper {
public static final String TAG = AppSignatureHelper.class.getSimpleName();

private static final String HASH_TYPE = "SHA-256";
public static final int NUM_HASHED_BYTES = 9;
public static final int NUM_BASE64_CHAR = 11;

public AppSignatureHelper(Context context) {
super(context);
}

/**
* Get all the app signatures for the current package
*
*
@return
*/
public ArrayList<String> getAppSignatures() {
ArrayList<String> appCodes = new ArrayList<>();

try {
// Get all package signatures for the current package
String packageName = getPackageName();
PackageManager packageManager = getPackageManager();
Signature[] signatures = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures;

// For each signature create a compatible hash
for (Signature signature : signatures) {
String hash = hash(packageName, signature.toCharsString());
if (hash != null) {
appCodes.add(String.format("%s", hash));
}
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Unable to find package to obtain hash.", e);
}
return appCodes;
}

private static String hash(String packageName, String signature) {
String appInfo = packageName + " " + signature;
try {
MessageDigest messageDigest = MessageDigest.getInstance(HASH_TYPE);
messageDigest.update(appInfo.getBytes(StandardCharsets.UTF_8));
byte[] hashSignature = messageDigest.digest();

// truncated into NUM_HASHED_BYTES
hashSignature = Arrays.copyOfRange(hashSignature, 0, NUM_HASHED_BYTES);
// encode into Base64
String base64Hash = Base64.encodeToString(hashSignature, Base64.NO_PADDING | Base64.NO_WRAP);
base64Hash = base64Hash.substring(0, NUM_BASE64_CHAR);

Log.d(TAG, String.format("pkg: %s -- hash: %s", packageName, base64Hash));
return base64Hash;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "hash:NoSuchAlgorithm", e);
}
return null;
}
}

STEP 9: Get the app signature from anywhere inside the and put it as a toast and capture it. Be sure to remove this Helper class after you got the code.

AppSignatureHelper appSignatureHelper = new AppSignatureHelper(activity);
appSignatureHelper.getAppSignatures(); // This will give you the key.

Alternatively, you can compute your app hash string using the key tool from this reference.

That’s it. Happy Coding 😁

--

--