aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt270
1 files changed, 270 insertions, 0 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt
new file mode 100644
index 00000000..5206a15f
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt
@@ -0,0 +1,270 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package com.zeapo.pwdstore.crypto
+
+import android.app.PendingIntent
+import android.content.ClipData
+import android.content.Intent
+import android.content.IntentSender
+import android.content.SharedPreferences
+import android.os.Build
+import android.os.Bundle
+import android.text.format.DateUtils
+import android.view.WindowManager
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.annotation.CallSuper
+import androidx.appcompat.app.AppCompatActivity
+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.snackbar.Snackbar
+import com.zeapo.pwdstore.ClipboardService
+import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.UserPreference
+import com.zeapo.pwdstore.utils.clipboard
+import com.zeapo.pwdstore.utils.snackbar
+import me.msfjarvis.openpgpktx.util.OpenPgpApi
+import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
+import org.openintents.openpgp.IOpenPgpService2
+import org.openintents.openpgp.OpenPgpError
+import java.io.File
+
+@Suppress("Registered")
+open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
+
+ /**
+ * Full path to the repository
+ */
+ val repoPath: String by lazy { intent.getStringExtra("REPO_PATH") }
+
+ /**
+ * Full path to the password file being worked on
+ */
+ val fullPath: String by lazy { intent.getStringExtra("FILE_PATH") }
+
+ /**
+ * Name of the password file
+ *
+ * Converts personal/auth.foo.org/john_doe@example.org.gpg to john_doe.example.org
+ */
+ val name: String by lazy { File(fullPath).nameWithoutExtension }
+
+ /**
+ * Get the timestamp for when this file was last modified.
+ */
+ val lastChangedString: CharSequence by lazy {
+ getLastChangedString(
+ intent.getLongExtra(
+ "LAST_CHANGED_TIMESTAMP",
+ -1L
+ )
+ )
+ }
+
+ /**
+ * [SharedPreferences] instance used by subclasses to persist settings
+ */
+ val settings: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
+
+ /**
+ * Read-only field for getting the list of OpenPGP key IDs that we have access to.
+ */
+ var keyIDs = emptySet<String>()
+ private set
+
+ /**
+ * Handle to the [OpenPgpApi] instance that is used by subclasses to interface with OpenKeychain.
+ */
+ private var serviceConnection: OpenPgpServiceConnection? = null
+ var api: OpenPgpApi? = null
+
+ /**
+ * [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots
+ * or recent apps screen and fills in [keyIDs] from [settings]
+ */
+ @CallSuper
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
+ tag(TAG)
+
+ keyIDs = settings.getStringSet("openpgp_key_ids_set", null) ?: emptySet()
+ }
+
+ /**
+ * [onDestroy] handles unbinding from the OpenPgp service linked with [serviceConnection]. This
+ * is annotated with [CallSuper] because it's critical to unbind the service to ensure we're not
+ * leaking things.
+ */
+ @CallSuper
+ override fun onDestroy() {
+ super.onDestroy()
+ serviceConnection?.unbindFromService()
+ }
+
+ /**
+ * Sets up [api] once the service is bound. Downstream consumers must call super this to
+ * initialize [api]
+ */
+ @CallSuper
+ override fun onBound(service: IOpenPgpService2) {
+ api = OpenPgpApi(this, service)
+ }
+
+ /**
+ * Mandatory error handling from [OpenPgpServiceConnection.OnBound]. All subclasses must handle
+ * their own errors, and hence this class simply logs and rethrows. Subclasses Must NOT call super.
+ */
+ override fun onError(e: Exception) {
+ e(e) { "Callers must handle their own exceptions" }
+ throw e
+ }
+
+ /**
+ * Method for subclasses to initiate binding with [OpenPgpServiceConnection]. The design choices
+ * here are a bit dubious at first glance. We require passing a [ActivityResultLauncher] because
+ * it lets us react to having a OpenPgp provider selected without relying on the now deprecated
+ * [startActivityForResult].
+ */
+ fun bindToOpenKeychain(onBoundListener: OpenPgpServiceConnection.OnBound, activityResult: ActivityResultLauncher<Intent>) {
+ val providerPackageName = settings.getString("openpgp_provider_list", "")
+ if (providerPackageName.isNullOrEmpty()) {
+ Toast.makeText(this, resources.getString(R.string.provider_toast_text), Toast.LENGTH_LONG).show()
+ activityResult.launch(Intent(this, UserPreference::class.java))
+ } else {
+ serviceConnection = OpenPgpServiceConnection(this, providerPackageName, onBoundListener)
+ serviceConnection?.bindToService()
+ }
+ }
+
+ /**
+ * Handle the case where OpenKeychain returns that it needs to interact with the user
+ *
+ * @param result The intent returned by OpenKeychain
+ */
+ fun getUserInteractionRequestIntent(result: Intent): IntentSender {
+ i { "RESULT_CODE_USER_INTERACTION_REQUIRED" }
+ return (result.getParcelableExtra(OpenPgpApi.RESULT_INTENT) as PendingIntent).intentSender
+ }
+
+ /**
+ * Gets a relative string describing when this shape was last changed
+ * (e.g. "one hour ago")
+ */
+ private fun getLastChangedString(timeStamp: Long): CharSequence {
+ if (timeStamp < 0) {
+ throw RuntimeException()
+ }
+
+ return DateUtils.getRelativeTimeSpanString(this, timeStamp, true)
+ }
+ /**
+ * Base handling of OpenKeychain errors based on the error contained in [result]. Subclasses
+ * can use this when they want to default to sane error handling.
+ */
+ fun handleError(result: Intent) {
+ val error: OpenPgpError? = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)
+ if (error != null) {
+ when (error.errorId) {
+ OpenPgpError.NO_OR_WRONG_PASSPHRASE -> {
+ snackbar(message = getString(R.string.openpgp_error_wrong_passphrase))
+ }
+ OpenPgpError.NO_USER_IDS -> {
+ snackbar(message = getString(R.string.openpgp_error_no_user_ids))
+ }
+ else -> {
+ snackbar(message = getString(R.string.openpgp_error_unknown, error.message))
+ e { "onError getErrorId: ${error.errorId}" }
+ e { "onError getMessage: ${error.message}" }
+ }
+ }
+ }
+ }
+
+ /**
+ * Copies provided [text] to the clipboard. Shows a [Snackbar] which can be disabled by passing
+ * [showSnackbar] as false.
+ */
+ fun copyTextToClipboard(text: String?, showSnackbar: Boolean = true) {
+ val clipboard = clipboard ?: return
+ val clip = ClipData.newPlainText("pgp_handler_result_pm", text)
+ clipboard.setPrimaryClip(clip)
+ if (showSnackbar) {
+ snackbar(message = resources.getString(R.string.clipboard_copied_text))
+ }
+ }
+
+ /**
+ * Copies a provided [password] string to the clipboard. This wraps [copyTextToClipboard] to
+ * hide the default [Snackbar] and starts off an instance of [ClipboardService] to provide a
+ * way of clearing the clipboard.
+ */
+ fun copyPasswordToClipboard(password: String?) {
+ copyTextToClipboard(password, showSnackbar = false)
+
+ var clearAfter = 45
+ try {
+ clearAfter = (settings.getString("general_show_time", "45") ?: "45").toInt()
+ } catch (_: NumberFormatException) {
+ }
+
+ if (clearAfter != 0) {
+ val service = Intent(this, ClipboardService::class.java).apply {
+ action = ClipboardService.ACTION_START
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(service)
+ } else {
+ startService(service)
+ }
+ snackbar(message = resources.getString(R.string.clipboard_password_toast_text, clearAfter))
+ } else {
+ snackbar(message = resources.getString(R.string.clipboard_password_no_clear_toast_text))
+ }
+ }
+
+ companion object {
+ private const val TAG = "APS/BasePgpActivity"
+ const val KEY_PWGEN_TYPE_CLASSIC = "classic"
+ const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd"
+
+ /**
+ * Gets the relative path to the repository
+ */
+ fun getRelativePath(fullPath: String, repositoryPath: String): String =
+ fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/")
+
+ /**
+ * Gets the Parent path, relative to the repository
+ */
+ fun getParentPath(fullPath: String, repositoryPath: String): String {
+ val relativePath = getRelativePath(fullPath, repositoryPath)
+ val index = relativePath.lastIndexOf("/")
+ return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/".replace("/+".toRegex(), "/")
+ }
+
+ /**
+ * /path/to/store/social/facebook.gpg -> social/facebook
+ */
+ @JvmStatic
+ fun getLongName(fullPath: String, repositoryPath: String, basename: String): String {
+ var relativePath = getRelativePath(fullPath, repositoryPath)
+ return if (relativePath.isNotEmpty() && relativePath != "/") {
+ // remove preceding '/'
+ relativePath = relativePath.substring(1)
+ if (relativePath.endsWith('/')) {
+ relativePath + basename
+ } else {
+ "$relativePath/$basename"
+ }
+ } else {
+ basename
+ }
+ }
+ }
+}