diff options
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt')
| -rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt | 584 |
1 files changed, 0 insertions, 584 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt deleted file mode 100644 index 4b40d8a4..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt +++ /dev/null @@ -1,584 +0,0 @@ -/* - * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -@file:Suppress("Deprecation") - -package com.zeapo.pwdstore.autofill - -import android.accessibilityservice.AccessibilityService -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.provider.Settings -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import android.view.Window -import android.view.WindowManager -import android.view.accessibility.AccessibilityEvent -import android.view.accessibility.AccessibilityNodeInfo -import android.view.accessibility.AccessibilityWindowInfo -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.core.os.bundleOf -import androidx.preference.PreferenceManager -import com.github.ajalt.timberkt.Timber.tag -import com.github.ajalt.timberkt.e -import com.github.ajalt.timberkt.i -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.zeapo.pwdstore.R -import com.zeapo.pwdstore.model.PasswordEntry -import com.zeapo.pwdstore.utils.PasswordRepository -import com.zeapo.pwdstore.utils.PreferenceKeys -import com.zeapo.pwdstore.utils.splitLines -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.io.UnsupportedEncodingException -import java.net.MalformedURLException -import java.net.URL -import java.util.ArrayList -import java.util.Locale -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import me.msfjarvis.openpgpktx.util.OpenPgpApi -import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection -import org.openintents.openpgp.IOpenPgpService2 -import org.openintents.openpgp.OpenPgpError - -class AutofillService : AccessibilityService(), CoroutineScope by CoroutineScope(Dispatchers.Default) { - - private var serviceConnection: OpenPgpServiceConnection? = null - private var settings: SharedPreferences? = null - private var info: AccessibilityNodeInfo? = null // the original source of the event (the edittext field) - private var items: ArrayList<File> = arrayListOf() // password choices - private var lastWhichItem: Int = 0 - private var dialog: AlertDialog? = null - private var window: AccessibilityWindowInfo? = null - private var resultData: Intent? = null // need the intent which contains results from user interaction - private var packageName: CharSequence? = null - private var ignoreActionFocus = false - private var webViewTitle: String? = null - private var webViewURL: String? = null - private var lastPassword: PasswordEntry? = null - private var lastPasswordMaxDate: Long = 0 - - fun setResultData(data: Intent) { - resultData = data - } - - fun setPickedPassword(path: String) { - items.add(File("${PasswordRepository.getRepositoryDirectory()}/$path.gpg")) - bindDecryptAndVerify() - } - - override fun onCreate() { - super.onCreate() - instance = this - } - - override fun onDestroy() { - super.onDestroy() - instance = null - cancel() - } - - override fun onServiceConnected() { - super.onServiceConnected() - serviceConnection = OpenPgpServiceConnection(this@AutofillService, "org.sufficientlysecure.keychain") - serviceConnection!!.bindToService() - settings = PreferenceManager.getDefaultSharedPreferences(this) - } - - override fun onAccessibilityEvent(event: AccessibilityEvent) { - // remove stored password from cache - if (lastPassword != null && System.currentTimeMillis() > lastPasswordMaxDate) { - lastPassword = null - } - - // if returning to the source app from a successful AutofillActivity - if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && - event.packageName != null && event.packageName == packageName && - resultData != null) { - bindDecryptAndVerify() - } - - // look for webView and trigger accessibility events if window changes - // or if page changes in chrome - if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED || (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED && - event.packageName != null && - (event.packageName == "com.android.chrome" || event.packageName == "com.android.browser"))) { - // there is a chance for getRootInActiveWindow() to return null at any time. save it. - try { - val root = rootInActiveWindow - webViewTitle = searchWebView(root) - webViewURL = null - if (webViewTitle != null) { - var nodes = root.findAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar") - if (nodes.isEmpty()) { - nodes = root.findAccessibilityNodeInfosByViewId("com.android.browser:id/url") - } - for (node in nodes) - if (node.text != null) { - try { - webViewURL = URL(node.text.toString()).host - } catch (e: MalformedURLException) { - if (e.toString().contains("Protocol not found")) { - try { - webViewURL = URL("http://" + node.text.toString()).host - } catch (ignored: MalformedURLException) { - } - } - } - } - } - } catch (e: Exception) { - // sadly we were unable to access the data we wanted - return - } - } - - // nothing to do if field is keychain app or system ui - if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED || - event.packageName != null && event.packageName == "org.sufficientlysecure.keychain" || - event.packageName != null && event.packageName == "com.android.systemui") { - dismissDialog() - return - } - - if (!event.isPassword) { - if (lastPassword != null && event.eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED && event.source.isEditable) { - showPasteUsernameDialog(event.source, lastPassword!!) - return - } else { - // nothing to do if not password field focus - dismissDialog() - return - } - } - - if (dialog != null && dialog!!.isShowing) { - // the current dialog must belong to this window; ignore clicks on this password field - // why handle clicks at all then? some cases e.g. Paypal there is no initial focus event - if (event.eventType == AccessibilityEvent.TYPE_VIEW_CLICKED) { - return - } - // if it was not a click, the field was refocused or another field was focused; recreate - dialog!!.dismiss() - dialog = null - } - - // ignore the ACTION_FOCUS from decryptAndVerify otherwise dialog will appear after Fill - if (ignoreActionFocus) { - ignoreActionFocus = false - return - } - - // need to request permission before attempting to draw dialog - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) { - val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, - Uri.parse("package:" + getPackageName())) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - return - } - - // we are now going to attempt to fill, save AccessibilityNodeInfo for later in decryptAndVerify - // (there should be a proper way to do this, although this seems to work 90% of the time) - info = event.source - if (info == null) return - - // save the dialog's corresponding window so we can use getWindows() in dismissDialog - window = info!!.window - - val packageName: String - val appName: String - val isWeb: Boolean - - // Match with the app if a webview was not found or one was found but - // there's no title or url to go by - if (webViewTitle == null || webViewTitle == "" && webViewURL == null) { - if (info!!.packageName == null) return - packageName = info!!.packageName.toString() - - // get the app name and find a corresponding password - val packageManager = packageManager - val applicationInfo: ApplicationInfo? = try { - packageManager.getApplicationInfo(event.packageName.toString(), 0) - } catch (e: PackageManager.NameNotFoundException) { - null - } - - appName = (if (applicationInfo != null) packageManager.getApplicationLabel(applicationInfo) else "").toString() - - isWeb = false - - setAppMatchingPasswords(appName, packageName) - } else { - // now we may have found a title but webViewURL could be null - // we set packagename so that we can find the website setting entry - packageName = setWebMatchingPasswords(webViewTitle!!, webViewURL) - appName = packageName - isWeb = true - } - - // if autofill_always checked, show dialog even if no matches (automatic - // or otherwise) - if (items.isEmpty() && !settings!!.getBoolean(PreferenceKeys.AUTOFILL_ALWAYS, false)) { - return - } - showSelectPasswordDialog(packageName, appName, isWeb) - } - - private fun searchWebView(source: AccessibilityNodeInfo?, depth: Int = 10): String? { - if (source == null || depth == 0) { - return null - } - for (i in 0 until source.childCount) { - val u = source.getChild(i) ?: continue - if (u.className != null && u.className == "android.webkit.WebView") { - return if (u.contentDescription != null) { - u.contentDescription.toString() - } else "" - } - val webView = searchWebView(u, depth - 1) - if (webView != null) { - return webView - } - u.recycle() - } - return null - } - - // dismiss the dialog if the window has changed - private fun dismissDialog() { - val dismiss = !windows.contains(window) - if (dismiss && dialog != null && dialog!!.isShowing) { - dialog!!.dismiss() - dialog = null - } - } - - private fun setWebMatchingPasswords(webViewTitle: String, webViewURL: String?): String { - // Return the URL needed to open the corresponding Settings. - var settingsURL = webViewURL - - // if autofill_default is checked and prefs.getString DNE, 'Automatically match with password'/"first" otherwise "never" - val defValue = if (settings!!.getBoolean(PreferenceKeys.AUTOFILL_DEFAULT, true)) "/first" else "/never" - val prefs: SharedPreferences = getSharedPreferences("autofill_web", Context.MODE_PRIVATE) - var preference: String - - preference = defValue - if (webViewURL != null) { - val webViewUrlLowerCase = webViewURL.toLowerCase(Locale.ROOT) - val prefsMap = prefs.all - for (key in prefsMap.keys) { - // for websites unlike apps there can be blank preference of "" which - // means use default, so ignore it. - val value = prefs.getString(key, null) - val keyLowerCase = key.toLowerCase(Locale.ROOT) - if (value != null && value != "" && - (webViewUrlLowerCase.contains(keyLowerCase) || keyLowerCase.contains(webViewUrlLowerCase))) { - preference = value - settingsURL = key - } - } - } - - when (preference) { - "/first" -> { - if (!PasswordRepository.isInitialized) { - PasswordRepository.initialize() - } - items = searchPasswords(PasswordRepository.getRepositoryDirectory(), webViewTitle) - } - "/never" -> items = ArrayList() - else -> getPreferredPasswords(preference) - } - - return settingsURL!! - } - - private fun setAppMatchingPasswords(appName: String, packageName: String) { - // if autofill_default is checked and prefs.getString DNE, 'Automatically match with password'/"first" otherwise "never" - val defValue = if (settings!!.getBoolean(PreferenceKeys.AUTOFILL_DEFAULT, true)) "/first" else "/never" - val prefs: SharedPreferences = getSharedPreferences("autofill", Context.MODE_PRIVATE) - val preference: String? - - preference = prefs.getString(packageName, defValue) ?: defValue - - when (preference) { - "/first" -> { - if (!PasswordRepository.isInitialized) { - PasswordRepository.initialize() - } - items = searchPasswords(PasswordRepository.getRepositoryDirectory(), appName) - } - "/never" -> items = ArrayList() - else -> getPreferredPasswords(preference) - } - } - - // Put the newline separated list of passwords from the SharedPreferences - // file into the items list. - private fun getPreferredPasswords(preference: String) { - if (!PasswordRepository.isInitialized) { - PasswordRepository.initialize() - } - val preferredPasswords = preference.splitLines() - items = ArrayList() - for (password in preferredPasswords) { - val path = PasswordRepository.getRepositoryDirectory().toString() + "/" + password + ".gpg" - if (File(path).exists()) { - items.add(File(path)) - } - } - } - - private fun searchPasswords(path: File?, appName: String): ArrayList<File> { - val passList = PasswordRepository.getFilesList(path) - - if (passList.size == 0) return ArrayList() - - val items = ArrayList<File>() - - for (file in passList) { - if (file.isFile) { - if (!file.isHidden && appName.toLowerCase(Locale.ROOT).contains(file.name.toLowerCase(Locale.ROOT).replace(".gpg", ""))) { - items.add(file) - } - } else { - if (!file.isHidden) { - items.addAll(searchPasswords(file, appName)) - } - } - } - return items - } - - private fun showPasteUsernameDialog(node: AccessibilityNodeInfo, password: PasswordEntry) { - if (dialog != null) { - dialog!!.dismiss() - dialog = null - } - - val builder = MaterialAlertDialogBuilder(this, R.style.AppTheme_Dialog) - builder.setNegativeButton(R.string.dialog_cancel) { _, _ -> - dialog!!.dismiss() - dialog = null - } - builder.setPositiveButton(R.string.autofill_paste) { _, _ -> - pasteText(node, password.username) - dialog!!.dismiss() - dialog = null - } - builder.setMessage(getString(R.string.autofill_paste_username, password.username)) - - dialog = builder.create() - require(dialog != null) { "Dialog should not be null at this stage" } - dialog!!.window!!.apply { - setDialogType(this) - addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) - clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) - } - dialog!!.show() - } - - private fun showSelectPasswordDialog(packageName: String, appName: String, isWeb: Boolean) { - if (dialog != null) { - dialog!!.dismiss() - dialog = null - } - - val builder = MaterialAlertDialogBuilder(this, R.style.AppTheme_Dialog) - builder.setNegativeButton(R.string.dialog_cancel) { _, _ -> - dialog!!.dismiss() - dialog = null - } - builder.setNeutralButton("Settings") { _, _ -> - // TODO make icon? gear? - // the user will have to return to the app themselves. - val intent = Intent(this@AutofillService, AutofillPreferenceActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) - intent.putExtra("packageName", packageName) - intent.putExtra("appName", appName) - intent.putExtra("isWeb", isWeb) - startActivity(intent) - } - - // populate the dialog items, always with pick + pick and match. Could - // make it optional (or make height a setting for the same effect) - val itemNames = arrayOfNulls<CharSequence>(items.size + 2) - val passwordDirectory = PasswordRepository.getRepositoryDirectory().toString() - val autofillFullPath = settings!!.getBoolean(PreferenceKeys.AUTOFILL_FULL_PATH, false) - for (i in items.indices) { - if (autofillFullPath) { - itemNames[i] = items[i].path.replace(".gpg", "") - .replace("$passwordDirectory/", "") - } else { - itemNames[i] = items[i].name.replace(".gpg", "") - } - } - itemNames[items.size] = getString(R.string.autofill_pick) - itemNames[items.size + 1] = getString(R.string.autofill_pick_and_match) - builder.setItems(itemNames) { _, which -> - lastWhichItem = which - when { - which < items.size -> bindDecryptAndVerify() - which == items.size -> { - val intent = Intent(this@AutofillService, AutofillActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) - intent.putExtra("pick", true) - startActivity(intent) - } - else -> { - lastWhichItem-- // will add one element to items, so lastWhichItem=items.size()+1 - val intent = Intent(this@AutofillService, AutofillActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) - intent.putExtra("pickMatchWith", true) - intent.putExtra("packageName", packageName) - intent.putExtra("isWeb", isWeb) - startActivity(intent) - } - } - } - - dialog = builder.create() - dialog?.window?.apply { - setDialogType(this) - val density = context.resources.displayMetrics.density - addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) - setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) - clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) - // arbitrary non-annoying size - setLayout((340 * density).toInt(), WRAP_CONTENT) - } - dialog?.show() - } - - @Suppress("DEPRECATION") - private fun setDialogType(window: Window) { - window.setType(if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) - WindowManager.LayoutParams.TYPE_SYSTEM_ALERT - else - WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY - ) - } - - override fun onInterrupt() {} - - private fun bindDecryptAndVerify() { - if (serviceConnection!!.service == null) { - // the service was disconnected, need to bind again - // give it a listener and in the callback we will decryptAndVerify - serviceConnection = OpenPgpServiceConnection(this@AutofillService, "org.sufficientlysecure.keychain", OnBoundListener()) - serviceConnection!!.bindToService() - } else { - decryptAndVerify() - } - } - - private fun decryptAndVerify() = launch { - packageName = info!!.packageName - val data: Intent - if (resultData == null) { - data = Intent() - data.action = OpenPgpApi.ACTION_DECRYPT_VERIFY - } else { - data = resultData!! - resultData = null - } - - var inputStream: InputStream? = null - withContext(Dispatchers.IO) { - try { - inputStream = items[lastWhichItem].inputStream() - } catch (e: IOException) { - e.printStackTrace() - cancel("", e) - } - } - - val os = ByteArrayOutputStream() - - val api = OpenPgpApi(this@AutofillService, serviceConnection!!.service!!) - val result = api.executeApi(data, inputStream, os) - when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { - OpenPgpApi.RESULT_CODE_SUCCESS -> { - try { - var entry: PasswordEntry? = null - withContext(Dispatchers.IO) { - entry = PasswordEntry(os) - } - withContext(Dispatchers.Main) { pasteText(info!!, entry?.password) } - // save password entry for pasting the username as well - if (entry?.hasUsername() == true) { - lastPassword = entry - val ttl = Integer.parseInt(settings!!.getString(PreferenceKeys.GENERAL_SHOW_TIME, "45")!!) - withContext(Dispatchers.Main) { Toast.makeText(applicationContext, getString(R.string.autofill_toast_username, ttl), Toast.LENGTH_LONG).show() } - lastPasswordMaxDate = System.currentTimeMillis() + ttl * 1000L - } - } catch (e: UnsupportedEncodingException) { - tag(Constants.TAG).e(e) - } - } - OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { - tag("PgpHandler").i { "RESULT_CODE_USER_INTERACTION_REQUIRED" } - val pi = result.getParcelableExtra<PendingIntent>(OpenPgpApi.RESULT_INTENT) - // need to start a blank activity to call startIntentSenderForResult - val intent = Intent(applicationContext, AutofillActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) - intent.putExtra("pending_intent", pi) - startActivity(intent) - } - OpenPgpApi.RESULT_CODE_ERROR -> { - val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR) - if (error != null) { - withContext(Dispatchers.Main) { Toast.makeText(applicationContext, "Error from OpenKeyChain : ${error.message}", Toast.LENGTH_LONG).show() } - tag(Constants.TAG).e { "onError getErrorId: ${error.errorId}" } - tag(Constants.TAG).e { "onError getMessage: ${error.message}" } - } - } - } - } - - private fun pasteText(node: AccessibilityNodeInfo, text: String?) { - // if the user focused on something else, take focus back - // but this will open another dialog...hack to ignore this - // & need to ensure performAction correct (i.e. what is info now?) - ignoreActionFocus = node.performAction(AccessibilityNodeInfo.ACTION_FOCUS) - val args = bundleOf(Pair(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text)) - node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args) - node.recycle() - } - - internal object Constants { - - const val TAG = "Keychain" - } - - private inner class OnBoundListener : OpenPgpServiceConnection.OnBound { - - override fun onBound(service: IOpenPgpService2) { - decryptAndVerify() - } - - override fun onError(e: Exception) { - e.printStackTrace() - } - } - - companion object { - - var instance: AutofillService? = null - private set - } -} |
