Add initial implementation of Gopenpgp-backed PGP (#1441)
This commit is contained in:
parent
9c388e4974
commit
6e4ffe2902
20 changed files with 966 additions and 4 deletions
|
@ -76,6 +76,7 @@ dependencies {
|
|||
implementation(libs.androidx.annotation)
|
||||
coreLibraryDesugaring(libs.android.desugarJdkLibs)
|
||||
implementation(projects.autofillParser)
|
||||
implementation(projects.cryptoPgp)
|
||||
implementation(projects.formatCommon)
|
||||
implementation(projects.openpgpKtx)
|
||||
implementation(libs.androidx.activity.ktx)
|
||||
|
|
|
@ -44,6 +44,10 @@
|
|||
android:name=".ui.proxy.ProxySelectorActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.crypto.GopenpgpDecryptActivity"
|
||||
android:exported="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.main.LaunchActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
|
@ -91,6 +95,10 @@
|
|||
android:label="@string/new_password_title"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity android:name=".ui.crypto.GopenpgpPasswordCreationActivity"
|
||||
android:label="@string/new_password_title"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.crypto.DecryptActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
@ -128,6 +136,9 @@
|
|||
<activity
|
||||
android:name=".ui.autofill.AutofillDecryptActivity"
|
||||
android:theme="@style/NoBackgroundTheme" />
|
||||
<activity
|
||||
android:name=".ui.autofill.GopenpgpAutofillDecryptActivity"
|
||||
android:theme="@style/NoBackgroundTheme" />
|
||||
<activity
|
||||
android:name=".ui.autofill.AutofillFilterView"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package dev.msfjarvis.aps.injection.crypto
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import dev.msfjarvis.aps.data.crypto.CryptoHandler
|
||||
import dev.msfjarvis.aps.data.crypto.GopenpgpCryptoHandler
|
||||
|
||||
/**
|
||||
* This module adds all [CryptoHandler] implementations into a Set which makes it easier to build
|
||||
* generic UIs which are not tied to a specific implementation because of injection.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object CryptoHandlerModule {
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun providePgpCryptoHandler(): CryptoHandler {
|
||||
return GopenpgpCryptoHandler()
|
||||
}
|
||||
}
|
||||
|
||||
/** Typealias for a [Set] of [CryptoHandler] instances injected by Dagger. */
|
||||
typealias CryptoSet = Set<@JvmSuppressWildcards CryptoHandler>
|
|
@ -29,6 +29,7 @@ import com.github.androidpasswordstore.autofillparser.FormOrigin
|
|||
import dev.msfjarvis.aps.R
|
||||
import dev.msfjarvis.aps.data.password.PasswordItem
|
||||
import dev.msfjarvis.aps.databinding.ActivityOreoAutofillFilterBinding
|
||||
import dev.msfjarvis.aps.util.FeatureFlags
|
||||
import dev.msfjarvis.aps.util.autofill.AutofillMatcher
|
||||
import dev.msfjarvis.aps.util.autofill.AutofillPreferences
|
||||
import dev.msfjarvis.aps.util.autofill.DirectoryStructure
|
||||
|
@ -220,7 +221,11 @@ class AutofillFilterView : AppCompatActivity() {
|
|||
AutofillMatcher.addMatchFor(applicationContext, formOrigin, item.file)
|
||||
// intent?.extras? is checked to be non-null in onCreate
|
||||
decryptAction.launch(
|
||||
if (FeatureFlags.ENABLE_GOPENPGP) {
|
||||
GopenpgpAutofillDecryptActivity.makeDecryptFileIntent(item.file, intent!!.extras!!, this)
|
||||
} else {
|
||||
AutofillDecryptActivity.makeDecryptFileIntent(item.file, intent!!.extras!!, this)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
package dev.msfjarvis.aps.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 androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.ajalt.timberkt.d
|
||||
import com.github.ajalt.timberkt.e
|
||||
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 dev.msfjarvis.aps.injection.crypto.CryptoSet
|
||||
import dev.msfjarvis.aps.injection.password.PasswordEntryFactory
|
||||
import dev.msfjarvis.aps.ui.crypto.GopenpgpDecryptActivity
|
||||
import dev.msfjarvis.aps.util.autofill.AutofillPreferences
|
||||
import dev.msfjarvis.aps.util.autofill.AutofillResponseBuilder
|
||||
import dev.msfjarvis.aps.util.autofill.DirectoryStructure
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@AndroidEntryPoint
|
||||
class GopenpgpAutofillDecryptActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_FILE_PATH = "dev.msfjarvis.aps.autofill.oreo.EXTRA_FILE_PATH"
|
||||
private const val EXTRA_SEARCH_ACTION = "dev.msfjarvis.aps.autofill.oreo.EXTRA_SEARCH_ACTION"
|
||||
|
||||
private var decryptFileRequestCode = 1
|
||||
|
||||
fun makeDecryptFileIntent(file: File, forwardedExtras: Bundle, context: Context): Intent {
|
||||
return Intent(context, GopenpgpAutofillDecryptActivity::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, GopenpgpAutofillDecryptActivity::class.java).apply {
|
||||
putExtra(EXTRA_SEARCH_ACTION, false)
|
||||
putExtra(EXTRA_FILE_PATH, file.absolutePath)
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
decryptFileRequestCode++,
|
||||
intent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
)
|
||||
.intentSender
|
||||
}
|
||||
}
|
||||
|
||||
@Inject lateinit var passwordEntryFactory: PasswordEntryFactory
|
||||
@Inject lateinit var cryptos: CryptoSet
|
||||
|
||||
private lateinit var directoryStructure: DirectoryStructure
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val filePath =
|
||||
intent?.getStringExtra(EXTRA_FILE_PATH)
|
||||
?: run {
|
||||
e { "GopenpgpAutofillDecryptActivity started without EXTRA_FILE_PATH" }
|
||||
finish()
|
||||
return
|
||||
}
|
||||
val clientState =
|
||||
intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE)
|
||||
?: run {
|
||||
e { "GopenpgpAutofillDecryptActivity 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)
|
||||
d { action.toString() }
|
||||
lifecycleScope.launch {
|
||||
val credentials = decryptCredential(File(filePath))
|
||||
if (credentials == null) {
|
||||
setResult(RESULT_CANCELED)
|
||||
} else {
|
||||
val fillInDataset =
|
||||
AutofillResponseBuilder.makeFillInDataset(
|
||||
this@GopenpgpAutofillDecryptActivity,
|
||||
credentials,
|
||||
clientState,
|
||||
action
|
||||
)
|
||||
withContext(Dispatchers.Main) {
|
||||
setResult(
|
||||
RESULT_OK,
|
||||
Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) }
|
||||
)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) { finish() }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun decryptCredential(file: File): Credentials? {
|
||||
runCatching { file.inputStream() }
|
||||
.onFailure { e ->
|
||||
e(e) { "File to decrypt not found" }
|
||||
return null
|
||||
}
|
||||
.onSuccess { encryptedInput ->
|
||||
runCatching {
|
||||
val crypto = cryptos.first { it.canHandle(file.absolutePath) }
|
||||
withContext(Dispatchers.IO) {
|
||||
crypto.decrypt(
|
||||
GopenpgpDecryptActivity.PRIV_KEY,
|
||||
GopenpgpDecryptActivity.PASS.toByteArray(charset = Charsets.UTF_8),
|
||||
encryptedInput.readBytes()
|
||||
)
|
||||
}
|
||||
}
|
||||
.onFailure { e ->
|
||||
e(e) { "Decryption with Gopenpgp failed" }
|
||||
return null
|
||||
}
|
||||
.onSuccess { result ->
|
||||
return runCatching {
|
||||
val entry = passwordEntryFactory.create(lifecycleScope, result)
|
||||
AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure)
|
||||
}
|
||||
.getOrElse { e ->
|
||||
e(e) { "Failed to parse password entry" }
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package dev.msfjarvis.aps.ui.crypto
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.msfjarvis.aps.R
|
||||
import dev.msfjarvis.aps.data.passfile.PasswordEntry
|
||||
import dev.msfjarvis.aps.data.password.FieldItem
|
||||
import dev.msfjarvis.aps.databinding.DecryptLayoutBinding
|
||||
import dev.msfjarvis.aps.injection.crypto.CryptoSet
|
||||
import dev.msfjarvis.aps.injection.password.PasswordEntryFactory
|
||||
import dev.msfjarvis.aps.ui.adapters.FieldItemAdapter
|
||||
import dev.msfjarvis.aps.util.extensions.unsafeLazy
|
||||
import dev.msfjarvis.aps.util.extensions.viewBinding
|
||||
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.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@AndroidEntryPoint
|
||||
class GopenpgpDecryptActivity : BasePgpActivity() {
|
||||
|
||||
private val binding by viewBinding(DecryptLayoutBinding::inflate)
|
||||
@Inject lateinit var passwordEntryFactory: PasswordEntryFactory
|
||||
@Inject lateinit var cryptos: CryptoSet
|
||||
private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) }
|
||||
|
||||
private var passwordEntry: PasswordEntry? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
title = name
|
||||
with(binding) {
|
||||
setContentView(root)
|
||||
passwordCategory.text = relativeParentPath
|
||||
passwordFile.text = name
|
||||
passwordFile.setOnLongClickListener {
|
||||
copyTextToClipboard(name)
|
||||
true
|
||||
}
|
||||
}
|
||||
decrypt()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.pgp_handler, menu)
|
||||
passwordEntry?.let { entry ->
|
||||
if (menu != null) {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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))
|
||||
)
|
||||
}
|
||||
|
||||
private fun decrypt() {
|
||||
lifecycleScope.launch {
|
||||
// TODO(msfjarvis): native methods are fallible, add error handling once out of testing
|
||||
val message = withContext(Dispatchers.IO) { File(fullPath).readBytes() }
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
val crypto = cryptos.first { it.canHandle(fullPath) }
|
||||
crypto.decrypt(
|
||||
PRIV_KEY,
|
||||
PASS.toByteArray(charset = Charsets.UTF_8),
|
||||
message,
|
||||
)
|
||||
}
|
||||
startAutoDismissTimer()
|
||||
|
||||
val entry = passwordEntryFactory.create(lifecycleScope, result)
|
||||
passwordEntry = entry
|
||||
invalidateOptionsMenu()
|
||||
|
||||
val items = arrayListOf<FieldItem>()
|
||||
val adapter = FieldItemAdapter(emptyList(), true) { text -> copyTextToClipboard(text) }
|
||||
if (!entry.password.isNullOrBlank()) {
|
||||
items.add(FieldItem.createPasswordField(entry.password!!))
|
||||
}
|
||||
|
||||
if (entry.hasTotp()) {
|
||||
lifecycleScope.launch {
|
||||
items.add(FieldItem.createOtpField(entry.totp.value))
|
||||
entry.totp.collect { code ->
|
||||
withContext(Dispatchers.Main) { adapter.updateOTPCode(code) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry.username.isNullOrBlank()) {
|
||||
items.add(FieldItem.createUsernameField(entry.username!!))
|
||||
}
|
||||
|
||||
entry.extraContent.forEach { (key, value) ->
|
||||
items.add(FieldItem(key, value, FieldItem.ActionType.COPY))
|
||||
}
|
||||
|
||||
binding.recyclerView.adapter = adapter
|
||||
adapter.updateItems(items)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// TODO(msfjarvis): source these from storage and user input
|
||||
const val PRIV_KEY = ""
|
||||
const val PASS = ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,432 @@
|
|||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package dev.msfjarvis.aps.ui.crypto
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.ajalt.timberkt.e
|
||||
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.zxing.integration.android.IntentIntegrator
|
||||
import com.google.zxing.integration.android.IntentIntegrator.QR_CODE
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.msfjarvis.aps.R
|
||||
import dev.msfjarvis.aps.databinding.PasswordCreationActivityBinding
|
||||
import dev.msfjarvis.aps.injection.crypto.CryptoSet
|
||||
import dev.msfjarvis.aps.injection.password.PasswordEntryFactory
|
||||
import dev.msfjarvis.aps.ui.dialogs.OtpImportDialogFragment
|
||||
import dev.msfjarvis.aps.ui.dialogs.PasswordGeneratorDialogFragment
|
||||
import dev.msfjarvis.aps.ui.dialogs.XkPasswordGeneratorDialogFragment
|
||||
import dev.msfjarvis.aps.util.autofill.AutofillPreferences
|
||||
import dev.msfjarvis.aps.util.autofill.DirectoryStructure
|
||||
import dev.msfjarvis.aps.util.extensions.base64
|
||||
import dev.msfjarvis.aps.util.extensions.commitChange
|
||||
import dev.msfjarvis.aps.util.extensions.getString
|
||||
import dev.msfjarvis.aps.util.extensions.isInsideRepository
|
||||
import dev.msfjarvis.aps.util.extensions.snackbar
|
||||
import dev.msfjarvis.aps.util.extensions.unsafeLazy
|
||||
import dev.msfjarvis.aps.util.extensions.viewBinding
|
||||
import dev.msfjarvis.aps.util.settings.PreferenceKeys
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@AndroidEntryPoint
|
||||
class GopenpgpPasswordCreationActivity : BasePgpActivity() {
|
||||
|
||||
private val binding by viewBinding(PasswordCreationActivityBinding::inflate)
|
||||
@Inject lateinit var passwordEntryFactory: PasswordEntryFactory
|
||||
@Inject lateinit var cryptos: CryptoSet
|
||||
|
||||
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 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))
|
||||
}
|
||||
}
|
||||
|
||||
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@GopenpgpPasswordCreationActivity
|
||||
) { 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_manual_entry)
|
||||
)
|
||||
MaterialAlertDialogBuilder(this@GopenpgpPasswordCreationActivity)
|
||||
.setItems(items) { _, index ->
|
||||
when (index) {
|
||||
0 ->
|
||||
otpImportAction.launch(
|
||||
IntentIntegrator(this@GopenpgpPasswordCreationActivity)
|
||||
.setOrientationLocked(false)
|
||||
.setBeepEnabled(false)
|
||||
.setDesiredBarcodeFormats(QR_CODE)
|
||||
.createScanIntent()
|
||||
)
|
||||
1 -> 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@GopenpgpPasswordCreationActivity) ==
|
||||
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(
|
||||
lifecycleScope,
|
||||
"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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
listOf(filename, extraContent).forEach {
|
||||
it.doOnTextChanged { _, _, _, _ -> updateViewState() }
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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_XKPASSWD ->
|
||||
XkPasswordGeneratorDialogFragment().show(supportFragmentManager, "xkpwgenerator")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateViewState() =
|
||||
with(binding) {
|
||||
// Use PasswordEntry to parse extras for username
|
||||
val entry =
|
||||
passwordEntryFactory.create(
|
||||
lifecycleScope,
|
||||
"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() {
|
||||
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)
|
||||
}
|
||||
|
||||
val content = "$editPass\n$editExtra"
|
||||
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) {
|
||||
runCatching {
|
||||
val crypto = cryptos.first { it.canHandle(path) }
|
||||
val result =
|
||||
withContext(Dispatchers.IO) { crypto.encrypt(PUB_KEY, content.encodeToByteArray()) }
|
||||
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(result) } }
|
||||
|
||||
// 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(lifecycleScope, 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@GopenpgpPasswordCreationActivity)
|
||||
.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) {
|
||||
e(e) { "Failed to write password file" }
|
||||
setResult(RESULT_CANCELED)
|
||||
MaterialAlertDialogBuilder(this@GopenpgpPasswordCreationActivity)
|
||||
.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 {
|
||||
e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_PWGEN_TYPE_CLASSIC = "classic"
|
||||
private const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd"
|
||||
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"
|
||||
// TODO(msfjarvis): source this from storage
|
||||
const val PUB_KEY = ""
|
||||
}
|
||||
}
|
|
@ -38,6 +38,7 @@ import dev.msfjarvis.aps.data.password.PasswordItem
|
|||
import dev.msfjarvis.aps.data.repo.PasswordRepository
|
||||
import dev.msfjarvis.aps.ui.crypto.BasePgpActivity.Companion.getLongName
|
||||
import dev.msfjarvis.aps.ui.crypto.DecryptActivity
|
||||
import dev.msfjarvis.aps.ui.crypto.GopenpgpDecryptActivity
|
||||
import dev.msfjarvis.aps.ui.crypto.PasswordCreationActivity
|
||||
import dev.msfjarvis.aps.ui.dialogs.BasicBottomSheet
|
||||
import dev.msfjarvis.aps.ui.dialogs.FolderCreationDialogFragment
|
||||
|
@ -46,6 +47,7 @@ import dev.msfjarvis.aps.ui.git.base.BaseGitActivity
|
|||
import dev.msfjarvis.aps.ui.onboarding.activity.OnboardingActivity
|
||||
import dev.msfjarvis.aps.ui.settings.DirectorySelectionActivity
|
||||
import dev.msfjarvis.aps.ui.settings.SettingsActivity
|
||||
import dev.msfjarvis.aps.util.FeatureFlags
|
||||
import dev.msfjarvis.aps.util.autofill.AutofillMatcher
|
||||
import dev.msfjarvis.aps.util.extensions.base64
|
||||
import dev.msfjarvis.aps.util.extensions.commitChange
|
||||
|
@ -422,7 +424,14 @@ class PasswordStore : BaseGitActivity() {
|
|||
val authDecryptIntent = item.createAuthEnabledIntent(this)
|
||||
val decryptIntent =
|
||||
(authDecryptIntent.clone() as Intent).setComponent(
|
||||
ComponentName(this, DecryptActivity::class.java)
|
||||
ComponentName(
|
||||
this,
|
||||
if (FeatureFlags.ENABLE_GOPENPGP) {
|
||||
GopenpgpDecryptActivity::class.java
|
||||
} else {
|
||||
DecryptActivity::class.java
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
startActivity(decryptIntent)
|
||||
|
|
6
app/src/main/java/dev/msfjarvis/aps/util/FeatureFlags.kt
Normal file
6
app/src/main/java/dev/msfjarvis/aps/util/FeatureFlags.kt
Normal file
|
@ -0,0 +1,6 @@
|
|||
package dev.msfjarvis.aps.util
|
||||
|
||||
/** Naive feature flagging functionality to allow merging incomplete features */
|
||||
object FeatureFlags {
|
||||
const val ENABLE_GOPENPGP = false
|
||||
}
|
|
@ -25,6 +25,8 @@ import dev.msfjarvis.aps.ui.autofill.AutofillDecryptActivity
|
|||
import dev.msfjarvis.aps.ui.autofill.AutofillFilterView
|
||||
import dev.msfjarvis.aps.ui.autofill.AutofillPublisherChangedActivity
|
||||
import dev.msfjarvis.aps.ui.autofill.AutofillSaveActivity
|
||||
import dev.msfjarvis.aps.ui.autofill.GopenpgpAutofillDecryptActivity
|
||||
import dev.msfjarvis.aps.util.FeatureFlags
|
||||
import java.io.File
|
||||
|
||||
/** Implements [AutofillResponseBuilder]'s methods for API 30 and above */
|
||||
|
@ -68,7 +70,12 @@ class Api30AutofillResponseBuilder(form: FillableForm) {
|
|||
): Dataset? {
|
||||
if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null
|
||||
val metadata = makeFillMatchMetadata(context, file)
|
||||
val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
|
||||
val intentSender =
|
||||
if (FeatureFlags.ENABLE_GOPENPGP) {
|
||||
GopenpgpAutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
|
||||
} else {
|
||||
AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
|
||||
}
|
||||
return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata, imeSpec)
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ import dev.msfjarvis.aps.ui.autofill.AutofillDecryptActivity
|
|||
import dev.msfjarvis.aps.ui.autofill.AutofillFilterView
|
||||
import dev.msfjarvis.aps.ui.autofill.AutofillPublisherChangedActivity
|
||||
import dev.msfjarvis.aps.ui.autofill.AutofillSaveActivity
|
||||
import dev.msfjarvis.aps.ui.autofill.GopenpgpAutofillDecryptActivity
|
||||
import dev.msfjarvis.aps.util.FeatureFlags
|
||||
import java.io.File
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
|
@ -56,7 +58,12 @@ class AutofillResponseBuilder(form: FillableForm) {
|
|||
private fun makeMatchDataset(context: Context, file: File): Dataset? {
|
||||
if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null
|
||||
val metadata = makeFillMatchMetadata(context, file)
|
||||
val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
|
||||
val intentSender =
|
||||
if (FeatureFlags.ENABLE_GOPENPGP) {
|
||||
GopenpgpAutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
|
||||
} else {
|
||||
AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
|
||||
}
|
||||
return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata)
|
||||
}
|
||||
|
||||
|
|
6
crypto-common/api/crypto-common.api
Normal file
6
crypto-common/api/crypto-common.api
Normal file
|
@ -0,0 +1,6 @@
|
|||
public abstract interface class dev/msfjarvis/aps/data/crypto/CryptoHandler {
|
||||
public abstract fun canHandle (Ljava/lang/String;)Z
|
||||
public abstract fun decrypt (Ljava/lang/String;[B[B)[B
|
||||
public abstract fun encrypt (Ljava/lang/String;[B)[B
|
||||
}
|
||||
|
8
crypto-common/build.gradle.kts
Normal file
8
crypto-common/build.gradle.kts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
`aps-plugin`
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package dev.msfjarvis.aps.data.crypto
|
||||
|
||||
/** Generic interface to implement cryptographic operations on top of. */
|
||||
public interface CryptoHandler {
|
||||
|
||||
/**
|
||||
* Decrypt the given [ciphertext] using a [privateKey] and [passphrase], returning a [ByteArray]
|
||||
* corresponding to the decrypted plaintext.
|
||||
*/
|
||||
public fun decrypt(privateKey: String, passphrase: ByteArray, ciphertext: ByteArray): ByteArray
|
||||
|
||||
/**
|
||||
* Encrypt the given [plaintext] to the provided [publicKey], returning the encrypted ciphertext
|
||||
* as a [ByteArray]
|
||||
*/
|
||||
public fun encrypt(publicKey: String, plaintext: ByteArray): ByteArray
|
||||
|
||||
/** Given a [fileName], return whether this instance can handle it. */
|
||||
public fun canHandle(fileName: String): Boolean
|
||||
}
|
7
crypto-pgp/api/crypto-pgp.api
Normal file
7
crypto-pgp/api/crypto-pgp.api
Normal file
|
@ -0,0 +1,7 @@
|
|||
public final class dev/msfjarvis/aps/data/crypto/GopenpgpCryptoHandler : dev/msfjarvis/aps/data/crypto/CryptoHandler {
|
||||
public fun <init> ()V
|
||||
public fun canHandle (Ljava/lang/String;)Z
|
||||
public fun decrypt (Ljava/lang/String;[B[B)[B
|
||||
public fun encrypt (Ljava/lang/String;[B)[B
|
||||
}
|
||||
|
15
crypto-pgp/build.gradle.kts
Normal file
15
crypto-pgp/build.gradle.kts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
`aps-plugin`
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.cryptoCommon)
|
||||
implementation(libs.aps.gopenpgp)
|
||||
implementation(libs.dagger.hilt.core)
|
||||
}
|
6
crypto-pgp/src/main/AndroidManifest.xml
Normal file
6
crypto-pgp/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
~ SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
|
||||
-->
|
||||
|
||||
<manifest package="dev.msfjarvis.aps.cryptopgp"></manifest>
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package dev.msfjarvis.aps.data.crypto
|
||||
|
||||
import com.proton.Gopenpgp.crypto.Crypto
|
||||
import com.proton.Gopenpgp.helper.Helper
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Gopenpgp backed implementation of [CryptoHandler]. */
|
||||
public class GopenpgpCryptoHandler @Inject constructor() : CryptoHandler {
|
||||
|
||||
/**
|
||||
* Decrypt the given [ciphertext] using the given PGP [privateKey] and corresponding [passphrase].
|
||||
*/
|
||||
override fun decrypt(
|
||||
privateKey: String,
|
||||
passphrase: ByteArray,
|
||||
ciphertext: ByteArray,
|
||||
): ByteArray {
|
||||
// Decode the incoming cipher into a string and try to guess if it's armored.
|
||||
val cipherString = ciphertext.decodeToString()
|
||||
val isArmor = cipherString.startsWith("-----BEGIN PGP MESSAGE-----")
|
||||
val message =
|
||||
if (isArmor) {
|
||||
Crypto.newPGPMessageFromArmored(cipherString)
|
||||
} else {
|
||||
Crypto.newPGPMessage(ciphertext)
|
||||
}
|
||||
return Helper.decryptBinaryMessageArmored(
|
||||
privateKey,
|
||||
passphrase,
|
||||
message.armored,
|
||||
)
|
||||
}
|
||||
|
||||
override fun encrypt(publicKey: String, plaintext: ByteArray): ByteArray {
|
||||
return Helper.encryptBinaryMessage(
|
||||
publicKey,
|
||||
plaintext,
|
||||
)
|
||||
}
|
||||
|
||||
override fun canHandle(fileName: String): Boolean {
|
||||
return fileName.split('.').last() == "gpg"
|
||||
}
|
||||
}
|
|
@ -72,6 +72,7 @@ dagger-hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt
|
|||
android-desugarJdkLibs = "com.android.tools:desugar_jdk_libs:1.1.5"
|
||||
|
||||
# First-party libraries
|
||||
aps-gopenpgp = "com.github.android-password-store:gopenpgp:0.1.5"
|
||||
aps-sublimeFuzzy = "com.github.android-password-store:sublime-fuzzy:1.0.0"
|
||||
aps-zxingAndroidEmbedded = "com.github.android-password-store:zxing-android-embedded:4.2.0"
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ include(":app")
|
|||
|
||||
include(":autofill-parser")
|
||||
|
||||
include(":crypto-common")
|
||||
|
||||
include(":crypto-pgp")
|
||||
|
||||
include(":format-common")
|
||||
|
||||
include(":openpgp-ktx")
|
||||
|
|
Loading…
Reference in a new issue