Remove OpenKeychain code and leave TODOs for missing functionality

This commit is contained in:
Harsh Shandilya 2022-07-15 14:00:15 +05:30
parent a6bcdd1d9d
commit bcf33e90a5
No known key found for this signature in database
GPG key ID: 366D7BBAD1031E80
39 changed files with 61 additions and 1978 deletions

View file

@ -48,7 +48,6 @@ dependencies {
implementation(projects.coroutineUtils)
implementation(projects.cryptoPgpainless)
implementation(projects.formatCommon)
implementation(projects.openpgpKtx)
implementation(projects.passgen.diceware)
implementation(projects.passgen.random)
implementation(projects.uiCompose)
@ -85,7 +84,6 @@ dependencies {
implementation(libs.thirdparty.logcat)
implementation(libs.thirdparty.modernAndroidPrefs)
implementation(libs.thirdparty.plumber)
implementation(libs.thirdparty.sshauth)
implementation(libs.thirdparty.sshj) { exclude(group = "org.bouncycastle") }
implementation(libs.thirdparty.bouncycastle.bcprov)
implementation(libs.thirdparty.bouncycastle.bcpkix)

View file

@ -95,28 +95,12 @@
android:label="@string/action_settings"
android:parentActivityName=".ui.passwords.PasswordStore" />
<activity
android:name=".ui.crypto.PasswordCreationActivity"
android:exported="false"
android:label="@string/new_password_title"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.crypto.PasswordCreationActivityV2"
android:exported="false"
android:label="@string/new_password_title"
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
android:name=".util.services.ClipboardService"
android:exported="false"
@ -150,10 +134,6 @@
android:exported="false"
android:label="@string/pref_ssh_keygen_title"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.autofill.AutofillDecryptActivity"
android:exported="false"
android:theme="@style/NoBackgroundThemeM3" />
<activity
android:name=".ui.autofill.AutofillDecryptActivityV2"
android:exported="false"

View file

@ -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
}
}

View file

@ -15,7 +15,6 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import app.passwordstore.data.repo.PasswordRepository
import app.passwordstore.ui.crypto.PasswordCreationActivity
import app.passwordstore.ui.crypto.PasswordCreationActivityV2
import app.passwordstore.util.autofill.AutofillMatcher
import app.passwordstore.util.autofill.AutofillPreferences
@ -114,9 +113,9 @@ class AutofillSaveActivity : AppCompatActivity() {
bundleOf(
"REPO_PATH" to repo.absolutePath,
"FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath,
PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME),
PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD),
PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to
PasswordCreationActivityV2.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME),
PasswordCreationActivityV2.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD),
PasswordCreationActivityV2.EXTRA_GENERATE_PASSWORD to
intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
)
)

View file

@ -5,12 +5,9 @@
package app.passwordstore.ui.crypto
import android.app.PendingIntent
import android.content.ClipData
import android.content.Intent
import android.content.IntentSender
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
@ -19,33 +16,20 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import app.passwordstore.R
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.getString
import app.passwordstore.util.extensions.snackbar
import app.passwordstore.util.extensions.unsafeLazy
import app.passwordstore.util.features.Features
import app.passwordstore.util.services.ClipboardService
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 dagger.hilt.android.AndroidEntryPoint
import java.io.File
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")
@AndroidEntryPoint
open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
open class BasePgpActivity : AppCompatActivity() {
/** Full path to the repository */
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 */
@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
* recent apps screen.
@ -87,124 +57,6 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
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
* [showSnackbar] as false.
@ -251,7 +103,6 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
companion object {
private const val TAG = "APS/BasePgpActivity"
const val EXTRA_FILE_PATH = "FILE_PATH"
const val EXTRA_REPO_PATH = "REPO_PATH"

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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"
}
}

View file

@ -134,7 +134,6 @@ class PasswordCreationActivityV2 : BasePgpActivity() {
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) {

View file

@ -19,7 +19,7 @@ import app.passwordstore.R
import app.passwordstore.databinding.FragmentPwgenDicewareBinding
import app.passwordstore.injection.prefs.PasswordGeneratorPreferences
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.settings.PreferenceKeys.DICEWARE_LENGTH
import app.passwordstore.util.settings.PreferenceKeys.DICEWARE_SEPARATOR
@ -58,8 +58,8 @@ class DicewarePasswordGeneratorDialogFragment : DialogFragment() {
setTitle(R.string.pwgen_title)
setPositiveButton(R.string.dialog_ok) { _, _ ->
setFragmentResult(
PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY,
bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}")
PasswordCreationActivityV2.PASSWORD_RESULT_REQUEST_KEY,
bundleOf(PasswordCreationActivityV2.RESULT to "${binding.passwordText.text}")
)
}
setNeutralButton(R.string.dialog_cancel) { _, _ -> }

View file

@ -5,60 +5,21 @@
package app.passwordstore.ui.dialogs
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
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.util.extensions.commitChange
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import java.io.File
import kotlinx.coroutines.launch
import me.msfjarvis.openpgpktx.util.OpenPgpApi
class FolderCreationDialogFragment : DialogFragment() {
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 {
val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext())
alertDialogBuilder.setTitle(R.string.title_create_folder)
@ -89,12 +50,16 @@ class FolderCreationDialogFragment : DialogFragment() {
if (folderNameViewContainer.error != null) return
newFolder.mkdirs()
(requireActivity() as PasswordStore).refreshPasswordList(newFolder)
if (dialog.findViewById<MaterialCheckBox>(R.id.set_gpg_key).isChecked) {
keySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java))
return
} else {
dismiss()
}
// TODO(msfjarvis): Restore this functionality
/*
if (dialog.findViewById<MaterialCheckBox>(R.id.set_gpg_key).isChecked) {
keySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java))
return
} else {
dismiss()
}
*/
dismiss()
}
companion object {

View file

@ -13,7 +13,7 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.setFragmentResult
import app.passwordstore.databinding.FragmentManualOtpEntryBinding
import app.passwordstore.ui.crypto.PasswordCreationActivity
import app.passwordstore.ui.crypto.PasswordCreationActivityV2
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class OtpImportDialogFragment : DialogFragment() {
@ -24,8 +24,8 @@ class OtpImportDialogFragment : DialogFragment() {
builder.setView(binding.root)
builder.setPositiveButton(android.R.string.ok) { _, _ ->
setFragmentResult(
PasswordCreationActivity.OTP_RESULT_REQUEST_KEY,
bundleOf(PasswordCreationActivity.RESULT to getTOTPUri(binding))
PasswordCreationActivityV2.OTP_RESULT_REQUEST_KEY,
bundleOf(PasswordCreationActivityV2.RESULT to getTOTPUri(binding))
)
}
val dialog = builder.create()

View file

@ -26,7 +26,7 @@ import app.passwordstore.passgen.random.NoCharactersIncludedException
import app.passwordstore.passgen.random.PasswordGenerator
import app.passwordstore.passgen.random.PasswordLengthTooShortException
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 com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.runCatching
@ -72,8 +72,8 @@ class PasswordGeneratorDialogFragment : DialogFragment() {
setTitle(R.string.pwgen_title)
setPositiveButton(R.string.dialog_ok) { _, _ ->
setFragmentResult(
PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY,
bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}")
PasswordCreationActivityV2.PASSWORD_RESULT_REQUEST_KEY,
bundleOf(PasswordCreationActivityV2.RESULT to "${binding.passwordText.text}")
)
}
setNeutralButton(R.string.dialog_cancel) { _, _ -> }

View file

@ -18,7 +18,6 @@ import app.passwordstore.util.git.operation.PullOperation
import app.passwordstore.util.git.operation.PushOperation
import app.passwordstore.util.git.operation.ResetToRemoteOperation
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.PreferenceKeys
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.
*/
@AndroidEntryPoint
abstract class BaseGitActivity : ContinuationContainerActivity() {
abstract class BaseGitActivity : AppCompatActivity() {
/** Enum of possible Git operations than can be run through [launchGitOperation]. */
enum class GitOp {

View file

@ -62,7 +62,6 @@ class GitServerConfigActivity : BaseGitActivity() {
when (newAuthMode) {
AuthMode.SshKey -> check(binding.authModeSshKey.id)
AuthMode.Password -> check(binding.authModePassword.id)
AuthMode.OpenKeychain -> check(binding.authModeOpenKeychain.id)
AuthMode.None -> check(View.NO_ID)
}
addOnButtonCheckedListener { _, checkedId, isChecked ->
@ -72,7 +71,6 @@ class GitServerConfigActivity : BaseGitActivity() {
}
when (checkedId) {
binding.authModeSshKey.id -> newAuthMode = AuthMode.SshKey
binding.authModeOpenKeychain.id -> newAuthMode = AuthMode.OpenKeychain
binding.authModePassword.id -> newAuthMode = AuthMode.Password
View.NO_ID -> newAuthMode = AuthMode.None
}
@ -215,12 +213,10 @@ class GitServerConfigActivity : BaseGitActivity() {
with(binding) {
if (isHttps) {
authModeSshKey.isVisible = false
authModeOpenKeychain.isVisible = false
authModePassword.isVisible = true
if (authModeGroup.checkedButtonId != authModePassword.id) authModeGroup.check(View.NO_ID)
} else {
authModeSshKey.isVisible = true
authModeOpenKeychain.isVisible = true
authModePassword.isVisible = true
if (authModeGroup.checkedButtonId == View.NO_ID) authModeGroup.check(authModeSshKey.id)
}

View file

@ -11,13 +11,11 @@ import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import app.passwordstore.ui.crypto.BasePgpActivity
import app.passwordstore.ui.crypto.DecryptActivity
import app.passwordstore.ui.crypto.DecryptActivityV2
import app.passwordstore.ui.passwords.PasswordStore
import app.passwordstore.util.auth.BiometricAuthenticator
import app.passwordstore.util.auth.BiometricAuthenticator.Result
import app.passwordstore.util.extensions.sharedPrefs
import app.passwordstore.util.features.Feature
import app.passwordstore.util.features.Features
import app.passwordstore.util.settings.PreferenceKeys
import dagger.hilt.android.AndroidEntryPoint

View file

@ -5,65 +5,21 @@
package app.passwordstore.ui.onboarding.fragments
import android.content.Intent
import android.os.Bundle
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.lifecycle.lifecycleScope
import app.passwordstore.R
import app.passwordstore.data.repo.PasswordRepository
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.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) {
private val settings by unsafeLazy { requireActivity().applicationContext.sharedPrefs }
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?) {
super.onViewCreated(view, savedInstanceState)
binding.selectKey.setOnClickListener {
gpgKeySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java))
// TODO(msfjarvis): Restore this functionality
// gpgKeySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java))
}
}

View file

@ -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) }
}
}
}

View file

@ -4,16 +4,15 @@
*/
package app.passwordstore.util.git.operation
import androidx.appcompat.app.AppCompatActivity
import app.passwordstore.R
import app.passwordstore.util.extensions.unsafeLazy
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.eclipse.jgit.api.RebaseCommand
import org.eclipse.jgit.api.ResetCommand
import org.eclipse.jgit.lib.RepositoryState
class BreakOutOfDetached(callingActivity: ContinuationContainerActivity) :
GitOperation(callingActivity) {
class BreakOutOfDetached(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
private val merging = repository.repositoryState == RepositoryState.MERGING
private val resetCommands =

View file

@ -4,7 +4,7 @@
*/
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.GitCommand
@ -14,7 +14,7 @@ import org.eclipse.jgit.api.GitCommand
* @param uri URL to clone the repository from
* @param callingActivity the calling activity
*/
class CloneOperation(callingActivity: ContinuationContainerActivity, uri: String) :
class CloneOperation(callingActivity: AppCompatActivity, uri: String) :
GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> =

View file

@ -5,7 +5,7 @@
package app.passwordstore.util.git.operation
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
import androidx.appcompat.app.AppCompatActivity
import org.eclipse.jgit.api.GitCommand
/**
@ -13,7 +13,7 @@ import org.eclipse.jgit.api.GitCommand
* achieve the best compression.
*/
class GcOperation(
callingActivity: ContinuationContainerActivity,
callingActivity: AppCompatActivity,
) : GitOperation(callingActivity) {
override val requiresAuth: Boolean = false

View file

@ -6,6 +6,7 @@ package app.passwordstore.util.git.operation
import android.content.Intent
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentActivity
import app.passwordstore.R
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.Result.*
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.SshKey
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 remoteBranch = hiltEntryPoint.gitSettings().branch
private val authActivity
get() = callingActivity as ContinuationContainerActivity
get() = callingActivity as AppCompatActivity
private class HttpsCredentialsProvider(private val passwordFinder: PasswordFinder) :
CredentialsProvider() {
@ -213,7 +213,6 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
// error, allowing users to make the SSH key selection.
return Err(SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER))
}
AuthMode.OpenKeychain -> registerAuthProviders(SshAuthMethod.OpenKeychain(authActivity))
AuthMode.Password -> {
val httpsCredentialProvider =
HttpsCredentialsProvider(CredentialFinder(callingActivity, AuthMode.Password))

View file

@ -4,11 +4,11 @@
*/
package app.passwordstore.util.git.operation
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
import androidx.appcompat.app.AppCompatActivity
import org.eclipse.jgit.api.GitCommand
class PullOperation(
callingActivity: ContinuationContainerActivity,
callingActivity: AppCompatActivity,
rebase: Boolean,
) : GitOperation(callingActivity) {

View file

@ -4,11 +4,10 @@
*/
package app.passwordstore.util.git.operation
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
import androidx.appcompat.app.AppCompatActivity
import org.eclipse.jgit.api.GitCommand
class PushOperation(callingActivity: ContinuationContainerActivity) :
GitOperation(callingActivity) {
class PushOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> =
arrayOf(

View file

@ -4,11 +4,10 @@
*/
package app.passwordstore.util.git.operation
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
import androidx.appcompat.app.AppCompatActivity
import org.eclipse.jgit.api.ResetCommand
class ResetToRemoteOperation(callingActivity: ContinuationContainerActivity) :
GitOperation(callingActivity) {
class ResetToRemoteOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
override val commands =
arrayOf(

View file

@ -4,10 +4,10 @@
*/
package app.passwordstore.util.git.operation
import app.passwordstore.util.git.sshj.ContinuationContainerActivity
import androidx.appcompat.app.AppCompatActivity
class SyncOperation(
callingActivity: ContinuationContainerActivity,
callingActivity: AppCompatActivity,
rebase: Boolean,
) : GitOperation(callingActivity) {

View file

@ -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))
}
}
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -232,16 +232,15 @@ class SshjConfig : ConfigImpl() {
private fun initKeyAlgorithms() {
keyAlgorithms =
listOf(
KeyAlgorithms.SSHRSACertV01(),
KeyAlgorithms.EdDSA25519(),
KeyAlgorithms.ECDSASHANistp521(),
KeyAlgorithms.ECDSASHANistp384(),
KeyAlgorithms.ECDSASHANistp256(),
KeyAlgorithms.RSASHA512(),
KeyAlgorithms.RSASHA256(),
KeyAlgorithms.SSHRSA(),
)
.map { OpenKeychainWrappedKeyAlgorithmFactory(it) }
KeyAlgorithms.SSHRSACertV01(),
KeyAlgorithms.EdDSA25519(),
KeyAlgorithms.ECDSASHANistp521(),
KeyAlgorithms.ECDSASHANistp384(),
KeyAlgorithms.ECDSASHANistp256(),
KeyAlgorithms.RSASHA512(),
KeyAlgorithms.RSASHA256(),
KeyAlgorithms.SSHRSA(),
)
}
private fun initRandomFactory() {

View file

@ -5,6 +5,7 @@
package app.passwordstore.util.git.sshj
import android.util.Base64
import androidx.appcompat.app.AppCompatActivity
import app.passwordstore.util.git.operation.CredentialFinder
import app.passwordstore.util.settings.AuthMode
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.util.FS
sealed class SshAuthMethod(val activity: ContinuationContainerActivity) {
class Password(activity: ContinuationContainerActivity) : SshAuthMethod(activity)
class SshKey(activity: ContinuationContainerActivity) : SshAuthMethod(activity)
class OpenKeychain(activity: ContinuationContainerActivity) : SshAuthMethod(activity)
sealed class SshAuthMethod(val activity: AppCompatActivity) {
class Password(activity: AppCompatActivity) : SshAuthMethod(activity)
class SshKey(activity: AppCompatActivity) : SshAuthMethod(activity)
}
abstract class InteractivePasswordFinder : PasswordFinder {
@ -157,14 +157,6 @@ private class SshjSession(
AuthPublickey(SshKey.provide(ssh, CredentialFinder(authMethod.activity, AuthMode.SshKey)))
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
}

View file

@ -37,7 +37,6 @@ enum class Protocol(val pref: String) {
enum class AuthMode(val pref: String) {
SshKey("ssh-key"),
Password("username/password"),
OpenKeychain("OpenKeychain"),
None("None"),
;
@ -156,7 +155,7 @@ constructor(
)
return UpdateConnectionSettingsResult.MissingUsername(newProtocol)
val validHttpsAuth = listOf(AuthMode.None, AuthMode.Password)
val validSshAuth = listOf(AuthMode.OpenKeychain, AuthMode.Password, AuthMode.SshKey)
val validSshAuth = listOf(AuthMode.Password, AuthMode.SshKey)
when {
newProtocol == Protocol.Https && newAuthMode !in validHttpsAuth -> {
return UpdateConnectionSettingsResult.AuthModeMismatch(newProtocol, validHttpsAuth)

View file

@ -89,12 +89,6 @@
android:layout_height="wrap_content"
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.MaterialButton

View file

@ -10,7 +10,7 @@
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="16dp"
tools:context="app.passwordstore.ui.crypto.DecryptActivity">
tools:context="app.passwordstore.ui.crypto.DecryptActivityV2">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/password_category"

View file

@ -26,9 +26,11 @@
<requestFocus />
</com.google.android.material.textfield.TextInputLayout>
<!-- TODO(msfjarvis): Restore this functionality -->
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/set_gpg_key"
android:layout_width="0dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:text="@string/new_folder_set_gpg_key"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -10,7 +10,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="false"
tools:context="app.passwordstore.ui.crypto.PasswordCreationActivity">
tools:context="app.passwordstore.ui.crypto.PasswordCreationActivityV2">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"

View file

@ -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 }
}
}

View file

@ -87,7 +87,6 @@ thirdparty-nonfree-googlePlayAuthApiPhone = "com.google.android.gms:play-service
thirdparty-nonfree-sentry = "io.sentry:sentry-android:6.2.1"
thirdparty-pgpainless = "org.pgpainless:pgpainless-core:1.3.1"
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
thirdparty-sshj = "com.hierynomus:sshj:0.33.0"
thirdparty-whatthestack = "com.github.haroldadmin:WhatTheStack:1.0.0-alpha04"

View file

@ -91,10 +91,7 @@ dependencyResolutionManagement {
}
exclusiveContent {
forRepository { maven("https://jitpack.io") }
filter {
includeModule("com.github.haroldadmin", "WhatTheStack")
includeModule("com.github.open-keychain.open-keychain", "sshauthentication-api")
}
filter { includeModule("com.github.haroldadmin", "WhatTheStack") }
}
exclusiveContent {
forRepository { maven("https://storage.googleapis.com/r8-releases/raw") }