aboutsummaryrefslogtreecommitdiff
path: root/app/src
diff options
context:
space:
mode:
authorFabian Henneke <FabianHenneke@users.noreply.github.com>2020-07-02 13:49:32 +0200
committerGitHub <noreply@github.com>2020-07-02 13:49:32 +0200
commitca9c951a536e9ccd2bf3e8f0e2e0a48992d0d655 (patch)
treebcf32f9bf6178051632baed95d5c70d8355f8e29 /app/src
parentc702d4aa9ea09ae27e613d85440a207b37995e86 (diff)
Fill OTP fields with SMS codes (#900)
* Fill OTP fields with SMS codes * Allow SMS OTP fill also for web origins * Introduce free and nonFree build variants * Fix up workflow * Improve layout and feature detection * Workflow changes * Add Changelog entry * github: update release workflow for nonFree/Free split Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * Switch to lifecycleScope * github: make snapshot deploy free variant Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'app/src')
-rw-r--r--app/src/free/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSmsActivity.kt28
-rw-r--r--app/src/main/AndroidManifest.xml5
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillHelper.kt9
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillScenario.kt7
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Form.kt13
-rw-r--r--app/src/main/res/drawable/ic_autofill_sms.xml10
-rw-r--r--app/src/main/res/layout/activity_oreo_autofill_sms.xml61
-rw-r--r--app/src/main/res/values/strings.xml2
-rw-r--r--app/src/nonFree/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSmsActivity.kt136
9 files changed, 269 insertions, 2 deletions
diff --git a/app/src/free/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSmsActivity.kt b/app/src/free/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSmsActivity.kt
new file mode 100644
index 00000000..f86e5d4c
--- /dev/null
+++ b/app/src/free/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSmsActivity.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.autofill.oreo.ui
+
+import android.content.Context
+import android.content.IntentSender
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import com.zeapo.pwdstore.autofill.oreo.FormOrigin
+
+@RequiresApi(Build.VERSION_CODES.O)
+@Suppress("UNUSED_PARAMETER")
+class AutofillSmsActivity : AppCompatActivity() {
+
+ companion object {
+
+ fun shouldOfferFillFromSms(context: Context): Boolean {
+ return false
+ }
+
+ fun makeFillOtpFromSmsIntentSender(context: Context): IntentSender {
+ throw NotImplementedError("Filling OTPs from SMS requires non-free dependencies")
+ }
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2098abc9..40bcb481 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -139,6 +139,11 @@
android:name=".autofill.oreo.ui.AutofillSaveActivity"
android:theme="@style/NoBackgroundTheme" />
<activity
+ android:name=".autofill.oreo.ui.AutofillSmsActivity"
+ android:configChanges="orientation"
+ android:theme="@style/DialogLikeTheme"
+ android:windowSoftInputMode="adjustNothing" />
+ <activity
android:name=".autofill.oreo.ui.AutofillPublisherChangedActivity"
android:configChanges="orientation|keyboardHidden"
android:theme="@style/DialogLikeTheme"
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillHelper.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillHelper.kt
index 838b7a05..e9b2c630 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillHelper.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillHelper.kt
@@ -87,7 +87,7 @@ val AssistStructure.ViewNode.webOrigin: String?
"$scheme://$domain"
}
-data class Credentials(val username: String?, val password: String, val otp: String?) {
+data class Credentials(val username: String?, val password: String?, val otp: String?) {
companion object {
fun fromStoreEntry(
context: Context,
@@ -141,6 +141,13 @@ fun makeGenerateAndFillRemoteView(context: Context, formOrigin: FormOrigin): Rem
return makeRemoteView(context, title, summary, iconRes)
}
+fun makeFillOtpFromSmsRemoteView(context: Context, formOrigin: FormOrigin): RemoteViews {
+ val title = formOrigin.getPrettyIdentifier(context, untrusted = true)
+ val summary = context.getString(R.string.oreo_autofill_fill_otp_from_sms)
+ val iconRes = R.drawable.ic_autofill_sms
+ return makeRemoteView(context, title, summary, iconRes)
+}
+
fun makePlaceholderRemoteView(context: Context): RemoteViews {
return makeRemoteView(context, "PLACEHOLDER", "PLACEHOLDER", R.mipmap.ic_launcher)
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillScenario.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillScenario.kt
index 8e209a60..ee8e3602 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillScenario.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillScenario.kt
@@ -14,7 +14,7 @@ import androidx.annotation.RequiresApi
import com.github.ajalt.timberkt.e
enum class AutofillAction {
- Match, Search, Generate
+ Match, Search, Generate, FillOtpFromSms
}
/**
@@ -112,8 +112,13 @@ sealed class AutofillScenario<out T : Any> {
AutofillAction.Match -> passwordFieldsToFillOnMatch + listOfNotNull(otp)
AutofillAction.Search -> passwordFieldsToFillOnSearch + listOfNotNull(otp)
AutofillAction.Generate -> passwordFieldsToFillOnGenerate
+ AutofillAction.FillOtpFromSms -> listOfNotNull(otp)
}
return when {
+ action == AutofillAction.FillOtpFromSms -> {
+ // When filling from an SMS, we cannot get any data other than the OTP itself.
+ credentialFieldsToFill
+ }
credentialFieldsToFill.isNotEmpty() -> {
// If the current action would fill into any password field, we also fill into the
// username field if possible.
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Form.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Form.kt
index 210fefab..e4ae1f75 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Form.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Form.kt
@@ -25,6 +25,7 @@ import com.zeapo.pwdstore.autofill.oreo.ui.AutofillDecryptActivity
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillFilterView
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillPublisherChangedActivity
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSaveActivity
+import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSmsActivity
import java.io.File
/**
@@ -285,6 +286,14 @@ class FillableForm private constructor(
return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Generate)
}
+ private fun makeFillOtpFromSmsDataset(context: Context): Dataset? {
+ if (scenario.fieldsToFillOn(AutofillAction.FillOtpFromSms).isEmpty()) return null
+ if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null
+ val remoteView = makeFillOtpFromSmsRemoteView(context, formOrigin)
+ val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context)
+ return makePlaceholderDataset(remoteView, intentSender, AutofillAction.FillOtpFromSms)
+ }
+
private fun makePublisherChangedDataset(
context: Context,
publisherChangedException: AutofillPublisherChangedException
@@ -341,6 +350,10 @@ class FillableForm private constructor(
hasDataset = true
addDataset(it)
}
+ makeFillOtpFromSmsDataset(context)?.let {
+ hasDataset = true
+ addDataset(it)
+ }
if (!hasDataset) return null
makeSaveInfo()?.let { setSaveInfo(it) }
setClientState(clientState)
diff --git a/app/src/main/res/drawable/ic_autofill_sms.xml b/app/src/main/res/drawable/ic_autofill_sms.xml
new file mode 100644
index 00000000..e58c33c4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_autofill_sms.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM9,11L7,11L7,9h2v2zM13,11h-2L11,9h2v2zM17,11h-2L15,9h2v2z" />
+</vector>
diff --git a/app/src/main/res/layout/activity_oreo_autofill_sms.xml b/app/src/main/res/layout/activity_oreo_autofill_sms.xml
new file mode 100644
index 00000000..608727d0
--- /dev/null
+++ b/app/src/main/res/layout/activity_oreo_autofill_sms.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ ~ SPDX-License-Identifier: GPL-3.0-only
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="10dp"
+ tools:context="com.zeapo.pwdstore.autofill.oreo.ui.AutofillFilterView">
+
+ <ImageView
+ android:id="@+id/cover"
+ android:layout_width="0dp"
+ android:layout_height="50dp"
+ android:contentDescription="@string/app_name"
+ android:src="@drawable/ic_launcher_foreground"
+ app:layout_constraintBottom_toTopOf="@id/text"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ android:layout_margin="10dp"
+ app:layout_constraintVertical_bias="0.0" />
+
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/oreo_autofill_waiting_for_sms"
+ android:layout_margin="10dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/cover" />
+
+ <ProgressBar
+ android:id="@+id/progress"
+ style="@style/Widget.MaterialComponents.ProgressIndicator.Circular.Indeterminate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ app:layout_constraintBottom_toTopOf="@id/cancelButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/text" />
+
+ <Button
+ android:id="@+id/cancelButton"
+ style="@style/Widget.MaterialComponents.Button.TextButton"
+ android:layout_margin="10dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/dialog_cancel"
+ android:textColor="?attr/colorSecondary"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/progress" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3023d995..6d06a7a4 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -253,6 +253,8 @@
<string name="oreo_autofill_save_app_not_supported">This app is currently not supported</string>
<string name="oreo_autofill_save_passwords_dont_match">Passwords don\'t match</string>
<string name="oreo_autofill_generate_password">Generate password…</string>
+ <string name="oreo_autofill_fill_otp_from_sms">Extract code from SMS…</string>
+ <string name="oreo_autofill_waiting_for_sms">Waiting for SMS…</string>
<string name="oreo_autofill_max_matches_reached">Maximum number of matches (%1$d) reached; clear matches before adding new ones.</string>
<string name="oreo_autofill_warning_publisher_header">This app\'s publisher has changed since you first associated a Password Store entry with it:</string>
<string name="oreo_autofill_warning_publisher_footer"><b>The currently installed app may be trying to steal your credentials by pretending to be a trusted app.</b>\n\nTry to uninstall and reinstall the app from a trusted source, such as the Play Store, Amazon Appstore, F-Droid, or your phone manufacturer\'s store.</string>
diff --git a/app/src/nonFree/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSmsActivity.kt b/app/src/nonFree/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSmsActivity.kt
new file mode 100644
index 00000000..02394867
--- /dev/null
+++ b/app/src/nonFree/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSmsActivity.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.autofill.oreo.ui
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.IntentSender
+import android.os.Build
+import android.os.Bundle
+import android.view.autofill.AutofillManager
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import com.github.ajalt.timberkt.e
+import com.github.ajalt.timberkt.w
+import com.google.android.gms.auth.api.phone.SmsCodeRetriever
+import com.google.android.gms.auth.api.phone.SmsRetriever
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
+import com.google.android.gms.common.api.ResolvableApiException
+import com.google.android.gms.tasks.Tasks
+import com.zeapo.pwdstore.autofill.oreo.AutofillAction
+import com.zeapo.pwdstore.autofill.oreo.Credentials
+import com.zeapo.pwdstore.autofill.oreo.FillableForm
+import com.zeapo.pwdstore.databinding.ActivityOreoAutofillSmsBinding
+import com.zeapo.pwdstore.utils.viewBinding
+import kotlinx.coroutines.launch
+
+@RequiresApi(Build.VERSION_CODES.O)
+class AutofillSmsActivity : AppCompatActivity() {
+
+ companion object {
+
+ private var fillOtpFromSmsRequestCode = 1
+
+ fun shouldOfferFillFromSms(context: Context): Boolean {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
+ return false
+ val googleApiAvailabilityInstance = GoogleApiAvailability.getInstance()
+ val googleApiStatus = googleApiAvailabilityInstance.isGooglePlayServicesAvailable(context)
+ if (googleApiStatus != ConnectionResult.SUCCESS) {
+ w { "Google Play Services unavailable or not updated: ${googleApiAvailabilityInstance.getErrorString(googleApiStatus)}" }
+ return false
+ }
+ // https://developer.android.com/guide/topics/text/autofill-services#sms-autofill
+ if (googleApiAvailabilityInstance.getApkVersion(context) < 190056000) {
+ w { "Google Play Service 19.0.56 or higher required for SMS OTP Autofill" }
+ return false
+ }
+ return true
+ }
+
+ fun makeFillOtpFromSmsIntentSender(context: Context): IntentSender {
+ val intent = Intent(context, AutofillSmsActivity::class.java)
+ return PendingIntent.getActivity(
+ context,
+ fillOtpFromSmsRequestCode++,
+ intent,
+ PendingIntent.FLAG_CANCEL_CURRENT
+ ).intentSender
+ }
+ }
+
+ private val binding by viewBinding(ActivityOreoAutofillSmsBinding::inflate)
+
+ private lateinit var clientState: Bundle
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(binding.root)
+ setResult(RESULT_CANCELED)
+ binding.cancelButton.setOnClickListener {
+ finish()
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ clientState = intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) ?: run {
+ e { "AutofillSmsActivity started without EXTRA_CLIENT_STATE" }
+ finish()
+ return
+ }
+
+ registerReceiver(smsCodeRetrievedReceiver, IntentFilter(SmsCodeRetriever.SMS_CODE_RETRIEVED_ACTION), SmsRetriever.SEND_PERMISSION, null)
+ lifecycleScope.launch {
+ waitForSms()
+ }
+ }
+
+ // Retry starting the SMS code retriever after a permission request.
+ @Suppress("DEPRECATION")
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (resultCode != Activity.RESULT_OK)
+ return
+ lifecycleScope.launch {
+ waitForSms()
+ }
+ }
+
+ private fun waitForSms() {
+ val smsClient = SmsCodeRetriever.getAutofillClient(this@AutofillSmsActivity)
+ try {
+ Tasks.await(smsClient.startSmsCodeRetriever())
+ } catch (e: ResolvableApiException) {
+ e.startResolutionForResult(this, 1)
+ } catch (e: Exception) {
+ e(e)
+ finish()
+ }
+ }
+
+ private val smsCodeRetrievedReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val smsCode = intent.getStringExtra(SmsCodeRetriever.EXTRA_SMS_CODE)
+ val fillInDataset =
+ FillableForm.makeFillInDataset(
+ this@AutofillSmsActivity,
+ Credentials(null, null, smsCode),
+ clientState,
+ AutofillAction.FillOtpFromSms
+ )
+ setResult(RESULT_OK, Intent().apply {
+ putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset)
+ })
+ finish()
+ }
+ }
+}