Verify OTP Without SMS Permission In Android Using Kotlin

Verify OTP Without SMS Permission In Android Using Kotlin
 

Introduction

 
Google has updated the policy in Google Developer Blog about user privacy and security. As per the policy, we must remove SMS and Call Log permissions from manifest or else our app will be removed from the Google play store. But our app needs SMS permission for automatically authenticating app users. What is the solution?
 
Google offers an API named SMS Retriever API to allow our app to read SMS without SMS permission and we need to follow a set of rules while formatting the verification message. In this article, we will learn how to use SMS Retriever API in Kotlin to read SMS and rules need to be followed. If you are new to Kotlin, read my previous articles to read Kotlin from scratch.
 

Verification Message Format

 
We should follow the below rules while formatting verification message.
  1. A message should have a maximum of 140 bytes length.
  2. Should start with “<#>”.
  3. Followed by OTP - One Time Pass (code/word).
  4. 11 character length hash for the app (it is generated by our app).
Example
  1. <#> Your Example App code is: 123ABC78   
  2. FA+9qCX9VSu 

Coding Part

 
I have detailed the article as in the following steps.
  • Step 1: Creating a New Project with Empty Activity.
  • Step 2: Setting up the Google Auth Libraries.
  • Step 3: Implementation of SMS Retriever API using Kotlin.
Step 1 - Creating a new project with Kotlin
  1. Open Android Studio and select "Create new project".
  2. Name the project as your wish and tick the Kotlin Support checkbox.
  3. Then Select your Activity type (For example, Navigation Drawer Activity, Empty Activity, etc.).

    Verify OTP Without SMS Permission In Android Using Kotlin

  4. Then, click the “Finish” button to create a new project in Android Studio.
Step 2 - Setting up the Google Auth Libraries
 
In this part, we will see how to set up the library for the project.
  1. Then add the following lines in app level build.gradle file to apply Google services to your project.
    1. dependencies {   
    2.    …  
    3.     implementation 'com.google.android.gms:play-services-base:11.6.0'  
    4.     implementation 'com.google.android.gms:play-services-auth-api-phone:11.6.0'  
    5.     …  
    6. }  
  1. Then click “Sync Now” to setup your project.
  2. Now the project is ready and no need to add any permissions in Manifest.
Step 3 - Implementation of SMS Retriever API using Kotlin
  • Create a user interface to display your OTP read through the API. Open or create “activity_main.xml” and paste the following code snippet.
    1. <?xml version="1.0" encoding="utf-8"?>  
    2. <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    3.     xmlns:app="http://schemas.android.com/apk/res-auto"  
    4.     xmlns:tools="http://schemas.android.com/tools"  
    5.     android:layout_width="match_parent"  
    6.     android:layout_height="match_parent"  
    7.     tools:context="com.androidmad.smsretrieverapisample.MainActivity">  
    8.   
    9.     <EditText  
    10.         android:id="@+id/editText"  
    11.         android:layout_width="match_parent"  
    12.         android:layout_height="wrap_content"  
    13.         android:layout_marginLeft="20dp"  
    14.         android:layout_marginRight="20dp"  
    15.         app:layout_constraintBottom_toBottomOf="parent"  
    16.         app:layout_constraintLeft_toLeftOf="parent"  
    17.         app:layout_constraintRight_toRightOf="parent"  
    18.         app:layout_constraintTop_toTopOf="parent" />  
    19.   
    20.     <Button  
    21.         android:onClick="onBtnResendClick"  
    22.         android:id="@+id/btn_restart"  
    23.         android:text="Resend"  
    24.         android:layout_width="0dp"  
    25.         android:layout_height="wrap_content"  
    26.         android:layout_marginLeft="20dp"  
    27.         android:layout_marginRight="20dp"  
    28.         app:layout_constraintBottom_toBottomOf="parent"  
    29.         app:layout_constraintEnd_toEndOf="parent"  
    30.         app:layout_constraintHorizontal_bias="0.0"  
    31.         app:layout_constraintLeft_toLeftOf="parent"  
    32.         app:layout_constraintRight_toRightOf="parent"  
    33.         app:layout_constraintStart_toStartOf="parent"  
    34.         app:layout_constraintTop_toTopOf="@id/editText"  
    35.         app:layout_constraintVertical_bias="0.247" />  
    36.   
    37. </android.support.constraint.ConstraintLayout>  
  • We need to create a helper class to provide the hash value for our app to construct the verification message. Create a Kotlin class file named as “AppSignatureHelper.kt” and paste the following code snippet which is helpful to create an 11 character length hash for our application.
    1. package com.androidmad.smsretrieverapisample  
    2.   
    3. import android.annotation.SuppressLint  
    4. import android.content.Context  
    5. import android.content.ContextWrapper  
    6. import android.content.pm.PackageManager  
    7. import android.os.Build  
    8. import android.support.annotation.RequiresApi  
    9. import android.util.Base64  
    10. import android.util.Log  
    11. import java.nio.charset.StandardCharsets  
    12. import java.security.MessageDigest  
    13. import java.security.NoSuchAlgorithmException  
    14. import java.util.*  
    15.   
    16. class AppSignatureHelper(context: Context) : ContextWrapper(context) {  
    17.   
    18.     /** 
    19.      * Get all the app signatures for the current package 
    20.      * 
    21.      * @return 
    22.      */  
    23.     // Get all package signatures for the current package  
    24.     // For each signature create a compatible hash  
    25.     val appSignatures: ArrayList<String>  
    26.         @SuppressLint("PackageManagerGetSignatures")  
    27.         @RequiresApi(api = Build.VERSION_CODES.KITKAT)  
    28.         get() {  
    29.             val appCodes = ArrayList<String>()  
    30.   
    31.             try {  
    32.                 val packageName = packageName  
    33.                 val packageManager = packageManager  
    34.                 val signatures = packageManager.getPackageInfo(packageName,  
    35.                         PackageManager.GET_SIGNATURES).signatures  
    36.                 signatures  
    37.                         .mapNotNull { hash(packageName, it.toCharsString()) }  
    38.                         .mapTo(appCodes) { String.format("%s", it) }  
    39.             } catch (e: PackageManager.NameNotFoundException) {  
    40.                 Log.v(TAG, "Unable to find package to obtain hash.", e)  
    41.             }  
    42.   
    43.             return appCodes  
    44.         }  
    45.   
    46.     companion object {  
    47.         val TAG = AppSignatureHelper::class.java.simpleName!!  
    48.         private val HASH_TYPE = "SHA-256"  
    49.         private val NUM_HASHED_BYTES = 9  
    50.         private val NUM_BASE64_CHAR = 11  
    51.   
    52.         @RequiresApi(api = Build.VERSION_CODES.KITKAT)  
    53.         private fun hash(packageName: String, signature: String): String? {  
    54.             val appInfo = packageName + " " + signature  
    55.             try {  
    56.                 val messageDigest = MessageDigest.getInstance(HASH_TYPE)  
    57.                 messageDigest.update(appInfo.toByteArray(StandardCharsets.UTF_8))  
    58.                 var hashSignature = messageDigest.digest()  
    59.   
    60.                 // truncated into NUM_HASHED_BYTES  
    61.                 hashSignature = Arrays.copyOfRange(hashSignature, 0, NUM_HASHED_BYTES)  
    62.                 // encode into Base64  
    63.                 var base64Hash = Base64.encodeToString(hashSignature, Base64.NO_PADDING or Base64.NO_WRAP)  
    64.                 base64Hash = base64Hash.substring(0, NUM_BASE64_CHAR)  
    65.   
    66.                 Log.v(TAG + "sms_sample_test", String.format("pkg: %s -- hash: %s", packageName, base64Hash))  
    67.                 return base64Hash  
    68.             } catch (e: NoSuchAlgorithmException) {  
    69.                 Log.v(TAG + "sms_sample_test""hash:NoSuchAlgorithm", e)  
    70.             }  
    71.   
    72.             return null  
    73.         }  
    74.     }  
    75. }  
  • Then, we need to get the hash from the class by creating an object for the helper class and calling the hash creation method which returns the value.
    1. // This code requires one time to get Hash keys do comment and share key  
    2. val appSignature = AppSignatureHelper(this)  
    3. Log.v("AppSignature", appSignature.appSignatures.toString())  
  • Create a Custom BroadCastReceiver class named as “MySMSBroadcastReceiver.kt” and add the following code snippets.
    1. class MySMSBroadcastReceiver : BroadcastReceiver() {  
    2.   
    3.     private var otpReceiver: OTPReceiveListener? = null  
    4.   
    5.     fun initOTPListener(receiver: OTPReceiveListener) {  
    6.         this.otpReceiver = receiver  
    7.     }  
    8.   
    9.     override fun onReceive(context: Context, intent: Intent) {  
    10.         if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {  
    11.             val extras = intent.extras  
    12.             val status = extras!!.get(SmsRetriever.EXTRA_STATUS) as Status  
    13.   
    14.             when (status.statusCode) {  
    15.                 CommonStatusCodes.SUCCESS -> {  
    16.                     // Get SMS message contents  
    17.                     var otp: String = extras.get(SmsRetriever.EXTRA_SMS_MESSAGE) as String  
    18.                     Log.d("OTP_Message", otp)  
    19.                     // Extract one-time code from the message and complete verification  
    20.                     // by sending the code back to your server for SMS authenticity.  
    21.                     // But here we are just passing it to MainActivity  
    22.                     if (otpReceiver != null) {  
    23.                         otp = otp.replace("<#> """).split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0]  
    24.                         otpReceiver!!.onOTPReceived(otp)  
    25.                     }  
    26.                 }  
    27.   
    28.                 CommonStatusCodes.TIMEOUT ->  
    29.                     // Waiting for SMS timed out (5 minutes)  
    30.                     // Handle the error ...  
    31.                     if (otpReceiver != null)  
    32.                         otpReceiver!!.onOTPTimeOut()  
    33.             }  
    34.         }  
    35.     }  
    36.   
    37.     interface OTPReceiveListener {  
    38.   
    39.         fun onOTPReceived(otp: String)  
    40.   
    41.         fun onOTPTimeOut()  
    42.     }  
    43. }  
  • Here, OTPReceiveListener is an interface used to call back the events from broad cast receiver. Then open your Activity and implement OTPReceiverListener.

  • Register the receiver in AndroidManifest.xml with SMS_RETRIEVED action.
    1. <receiver android:name=".MySMSBroadcastReceiver" android:exported="true">  
    2.     <intent-filter>  
    3.         <action android:name="com.google.android.gms.auth.api.phone.SMS_RETRIEVED" />  
    4.     </intent-filter>  
    5. </receiver>  
  • Then, start the SmsRetriever API as shown in below. Also, register your broadcast receiver with SmsRetriever.SMS_RETRIEVED_ACTION using intent filter.
    1. private fun startSMSListener() {  
    2.     try {  
    3.         smsReceiver = MySMSBroadcastReceiver()  
    4.         smsReceiver!!.initOTPListener(this)  
    5.   
    6.         val intentFilter = IntentFilter()  
    7.         intentFilter.addAction(SmsRetriever.SMS_RETRIEVED_ACTION)  
    8.         this.registerReceiver(smsReceiver, intentFilter)  
    9.   
    10.         val client = SmsRetriever.getClient(this)  
    11.   
    12.         val task = client.startSmsRetriever()  
    13.         task.addOnSuccessListener {  
    14.             // API successfully started  
    15.         }  
    16.   
    17.         task.addOnFailureListener {  
    18.             // Fail to start API  
    19.         }  
    20.     } catch (e: Exception) {  
    21.         e.printStackTrace()  
    22.     }  
    23.   
    24. }  

You will receive the OTP in call back methods implemented in your Activity.

  1. override fun onOTPReceived(otp: String) {  
  2.     //showToast("OTP Received: " + otp)  
  3.     editText.setText(otp)  
  4.     if (smsReceiver != null) {  
  5.         LocalBroadcastManager.getInstance(this).unregisterReceiver(smsReceiver)  
  6.     }  
  7. }  
  8.   
  9. override fun onOTPTimeOut() {  
  10.     showToast("OTP Time out")  
  11. }  

Full Code

You can find the full code implementation of the Activity here.
  1. class MainActivity : AppCompatActivity(), OTPReceiveListener {  
  2.   
  3.     private var smsReceiver: MySMSBroadcastReceiver? = null  
  4.   
  5.     override fun onCreate(savedInstanceState: Bundle?) {  
  6.         super.onCreate(savedInstanceState)  
  7.         setContentView(R.layout.activity_main)  
  8.         startSMSListener()  
  9.     }  
  10.   
  11.   
  12.     /** 
  13.      * Starts SmsRetriever, which waits for ONE matching SMS message until timeout 
  14.      * (5 minutes). The matching SMS message will be sent via a Broadcast Intent with 
  15.      * action SmsRetriever#SMS_RETRIEVED_ACTION. 
  16.      */  
  17.     private fun startSMSListener() {  
  18.         try {  
  19.             smsReceiver = MySMSBroadcastReceiver()  
  20.             smsReceiver!!.initOTPListener(this)  
  21.   
  22.             val intentFilter = IntentFilter()  
  23.             intentFilter.addAction(SmsRetriever.SMS_RETRIEVED_ACTION)  
  24.             this.registerReceiver(smsReceiver, intentFilter)  
  25.   
  26.             val client = SmsRetriever.getClient(this)  
  27.   
  28.             val task = client.startSmsRetriever()  
  29.             task.addOnSuccessListener {  
  30.                 // API successfully started  
  31.             }  
  32.   
  33.             task.addOnFailureListener {  
  34.                 // Fail to start API  
  35.             }  
  36.         } catch (e: Exception) {  
  37.             e.printStackTrace()  
  38.         }  
  39.   
  40.     }  
  41.   
  42.     fun onBtnResendClick(view: View){  
  43.         startSMSListener()  
  44.     }  
  45.   
  46.     override fun onOTPReceived(otp: String) {  
  47.         //showToast("OTP Received: " + otp)  
  48.         editText.setText(otp)  
  49.         if (smsReceiver != null) {  
  50.             LocalBroadcastManager.getInstance(this).unregisterReceiver(smsReceiver)  
  51.         }  
  52.     }  
  53.   
  54.     override fun onOTPTimeOut() {  
  55.         showToast("OTP Time out")  
  56.     }  
  57.   
  58.   
  59.     override fun onDestroy() {  
  60.         super.onDestroy()  
  61.         if (smsReceiver != null) {  
  62.             LocalBroadcastManager.getInstance(this).unregisterReceiver(smsReceiver)  
  63.         }  
  64.     }  
  65.     private fun showToast(msg: String) {  
  66.         Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()  
  67.     }  
  68. }  
Reference
 
SMS Retriever API for Android
https://developers.google.com/identity/sms-retriever/overview
Google Announcement on Security Policy
https://android-developers.googleblog.com/2019/01/reminder-smscall-log-policy-changes.html
 
If you have any doubts or need any help, contact me.
 
Download Code
 
You can download the full source code of the article in GitHub. If you like this article, do star the repo on GitHub.