diff options
11 files changed, 221 insertions, 172 deletions
diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 293af246..3f1a5a5d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -141,9 +141,6 @@ dependencies { androidTestImplementation(Dependencies.Testing.kotlin_test_junit) androidTestImplementation(Dependencies.Testing.AndroidX.runner) androidTestImplementation(Dependencies.Testing.AndroidX.rules) - androidTestImplementation(Dependencies.Testing.AndroidX.junit) - androidTestImplementation(Dependencies.Testing.AndroidX.espresso_core) - androidTestImplementation(Dependencies.Testing.AndroidX.espresso_intents) testImplementation(Dependencies.Testing.junit) testImplementation(Dependencies.Testing.kotlin_test_junit) diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt index 7aeec148..62ce8b46 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt @@ -54,7 +54,7 @@ import com.zeapo.pwdstore.utils.BiometricAuthenticator import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.autofillManager -import com.zeapo.pwdstore.utils.getEncryptedPrefs +import com.zeapo.pwdstore.utils.getEncryptedGitPrefs import com.zeapo.pwdstore.utils.getString import com.zeapo.pwdstore.utils.sharedPrefs import java.io.File @@ -81,7 +81,7 @@ class UserPreference : AppCompatActivity() { prefsActivity = requireActivity() as UserPreference val context = requireContext() sharedPreferences = preferenceManager.sharedPreferences - encryptedPreferences = requireActivity().getEncryptedPrefs("git_operation") + encryptedPreferences = requireActivity().getEncryptedGitPrefs() addPreferencesFromResource(R.xml.preference) diff --git a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt index 6a975f6c..b0aed087 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt @@ -21,7 +21,7 @@ import com.zeapo.pwdstore.git.operation.PushOperation import com.zeapo.pwdstore.git.operation.ResetToRemoteOperation import com.zeapo.pwdstore.git.operation.SyncOperation import com.zeapo.pwdstore.utils.PreferenceKeys -import com.zeapo.pwdstore.utils.getEncryptedPrefs +import com.zeapo.pwdstore.utils.getEncryptedGitPrefs import com.zeapo.pwdstore.utils.sharedPrefs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -86,7 +86,7 @@ abstract class BaseGitActivity : AppCompatActivity() { suspend fun promptOnErrorHandler(err: Throwable, onPromptDone: () -> Unit = {}) { val error = rootCauseException(err) if (!isExplicitlyUserInitiatedError(error)) { - getEncryptedPrefs("git_operation").edit { + getEncryptedGitPrefs().edit { remove(PreferenceKeys.HTTPS_PASSWORD) } sharedPrefs.edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt b/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt index f4cbc04f..e43deaf4 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt @@ -10,7 +10,7 @@ import com.github.michaelbull.result.runCatching import com.zeapo.pwdstore.Application import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PreferenceKeys -import com.zeapo.pwdstore.utils.getEncryptedPrefs +import com.zeapo.pwdstore.utils.getEncryptedGitPrefs import com.zeapo.pwdstore.utils.getString import com.zeapo.pwdstore.utils.sharedPrefs import java.io.File @@ -53,7 +53,7 @@ object GitSettings { private const val DEFAULT_BRANCH = "master" private val settings by lazy { Application.instance.sharedPrefs } - private val encryptedSettings by lazy { Application.instance.getEncryptedPrefs("git_operation") } + private val encryptedSettings by lazy { Application.instance.getEncryptedGitPrefs() } var authMode get() = AuthMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH)) diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt index 742361c0..c34af18a 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt @@ -14,7 +14,7 @@ import com.zeapo.pwdstore.R import com.zeapo.pwdstore.git.config.AuthMode import com.zeapo.pwdstore.git.sshj.InteractivePasswordFinder import com.zeapo.pwdstore.utils.PreferenceKeys -import com.zeapo.pwdstore.utils.getEncryptedPrefs +import com.zeapo.pwdstore.utils.getEncryptedGitPrefs import com.zeapo.pwdstore.utils.requestInputFocusOnView import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -25,7 +25,7 @@ class CredentialFinder( ) : InteractivePasswordFinder() { override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) { - val gitOperationPrefs = callingActivity.getEncryptedPrefs("git_operation") + val gitOperationPrefs = callingActivity.getEncryptedGitPrefs() val credentialPref: String @StringRes val messageRes: Int @StringRes val hintRes: Int diff --git a/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshKey.kt b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshKey.kt index ce09f1a9..73072f7d 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshKey.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshKey.kt @@ -23,7 +23,7 @@ import com.github.michaelbull.result.runCatching import com.zeapo.pwdstore.Application import com.zeapo.pwdstore.R import com.zeapo.pwdstore.utils.PreferenceKeys -import com.zeapo.pwdstore.utils.getEncryptedPrefs +import com.zeapo.pwdstore.utils.getEncryptedGitPrefs import com.zeapo.pwdstore.utils.getString import com.zeapo.pwdstore.utils.sharedPrefs import java.io.File @@ -169,7 +169,7 @@ object SshKey { if (publicKeyFile.isFile) { publicKeyFile.delete() } - context.getEncryptedPrefs("git_operation").edit { + context.getEncryptedGitPrefs().edit { remove(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) } type = null diff --git a/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt index c7277455..d2fdab61 100644 --- a/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt @@ -20,7 +20,7 @@ import com.zeapo.pwdstore.R import com.zeapo.pwdstore.databinding.ActivitySshKeygenBinding import com.zeapo.pwdstore.git.sshj.SshKey import com.zeapo.pwdstore.utils.BiometricAuthenticator -import com.zeapo.pwdstore.utils.getEncryptedPrefs +import com.zeapo.pwdstore.utils.getEncryptedGitPrefs import com.zeapo.pwdstore.utils.keyguardManager import com.zeapo.pwdstore.utils.viewBinding import kotlin.coroutines.resume @@ -128,7 +128,7 @@ class SshKeyGenActivity : AppCompatActivity() { keyGenType.generateKey(requireAuthentication) } } - getEncryptedPrefs("git_operation").edit { + getEncryptedGitPrefs().edit { remove("ssh_key_local_passphrase") } binding.generate.apply { diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt new file mode 100644 index 00000000..3ee1820d --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt @@ -0,0 +1,172 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.utils + +import android.app.KeyguardManager +import android.content.ClipboardManager +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.os.Build +import android.util.Base64 +import android.util.TypedValue +import android.view.View +import android.view.autofill.AutofillManager +import android.view.inputmethod.InputMethodManager +import androidx.annotation.IdRes +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import androidx.fragment.app.FragmentActivity +import androidx.preference.PreferenceManager +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.github.ajalt.timberkt.d +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.Result +import com.google.android.material.snackbar.Snackbar +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.git.operation.GitOperation + +/** + * Extension function for [AlertDialog] that requests focus for the + * view whose id is [id]. Solution based on a StackOverflow + * answer: https://stackoverflow.com/a/13056259/297261 + */ +fun <T : View> AlertDialog.requestInputFocusOnView(@IdRes id: Int) { + setOnShowListener { + findViewById<T>(id)?.apply { + setOnFocusChangeListener { v, _ -> + v.post { + context.getSystemService<InputMethodManager>() + ?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT) + } + } + requestFocus() + } + } +} + +/** + * Get an instance of [AutofillManager]. Only + * available on Android Oreo and above + */ +val Context.autofillManager: AutofillManager? + @RequiresApi(Build.VERSION_CODES.O) + get() = getSystemService() + +/** + * Get an instance of [ClipboardManager] + */ +val Context.clipboard + get() = getSystemService<ClipboardManager>() + +/** + * Wrapper for [getEncryptedPrefs] to avoid open-coding the file name at + * each call site + */ +fun Context.getEncryptedGitPrefs() = getEncryptedPrefs("git_operation") + +/** + * Get an instance of [EncryptedSharedPreferences] with the given [fileName] + */ +private fun Context.getEncryptedPrefs(fileName: String): SharedPreferences { + val masterKeyAlias = MasterKey.Builder(applicationContext) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + return EncryptedSharedPreferences.create( + applicationContext, + fileName, + masterKeyAlias, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) +} + +/** + * Get an instance of [KeyguardManager] + */ +val Context.keyguardManager: KeyguardManager + get() = getSystemService()!! + +/** + * Get the default [SharedPreferences] instance + */ +val Context.sharedPrefs: SharedPreferences + get() = PreferenceManager.getDefaultSharedPreferences(applicationContext) + + +/** + * Resolve [attr] from the [Context]'s theme + */ +fun Context.resolveAttribute(attr: Int): Int { + val typedValue = TypedValue() + this.theme.resolveAttribute(attr, typedValue, true) + return typedValue.data +} + +/** + * Commit changes to the store from a [FragmentActivity] using + * a custom implementation of [GitOperation] + */ +suspend fun FragmentActivity.commitChange( + message: String, +): Result<Unit, Throwable> { + if (!PasswordRepository.isGitRepo()) { + return Ok(Unit) + } + return object : GitOperation(this@commitChange) { + override val commands = arrayOf( + // Stage all files + git.add().addFilepattern("."), + // Populate the changed files count + git.status(), + // Commit everything! If anything changed, that is. + git.commit().setAll(true).setMessage(message), + ) + + override fun preExecute(): Boolean { + d { "Committing with message: '$message'" } + return true + } + }.execute() +} + +/** + * Check if [permission] has been granted to the app. + */ +fun FragmentActivity.isPermissionGranted(permission: String): Boolean { + return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED +} + +/** + * Show a [Snackbar] in a [FragmentActivity] and correctly + * anchor it to a [com.google.android.material.floatingactionbutton.FloatingActionButton] + * if one exists in the [view] + */ +fun FragmentActivity.snackbar( + view: View = findViewById(android.R.id.content), + message: String, + length: Int = Snackbar.LENGTH_SHORT, +): Snackbar { + val snackbar = Snackbar.make(view, message, length) + snackbar.anchorView = findViewById(R.id.fab) + snackbar.show() + return snackbar +} + +/** + * Simplifies the common `getString(key, null) ?: defaultValue` case slightly + */ +fun SharedPreferences.getString(key: String): String? = getString(key, null) + +/** + * Convert this [String] to its [Base64] representation + */ +fun String.base64(): String { + return Base64.encodeToString(encodeToByteArray(), Base64.NO_WRAP) +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt index 2a11f56a..10a29cf0 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt @@ -4,74 +4,33 @@ */ package com.zeapo.pwdstore.utils -import android.app.KeyguardManager -import android.content.ClipboardManager -import android.content.Context -import android.content.SharedPreferences -import android.content.pm.PackageManager -import android.os.Build -import android.util.Base64 -import android.util.TypedValue -import android.view.View -import android.view.autofill.AutofillManager -import android.view.inputmethod.InputMethodManager -import androidx.annotation.IdRes -import androidx.annotation.RequiresApi -import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import androidx.core.content.getSystemService -import androidx.fragment.app.FragmentActivity -import androidx.preference.PreferenceManager -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import com.github.ajalt.timberkt.d -import com.github.michaelbull.result.Ok -import com.github.michaelbull.result.Result import com.github.michaelbull.result.getOrElse import com.github.michaelbull.result.runCatching -import com.google.android.material.snackbar.Snackbar -import com.zeapo.pwdstore.R -import com.zeapo.pwdstore.git.operation.GitOperation import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory import java.io.File import java.util.Date import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.revwalk.RevCommit +/** + * The default OpenPGP provider for the app + */ const val OPENPGP_PROVIDER = "org.sufficientlysecure.keychain" +/** + * Clears the given [flag] from the value of this [Int] + */ fun Int.clearFlag(flag: Int): Int { return this and flag.inv() } +/** + * Checks if this [Int] contains the given [flag] + */ infix fun Int.hasFlag(flag: Int): Boolean { return this and flag == flag } -fun String.splitLines(): Array<String> { - return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() -} - -fun String.base64(): String { - return Base64.encodeToString(encodeToByteArray(), Base64.NO_WRAP) -} - -val Context.clipboard - get() = getSystemService<ClipboardManager>() - -fun FragmentActivity.snackbar( - view: View = findViewById(android.R.id.content), - message: String, - length: Int = Snackbar.LENGTH_SHORT, -): Snackbar { - val snackbar = Snackbar.make(view, message, length) - snackbar.anchorView = findViewById(R.id.fab) - snackbar.show() - return snackbar -} - -fun File.listFilesRecursively() = walkTopDown().filter { !it.isDirectory }.toList() - /** * Checks whether this [File] is a directory that contains [other]. */ @@ -89,91 +48,23 @@ fun File.contains(other: File): Boolean { return relativePath.path == other.name } -fun Context.resolveAttribute(attr: Int): Int { - val typedValue = TypedValue() - this.theme.resolveAttribute(attr, typedValue, true) - return typedValue.data -} - -fun Context.getEncryptedPrefs(fileName: String): SharedPreferences { - val masterKeyAlias = MasterKey.Builder(applicationContext) - .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - .build() - return EncryptedSharedPreferences.create( - applicationContext, - fileName, - masterKeyAlias, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) -} - -val Context.sharedPrefs: SharedPreferences - get() = PreferenceManager.getDefaultSharedPreferences(applicationContext) - -fun SharedPreferences.getString(key: String): String? = getString(key, null) - -suspend fun FragmentActivity.commitChange( - message: String, -): Result<Unit, Throwable> { - if (!PasswordRepository.isGitRepo()) { - return Ok(Unit) - } - return object : GitOperation(this@commitChange) { - override val commands = arrayOf( - // Stage all files - git.add().addFilepattern("."), - // Populate the changed files count - git.status(), - // Commit everything! If anything changed, that is. - git.commit().setAll(true).setMessage(message), - ) - - override fun preExecute(): Boolean { - d { "Committing with message: '$message'" } - return true - } - }.execute() -} - -fun FragmentActivity.isPermissionGranted(permission: String): Boolean { - return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED -} - /** - * Extension function for [AlertDialog] that requests focus for the - * view whose id is [id]. Solution based on a StackOverflow - * answer: https://stackoverflow.com/a/13056259/297261 + * Checks if this [File] is in the password repository directory as given + * by [getRepositoryDirectory] */ -fun <T : View> AlertDialog.requestInputFocusOnView(@IdRes id: Int) { - setOnShowListener { - findViewById<T>(id)?.apply { - setOnFocusChangeListener { v, _ -> - v.post { - context.getSystemService<InputMethodManager>() - ?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT) - } - } - requestFocus() - } - } -} - -val Context.autofillManager: AutofillManager? - @RequiresApi(Build.VERSION_CODES.O) - get() = getSystemService() - -val Context.keyguardManager: KeyguardManager - get() = getSystemService()!! - fun File.isInsideRepository(): Boolean { return canonicalPath.contains(getRepositoryDirectory().canonicalPath) } /** + * Recursively lists the files in this [File], skipping any directories it encounters. + */ +fun File.listFilesRecursively() = walkTopDown().filter { !it.isDirectory }.toList() + +/** * Unique SHA-1 hash of this commit as hexadecimal string. * - * @see RevCommit.id + * @see RevCommit.getId */ val RevCommit.hash: String get() = ObjectId.toString(id) @@ -189,3 +80,11 @@ val RevCommit.time: Date val epochMilliseconds = epochSeconds * 1000 return Date(epochMilliseconds) } + +/** + * Splits this [String] into an [Array] of [String]s, split on the UNIX LF line ending + * and stripped of any empty lines. + */ +fun String.splitLines(): Array<String> { + return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/FragmentExtensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/FragmentExtensions.kt index 01103d19..251f5259 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/FragmentExtensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/FragmentExtensions.kt @@ -1,34 +1,29 @@ package com.zeapo.pwdstore.utils -import android.content.pm.PackageManager -import android.view.View import androidx.annotation.IdRes -import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.commit import com.zeapo.pwdstore.R +/** + * Check if [permission] is granted to the app. Aliases to [isPermissionGranted] internally. + */ fun Fragment.isPermissionGranted(permission: String): Boolean { - return ContextCompat.checkSelfPermission(requireActivity(), permission) == PackageManager.PERMISSION_GRANTED + return requireActivity().isPermissionGranted(permission) } +/** + * Calls `finish()` on the enclosing [androidx.fragment.app.FragmentActivity] + */ fun Fragment.finish() = requireActivity().finish() -fun FragmentManager.performTransaction(destinationFragment: Fragment, @IdRes containerViewId: Int = android.R.id.content) { - this.commit { - beginTransaction() - setCustomAnimations( - R.animator.slide_in_left, - R.animator.slide_out_left, - R.animator.slide_in_right, - R.animator.slide_out_right) - replace(containerViewId, destinationFragment) - } -} - +/** + * Perform a [commit] on this [FragmentManager] with custom animations and adding the [destinationFragment] + * to the fragment backstack + */ fun FragmentManager.performTransactionWithBackStack(destinationFragment: Fragment, @IdRes containerViewId: Int = android.R.id.content) { - this.commit { + commit { beginTransaction() addToBackStack(destinationFragment.tag) setCustomAnimations( @@ -39,14 +34,3 @@ fun FragmentManager.performTransactionWithBackStack(destinationFragment: Fragmen replace(containerViewId, destinationFragment) } } - -fun FragmentManager.performSharedElementTransaction(destinationFragment: Fragment, views: List<View>, @IdRes containerViewId: Int = android.R.id.content) { - this.commit { - beginTransaction() - for (view in views) { - addSharedElement(view, view.transitionName) - } - addToBackStack(destinationFragment.tag) - replace(containerViewId, destinationFragment) - } -} diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 52c79bc0..00d1ba8e 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -83,9 +83,6 @@ object Dependencies { const val runner = "androidx.test:runner:1.3.0" const val rules = "androidx.test:rules:1.3.0" - const val junit = "androidx.test.ext:junit:1.1.2" - const val espresso_core = "androidx.test.espresso:espresso-core:3.3.0" - const val espresso_intents = "androidx.test.espresso:espresso-intents:3.3.0" } } } |