Skip to content

Commit

Permalink
Implement OTP Autofill from SMS module
Browse files Browse the repository at this point in the history
  • Loading branch information
AsemLab committed Sep 26, 2024
1 parent ae7e12e commit 2de94ff
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Within each module, you will find detailed TODO comments that guide you through
Currently it contains codebases for:

- [App Shortcuts](https://developer.android.com/develop/ui/views/launch/shortcuts): Shortcuts can be displayed in a supported launcher. They help users quickly start common or recommended tasks within apps.
- [Autofill](https://developers.google.com/identity/sms-retriever/request): Retrieve otp from SMS automatically using Google SMS Retriever API.
- [Broadcast Receiver](https://github.com/AsemLab/Samples/tree/main/broadcast_receiver): Create a custom broadcast receiver.
- [Chuncker](https://github.com/ChuckerTeam/chucker): an HTTP inspector for Android & OkHTTP. Apps using Chucker will display a notification showing a summary of ongoing HTTP activity.
- [Firebase](https://firebase.google.com/):
Expand Down
18 changes: 16 additions & 2 deletions autofill/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ android {

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}


// TODO 1.1 Must be signed with YOUR keystore
signingConfigs {
create("release") {
storeFile = File(project.parent?.projectDir!!.path + "/samples.jks")
storePassword = "12345678"
keyAlias = "sampleskey"
keyPassword = "12345678"
}
}

buildTypes {
release {
Expand All @@ -28,11 +37,13 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("release")
}

debug {
isDebuggable = true
isMinifyEnabled = false
signingConfig = signingConfigs.getByName("release")
}
}
compileOptions {
Expand All @@ -42,7 +53,7 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures{
buildFeatures {
dataBinding = true
}
}
Expand All @@ -58,4 +69,7 @@ dependencies {
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

// TODO 1. Add Play Services SMS dependencies
implementation(libs.play.services.auth)
implementation(libs.play.services.auth.api.phone)
}
11 changes: 11 additions & 0 deletions autofill/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:name=".AutoFillApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
Expand All @@ -19,6 +20,16 @@
</intent-filter>
</activity>

<!-- TODO 4. Add SMSReceiver with IntentFilter -->
<receiver
android:name=".services.SMSReceiver"
android:enabled="true"
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>

</application>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.asemlab.samples.autofill

import android.app.Application
import androidx.lifecycle.MutableLiveData

class AutoFillApp : Application() {

val otp = MutableLiveData<String>()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.asemlab.samples.autofill.services

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import com.asemlab.samples.autofill.AutoFillApp
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.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.
*/
class SMSReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {
Log.d("SMS Receiver", "OnReceive")

if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
val extras = intent.extras
val status = extras?.get(SmsRetriever.EXTRA_STATUS) as Status

when (status.statusCode) {
CommonStatusCodes.SUCCESS -> {
// TODO 5. Get SMS message contents and parse the otp
val message = extras.getString(SmsRetriever.EXTRA_SMS_MESSAGE)
val pattern = (Pattern.compile("Your code is: (\\d{4}).*"))
message?.let {
val matcher = pattern.matcher(it)
if (matcher.find()) {
(context.applicationContext as AutoFillApp).otp.postValue(matcher.group(1))
}
}

Log.d("SMS Receiver", "message: $message")
}

CommonStatusCodes.TIMEOUT -> {
Log.e("SMS Receiver", "Timeout: ${status.statusMessage}")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
package com.asemlab.samples.autofill.ui

import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.databinding.DataBindingUtil
import com.asemlab.samples.autofill.AutoFillApp
import com.asemlab.samples.autofill.R
import com.asemlab.samples.autofill.databinding.ActivityMainBinding
import com.asemlab.samples.autofill.utils.AppSignatureHelper
import com.google.android.gms.auth.api.phone.SmsRetriever


/***
* TODO This is an otp retrieval demo (get otp from SMS automatically)
*
* the SMS should be in form:
* """ Your code is: ####
* THE APP_SIGNATURE HASH """
*
*/
class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding
Expand All @@ -25,5 +37,36 @@ class MainActivity : AppCompatActivity() {
insets
}

// TODO 2. Get hash string for this app to added to otp SMS
val appHash = AppSignatureHelper(this).appSignatures[0]
Log.d("MainActivity", appHash)

// TODO 3. Start listening for SMS
// Get an instance of SmsRetrieverClient, used to start listening for a matching SMS message.
val client = SmsRetriever.getClient(this)

// Starts SmsRetriever, which waits for ONE matching SMS message until timeout
// (5 minutes). The matching SMS message will be sent via a Broadcast Intent with
// action SmsRetriever#SMS_RETRIEVED_ACTION.
val task = client.startSmsRetriever()
task.addOnSuccessListener {
// Successfully started retriever, expect broadcast intent
Toast.makeText(this, "Success", Toast.LENGTH_SHORT).show()
}
task.addOnFailureListener {
// Failed to start retriever, inspect Exception for more details
Toast.makeText(this, "Failed", Toast.LENGTH_SHORT).show()
}


// TODO 6. Insert the otp into the EditText field
(application as AutoFillApp).otp.observe(this) {
binding.smsOtp.text.apply {
clear()
append(it)
}

}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.asemlab.samples.autofill.utils;

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;

/**
* This is a helper class to generate your message hash to be included in your SMS message.
*
* Without the correct hash, your app won't receive the message callback. This only needs to be
* generated once per app and stored. Then you can remove this helper class from your code.
*/
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);
}


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;
}
}
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ android-application = "8.1.0"
kotlin = "2.0.10"
google-shortcuts = "1.0.0"
okhttpBom = "4.10.0"
playServicesAuth = "21.2.0"
playServicesAuthApiPhone = "18.1.0"
robolectric = "4.11.1"
stripeAndroid = "20.42.0"
timber = "5.0.1"
Expand Down Expand Up @@ -83,6 +85,8 @@ mpAndroidChart = { module = "com.github.PhilJay:MPAndroidChart", version.ref = "
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" }
okhttp = { module = "com.squareup.okhttp3:okhttp" }
okhttp-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" }
play-services-auth = { module = "com.google.android.gms:play-services-auth", version.ref = "playServicesAuth" }
play-services-auth-api-phone = { module = "com.google.android.gms:play-services-auth-api-phone", version.ref = "playServicesAuthApiPhone" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
robolectric-annotations = { module = "org.robolectric:annotations", version.ref = "robolectric" }
stripe-android = { module = "com.stripe:stripe-android", version.ref = "stripeAndroid" }
Expand Down

0 comments on commit 2de94ff

Please sign in to comment.