Remove OpenKeychain code and leave TODOs for missing functionality
This commit is contained in:
parent
a6bcdd1d9d
commit
bcf33e90a5
39 changed files with 61 additions and 1978 deletions
|
@ -48,7 +48,6 @@ dependencies {
|
||||||
implementation(projects.coroutineUtils)
|
implementation(projects.coroutineUtils)
|
||||||
implementation(projects.cryptoPgpainless)
|
implementation(projects.cryptoPgpainless)
|
||||||
implementation(projects.formatCommon)
|
implementation(projects.formatCommon)
|
||||||
implementation(projects.openpgpKtx)
|
|
||||||
implementation(projects.passgen.diceware)
|
implementation(projects.passgen.diceware)
|
||||||
implementation(projects.passgen.random)
|
implementation(projects.passgen.random)
|
||||||
implementation(projects.uiCompose)
|
implementation(projects.uiCompose)
|
||||||
|
@ -85,7 +84,6 @@ dependencies {
|
||||||
implementation(libs.thirdparty.logcat)
|
implementation(libs.thirdparty.logcat)
|
||||||
implementation(libs.thirdparty.modernAndroidPrefs)
|
implementation(libs.thirdparty.modernAndroidPrefs)
|
||||||
implementation(libs.thirdparty.plumber)
|
implementation(libs.thirdparty.plumber)
|
||||||
implementation(libs.thirdparty.sshauth)
|
|
||||||
implementation(libs.thirdparty.sshj) { exclude(group = "org.bouncycastle") }
|
implementation(libs.thirdparty.sshj) { exclude(group = "org.bouncycastle") }
|
||||||
implementation(libs.thirdparty.bouncycastle.bcprov)
|
implementation(libs.thirdparty.bouncycastle.bcprov)
|
||||||
implementation(libs.thirdparty.bouncycastle.bcpkix)
|
implementation(libs.thirdparty.bouncycastle.bcpkix)
|
||||||
|
|
|
@ -95,28 +95,12 @@
|
||||||
android:label="@string/action_settings"
|
android:label="@string/action_settings"
|
||||||
android:parentActivityName=".ui.passwords.PasswordStore" />
|
android:parentActivityName=".ui.passwords.PasswordStore" />
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".ui.crypto.PasswordCreationActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="@string/new_password_title"
|
|
||||||
android:windowSoftInputMode="adjustResize" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.crypto.PasswordCreationActivityV2"
|
android:name=".ui.crypto.PasswordCreationActivityV2"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/new_password_title"
|
android:label="@string/new_password_title"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".ui.crypto.DecryptActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:windowSoftInputMode="adjustResize" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".ui.crypto.GetKeyIdsActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:theme="@style/NoBackgroundThemeM3" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".util.services.ClipboardService"
|
android:name=".util.services.ClipboardService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
@ -150,10 +134,6 @@
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/pref_ssh_keygen_title"
|
android:label="@string/pref_ssh_keygen_title"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
|
||||||
android:name=".ui.autofill.AutofillDecryptActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:theme="@style/NoBackgroundThemeM3" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.autofill.AutofillDecryptActivityV2"
|
android:name=".ui.autofill.AutofillDecryptActivityV2"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|
|
@ -1,269 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
*/
|
|
||||||
package app.passwordstore.ui.autofill
|
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentSender
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.autofill.AutofillManager
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.IntentSenderRequest
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import app.passwordstore.data.passfile.PasswordEntry
|
|
||||||
import app.passwordstore.util.autofill.AutofillPreferences
|
|
||||||
import app.passwordstore.util.autofill.AutofillResponseBuilder
|
|
||||||
import app.passwordstore.util.autofill.DirectoryStructure
|
|
||||||
import app.passwordstore.util.extensions.OPENPGP_PROVIDER
|
|
||||||
import app.passwordstore.util.extensions.asLog
|
|
||||||
import com.github.androidpasswordstore.autofillparser.AutofillAction
|
|
||||||
import com.github.androidpasswordstore.autofillparser.Credentials
|
|
||||||
import com.github.michaelbull.result.getOrElse
|
|
||||||
import com.github.michaelbull.result.onFailure
|
|
||||||
import com.github.michaelbull.result.onSuccess
|
|
||||||
import com.github.michaelbull.result.runCatching
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import logcat.LogPriority.ERROR
|
|
||||||
import logcat.logcat
|
|
||||||
import me.msfjarvis.openpgpktx.util.OpenPgpApi
|
|
||||||
import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
|
|
||||||
import org.openintents.openpgp.IOpenPgpService2
|
|
||||||
import org.openintents.openpgp.OpenPgpError
|
|
||||||
|
|
||||||
@RequiresApi(26)
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class AutofillDecryptActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val EXTRA_FILE_PATH = "app.passwordstore.autofill.oreo.EXTRA_FILE_PATH"
|
|
||||||
private const val EXTRA_SEARCH_ACTION = "app.passwordstore.autofill.oreo.EXTRA_SEARCH_ACTION"
|
|
||||||
|
|
||||||
private var decryptFileRequestCode = 1
|
|
||||||
|
|
||||||
fun makeDecryptFileIntent(file: File, forwardedExtras: Bundle, context: Context): Intent {
|
|
||||||
return Intent(context, AutofillDecryptActivity::class.java).apply {
|
|
||||||
putExtras(forwardedExtras)
|
|
||||||
putExtra(EXTRA_SEARCH_ACTION, true)
|
|
||||||
putExtra(EXTRA_FILE_PATH, file.absolutePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun makeDecryptFileIntentSender(file: File, context: Context): IntentSender {
|
|
||||||
val intent =
|
|
||||||
Intent(context, AutofillDecryptActivity::class.java).apply {
|
|
||||||
putExtra(EXTRA_SEARCH_ACTION, false)
|
|
||||||
putExtra(EXTRA_FILE_PATH, file.absolutePath)
|
|
||||||
}
|
|
||||||
return PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
decryptFileRequestCode++,
|
|
||||||
intent,
|
|
||||||
if (Build.VERSION.SDK_INT >= 31) {
|
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
||||||
} else {
|
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.intentSender
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory
|
|
||||||
|
|
||||||
private val decryptInteractionRequiredAction =
|
|
||||||
registerForActivityResult(StartIntentSenderForResult()) { result ->
|
|
||||||
if (continueAfterUserInteraction != null) {
|
|
||||||
val data = result.data
|
|
||||||
if (result.resultCode == RESULT_OK && data != null) {
|
|
||||||
continueAfterUserInteraction?.resume(data)
|
|
||||||
} else {
|
|
||||||
continueAfterUserInteraction?.resumeWithException(
|
|
||||||
Exception("OpenPgpApi ACTION_DECRYPT_VERIFY failed to continue after user interaction")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
continueAfterUserInteraction = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var continueAfterUserInteraction: Continuation<Intent>? = null
|
|
||||||
private lateinit var directoryStructure: DirectoryStructure
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
val filePath =
|
|
||||||
intent?.getStringExtra(EXTRA_FILE_PATH)
|
|
||||||
?: run {
|
|
||||||
logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_FILE_PATH" }
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val clientState =
|
|
||||||
intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE)
|
|
||||||
?: run {
|
|
||||||
logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_CLIENT_STATE" }
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val isSearchAction = intent?.getBooleanExtra(EXTRA_SEARCH_ACTION, true)!!
|
|
||||||
val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match
|
|
||||||
directoryStructure = AutofillPreferences.directoryStructure(this)
|
|
||||||
logcat { action.toString() }
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val credentials = decryptCredential(File(filePath))
|
|
||||||
if (credentials == null) {
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
} else {
|
|
||||||
val fillInDataset =
|
|
||||||
AutofillResponseBuilder.makeFillInDataset(
|
|
||||||
this@AutofillDecryptActivity,
|
|
||||||
credentials,
|
|
||||||
clientState,
|
|
||||||
action
|
|
||||||
)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
setResult(
|
|
||||||
RESULT_OK,
|
|
||||||
Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) { finish() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun executeOpenPgpApi(
|
|
||||||
data: Intent,
|
|
||||||
input: InputStream,
|
|
||||||
output: OutputStream
|
|
||||||
): Intent {
|
|
||||||
var openPgpServiceConnection: OpenPgpServiceConnection? = null
|
|
||||||
val openPgpService =
|
|
||||||
suspendCoroutine<IOpenPgpService2> { cont ->
|
|
||||||
openPgpServiceConnection =
|
|
||||||
OpenPgpServiceConnection(
|
|
||||||
this,
|
|
||||||
OPENPGP_PROVIDER,
|
|
||||||
object : OpenPgpServiceConnection.OnBound {
|
|
||||||
override fun onBound(service: IOpenPgpService2) {
|
|
||||||
cont.resume(service)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Exception) {
|
|
||||||
cont.resumeWithException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.also { it.bindToService() }
|
|
||||||
}
|
|
||||||
return OpenPgpApi(this, openPgpService).executeApi(data, input, output).also {
|
|
||||||
openPgpServiceConnection?.unbindFromService()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun decryptCredential(file: File, resumeIntent: Intent? = null): Credentials? {
|
|
||||||
val command = resumeIntent ?: Intent().apply { action = OpenPgpApi.ACTION_DECRYPT_VERIFY }
|
|
||||||
runCatching { file.inputStream() }
|
|
||||||
.onFailure { e ->
|
|
||||||
logcat(ERROR) { e.asLog("File to decrypt not found") }
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
.onSuccess { encryptedInput ->
|
|
||||||
val decryptedOutput = ByteArrayOutputStream()
|
|
||||||
runCatching { executeOpenPgpApi(command, encryptedInput, decryptedOutput) }
|
|
||||||
.onFailure { e ->
|
|
||||||
logcat(ERROR) { e.asLog("OpenPgpApi ACTION_DECRYPT_VERIFY failed") }
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
.onSuccess { result ->
|
|
||||||
return when (
|
|
||||||
val resultCode =
|
|
||||||
result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)
|
|
||||||
) {
|
|
||||||
OpenPgpApi.RESULT_CODE_SUCCESS -> {
|
|
||||||
runCatching {
|
|
||||||
val entry =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
passwordEntryFactory.create(decryptedOutput.toByteArray())
|
|
||||||
}
|
|
||||||
AutofillPreferences.credentialsFromStoreEntry(
|
|
||||||
this,
|
|
||||||
file,
|
|
||||||
entry,
|
|
||||||
directoryStructure
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.getOrElse { e ->
|
|
||||||
logcat(ERROR) { e.asLog("Failed to parse password entry") }
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
|
|
||||||
val pendingIntent: PendingIntent =
|
|
||||||
result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!!
|
|
||||||
runCatching {
|
|
||||||
val intentToResume =
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
suspendCoroutine<Intent> { cont ->
|
|
||||||
continueAfterUserInteraction = cont
|
|
||||||
decryptInteractionRequiredAction.launch(
|
|
||||||
IntentSenderRequest.Builder(pendingIntent.intentSender).build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
decryptCredential(file, intentToResume)
|
|
||||||
}
|
|
||||||
.getOrElse { e ->
|
|
||||||
logcat(ERROR) {
|
|
||||||
e.asLog("OpenPgpApi ACTION_DECRYPT_VERIFY failed with user interaction")
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
logcat(ERROR) {
|
|
||||||
"OpenPgpApi ACTION_DECRYPT_VERIFY failed (${error.errorId}): ${error.message}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
null
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
logcat(ERROR) { "Unrecognized OpenPgpApi result: $resultCode" }
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,7 +15,6 @@ import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import app.passwordstore.data.repo.PasswordRepository
|
import app.passwordstore.data.repo.PasswordRepository
|
||||||
import app.passwordstore.ui.crypto.PasswordCreationActivity
|
|
||||||
import app.passwordstore.ui.crypto.PasswordCreationActivityV2
|
import app.passwordstore.ui.crypto.PasswordCreationActivityV2
|
||||||
import app.passwordstore.util.autofill.AutofillMatcher
|
import app.passwordstore.util.autofill.AutofillMatcher
|
||||||
import app.passwordstore.util.autofill.AutofillPreferences
|
import app.passwordstore.util.autofill.AutofillPreferences
|
||||||
|
@ -114,9 +113,9 @@ class AutofillSaveActivity : AppCompatActivity() {
|
||||||
bundleOf(
|
bundleOf(
|
||||||
"REPO_PATH" to repo.absolutePath,
|
"REPO_PATH" to repo.absolutePath,
|
||||||
"FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath,
|
"FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath,
|
||||||
PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME),
|
PasswordCreationActivityV2.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME),
|
||||||
PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD),
|
PasswordCreationActivityV2.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD),
|
||||||
PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to
|
PasswordCreationActivityV2.EXTRA_GENERATE_PASSWORD to
|
||||||
intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
|
intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,12 +5,9 @@
|
||||||
|
|
||||||
package app.passwordstore.ui.crypto
|
package app.passwordstore.ui.crypto
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentSender
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
@ -19,33 +16,20 @@ import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import app.passwordstore.R
|
import app.passwordstore.R
|
||||||
import app.passwordstore.injection.prefs.SettingsPreferences
|
import app.passwordstore.injection.prefs.SettingsPreferences
|
||||||
import app.passwordstore.util.extensions.OPENPGP_PROVIDER
|
|
||||||
import app.passwordstore.util.extensions.asLog
|
|
||||||
import app.passwordstore.util.extensions.clipboard
|
import app.passwordstore.util.extensions.clipboard
|
||||||
import app.passwordstore.util.extensions.getString
|
import app.passwordstore.util.extensions.getString
|
||||||
import app.passwordstore.util.extensions.snackbar
|
import app.passwordstore.util.extensions.snackbar
|
||||||
import app.passwordstore.util.extensions.unsafeLazy
|
import app.passwordstore.util.extensions.unsafeLazy
|
||||||
import app.passwordstore.util.features.Features
|
|
||||||
import app.passwordstore.util.services.ClipboardService
|
import app.passwordstore.util.services.ClipboardService
|
||||||
import app.passwordstore.util.settings.PreferenceKeys
|
import app.passwordstore.util.settings.PreferenceKeys
|
||||||
import com.github.michaelbull.result.getOr
|
|
||||||
import com.github.michaelbull.result.runCatching
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import logcat.LogPriority.ERROR
|
|
||||||
import logcat.LogPriority.INFO
|
|
||||||
import logcat.logcat
|
|
||||||
import me.msfjarvis.openpgpktx.util.OpenPgpApi
|
|
||||||
import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
|
|
||||||
import org.openintents.openpgp.IOpenPgpService2
|
|
||||||
import org.openintents.openpgp.OpenPgpError
|
|
||||||
|
|
||||||
@Suppress("Registered")
|
@Suppress("Registered")
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
open class BasePgpActivity : AppCompatActivity() {
|
||||||
|
|
||||||
/** Full path to the repository */
|
/** Full path to the repository */
|
||||||
val repoPath by unsafeLazy { intent.getStringExtra("REPO_PATH")!! }
|
val repoPath by unsafeLazy { intent.getStringExtra("REPO_PATH")!! }
|
||||||
|
@ -63,20 +47,6 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
|
||||||
/** [SharedPreferences] instance used by subclasses to persist settings */
|
/** [SharedPreferences] instance used by subclasses to persist settings */
|
||||||
@SettingsPreferences @Inject lateinit var settings: SharedPreferences
|
@SettingsPreferences @Inject lateinit var settings: SharedPreferences
|
||||||
|
|
||||||
@Inject lateinit var features: Features
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle to the [OpenPgpApi] instance that is used by subclasses to interface with OpenKeychain.
|
|
||||||
*/
|
|
||||||
private var serviceConnection: OpenPgpServiceConnection? = null
|
|
||||||
var api: OpenPgpApi? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [OpenPgpServiceConnection.OnBound] instance for the last listener that we wish to bind with
|
|
||||||
* in case the previous attempt was cancelled due to missing [OPENPGP_PROVIDER] package.
|
|
||||||
*/
|
|
||||||
private var previousListener: OpenPgpServiceConnection.OnBound? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots or
|
* [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots or
|
||||||
* recent apps screen.
|
* recent apps screen.
|
||||||
|
@ -87,124 +57,6 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
|
||||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* [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()
|
|
||||||
previousListener = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [onResume] controls the flow for resumption of a PGP operation that was previously interrupted
|
|
||||||
* by the [OPENPGP_PROVIDER] package being missing.
|
|
||||||
*/
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
previousListener?.let { bindToOpenKeychain(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
logcat(ERROR) { e.asLog("Callers must handle their own exceptions") }
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Method for subclasses to initiate binding with [OpenPgpServiceConnection]. */
|
|
||||||
fun bindToOpenKeychain(onBoundListener: OpenPgpServiceConnection.OnBound) {
|
|
||||||
if (true) return
|
|
||||||
val installed =
|
|
||||||
runCatching {
|
|
||||||
packageManager.getPackageInfo(OPENPGP_PROVIDER, 0)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
.getOr(false)
|
|
||||||
if (!installed) {
|
|
||||||
previousListener = onBoundListener
|
|
||||||
MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(getString(R.string.openkeychain_not_installed_title))
|
|
||||||
.setMessage(getString(R.string.openkeychain_not_installed_message))
|
|
||||||
.setPositiveButton(getString(R.string.openkeychain_not_installed_google_play)) { _, _ ->
|
|
||||||
runCatching {
|
|
||||||
val intent =
|
|
||||||
Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
data = Uri.parse(getString(R.string.play_deeplink_template, OPENPGP_PROVIDER))
|
|
||||||
setPackage("com.android.vending")
|
|
||||||
}
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNeutralButton(getString(R.string.openkeychain_not_installed_fdroid)) { _, _ ->
|
|
||||||
runCatching {
|
|
||||||
val intent =
|
|
||||||
Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
data = Uri.parse(getString(R.string.fdroid_deeplink_template, OPENPGP_PROVIDER))
|
|
||||||
}
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setOnCancelListener { finish() }
|
|
||||||
.show()
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
previousListener = null
|
|
||||||
serviceConnection =
|
|
||||||
OpenPgpServiceConnection(this, OPENPGP_PROVIDER, onBoundListener).also {
|
|
||||||
it.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 {
|
|
||||||
logcat(INFO) { "RESULT_CODE_USER_INTERACTION_REQUIRED" }
|
|
||||||
return result.getParcelableExtra<PendingIntent>(OpenPgpApi.RESULT_INTENT)!!.intentSender
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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))
|
|
||||||
logcat(ERROR) { "onError getErrorId: ${error.errorId}" }
|
|
||||||
logcat(ERROR) { "onError getMessage: ${error.message}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies provided [text] to the clipboard. Shows a [Snackbar] which can be disabled by passing
|
* Copies provided [text] to the clipboard. Shows a [Snackbar] which can be disabled by passing
|
||||||
* [showSnackbar] as false.
|
* [showSnackbar] as false.
|
||||||
|
@ -251,7 +103,6 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "APS/BasePgpActivity"
|
|
||||||
const val EXTRA_FILE_PATH = "FILE_PATH"
|
const val EXTRA_FILE_PATH = "FILE_PATH"
|
||||||
const val EXTRA_REPO_PATH = "REPO_PATH"
|
const val EXTRA_REPO_PATH = "REPO_PATH"
|
||||||
|
|
||||||
|
|
|
@ -1,227 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package app.passwordstore.ui.crypto
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.activity.result.IntentSenderRequest
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import app.passwordstore.R
|
|
||||||
import app.passwordstore.data.passfile.PasswordEntry
|
|
||||||
import app.passwordstore.data.password.FieldItem
|
|
||||||
import app.passwordstore.databinding.DecryptLayoutBinding
|
|
||||||
import app.passwordstore.ui.adapters.FieldItemAdapter
|
|
||||||
import app.passwordstore.util.extensions.unsafeLazy
|
|
||||||
import app.passwordstore.util.extensions.viewBinding
|
|
||||||
import app.passwordstore.util.settings.PreferenceKeys
|
|
||||||
import com.github.michaelbull.result.onFailure
|
|
||||||
import com.github.michaelbull.result.runCatching
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.File
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import logcat.LogPriority.ERROR
|
|
||||||
import logcat.asLog
|
|
||||||
import logcat.logcat
|
|
||||||
import me.msfjarvis.openpgpktx.util.OpenPgpApi
|
|
||||||
import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
|
|
||||||
import org.openintents.openpgp.IOpenPgpService2
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
|
|
||||||
|
|
||||||
private val binding by viewBinding(DecryptLayoutBinding::inflate)
|
|
||||||
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory
|
|
||||||
|
|
||||||
private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) }
|
|
||||||
private var passwordEntry: PasswordEntry? = null
|
|
||||||
|
|
||||||
private val userInteractionRequiredResult =
|
|
||||||
registerForActivityResult(StartIntentSenderForResult()) { result ->
|
|
||||||
if (result.data == null) {
|
|
||||||
setResult(RESULT_CANCELED, null)
|
|
||||||
finish()
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
when (result.resultCode) {
|
|
||||||
RESULT_OK -> decryptAndVerify(result.data)
|
|
||||||
RESULT_CANCELED -> {
|
|
||||||
setResult(RESULT_CANCELED, result.data)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
bindToOpenKeychain(this)
|
|
||||||
title = name
|
|
||||||
with(binding) {
|
|
||||||
setContentView(root)
|
|
||||||
passwordCategory.text = relativeParentPath
|
|
||||||
passwordFile.text = name
|
|
||||||
passwordFile.setOnLongClickListener {
|
|
||||||
copyTextToClipboard(name)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.pgp_handler, menu)
|
|
||||||
passwordEntry?.let { entry ->
|
|
||||||
menu.findItem(R.id.edit_password).isVisible = true
|
|
||||||
if (!entry.password.isNullOrBlank()) {
|
|
||||||
menu.findItem(R.id.share_password_as_plaintext).isVisible = true
|
|
||||||
menu.findItem(R.id.copy_password).isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
android.R.id.home -> onBackPressed()
|
|
||||||
R.id.edit_password -> editPassword()
|
|
||||||
R.id.share_password_as_plaintext -> shareAsPlaintext()
|
|
||||||
R.id.copy_password -> copyPasswordToClipboard(passwordEntry?.password)
|
|
||||||
else -> return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBound(service: IOpenPgpService2) {
|
|
||||||
super.onBound(service)
|
|
||||||
decryptAndVerify()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Exception) {
|
|
||||||
logcat(ERROR) { e.asLog() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatically finishes the activity 60 seconds after decryption succeeded to prevent
|
|
||||||
* information leaks from stale activities.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
private fun startAutoDismissTimer() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
delay(Duration.seconds(60))
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edit the current password and hide all the fields populated by encrypted data so that when the
|
|
||||||
* result triggers they can be repopulated with new data.
|
|
||||||
*/
|
|
||||||
private fun editPassword() {
|
|
||||||
val intent = Intent(this, PasswordCreationActivity::class.java)
|
|
||||||
intent.putExtra("FILE_PATH", relativeParentPath)
|
|
||||||
intent.putExtra("REPO_PATH", repoPath)
|
|
||||||
intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name)
|
|
||||||
intent.putExtra(PasswordCreationActivity.EXTRA_PASSWORD, passwordEntry?.password)
|
|
||||||
intent.putExtra(PasswordCreationActivity.EXTRA_EXTRA_CONTENT, passwordEntry?.extraContentString)
|
|
||||||
intent.putExtra(PasswordCreationActivity.EXTRA_EDITING, true)
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shareAsPlaintext() {
|
|
||||||
val sendIntent =
|
|
||||||
Intent().apply {
|
|
||||||
action = Intent.ACTION_SEND
|
|
||||||
putExtra(Intent.EXTRA_TEXT, passwordEntry?.password)
|
|
||||||
type = "text/plain"
|
|
||||||
}
|
|
||||||
// Always show a picker to give the user a chance to cancel
|
|
||||||
startActivity(
|
|
||||||
Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
private fun decryptAndVerify(receivedIntent: Intent? = null) {
|
|
||||||
if (api == null) {
|
|
||||||
bindToOpenKeychain(this)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val data = receivedIntent ?: Intent()
|
|
||||||
data.action = OpenPgpApi.ACTION_DECRYPT_VERIFY
|
|
||||||
|
|
||||||
val inputStream = File(fullPath).inputStream()
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
val result =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
checkNotNull(api).executeApi(data, inputStream, outputStream)
|
|
||||||
}
|
|
||||||
when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
|
||||||
OpenPgpApi.RESULT_CODE_SUCCESS -> {
|
|
||||||
startAutoDismissTimer()
|
|
||||||
runCatching {
|
|
||||||
val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true)
|
|
||||||
val entry = passwordEntryFactory.create(outputStream.toByteArray())
|
|
||||||
|
|
||||||
if (settings.getBoolean(PreferenceKeys.COPY_ON_DECRYPT, false)) {
|
|
||||||
copyPasswordToClipboard(entry.password)
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordEntry = entry
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
|
|
||||||
val items = arrayListOf<FieldItem>()
|
|
||||||
if (!entry.password.isNullOrBlank()) {
|
|
||||||
items.add(FieldItem.createPasswordField(entry.password!!))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.hasTotp()) {
|
|
||||||
items.add(FieldItem.createOtpField(entry.totp.first()))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entry.username.isNullOrBlank()) {
|
|
||||||
items.add(FieldItem.createUsernameField(entry.username!!))
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.extraContent.forEach { (key, value) ->
|
|
||||||
items.add(FieldItem(key, value, FieldItem.ActionType.COPY))
|
|
||||||
}
|
|
||||||
|
|
||||||
val adapter =
|
|
||||||
FieldItemAdapter(items, showPassword) { text -> copyTextToClipboard(text) }
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
binding.recyclerView.itemAnimator = null
|
|
||||||
|
|
||||||
if (entry.hasTotp()) {
|
|
||||||
entry.totp.onEach(adapter::updateOTPCode).launchIn(lifecycleScope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onFailure { e -> logcat(ERROR) { e.asLog() } }
|
|
||||||
}
|
|
||||||
OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
|
|
||||||
val sender = getUserInteractionRequestIntent(result)
|
|
||||||
userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build())
|
|
||||||
}
|
|
||||||
OpenPgpApi.RESULT_CODE_ERROR -> handleError(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package app.passwordstore.ui.crypto
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.result.IntentSenderRequest
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.github.michaelbull.result.onFailure
|
|
||||||
import com.github.michaelbull.result.runCatching
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import logcat.LogPriority.ERROR
|
|
||||||
import logcat.asLog
|
|
||||||
import logcat.logcat
|
|
||||||
import me.msfjarvis.openpgpktx.util.OpenPgpApi
|
|
||||||
import me.msfjarvis.openpgpktx.util.OpenPgpUtils
|
|
||||||
import org.openintents.openpgp.IOpenPgpService2
|
|
||||||
|
|
||||||
class GetKeyIdsActivity : BasePgpActivity() {
|
|
||||||
|
|
||||||
private val userInteractionRequiredResult =
|
|
||||||
registerForActivityResult(StartIntentSenderForResult()) { result ->
|
|
||||||
if (result.data == null || result.resultCode == RESULT_CANCELED) {
|
|
||||||
setResult(RESULT_CANCELED, result.data)
|
|
||||||
finish()
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
getKeyIds(result.data!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
bindToOpenKeychain(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBound(service: IOpenPgpService2) {
|
|
||||||
super.onBound(service)
|
|
||||||
getKeyIds()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Exception) {
|
|
||||||
logcat(ERROR) { e.asLog() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the Key ids from OpenKeychain */
|
|
||||||
private fun getKeyIds(data: Intent = Intent()) {
|
|
||||||
data.action = OpenPgpApi.ACTION_GET_KEY_IDS
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
val result = withContext(Dispatchers.IO) { checkNotNull(api).executeApi(data, null, null) }
|
|
||||||
when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
|
||||||
OpenPgpApi.RESULT_CODE_SUCCESS -> {
|
|
||||||
runCatching {
|
|
||||||
val ids =
|
|
||||||
result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)?.map {
|
|
||||||
OpenPgpUtils.convertKeyIdToHex(it)
|
|
||||||
}
|
|
||||||
?: emptyList()
|
|
||||||
val keyResult = Intent().putExtra(OpenPgpApi.EXTRA_KEY_IDS, ids.toTypedArray())
|
|
||||||
setResult(RESULT_OK, keyResult)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
.onFailure { e -> logcat(ERROR) { e.asLog() } }
|
|
||||||
}
|
|
||||||
OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
|
|
||||||
val sender = getUserInteractionRequestIntent(result)
|
|
||||||
userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build())
|
|
||||||
}
|
|
||||||
OpenPgpApi.RESULT_CODE_ERROR -> handleError(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,617 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package app.passwordstore.ui.crypto
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.ImageDecoder
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.text.InputType
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.result.IntentSenderRequest
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.widget.doAfterTextChanged
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import app.passwordstore.R
|
|
||||||
import app.passwordstore.data.passfile.PasswordEntry
|
|
||||||
import app.passwordstore.data.repo.PasswordRepository
|
|
||||||
import app.passwordstore.databinding.PasswordCreationActivityBinding
|
|
||||||
import app.passwordstore.ui.dialogs.DicewarePasswordGeneratorDialogFragment
|
|
||||||
import app.passwordstore.ui.dialogs.OtpImportDialogFragment
|
|
||||||
import app.passwordstore.ui.dialogs.PasswordGeneratorDialogFragment
|
|
||||||
import app.passwordstore.util.autofill.AutofillPreferences
|
|
||||||
import app.passwordstore.util.autofill.DirectoryStructure
|
|
||||||
import app.passwordstore.util.crypto.GpgIdentifier
|
|
||||||
import app.passwordstore.util.extensions.asLog
|
|
||||||
import app.passwordstore.util.extensions.base64
|
|
||||||
import app.passwordstore.util.extensions.commitChange
|
|
||||||
import app.passwordstore.util.extensions.getString
|
|
||||||
import app.passwordstore.util.extensions.isInsideRepository
|
|
||||||
import app.passwordstore.util.extensions.snackbar
|
|
||||||
import app.passwordstore.util.extensions.unsafeLazy
|
|
||||||
import app.passwordstore.util.extensions.viewBinding
|
|
||||||
import app.passwordstore.util.settings.PreferenceKeys
|
|
||||||
import com.github.michaelbull.result.onFailure
|
|
||||||
import com.github.michaelbull.result.onSuccess
|
|
||||||
import com.github.michaelbull.result.runCatching
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.google.zxing.BinaryBitmap
|
|
||||||
import com.google.zxing.LuminanceSource
|
|
||||||
import com.google.zxing.RGBLuminanceSource
|
|
||||||
import com.google.zxing.common.HybridBinarizer
|
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
|
||||||
import com.google.zxing.integration.android.IntentIntegrator.QR_CODE
|
|
||||||
import com.google.zxing.qrcode.QRCodeReader
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import logcat.LogPriority.ERROR
|
|
||||||
import logcat.asLog
|
|
||||||
import logcat.logcat
|
|
||||||
import me.msfjarvis.openpgpktx.util.OpenPgpApi
|
|
||||||
import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
|
|
||||||
|
|
||||||
private val binding by viewBinding(PasswordCreationActivityBinding::inflate)
|
|
||||||
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory
|
|
||||||
|
|
||||||
private val suggestedName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) }
|
|
||||||
private val suggestedPass by unsafeLazy { intent.getStringExtra(EXTRA_PASSWORD) }
|
|
||||||
private val suggestedExtra by unsafeLazy { intent.getStringExtra(EXTRA_EXTRA_CONTENT) }
|
|
||||||
private val shouldGeneratePassword by unsafeLazy {
|
|
||||||
intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
|
|
||||||
}
|
|
||||||
private val editing by unsafeLazy { intent.getBooleanExtra(EXTRA_EDITING, false) }
|
|
||||||
private val oldFileName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) }
|
|
||||||
private var oldCategory: String? = null
|
|
||||||
private var copy: Boolean = false
|
|
||||||
private var encryptionIntent: Intent = Intent()
|
|
||||||
|
|
||||||
private val userInteractionRequiredResult =
|
|
||||||
registerForActivityResult(StartIntentSenderForResult()) { result ->
|
|
||||||
if (result.data == null) {
|
|
||||||
setResult(RESULT_CANCELED, null)
|
|
||||||
finish()
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
when (result.resultCode) {
|
|
||||||
RESULT_OK -> encrypt(result.data)
|
|
||||||
RESULT_CANCELED -> {
|
|
||||||
setResult(RESULT_CANCELED, result.data)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val otpImportAction =
|
|
||||||
registerForActivityResult(StartActivityForResult()) { result ->
|
|
||||||
if (result.resultCode == RESULT_OK) {
|
|
||||||
binding.otpImportButton.isVisible = false
|
|
||||||
val intentResult = IntentIntegrator.parseActivityResult(RESULT_OK, result.data)
|
|
||||||
val contents = "${intentResult.contents}\n"
|
|
||||||
val currentExtras = binding.extraContent.text.toString()
|
|
||||||
if (currentExtras.isNotEmpty() && currentExtras.last() != '\n')
|
|
||||||
binding.extraContent.append("\n$contents")
|
|
||||||
else binding.extraContent.append(contents)
|
|
||||||
snackbar(message = getString(R.string.otp_import_success))
|
|
||||||
} else {
|
|
||||||
snackbar(message = getString(R.string.otp_import_failure))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val imageImportAction =
|
|
||||||
registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri ->
|
|
||||||
if (imageUri == null) {
|
|
||||||
snackbar(message = getString(R.string.otp_import_failure))
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
val bitmap =
|
|
||||||
if (Build.VERSION.SDK_INT >= 28) {
|
|
||||||
ImageDecoder.decodeBitmap(ImageDecoder.createSource(contentResolver, imageUri))
|
|
||||||
.copy(Bitmap.Config.ARGB_8888, true)
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION") MediaStore.Images.Media.getBitmap(contentResolver, imageUri)
|
|
||||||
}
|
|
||||||
val intArray = IntArray(bitmap.width * bitmap.height)
|
|
||||||
// copy pixel data from the Bitmap into the 'intArray' array
|
|
||||||
bitmap.getPixels(intArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
|
|
||||||
val source: LuminanceSource = RGBLuminanceSource(bitmap.width, bitmap.height, intArray)
|
|
||||||
val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
|
|
||||||
|
|
||||||
val reader = QRCodeReader()
|
|
||||||
runCatching {
|
|
||||||
val result = reader.decode(binaryBitmap)
|
|
||||||
val text = result.text
|
|
||||||
val currentExtras = binding.extraContent.text.toString()
|
|
||||||
if (currentExtras.isNotEmpty() && currentExtras.last() != '\n')
|
|
||||||
binding.extraContent.append("\n$text")
|
|
||||||
else binding.extraContent.append(text)
|
|
||||||
snackbar(message = getString(R.string.otp_import_success))
|
|
||||||
binding.otpImportButton.isVisible = false
|
|
||||||
}
|
|
||||||
.onFailure { snackbar(message = getString(R.string.otp_import_failure)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private val gpgKeySelectAction =
|
|
||||||
registerForActivityResult(StartActivityForResult()) { result ->
|
|
||||||
if (result.resultCode == RESULT_OK) {
|
|
||||||
result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds ->
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id")
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
gpgIdentifierFile.writeText((keyIds + "").joinToString("\n"))
|
|
||||||
}
|
|
||||||
commitChange(
|
|
||||||
getString(
|
|
||||||
R.string.git_commit_gpg_id,
|
|
||||||
getLongName(
|
|
||||||
gpgIdentifierFile.parentFile!!.absolutePath,
|
|
||||||
repoPath,
|
|
||||||
gpgIdentifierFile.name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.onSuccess { encrypt(encryptionIntent) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
snackbar(
|
|
||||||
message = getString(R.string.gpg_key_select_mandatory),
|
|
||||||
length = Snackbar.LENGTH_LONG
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun File.findTillRoot(fileName: String, rootPath: File): File? {
|
|
||||||
val gpgFile = File(this, fileName)
|
|
||||||
if (gpgFile.exists()) return gpgFile
|
|
||||||
|
|
||||||
if (this.absolutePath == rootPath.absolutePath) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val parent = parentFile
|
|
||||||
return if (parent != null && parent.exists()) {
|
|
||||||
parent.findTillRoot(fileName, rootPath)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
bindToOpenKeychain(this)
|
|
||||||
title =
|
|
||||||
if (editing) getString(R.string.edit_password) else getString(R.string.new_password_title)
|
|
||||||
with(binding) {
|
|
||||||
setContentView(root)
|
|
||||||
generatePassword.setOnClickListener { generatePassword() }
|
|
||||||
otpImportButton.setOnClickListener {
|
|
||||||
supportFragmentManager.setFragmentResultListener(
|
|
||||||
OTP_RESULT_REQUEST_KEY,
|
|
||||||
this@PasswordCreationActivity
|
|
||||||
) { requestKey, bundle ->
|
|
||||||
if (requestKey == OTP_RESULT_REQUEST_KEY) {
|
|
||||||
val contents = bundle.getString(RESULT)
|
|
||||||
val currentExtras = binding.extraContent.text.toString()
|
|
||||||
if (currentExtras.isNotEmpty() && currentExtras.last() != '\n')
|
|
||||||
binding.extraContent.append("\n$contents")
|
|
||||||
else binding.extraContent.append(contents)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val hasCamera = packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) == true
|
|
||||||
if (hasCamera) {
|
|
||||||
val items =
|
|
||||||
arrayOf(
|
|
||||||
getString(R.string.otp_import_qr_code),
|
|
||||||
getString(R.string.otp_import_from_file),
|
|
||||||
getString(R.string.otp_import_manual_entry),
|
|
||||||
)
|
|
||||||
MaterialAlertDialogBuilder(this@PasswordCreationActivity)
|
|
||||||
.setItems(items) { _, index ->
|
|
||||||
when (index) {
|
|
||||||
0 ->
|
|
||||||
otpImportAction.launch(
|
|
||||||
IntentIntegrator(this@PasswordCreationActivity)
|
|
||||||
.setOrientationLocked(false)
|
|
||||||
.setBeepEnabled(false)
|
|
||||||
.setDesiredBarcodeFormats(QR_CODE)
|
|
||||||
.createScanIntent()
|
|
||||||
)
|
|
||||||
1 -> imageImportAction.launch("image/*")
|
|
||||||
2 -> OtpImportDialogFragment().show(supportFragmentManager, "OtpImport")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
} else {
|
|
||||||
OtpImportDialogFragment().show(supportFragmentManager, "OtpImport")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
directoryInputLayout.apply {
|
|
||||||
if (suggestedName != null || suggestedPass != null || shouldGeneratePassword) {
|
|
||||||
isEnabled = true
|
|
||||||
} else {
|
|
||||||
setBackgroundColor(getColor(android.R.color.transparent))
|
|
||||||
}
|
|
||||||
val path = getRelativePath(fullPath, repoPath)
|
|
||||||
// Keep empty path field visible if it is editable.
|
|
||||||
if (path.isEmpty() && !isEnabled) visibility = View.GONE
|
|
||||||
else {
|
|
||||||
directory.setText(path)
|
|
||||||
oldCategory = path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (suggestedName != null) {
|
|
||||||
filename.setText(suggestedName)
|
|
||||||
} else {
|
|
||||||
filename.requestFocus()
|
|
||||||
}
|
|
||||||
// Allow the user to quickly switch between storing the username as the filename or
|
|
||||||
// in the encrypted extras. This only makes sense if the directory structure is
|
|
||||||
// FileBased.
|
|
||||||
if (
|
|
||||||
suggestedName == null &&
|
|
||||||
AutofillPreferences.directoryStructure(this@PasswordCreationActivity) ==
|
|
||||||
DirectoryStructure.FileBased
|
|
||||||
) {
|
|
||||||
encryptUsername.apply {
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
setOnClickListener {
|
|
||||||
if (isChecked) {
|
|
||||||
// User wants to enable username encryption, so we add it to the
|
|
||||||
// encrypted extras as the first line.
|
|
||||||
val username = filename.text.toString()
|
|
||||||
val extras = "username:$username\n${extraContent.text}"
|
|
||||||
|
|
||||||
filename.text?.clear()
|
|
||||||
extraContent.setText(extras)
|
|
||||||
} else {
|
|
||||||
// User wants to disable username encryption, so we extract the
|
|
||||||
// username from the encrypted extras and use it as the filename.
|
|
||||||
val entry =
|
|
||||||
passwordEntryFactory.create("PASSWORD\n${extraContent.text}".encodeToByteArray())
|
|
||||||
val username = entry.username
|
|
||||||
|
|
||||||
// username should not be null here by the logic in
|
|
||||||
// updateViewState, but it could still happen due to
|
|
||||||
// input lag.
|
|
||||||
if (username != null) {
|
|
||||||
filename.setText(username)
|
|
||||||
extraContent.setText(entry.extraContentWithoutAuthData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
suggestedPass?.let {
|
|
||||||
password.setText(it)
|
|
||||||
password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
|
||||||
}
|
|
||||||
suggestedExtra?.let { extraContent.setText(it) }
|
|
||||||
if (shouldGeneratePassword) {
|
|
||||||
generatePassword()
|
|
||||||
password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
|
||||||
}
|
|
||||||
}
|
|
||||||
listOf(binding.filename, binding.extraContent).forEach {
|
|
||||||
it.doAfterTextChanged { updateViewState() }
|
|
||||||
}
|
|
||||||
updateViewState()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.pgp_handler_new_password, menu)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
android.R.id.home -> {
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
onBackPressed()
|
|
||||||
}
|
|
||||||
R.id.save_password -> {
|
|
||||||
copy = false
|
|
||||||
encrypt()
|
|
||||||
}
|
|
||||||
R.id.save_and_copy_password -> {
|
|
||||||
copy = true
|
|
||||||
encrypt()
|
|
||||||
}
|
|
||||||
else -> return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generatePassword() {
|
|
||||||
supportFragmentManager.setFragmentResultListener(PASSWORD_RESULT_REQUEST_KEY, this) {
|
|
||||||
requestKey,
|
|
||||||
bundle ->
|
|
||||||
if (requestKey == PASSWORD_RESULT_REQUEST_KEY) {
|
|
||||||
binding.password.setText(bundle.getString(RESULT))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) {
|
|
||||||
KEY_PWGEN_TYPE_CLASSIC ->
|
|
||||||
PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator")
|
|
||||||
KEY_PWGEN_TYPE_DICEWARE ->
|
|
||||||
DicewarePasswordGeneratorDialogFragment().show(supportFragmentManager, "generator")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateViewState() =
|
|
||||||
with(binding) {
|
|
||||||
// Use PasswordEntry to parse extras for username
|
|
||||||
val entry =
|
|
||||||
passwordEntryFactory.create("PLACEHOLDER\n${extraContent.text}".encodeToByteArray())
|
|
||||||
encryptUsername.apply {
|
|
||||||
if (visibility != View.VISIBLE) return@apply
|
|
||||||
val hasUsernameInFileName = filename.text.toString().isNotBlank()
|
|
||||||
val hasUsernameInExtras = !entry.username.isNullOrBlank()
|
|
||||||
isEnabled = hasUsernameInFileName xor hasUsernameInExtras
|
|
||||||
isChecked = hasUsernameInExtras
|
|
||||||
}
|
|
||||||
otpImportButton.isVisible = !entry.hasTotp()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Encrypts the password and the extra content */
|
|
||||||
private fun encrypt(receivedIntent: Intent? = null) {
|
|
||||||
with(binding) {
|
|
||||||
val editName = filename.text.toString().trim()
|
|
||||||
val editPass = password.text.toString()
|
|
||||||
val editExtra = extraContent.text.toString()
|
|
||||||
|
|
||||||
if (editName.isEmpty()) {
|
|
||||||
snackbar(message = resources.getString(R.string.file_toast_text))
|
|
||||||
return@with
|
|
||||||
} else if (editName.contains('/')) {
|
|
||||||
snackbar(message = resources.getString(R.string.invalid_filename_text))
|
|
||||||
return@with
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editPass.isEmpty() && editExtra.isEmpty()) {
|
|
||||||
snackbar(message = resources.getString(R.string.empty_toast_text))
|
|
||||||
return@with
|
|
||||||
}
|
|
||||||
|
|
||||||
if (copy) {
|
|
||||||
copyPasswordToClipboard(editPass)
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptionIntent = receivedIntent ?: Intent()
|
|
||||||
encryptionIntent.action = OpenPgpApi.ACTION_ENCRYPT
|
|
||||||
|
|
||||||
// pass enters the key ID into `.gpg-id`.
|
|
||||||
val repoRoot = PasswordRepository.getRepositoryDirectory()
|
|
||||||
val gpgIdentifierFile =
|
|
||||||
File(repoRoot, directory.text.toString()).findTillRoot(".gpg-id", repoRoot)
|
|
||||||
?: File(repoRoot, ".gpg-id").apply { createNewFile() }
|
|
||||||
val gpgIdentifiers =
|
|
||||||
gpgIdentifierFile
|
|
||||||
.readLines()
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.map { line ->
|
|
||||||
GpgIdentifier.fromString(line)
|
|
||||||
?: run {
|
|
||||||
// The line being empty means this is most likely an empty `.gpg-id`
|
|
||||||
// file we created. Skip the validation so we can make the user add a
|
|
||||||
// real ID.
|
|
||||||
if (line.isEmpty()) return@run
|
|
||||||
if (line.removePrefix("0x").matches("[a-fA-F0-9]{8}".toRegex())) {
|
|
||||||
snackbar(message = resources.getString(R.string.short_key_ids_unsupported))
|
|
||||||
} else {
|
|
||||||
snackbar(message = resources.getString(R.string.invalid_gpg_id))
|
|
||||||
}
|
|
||||||
return@with
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (gpgIdentifiers.isEmpty()) {
|
|
||||||
gpgKeySelectAction.launch(
|
|
||||||
Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java)
|
|
||||||
)
|
|
||||||
return@with
|
|
||||||
}
|
|
||||||
val keyIds =
|
|
||||||
gpgIdentifiers.filterIsInstance<GpgIdentifier.KeyId>().map { it.id }.toLongArray()
|
|
||||||
if (keyIds.isNotEmpty()) {
|
|
||||||
encryptionIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keyIds)
|
|
||||||
}
|
|
||||||
val userIds =
|
|
||||||
gpgIdentifiers.filterIsInstance<GpgIdentifier.UserId>().map { it.email }.toTypedArray()
|
|
||||||
if (userIds.isNotEmpty()) {
|
|
||||||
encryptionIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, userIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptionIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, false)
|
|
||||||
|
|
||||||
val content = "$editPass\n$editExtra"
|
|
||||||
val inputStream = ByteArrayInputStream(content.toByteArray())
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
|
|
||||||
val path =
|
|
||||||
when {
|
|
||||||
// If we allowed the user to edit the relative path, we have to consider it here
|
|
||||||
// instead
|
|
||||||
// of fullPath.
|
|
||||||
directoryInputLayout.isEnabled -> {
|
|
||||||
val editRelativePath = directory.text.toString().trim()
|
|
||||||
if (editRelativePath.isEmpty()) {
|
|
||||||
snackbar(message = resources.getString(R.string.path_toast_text))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val passwordDirectory = File("$repoPath/${editRelativePath.trim('/')}")
|
|
||||||
if (!passwordDirectory.exists() && !passwordDirectory.mkdir()) {
|
|
||||||
snackbar(message = "Failed to create directory ${editRelativePath.trim('/')}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
"${passwordDirectory.path}/$editName.gpg"
|
|
||||||
}
|
|
||||||
else -> "$fullPath/$editName.gpg"
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
val result =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
checkNotNull(api).executeApi(encryptionIntent, inputStream, outputStream)
|
|
||||||
}
|
|
||||||
when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
|
||||||
OpenPgpApi.RESULT_CODE_SUCCESS -> {
|
|
||||||
runCatching {
|
|
||||||
val file = File(path)
|
|
||||||
// If we're not editing, this file should not already exist!
|
|
||||||
// Additionally, if we were editing and the incoming and outgoing
|
|
||||||
// filenames differ, it means we renamed. Ensure that the target
|
|
||||||
// doesn't already exist to prevent an accidental overwrite.
|
|
||||||
if (
|
|
||||||
(!editing || (editing && suggestedName != file.nameWithoutExtension)) &&
|
|
||||||
file.exists()
|
|
||||||
) {
|
|
||||||
snackbar(message = getString(R.string.password_creation_duplicate_error))
|
|
||||||
return@runCatching
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.isInsideRepository()) {
|
|
||||||
snackbar(message = getString(R.string.message_error_destination_outside_repo))
|
|
||||||
return@runCatching
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
file.outputStream().use { it.write(outputStream.toByteArray()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// associate the new password name with the last name's timestamp in
|
|
||||||
// history
|
|
||||||
val preference =
|
|
||||||
getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
|
|
||||||
val oldFilePathHash =
|
|
||||||
"$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg".base64()
|
|
||||||
val timestamp = preference.getString(oldFilePathHash)
|
|
||||||
if (timestamp != null) {
|
|
||||||
preference.edit {
|
|
||||||
remove(oldFilePathHash)
|
|
||||||
putString(file.absolutePath.base64(), timestamp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val returnIntent = Intent()
|
|
||||||
returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path)
|
|
||||||
returnIntent.putExtra(RETURN_EXTRA_NAME, editName)
|
|
||||||
returnIntent.putExtra(
|
|
||||||
RETURN_EXTRA_LONG_NAME,
|
|
||||||
getLongName(fullPath, repoPath, editName)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (shouldGeneratePassword) {
|
|
||||||
val directoryStructure =
|
|
||||||
AutofillPreferences.directoryStructure(applicationContext)
|
|
||||||
val entry = passwordEntryFactory.create(content.encodeToByteArray())
|
|
||||||
returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password)
|
|
||||||
val username = entry.username ?: directoryStructure.getUsernameFor(file)
|
|
||||||
returnIntent.putExtra(RETURN_EXTRA_USERNAME, username)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
directoryInputLayout.isVisible &&
|
|
||||||
directoryInputLayout.isEnabled &&
|
|
||||||
oldFileName != null
|
|
||||||
) {
|
|
||||||
val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg")
|
|
||||||
if (oldFile.path != file.path && !oldFile.delete()) {
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
MaterialAlertDialogBuilder(this@PasswordCreationActivity)
|
|
||||||
.setTitle(R.string.password_creation_file_fail_title)
|
|
||||||
.setMessage(
|
|
||||||
getString(R.string.password_creation_file_delete_fail_message, oldFileName)
|
|
||||||
)
|
|
||||||
.setCancelable(false)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> finish() }
|
|
||||||
.show()
|
|
||||||
return@runCatching
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val commitMessageRes =
|
|
||||||
if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text
|
|
||||||
lifecycleScope.launch {
|
|
||||||
commitChange(
|
|
||||||
resources.getString(
|
|
||||||
commitMessageRes,
|
|
||||||
getLongName(fullPath, repoPath, editName)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.onSuccess {
|
|
||||||
setResult(RESULT_OK, returnIntent)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onFailure { e ->
|
|
||||||
if (e is IOException) {
|
|
||||||
logcat(ERROR) { e.asLog("Failed to write password file") }
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
MaterialAlertDialogBuilder(this@PasswordCreationActivity)
|
|
||||||
.setTitle(getString(R.string.password_creation_file_fail_title))
|
|
||||||
.setMessage(getString(R.string.password_creation_file_write_fail_message))
|
|
||||||
.setCancelable(false)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> finish() }
|
|
||||||
.show()
|
|
||||||
} else {
|
|
||||||
logcat(ERROR) { e.asLog() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
|
|
||||||
val sender = getUserInteractionRequestIntent(result)
|
|
||||||
userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build())
|
|
||||||
}
|
|
||||||
OpenPgpApi.RESULT_CODE_ERROR -> handleError(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val KEY_PWGEN_TYPE_CLASSIC = "classic"
|
|
||||||
private const val KEY_PWGEN_TYPE_DICEWARE = "diceware"
|
|
||||||
const val PASSWORD_RESULT_REQUEST_KEY = "PASSWORD_GENERATOR"
|
|
||||||
const val OTP_RESULT_REQUEST_KEY = "OTP_IMPORT"
|
|
||||||
const val RESULT = "RESULT"
|
|
||||||
const val RETURN_EXTRA_CREATED_FILE = "CREATED_FILE"
|
|
||||||
const val RETURN_EXTRA_NAME = "NAME"
|
|
||||||
const val RETURN_EXTRA_LONG_NAME = "LONG_NAME"
|
|
||||||
const val RETURN_EXTRA_USERNAME = "USERNAME"
|
|
||||||
const val RETURN_EXTRA_PASSWORD = "PASSWORD"
|
|
||||||
const val EXTRA_FILE_NAME = "FILENAME"
|
|
||||||
const val EXTRA_PASSWORD = "PASSWORD"
|
|
||||||
const val EXTRA_EXTRA_CONTENT = "EXTRA_CONTENT"
|
|
||||||
const val EXTRA_GENERATE_PASSWORD = "GENERATE_PASSWORD"
|
|
||||||
const val EXTRA_EDITING = "EDITING"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -134,7 +134,6 @@ class PasswordCreationActivityV2 : BasePgpActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
bindToOpenKeychain(this)
|
|
||||||
title =
|
title =
|
||||||
if (editing) getString(R.string.edit_password) else getString(R.string.new_password_title)
|
if (editing) getString(R.string.edit_password) else getString(R.string.new_password_title)
|
||||||
with(binding) {
|
with(binding) {
|
||||||
|
|
|
@ -19,7 +19,7 @@ import app.passwordstore.R
|
||||||
import app.passwordstore.databinding.FragmentPwgenDicewareBinding
|
import app.passwordstore.databinding.FragmentPwgenDicewareBinding
|
||||||
import app.passwordstore.injection.prefs.PasswordGeneratorPreferences
|
import app.passwordstore.injection.prefs.PasswordGeneratorPreferences
|
||||||
import app.passwordstore.passgen.diceware.DicewarePassphraseGenerator
|
import app.passwordstore.passgen.diceware.DicewarePassphraseGenerator
|
||||||
import app.passwordstore.ui.crypto.PasswordCreationActivity
|
import app.passwordstore.ui.crypto.PasswordCreationActivityV2
|
||||||
import app.passwordstore.util.extensions.getString
|
import app.passwordstore.util.extensions.getString
|
||||||
import app.passwordstore.util.settings.PreferenceKeys.DICEWARE_LENGTH
|
import app.passwordstore.util.settings.PreferenceKeys.DICEWARE_LENGTH
|
||||||
import app.passwordstore.util.settings.PreferenceKeys.DICEWARE_SEPARATOR
|
import app.passwordstore.util.settings.PreferenceKeys.DICEWARE_SEPARATOR
|
||||||
|
@ -58,8 +58,8 @@ class DicewarePasswordGeneratorDialogFragment : DialogFragment() {
|
||||||
setTitle(R.string.pwgen_title)
|
setTitle(R.string.pwgen_title)
|
||||||
setPositiveButton(R.string.dialog_ok) { _, _ ->
|
setPositiveButton(R.string.dialog_ok) { _, _ ->
|
||||||
setFragmentResult(
|
setFragmentResult(
|
||||||
PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY,
|
PasswordCreationActivityV2.PASSWORD_RESULT_REQUEST_KEY,
|
||||||
bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}")
|
bundleOf(PasswordCreationActivityV2.RESULT to "${binding.passwordText.text}")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setNeutralButton(R.string.dialog_cancel) { _, _ -> }
|
setNeutralButton(R.string.dialog_cancel) { _, _ -> }
|
||||||
|
|
|
@ -5,60 +5,21 @@
|
||||||
package app.passwordstore.ui.dialogs
|
package app.passwordstore.ui.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import app.passwordstore.R
|
import app.passwordstore.R
|
||||||
import app.passwordstore.data.repo.PasswordRepository
|
|
||||||
import app.passwordstore.ui.crypto.BasePgpActivity
|
|
||||||
import app.passwordstore.ui.crypto.GetKeyIdsActivity
|
|
||||||
import app.passwordstore.ui.passwords.PasswordStore
|
import app.passwordstore.ui.passwords.PasswordStore
|
||||||
import app.passwordstore.util.extensions.commitChange
|
|
||||||
import com.google.android.material.checkbox.MaterialCheckBox
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import me.msfjarvis.openpgpktx.util.OpenPgpApi
|
|
||||||
|
|
||||||
class FolderCreationDialogFragment : DialogFragment() {
|
class FolderCreationDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
private lateinit var newFolder: File
|
private lateinit var newFolder: File
|
||||||
|
|
||||||
private val keySelectAction =
|
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
|
||||||
if (result.resultCode == AppCompatActivity.RESULT_OK) {
|
|
||||||
result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds ->
|
|
||||||
val gpgIdentifierFile = File(newFolder, ".gpg-id")
|
|
||||||
gpgIdentifierFile.writeText(keyIds.joinToString("\n"))
|
|
||||||
if (PasswordRepository.repository != null) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val repoPath = PasswordRepository.getRepositoryDirectory().absolutePath
|
|
||||||
requireActivity()
|
|
||||||
.commitChange(
|
|
||||||
getString(
|
|
||||||
R.string.git_commit_gpg_id,
|
|
||||||
BasePgpActivity.getLongName(
|
|
||||||
gpgIdentifierFile.parentFile!!.absolutePath,
|
|
||||||
repoPath,
|
|
||||||
gpgIdentifierFile.name
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext())
|
val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext())
|
||||||
alertDialogBuilder.setTitle(R.string.title_create_folder)
|
alertDialogBuilder.setTitle(R.string.title_create_folder)
|
||||||
|
@ -89,12 +50,16 @@ class FolderCreationDialogFragment : DialogFragment() {
|
||||||
if (folderNameViewContainer.error != null) return
|
if (folderNameViewContainer.error != null) return
|
||||||
newFolder.mkdirs()
|
newFolder.mkdirs()
|
||||||
(requireActivity() as PasswordStore).refreshPasswordList(newFolder)
|
(requireActivity() as PasswordStore).refreshPasswordList(newFolder)
|
||||||
|
// TODO(msfjarvis): Restore this functionality
|
||||||
|
/*
|
||||||
if (dialog.findViewById<MaterialCheckBox>(R.id.set_gpg_key).isChecked) {
|
if (dialog.findViewById<MaterialCheckBox>(R.id.set_gpg_key).isChecked) {
|
||||||
keySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java))
|
keySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java))
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.setFragmentResult
|
import androidx.fragment.app.setFragmentResult
|
||||||
import app.passwordstore.databinding.FragmentManualOtpEntryBinding
|
import app.passwordstore.databinding.FragmentManualOtpEntryBinding
|
||||||
import app.passwordstore.ui.crypto.PasswordCreationActivity
|
import app.passwordstore.ui.crypto.PasswordCreationActivityV2
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
|
||||||
class OtpImportDialogFragment : DialogFragment() {
|
class OtpImportDialogFragment : DialogFragment() {
|
||||||
|
@ -24,8 +24,8 @@ class OtpImportDialogFragment : DialogFragment() {
|
||||||
builder.setView(binding.root)
|
builder.setView(binding.root)
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
setFragmentResult(
|
setFragmentResult(
|
||||||
PasswordCreationActivity.OTP_RESULT_REQUEST_KEY,
|
PasswordCreationActivityV2.OTP_RESULT_REQUEST_KEY,
|
||||||
bundleOf(PasswordCreationActivity.RESULT to getTOTPUri(binding))
|
bundleOf(PasswordCreationActivityV2.RESULT to getTOTPUri(binding))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
|
|
|
@ -26,7 +26,7 @@ import app.passwordstore.passgen.random.NoCharactersIncludedException
|
||||||
import app.passwordstore.passgen.random.PasswordGenerator
|
import app.passwordstore.passgen.random.PasswordGenerator
|
||||||
import app.passwordstore.passgen.random.PasswordLengthTooShortException
|
import app.passwordstore.passgen.random.PasswordLengthTooShortException
|
||||||
import app.passwordstore.passgen.random.PasswordOption
|
import app.passwordstore.passgen.random.PasswordOption
|
||||||
import app.passwordstore.ui.crypto.PasswordCreationActivity
|
import app.passwordstore.ui.crypto.PasswordCreationActivityV2
|
||||||
import app.passwordstore.util.settings.PreferenceKeys
|
import app.passwordstore.util.settings.PreferenceKeys
|
||||||
import com.github.michaelbull.result.getOrElse
|
import com.github.michaelbull.result.getOrElse
|
||||||
import com.github.michaelbull.result.runCatching
|
import com.github.michaelbull.result.runCatching
|
||||||
|
@ -72,8 +72,8 @@ class PasswordGeneratorDialogFragment : DialogFragment() {
|
||||||
setTitle(R.string.pwgen_title)
|
setTitle(R.string.pwgen_title)
|
||||||
setPositiveButton(R.string.dialog_ok) { _, _ ->
|
setPositiveButton(R.string.dialog_ok) { _, _ ->
|
||||||
setFragmentResult(
|
setFragmentResult(
|
||||||
PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY,
|
PasswordCreationActivityV2.PASSWORD_RESULT_REQUEST_KEY,
|
||||||
bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}")
|
bundleOf(PasswordCreationActivityV2.RESULT to "${binding.passwordText.text}")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setNeutralButton(R.string.dialog_cancel) { _, _ -> }
|
setNeutralButton(R.string.dialog_cancel) { _, _ -> }
|
||||||
|
|
|
@ -18,7 +18,6 @@ import app.passwordstore.util.git.operation.PullOperation
|
||||||
import app.passwordstore.util.git.operation.PushOperation
|
import app.passwordstore.util.git.operation.PushOperation
|
||||||
import app.passwordstore.util.git.operation.ResetToRemoteOperation
|
import app.passwordstore.util.git.operation.ResetToRemoteOperation
|
||||||
import app.passwordstore.util.git.operation.SyncOperation
|
import app.passwordstore.util.git.operation.SyncOperation
|
||||||
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
|
|
||||||
import app.passwordstore.util.settings.GitSettings
|
import app.passwordstore.util.settings.GitSettings
|
||||||
import app.passwordstore.util.settings.PreferenceKeys
|
import app.passwordstore.util.settings.PreferenceKeys
|
||||||
import com.github.michaelbull.result.Err
|
import com.github.michaelbull.result.Err
|
||||||
|
@ -42,7 +41,7 @@ import net.schmizz.sshj.userauth.UserAuthException
|
||||||
* git-related tasks and makes sense to be held here.
|
* git-related tasks and makes sense to be held here.
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
abstract class BaseGitActivity : ContinuationContainerActivity() {
|
abstract class BaseGitActivity : AppCompatActivity() {
|
||||||
|
|
||||||
/** Enum of possible Git operations than can be run through [launchGitOperation]. */
|
/** Enum of possible Git operations than can be run through [launchGitOperation]. */
|
||||||
enum class GitOp {
|
enum class GitOp {
|
||||||
|
|
|
@ -62,7 +62,6 @@ class GitServerConfigActivity : BaseGitActivity() {
|
||||||
when (newAuthMode) {
|
when (newAuthMode) {
|
||||||
AuthMode.SshKey -> check(binding.authModeSshKey.id)
|
AuthMode.SshKey -> check(binding.authModeSshKey.id)
|
||||||
AuthMode.Password -> check(binding.authModePassword.id)
|
AuthMode.Password -> check(binding.authModePassword.id)
|
||||||
AuthMode.OpenKeychain -> check(binding.authModeOpenKeychain.id)
|
|
||||||
AuthMode.None -> check(View.NO_ID)
|
AuthMode.None -> check(View.NO_ID)
|
||||||
}
|
}
|
||||||
addOnButtonCheckedListener { _, checkedId, isChecked ->
|
addOnButtonCheckedListener { _, checkedId, isChecked ->
|
||||||
|
@ -72,7 +71,6 @@ class GitServerConfigActivity : BaseGitActivity() {
|
||||||
}
|
}
|
||||||
when (checkedId) {
|
when (checkedId) {
|
||||||
binding.authModeSshKey.id -> newAuthMode = AuthMode.SshKey
|
binding.authModeSshKey.id -> newAuthMode = AuthMode.SshKey
|
||||||
binding.authModeOpenKeychain.id -> newAuthMode = AuthMode.OpenKeychain
|
|
||||||
binding.authModePassword.id -> newAuthMode = AuthMode.Password
|
binding.authModePassword.id -> newAuthMode = AuthMode.Password
|
||||||
View.NO_ID -> newAuthMode = AuthMode.None
|
View.NO_ID -> newAuthMode = AuthMode.None
|
||||||
}
|
}
|
||||||
|
@ -215,12 +213,10 @@ class GitServerConfigActivity : BaseGitActivity() {
|
||||||
with(binding) {
|
with(binding) {
|
||||||
if (isHttps) {
|
if (isHttps) {
|
||||||
authModeSshKey.isVisible = false
|
authModeSshKey.isVisible = false
|
||||||
authModeOpenKeychain.isVisible = false
|
|
||||||
authModePassword.isVisible = true
|
authModePassword.isVisible = true
|
||||||
if (authModeGroup.checkedButtonId != authModePassword.id) authModeGroup.check(View.NO_ID)
|
if (authModeGroup.checkedButtonId != authModePassword.id) authModeGroup.check(View.NO_ID)
|
||||||
} else {
|
} else {
|
||||||
authModeSshKey.isVisible = true
|
authModeSshKey.isVisible = true
|
||||||
authModeOpenKeychain.isVisible = true
|
|
||||||
authModePassword.isVisible = true
|
authModePassword.isVisible = true
|
||||||
if (authModeGroup.checkedButtonId == View.NO_ID) authModeGroup.check(authModeSshKey.id)
|
if (authModeGroup.checkedButtonId == View.NO_ID) authModeGroup.check(authModeSshKey.id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,13 +11,11 @@ import android.os.Looper
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import app.passwordstore.ui.crypto.BasePgpActivity
|
import app.passwordstore.ui.crypto.BasePgpActivity
|
||||||
import app.passwordstore.ui.crypto.DecryptActivity
|
|
||||||
import app.passwordstore.ui.crypto.DecryptActivityV2
|
import app.passwordstore.ui.crypto.DecryptActivityV2
|
||||||
import app.passwordstore.ui.passwords.PasswordStore
|
import app.passwordstore.ui.passwords.PasswordStore
|
||||||
import app.passwordstore.util.auth.BiometricAuthenticator
|
import app.passwordstore.util.auth.BiometricAuthenticator
|
||||||
import app.passwordstore.util.auth.BiometricAuthenticator.Result
|
import app.passwordstore.util.auth.BiometricAuthenticator.Result
|
||||||
import app.passwordstore.util.extensions.sharedPrefs
|
import app.passwordstore.util.extensions.sharedPrefs
|
||||||
import app.passwordstore.util.features.Feature
|
|
||||||
import app.passwordstore.util.features.Features
|
import app.passwordstore.util.features.Features
|
||||||
import app.passwordstore.util.settings.PreferenceKeys
|
import app.passwordstore.util.settings.PreferenceKeys
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
|
@ -5,65 +5,21 @@
|
||||||
|
|
||||||
package app.passwordstore.ui.onboarding.fragments
|
package app.passwordstore.ui.onboarding.fragments
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import app.passwordstore.R
|
import app.passwordstore.R
|
||||||
import app.passwordstore.data.repo.PasswordRepository
|
|
||||||
import app.passwordstore.databinding.FragmentKeySelectionBinding
|
import app.passwordstore.databinding.FragmentKeySelectionBinding
|
||||||
import app.passwordstore.ui.crypto.GetKeyIdsActivity
|
|
||||||
import app.passwordstore.util.extensions.commitChange
|
|
||||||
import app.passwordstore.util.extensions.finish
|
|
||||||
import app.passwordstore.util.extensions.sharedPrefs
|
|
||||||
import app.passwordstore.util.extensions.snackbar
|
|
||||||
import app.passwordstore.util.extensions.unsafeLazy
|
|
||||||
import app.passwordstore.util.extensions.viewBinding
|
import app.passwordstore.util.extensions.viewBinding
|
||||||
import app.passwordstore.util.settings.PreferenceKeys
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import java.io.File
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import me.msfjarvis.openpgpktx.util.OpenPgpApi
|
|
||||||
|
|
||||||
class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) {
|
class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) {
|
||||||
|
|
||||||
private val settings by unsafeLazy { requireActivity().applicationContext.sharedPrefs }
|
|
||||||
private val binding by viewBinding(FragmentKeySelectionBinding::bind)
|
private val binding by viewBinding(FragmentKeySelectionBinding::bind)
|
||||||
|
|
||||||
private val gpgKeySelectAction =
|
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
|
||||||
if (result.resultCode == AppCompatActivity.RESULT_OK) {
|
|
||||||
result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds ->
|
|
||||||
lifecycleScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id")
|
|
||||||
gpgIdentifierFile.writeText((keyIds + "").joinToString("\n"))
|
|
||||||
}
|
|
||||||
settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) }
|
|
||||||
requireActivity()
|
|
||||||
.commitChange(getString(R.string.git_commit_gpg_id, getString(R.string.app_name)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
requireActivity()
|
|
||||||
.snackbar(
|
|
||||||
message = getString(R.string.gpg_key_select_mandatory),
|
|
||||||
length = Snackbar.LENGTH_LONG
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding.selectKey.setOnClickListener {
|
binding.selectKey.setOnClickListener {
|
||||||
gpgKeySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java))
|
// TODO(msfjarvis): Restore this functionality
|
||||||
|
// gpgKeySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package app.passwordstore.util.crypto
|
|
||||||
|
|
||||||
import me.msfjarvis.openpgpktx.util.OpenPgpUtils
|
|
||||||
|
|
||||||
sealed class GpgIdentifier {
|
|
||||||
data class KeyId(val id: Long) : GpgIdentifier()
|
|
||||||
data class UserId(val email: String) : GpgIdentifier()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@OptIn(ExperimentalUnsignedTypes::class)
|
|
||||||
fun fromString(identifier: String): GpgIdentifier? {
|
|
||||||
if (identifier.isEmpty()) return null
|
|
||||||
// Match long key IDs:
|
|
||||||
// FF22334455667788 or 0xFF22334455667788
|
|
||||||
val maybeLongKeyId =
|
|
||||||
identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F0-9]{16}".toRegex()) }
|
|
||||||
if (maybeLongKeyId != null) {
|
|
||||||
val keyId = maybeLongKeyId.toULong(16)
|
|
||||||
return KeyId(keyId.toLong())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match fingerprints:
|
|
||||||
// FF223344556677889900112233445566778899 or 0xFF223344556677889900112233445566778899
|
|
||||||
val maybeFingerprint =
|
|
||||||
identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F0-9]{40}".toRegex()) }
|
|
||||||
if (maybeFingerprint != null) {
|
|
||||||
// Truncating to the long key ID is not a security issue since OpenKeychain only
|
|
||||||
// accepts
|
|
||||||
// non-ambiguous key IDs.
|
|
||||||
val keyId = maybeFingerprint.takeLast(16).toULong(16)
|
|
||||||
return KeyId(keyId.toLong())
|
|
||||||
}
|
|
||||||
|
|
||||||
return OpenPgpUtils.splitUserId(identifier).email?.let { UserId(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,16 +4,15 @@
|
||||||
*/
|
*/
|
||||||
package app.passwordstore.util.git.operation
|
package app.passwordstore.util.git.operation
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import app.passwordstore.R
|
import app.passwordstore.R
|
||||||
import app.passwordstore.util.extensions.unsafeLazy
|
import app.passwordstore.util.extensions.unsafeLazy
|
||||||
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.eclipse.jgit.api.RebaseCommand
|
import org.eclipse.jgit.api.RebaseCommand
|
||||||
import org.eclipse.jgit.api.ResetCommand
|
import org.eclipse.jgit.api.ResetCommand
|
||||||
import org.eclipse.jgit.lib.RepositoryState
|
import org.eclipse.jgit.lib.RepositoryState
|
||||||
|
|
||||||
class BreakOutOfDetached(callingActivity: ContinuationContainerActivity) :
|
class BreakOutOfDetached(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
|
||||||
GitOperation(callingActivity) {
|
|
||||||
|
|
||||||
private val merging = repository.repositoryState == RepositoryState.MERGING
|
private val merging = repository.repositoryState == RepositoryState.MERGING
|
||||||
private val resetCommands =
|
private val resetCommands =
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
package app.passwordstore.util.git.operation
|
package app.passwordstore.util.git.operation
|
||||||
|
|
||||||
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.api.GitCommand
|
import org.eclipse.jgit.api.GitCommand
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import org.eclipse.jgit.api.GitCommand
|
||||||
* @param uri URL to clone the repository from
|
* @param uri URL to clone the repository from
|
||||||
* @param callingActivity the calling activity
|
* @param callingActivity the calling activity
|
||||||
*/
|
*/
|
||||||
class CloneOperation(callingActivity: ContinuationContainerActivity, uri: String) :
|
class CloneOperation(callingActivity: AppCompatActivity, uri: String) :
|
||||||
GitOperation(callingActivity) {
|
GitOperation(callingActivity) {
|
||||||
|
|
||||||
override val commands: Array<GitCommand<out Any>> =
|
override val commands: Array<GitCommand<out Any>> =
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
package app.passwordstore.util.git.operation
|
package app.passwordstore.util.git.operation
|
||||||
|
|
||||||
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import org.eclipse.jgit.api.GitCommand
|
import org.eclipse.jgit.api.GitCommand
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,7 +13,7 @@ import org.eclipse.jgit.api.GitCommand
|
||||||
* achieve the best compression.
|
* achieve the best compression.
|
||||||
*/
|
*/
|
||||||
class GcOperation(
|
class GcOperation(
|
||||||
callingActivity: ContinuationContainerActivity,
|
callingActivity: AppCompatActivity,
|
||||||
) : GitOperation(callingActivity) {
|
) : GitOperation(callingActivity) {
|
||||||
|
|
||||||
override val requiresAuth: Boolean = false
|
override val requiresAuth: Boolean = false
|
||||||
|
|
|
@ -6,6 +6,7 @@ package app.passwordstore.util.git.operation
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import app.passwordstore.R
|
import app.passwordstore.R
|
||||||
import app.passwordstore.data.repo.PasswordRepository
|
import app.passwordstore.data.repo.PasswordRepository
|
||||||
|
@ -14,7 +15,6 @@ import app.passwordstore.ui.sshkeygen.SshKeyImportActivity
|
||||||
import app.passwordstore.util.auth.BiometricAuthenticator
|
import app.passwordstore.util.auth.BiometricAuthenticator
|
||||||
import app.passwordstore.util.auth.BiometricAuthenticator.Result.*
|
import app.passwordstore.util.auth.BiometricAuthenticator.Result.*
|
||||||
import app.passwordstore.util.git.GitCommandExecutor
|
import app.passwordstore.util.git.GitCommandExecutor
|
||||||
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
|
|
||||||
import app.passwordstore.util.git.sshj.SshAuthMethod
|
import app.passwordstore.util.git.sshj.SshAuthMethod
|
||||||
import app.passwordstore.util.git.sshj.SshKey
|
import app.passwordstore.util.git.sshj.SshKey
|
||||||
import app.passwordstore.util.git.sshj.SshjSessionFactory
|
import app.passwordstore.util.git.sshj.SshjSessionFactory
|
||||||
|
@ -74,7 +74,7 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
|
||||||
protected val git = Git(repository)
|
protected val git = Git(repository)
|
||||||
protected val remoteBranch = hiltEntryPoint.gitSettings().branch
|
protected val remoteBranch = hiltEntryPoint.gitSettings().branch
|
||||||
private val authActivity
|
private val authActivity
|
||||||
get() = callingActivity as ContinuationContainerActivity
|
get() = callingActivity as AppCompatActivity
|
||||||
|
|
||||||
private class HttpsCredentialsProvider(private val passwordFinder: PasswordFinder) :
|
private class HttpsCredentialsProvider(private val passwordFinder: PasswordFinder) :
|
||||||
CredentialsProvider() {
|
CredentialsProvider() {
|
||||||
|
@ -213,7 +213,6 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
|
||||||
// error, allowing users to make the SSH key selection.
|
// error, allowing users to make the SSH key selection.
|
||||||
return Err(SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER))
|
return Err(SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER))
|
||||||
}
|
}
|
||||||
AuthMode.OpenKeychain -> registerAuthProviders(SshAuthMethod.OpenKeychain(authActivity))
|
|
||||||
AuthMode.Password -> {
|
AuthMode.Password -> {
|
||||||
val httpsCredentialProvider =
|
val httpsCredentialProvider =
|
||||||
HttpsCredentialsProvider(CredentialFinder(callingActivity, AuthMode.Password))
|
HttpsCredentialsProvider(CredentialFinder(callingActivity, AuthMode.Password))
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
*/
|
*/
|
||||||
package app.passwordstore.util.git.operation
|
package app.passwordstore.util.git.operation
|
||||||
|
|
||||||
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import org.eclipse.jgit.api.GitCommand
|
import org.eclipse.jgit.api.GitCommand
|
||||||
|
|
||||||
class PullOperation(
|
class PullOperation(
|
||||||
callingActivity: ContinuationContainerActivity,
|
callingActivity: AppCompatActivity,
|
||||||
rebase: Boolean,
|
rebase: Boolean,
|
||||||
) : GitOperation(callingActivity) {
|
) : GitOperation(callingActivity) {
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,10 @@
|
||||||
*/
|
*/
|
||||||
package app.passwordstore.util.git.operation
|
package app.passwordstore.util.git.operation
|
||||||
|
|
||||||
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import org.eclipse.jgit.api.GitCommand
|
import org.eclipse.jgit.api.GitCommand
|
||||||
|
|
||||||
class PushOperation(callingActivity: ContinuationContainerActivity) :
|
class PushOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
|
||||||
GitOperation(callingActivity) {
|
|
||||||
|
|
||||||
override val commands: Array<GitCommand<out Any>> =
|
override val commands: Array<GitCommand<out Any>> =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
|
|
|
@ -4,11 +4,10 @@
|
||||||
*/
|
*/
|
||||||
package app.passwordstore.util.git.operation
|
package app.passwordstore.util.git.operation
|
||||||
|
|
||||||
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import org.eclipse.jgit.api.ResetCommand
|
import org.eclipse.jgit.api.ResetCommand
|
||||||
|
|
||||||
class ResetToRemoteOperation(callingActivity: ContinuationContainerActivity) :
|
class ResetToRemoteOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
|
||||||
GitOperation(callingActivity) {
|
|
||||||
|
|
||||||
override val commands =
|
override val commands =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
*/
|
*/
|
||||||
package app.passwordstore.util.git.operation
|
package app.passwordstore.util.git.operation
|
||||||
|
|
||||||
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
|
||||||
class SyncOperation(
|
class SyncOperation(
|
||||||
callingActivity: ContinuationContainerActivity,
|
callingActivity: AppCompatActivity,
|
||||||
rebase: Boolean,
|
rebase: Boolean,
|
||||||
) : GitOperation(callingActivity) {
|
) : GitOperation(callingActivity) {
|
||||||
|
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
*/
|
|
||||||
package app.passwordstore.util.git.sshj
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.LayoutRes
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
import net.schmizz.sshj.common.DisconnectReason
|
|
||||||
import net.schmizz.sshj.userauth.UserAuthException
|
|
||||||
|
|
||||||
/** Workaround for https://msfjarvis.dev/aps/issue/1164 */
|
|
||||||
open class ContinuationContainerActivity : AppCompatActivity {
|
|
||||||
|
|
||||||
constructor() : super()
|
|
||||||
constructor(@LayoutRes layoutRes: Int) : super(layoutRes)
|
|
||||||
|
|
||||||
var stashedCont: Continuation<Intent>? = null
|
|
||||||
|
|
||||||
val continueAfterUserInteraction =
|
|
||||||
registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
|
|
||||||
stashedCont?.let { cont ->
|
|
||||||
stashedCont = null
|
|
||||||
val data = result.data
|
|
||||||
if (data != null) cont.resume(data)
|
|
||||||
else cont.resumeWithException(UserAuthException(DisconnectReason.AUTH_CANCELLED_BY_USER))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,225 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
*/
|
|
||||||
package app.passwordstore.util.git.sshj
|
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.activity.result.IntentSenderRequest
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import app.passwordstore.util.extensions.OPENPGP_PROVIDER
|
|
||||||
import app.passwordstore.util.extensions.sharedPrefs
|
|
||||||
import app.passwordstore.util.settings.PreferenceKeys
|
|
||||||
import java.io.Closeable
|
|
||||||
import java.security.PublicKey
|
|
||||||
import java.security.interfaces.ECKey
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import logcat.logcat
|
|
||||||
import net.schmizz.sshj.common.DisconnectReason
|
|
||||||
import net.schmizz.sshj.common.KeyType
|
|
||||||
import net.schmizz.sshj.userauth.UserAuthException
|
|
||||||
import net.schmizz.sshj.userauth.keyprovider.KeyProvider
|
|
||||||
import org.openintents.ssh.authentication.ISshAuthenticationService
|
|
||||||
import org.openintents.ssh.authentication.SshAuthenticationApi
|
|
||||||
import org.openintents.ssh.authentication.SshAuthenticationApiError
|
|
||||||
import org.openintents.ssh.authentication.SshAuthenticationConnection
|
|
||||||
import org.openintents.ssh.authentication.request.KeySelectionRequest
|
|
||||||
import org.openintents.ssh.authentication.request.Request
|
|
||||||
import org.openintents.ssh.authentication.request.SigningRequest
|
|
||||||
import org.openintents.ssh.authentication.request.SshPublicKeyRequest
|
|
||||||
import org.openintents.ssh.authentication.response.KeySelectionResponse
|
|
||||||
import org.openintents.ssh.authentication.response.Response
|
|
||||||
import org.openintents.ssh.authentication.response.SigningResponse
|
|
||||||
import org.openintents.ssh.authentication.response.SshPublicKeyResponse
|
|
||||||
|
|
||||||
class OpenKeychainKeyProvider private constructor(val activity: ContinuationContainerActivity) :
|
|
||||||
KeyProvider, Closeable {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
suspend fun prepareAndUse(
|
|
||||||
activity: ContinuationContainerActivity,
|
|
||||||
block: (provider: OpenKeychainKeyProvider) -> Unit
|
|
||||||
) {
|
|
||||||
withContext(Dispatchers.Main) { OpenKeychainKeyProvider(activity) }.prepareAndUse(block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ApiResponse {
|
|
||||||
data class Success(val response: Response) : ApiResponse()
|
|
||||||
data class GeneralError(val exception: Exception) : ApiResponse()
|
|
||||||
data class NoSuchKey(val exception: Exception) : ApiResponse()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val context = activity.applicationContext
|
|
||||||
private val sshServiceConnection = SshAuthenticationConnection(context, OPENPGP_PROVIDER)
|
|
||||||
private val preferences = context.sharedPrefs
|
|
||||||
private lateinit var sshServiceApi: SshAuthenticationApi
|
|
||||||
|
|
||||||
private var keyId
|
|
||||||
get() = preferences.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null)
|
|
||||||
set(value) {
|
|
||||||
preferences.edit { putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, value) }
|
|
||||||
}
|
|
||||||
private var publicKey: PublicKey? = null
|
|
||||||
private var privateKey: OpenKeychainPrivateKey? = null
|
|
||||||
|
|
||||||
private suspend fun prepareAndUse(block: (provider: OpenKeychainKeyProvider) -> Unit) {
|
|
||||||
prepare()
|
|
||||||
use(block)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun prepare() {
|
|
||||||
sshServiceApi = suspendCoroutine { cont ->
|
|
||||||
sshServiceConnection.connect(
|
|
||||||
object : SshAuthenticationConnection.OnBound {
|
|
||||||
override fun onBound(sshAgent: ISshAuthenticationService) {
|
|
||||||
logcat { "Bound to SshAuthenticationApi: $OPENPGP_PROVIDER" }
|
|
||||||
cont.resume(SshAuthenticationApi(context, sshAgent))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError() {
|
|
||||||
throw UserAuthException(DisconnectReason.UNKNOWN, "OpenKeychain service unavailable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyId == null) {
|
|
||||||
selectKey()
|
|
||||||
}
|
|
||||||
check(keyId != null)
|
|
||||||
fetchPublicKey()
|
|
||||||
makePrivateKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchPublicKey(isRetry: Boolean = false) {
|
|
||||||
when (val sshPublicKeyResponse = executeApiRequest(SshPublicKeyRequest(keyId))) {
|
|
||||||
is ApiResponse.Success -> {
|
|
||||||
val response = sshPublicKeyResponse.response as SshPublicKeyResponse
|
|
||||||
val sshPublicKey = response.sshPublicKey!!
|
|
||||||
publicKey =
|
|
||||||
parseSshPublicKey(sshPublicKey)
|
|
||||||
?: throw IllegalStateException("OpenKeychain API returned invalid SSH key")
|
|
||||||
}
|
|
||||||
is ApiResponse.NoSuchKey ->
|
|
||||||
if (isRetry) {
|
|
||||||
throw sshPublicKeyResponse.exception
|
|
||||||
} else {
|
|
||||||
// Allow the user to reselect an authentication key and retry
|
|
||||||
selectKey()
|
|
||||||
fetchPublicKey(true)
|
|
||||||
}
|
|
||||||
is ApiResponse.GeneralError -> throw sshPublicKeyResponse.exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun selectKey() {
|
|
||||||
when (val keySelectionResponse = executeApiRequest(KeySelectionRequest())) {
|
|
||||||
is ApiResponse.Success ->
|
|
||||||
keyId = (keySelectionResponse.response as KeySelectionResponse).keyId
|
|
||||||
is ApiResponse.GeneralError -> throw keySelectionResponse.exception
|
|
||||||
is ApiResponse.NoSuchKey -> throw keySelectionResponse.exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun executeApiRequest(
|
|
||||||
request: Request,
|
|
||||||
resultOfUserInteraction: Intent? = null
|
|
||||||
): ApiResponse {
|
|
||||||
logcat { "executeRequest($request) called" }
|
|
||||||
val result =
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
// If the request required user interaction, the data returned from the
|
|
||||||
// PendingIntent
|
|
||||||
// is used as the real request.
|
|
||||||
sshServiceApi.executeApi(resultOfUserInteraction ?: request.toIntent())!!
|
|
||||||
}
|
|
||||||
return parseResult(request, result).also { logcat { "executeRequest($request): $it" } }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun parseResult(request: Request, result: Intent): ApiResponse {
|
|
||||||
return when (
|
|
||||||
result.getIntExtra(
|
|
||||||
SshAuthenticationApi.EXTRA_RESULT_CODE,
|
|
||||||
SshAuthenticationApi.RESULT_CODE_ERROR
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
SshAuthenticationApi.RESULT_CODE_SUCCESS -> {
|
|
||||||
ApiResponse.Success(
|
|
||||||
when (request) {
|
|
||||||
is KeySelectionRequest -> KeySelectionResponse(result)
|
|
||||||
is SshPublicKeyRequest -> SshPublicKeyResponse(result)
|
|
||||||
is SigningRequest -> SigningResponse(result)
|
|
||||||
else -> throw IllegalArgumentException("Unsupported OpenKeychain request type")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
|
|
||||||
val pendingIntent: PendingIntent =
|
|
||||||
result.getParcelableExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT)!!
|
|
||||||
val resultOfUserInteraction: Intent =
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
suspendCoroutine { cont ->
|
|
||||||
activity.stashedCont = cont
|
|
||||||
activity.continueAfterUserInteraction.launch(
|
|
||||||
IntentSenderRequest.Builder(pendingIntent).build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
executeApiRequest(request, resultOfUserInteraction)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val error =
|
|
||||||
result.getParcelableExtra<SshAuthenticationApiError>(SshAuthenticationApi.EXTRA_ERROR)
|
|
||||||
val exception =
|
|
||||||
UserAuthException(
|
|
||||||
DisconnectReason.UNKNOWN,
|
|
||||||
"Request ${request::class.simpleName} failed: ${error?.message}"
|
|
||||||
)
|
|
||||||
when (error?.error) {
|
|
||||||
SshAuthenticationApiError.NO_AUTH_KEY,
|
|
||||||
SshAuthenticationApiError.NO_SUCH_KEY -> ApiResponse.NoSuchKey(exception)
|
|
||||||
else -> ApiResponse.GeneralError(exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun makePrivateKey() {
|
|
||||||
check(keyId != null && publicKey != null)
|
|
||||||
privateKey =
|
|
||||||
object : OpenKeychainPrivateKey {
|
|
||||||
override suspend fun sign(challenge: ByteArray, hashAlgorithm: Int) =
|
|
||||||
when (
|
|
||||||
val signingResponse = executeApiRequest(SigningRequest(challenge, keyId, hashAlgorithm))
|
|
||||||
) {
|
|
||||||
is ApiResponse.Success -> (signingResponse.response as SigningResponse).signature
|
|
||||||
is ApiResponse.GeneralError -> throw signingResponse.exception
|
|
||||||
is ApiResponse.NoSuchKey -> throw signingResponse.exception
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAlgorithm() = publicKey!!.algorithm
|
|
||||||
override fun getParams() = (publicKey as? ECKey)?.params
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
activity.lifecycleScope.launch {
|
|
||||||
withContext(Dispatchers.Main) { activity.continueAfterUserInteraction.unregister() }
|
|
||||||
}
|
|
||||||
sshServiceConnection.disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPrivate() = privateKey
|
|
||||||
|
|
||||||
override fun getPublic() = publicKey
|
|
||||||
|
|
||||||
override fun getType(): KeyType = KeyType.fromKey(publicKey)
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
*/
|
|
||||||
package app.passwordstore.util.git.sshj
|
|
||||||
|
|
||||||
import com.hierynomus.sshj.key.KeyAlgorithm
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.security.PrivateKey
|
|
||||||
import java.security.interfaces.ECKey
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import net.schmizz.sshj.common.Buffer
|
|
||||||
import net.schmizz.sshj.common.Factory
|
|
||||||
import net.schmizz.sshj.signature.Signature
|
|
||||||
import org.openintents.ssh.authentication.SshAuthenticationApi
|
|
||||||
|
|
||||||
interface OpenKeychainPrivateKey : PrivateKey, ECKey {
|
|
||||||
|
|
||||||
suspend fun sign(challenge: ByteArray, hashAlgorithm: Int): ByteArray
|
|
||||||
|
|
||||||
override fun getFormat() = null
|
|
||||||
override fun getEncoded() = null
|
|
||||||
}
|
|
||||||
|
|
||||||
class OpenKeychainWrappedKeyAlgorithmFactory(private val factory: Factory.Named<KeyAlgorithm>) :
|
|
||||||
Factory.Named<KeyAlgorithm> by factory {
|
|
||||||
|
|
||||||
override fun create() = OpenKeychainWrappedKeyAlgorithm(factory.create())
|
|
||||||
}
|
|
||||||
|
|
||||||
class OpenKeychainWrappedKeyAlgorithm(private val keyAlgorithm: KeyAlgorithm) :
|
|
||||||
KeyAlgorithm by keyAlgorithm {
|
|
||||||
|
|
||||||
private val hashAlgorithm =
|
|
||||||
when (keyAlgorithm.keyAlgorithm) {
|
|
||||||
"rsa-sha2-512" -> SshAuthenticationApi.SHA512
|
|
||||||
"rsa-sha2-256" -> SshAuthenticationApi.SHA256
|
|
||||||
"ssh-rsa",
|
|
||||||
"ssh-rsa-cert-v01@openssh.com" -> SshAuthenticationApi.SHA1
|
|
||||||
// Other algorithms don't use this value, but it has to be valid.
|
|
||||||
else -> SshAuthenticationApi.SHA512
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newSignature() =
|
|
||||||
OpenKeychainWrappedSignature(keyAlgorithm.newSignature(), hashAlgorithm)
|
|
||||||
}
|
|
||||||
|
|
||||||
class OpenKeychainWrappedSignature(
|
|
||||||
private val wrappedSignature: Signature,
|
|
||||||
private val hashAlgorithm: Int
|
|
||||||
) : Signature by wrappedSignature {
|
|
||||||
|
|
||||||
private val data = ByteArrayOutputStream()
|
|
||||||
|
|
||||||
private var bridgedPrivateKey: OpenKeychainPrivateKey? = null
|
|
||||||
|
|
||||||
override fun initSign(prvkey: PrivateKey?) {
|
|
||||||
if (prvkey is OpenKeychainPrivateKey) {
|
|
||||||
bridgedPrivateKey = prvkey
|
|
||||||
} else {
|
|
||||||
wrappedSignature.initSign(prvkey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(H: ByteArray?) {
|
|
||||||
if (bridgedPrivateKey != null) {
|
|
||||||
data.write(H!!)
|
|
||||||
} else {
|
|
||||||
wrappedSignature.update(H)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(H: ByteArray?, off: Int, len: Int) {
|
|
||||||
if (bridgedPrivateKey != null) {
|
|
||||||
data.write(H!!, off, len)
|
|
||||||
} else {
|
|
||||||
wrappedSignature.update(H, off, len)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sign(): ByteArray? =
|
|
||||||
if (bridgedPrivateKey != null) {
|
|
||||||
runBlocking { bridgedPrivateKey!!.sign(data.toByteArray(), hashAlgorithm) }
|
|
||||||
} else {
|
|
||||||
wrappedSignature.sign()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun encode(signature: ByteArray?): ByteArray? =
|
|
||||||
if (bridgedPrivateKey != null) {
|
|
||||||
require(signature != null) { "OpenKeychain signature must not be null" }
|
|
||||||
val encodedSignature = Buffer.PlainBuffer(signature)
|
|
||||||
// We need to drop the algorithm name and extract the raw signature since SSHJ adds the
|
|
||||||
// name
|
|
||||||
// later.
|
|
||||||
encodedSignature.readString()
|
|
||||||
encodedSignature.readBytes().also {
|
|
||||||
bridgedPrivateKey = null
|
|
||||||
data.reset()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
wrappedSignature.encode(signature)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -241,7 +241,6 @@ class SshjConfig : ConfigImpl() {
|
||||||
KeyAlgorithms.RSASHA256(),
|
KeyAlgorithms.RSASHA256(),
|
||||||
KeyAlgorithms.SSHRSA(),
|
KeyAlgorithms.SSHRSA(),
|
||||||
)
|
)
|
||||||
.map { OpenKeychainWrappedKeyAlgorithmFactory(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initRandomFactory() {
|
private fun initRandomFactory() {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
package app.passwordstore.util.git.sshj
|
package app.passwordstore.util.git.sshj
|
||||||
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import app.passwordstore.util.git.operation.CredentialFinder
|
import app.passwordstore.util.git.operation.CredentialFinder
|
||||||
import app.passwordstore.util.settings.AuthMode
|
import app.passwordstore.util.settings.AuthMode
|
||||||
import com.github.michaelbull.result.getOrElse
|
import com.github.michaelbull.result.getOrElse
|
||||||
|
@ -41,10 +42,9 @@ import org.eclipse.jgit.transport.SshSessionFactory
|
||||||
import org.eclipse.jgit.transport.URIish
|
import org.eclipse.jgit.transport.URIish
|
||||||
import org.eclipse.jgit.util.FS
|
import org.eclipse.jgit.util.FS
|
||||||
|
|
||||||
sealed class SshAuthMethod(val activity: ContinuationContainerActivity) {
|
sealed class SshAuthMethod(val activity: AppCompatActivity) {
|
||||||
class Password(activity: ContinuationContainerActivity) : SshAuthMethod(activity)
|
class Password(activity: AppCompatActivity) : SshAuthMethod(activity)
|
||||||
class SshKey(activity: ContinuationContainerActivity) : SshAuthMethod(activity)
|
class SshKey(activity: AppCompatActivity) : SshAuthMethod(activity)
|
||||||
class OpenKeychain(activity: ContinuationContainerActivity) : SshAuthMethod(activity)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class InteractivePasswordFinder : PasswordFinder {
|
abstract class InteractivePasswordFinder : PasswordFinder {
|
||||||
|
@ -157,14 +157,6 @@ private class SshjSession(
|
||||||
AuthPublickey(SshKey.provide(ssh, CredentialFinder(authMethod.activity, AuthMode.SshKey)))
|
AuthPublickey(SshKey.provide(ssh, CredentialFinder(authMethod.activity, AuthMode.SshKey)))
|
||||||
ssh.auth(username, pubkeyAuth, passwordAuth)
|
ssh.auth(username, pubkeyAuth, passwordAuth)
|
||||||
}
|
}
|
||||||
is SshAuthMethod.OpenKeychain -> {
|
|
||||||
runBlocking {
|
|
||||||
OpenKeychainKeyProvider.prepareAndUse(authMethod.activity) { provider ->
|
|
||||||
val openKeychainAuth = AuthPublickey(provider)
|
|
||||||
ssh.auth(username, openKeychainAuth, passwordAuth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,6 @@ enum class Protocol(val pref: String) {
|
||||||
enum class AuthMode(val pref: String) {
|
enum class AuthMode(val pref: String) {
|
||||||
SshKey("ssh-key"),
|
SshKey("ssh-key"),
|
||||||
Password("username/password"),
|
Password("username/password"),
|
||||||
OpenKeychain("OpenKeychain"),
|
|
||||||
None("None"),
|
None("None"),
|
||||||
;
|
;
|
||||||
|
|
||||||
|
@ -156,7 +155,7 @@ constructor(
|
||||||
)
|
)
|
||||||
return UpdateConnectionSettingsResult.MissingUsername(newProtocol)
|
return UpdateConnectionSettingsResult.MissingUsername(newProtocol)
|
||||||
val validHttpsAuth = listOf(AuthMode.None, AuthMode.Password)
|
val validHttpsAuth = listOf(AuthMode.None, AuthMode.Password)
|
||||||
val validSshAuth = listOf(AuthMode.OpenKeychain, AuthMode.Password, AuthMode.SshKey)
|
val validSshAuth = listOf(AuthMode.Password, AuthMode.SshKey)
|
||||||
when {
|
when {
|
||||||
newProtocol == Protocol.Https && newAuthMode !in validHttpsAuth -> {
|
newProtocol == Protocol.Https && newAuthMode !in validHttpsAuth -> {
|
||||||
return UpdateConnectionSettingsResult.AuthModeMismatch(newProtocol, validHttpsAuth)
|
return UpdateConnectionSettingsResult.AuthModeMismatch(newProtocol, validHttpsAuth)
|
||||||
|
|
|
@ -89,12 +89,6 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/connection_mode_basic_authentication" />
|
android:text="@string/connection_mode_basic_authentication" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/auth_mode_open_keychain"
|
|
||||||
style="?attr/materialButtonOutlinedStyle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/connection_mode_openkeychain" />
|
|
||||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingTop="16dp"
|
android:paddingTop="16dp"
|
||||||
tools:context="app.passwordstore.ui.crypto.DecryptActivity">
|
tools:context="app.passwordstore.ui.crypto.DecryptActivityV2">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
android:id="@+id/password_category"
|
android:id="@+id/password_category"
|
||||||
|
|
|
@ -26,9 +26,11 @@
|
||||||
<requestFocus />
|
<requestFocus />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- TODO(msfjarvis): Restore this functionality -->
|
||||||
<com.google.android.material.checkbox.MaterialCheckBox
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
android:id="@+id/set_gpg_key"
|
android:id="@+id/set_gpg_key"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
android:visibility="gone"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/new_folder_set_gpg_key"
|
android:text="@string/new_folder_set_gpg_key"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:fillViewport="false"
|
android:fillViewport="false"
|
||||||
tools:context="app.passwordstore.ui.crypto.PasswordCreationActivity">
|
tools:context="app.passwordstore.ui.crypto.PasswordCreationActivityV2">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package app.passwordstore.util.crypto
|
|
||||||
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertNotNull
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class GpgIdentifierTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun parseHexKeyIdWithout0xPrefix() {
|
|
||||||
val identifier = GpgIdentifier.fromString("79E8208280490C77")
|
|
||||||
assertNotNull(identifier)
|
|
||||||
assertTrue { identifier is GpgIdentifier.KeyId }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun parseHexKeyId() {
|
|
||||||
val identifier = GpgIdentifier.fromString("0x79E8208280490C77")
|
|
||||||
assertNotNull(identifier)
|
|
||||||
assertTrue { identifier is GpgIdentifier.KeyId }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun parseValidEmail() {
|
|
||||||
val identifier = GpgIdentifier.fromString("john.doe@example.org")
|
|
||||||
assertNotNull(identifier)
|
|
||||||
assertTrue { identifier is GpgIdentifier.UserId }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun parseEmailWithoutTLD() {
|
|
||||||
val identifier = GpgIdentifier.fromString("john.doe@example")
|
|
||||||
assertNotNull(identifier)
|
|
||||||
assertTrue { identifier is GpgIdentifier.UserId }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -87,7 +87,6 @@ thirdparty-nonfree-googlePlayAuthApiPhone = "com.google.android.gms:play-service
|
||||||
thirdparty-nonfree-sentry = "io.sentry:sentry-android:6.2.1"
|
thirdparty-nonfree-sentry = "io.sentry:sentry-android:6.2.1"
|
||||||
thirdparty-pgpainless = "org.pgpainless:pgpainless-core:1.3.1"
|
thirdparty-pgpainless = "org.pgpainless:pgpainless-core:1.3.1"
|
||||||
thirdparty-plumber = { module = "com.squareup.leakcanary:plumber-android-startup", version.ref = "leakcanary" }
|
thirdparty-plumber = { module = "com.squareup.leakcanary:plumber-android-startup", version.ref = "leakcanary" }
|
||||||
thirdparty-sshauth = "com.github.open-keychain.open-keychain:sshauthentication-api:5.7.5"
|
|
||||||
# TODO: Remove the explicit bcpkix dependency when upgrading this to a BC 1.71 compatible version
|
# TODO: Remove the explicit bcpkix dependency when upgrading this to a BC 1.71 compatible version
|
||||||
thirdparty-sshj = "com.hierynomus:sshj:0.33.0"
|
thirdparty-sshj = "com.hierynomus:sshj:0.33.0"
|
||||||
thirdparty-whatthestack = "com.github.haroldadmin:WhatTheStack:1.0.0-alpha04"
|
thirdparty-whatthestack = "com.github.haroldadmin:WhatTheStack:1.0.0-alpha04"
|
||||||
|
|
|
@ -91,10 +91,7 @@ dependencyResolutionManagement {
|
||||||
}
|
}
|
||||||
exclusiveContent {
|
exclusiveContent {
|
||||||
forRepository { maven("https://jitpack.io") }
|
forRepository { maven("https://jitpack.io") }
|
||||||
filter {
|
filter { includeModule("com.github.haroldadmin", "WhatTheStack") }
|
||||||
includeModule("com.github.haroldadmin", "WhatTheStack")
|
|
||||||
includeModule("com.github.open-keychain.open-keychain", "sshauthentication-api")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
exclusiveContent {
|
exclusiveContent {
|
||||||
forRepository { maven("https://storage.googleapis.com/r8-releases/raw") }
|
forRepository { maven("https://storage.googleapis.com/r8-releases/raw") }
|
||||||
|
|
Loading…
Reference in a new issue