Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.

Add key management capabilities #1446

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b5aa996
crypto-pgp: add `KeyManager` interface
Skrilltrax Jun 26, 2021
02681c1
app: add `KeyManagerModule` to hilt
Skrilltrax Jun 26, 2021
c770dab
app: add viewmodel and fragment to import gpg keys
Skrilltrax Jun 26, 2021
93e6a3b
wip: decrypt files using key imported during onboarding
Skrilltrax Jun 28, 2021
93bd6c0
app: rename `AndroidKeyManager` to `GPGKeyManager`
Skrilltrax Jun 29, 2021
2d54653
app: inject CoroutineDispatcher using Dagger
msfjarvis Jun 30, 2021
700fa5c
crypto-common: add KeyPair interface
msfjarvis Jun 30, 2021
a9e395c
crypto-pgp: implement KeyPair as GPGKeyPair
msfjarvis Jun 30, 2021
204e75c
crypto-{common,pgp}: refactor and move KeyManager
msfjarvis Jun 30, 2021
8d72ab4
buildSrc: support kotlin srcDir for androidTest source set as well
msfjarvis Jul 1, 2021
4f88964
crypto-pgp: prepare for instrumentation tests
msfjarvis Jul 1, 2021
0daa83a
crypto-pgp: add basic encrypt/decrypt test
msfjarvis Jul 2, 2021
8d0d077
crypto-pgp: add identity tests
msfjarvis Jul 4, 2021
d46aa0d
app: do not create `.gpg-id` if it already exists
Skrilltrax Jul 5, 2021
d8ea35b
app: launch key import fragment after cloning repo
Skrilltrax Jul 5, 2021
b39f657
{app, crypto-pgp, crypto-common}: Add initial password input implemen…
Skrilltrax Jul 5, 2021
4573baa
crypto-pgp: add test for KeyManager
msfjarvis Jul 5, 2021
ec62864
Add key import UI
msfjarvis Jul 5, 2021
3ddcba0
crypto-{common,pgp}: update API baseline
msfjarvis Jul 5, 2021
b67588b
crypto-pgp: normalize filenames before lookup
msfjarvis Jul 5, 2021
7e8ec56
crypto: rename some KeyManager methods
msfjarvis Jul 11, 2021
2c66c7e
crypto-pgp: simplify `GPGKeyManager#getAllKeyIds`
msfjarvis Jul 12, 2021
ef7fa37
crypto-pgp: make key lookups more robust
msfjarvis Jul 12, 2021
73d92c8
app: Add option to import key during decryption
Skrilltrax Jul 31, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ obj/
.idea/gradle.xml
.idea/jarRepositories.xml
.idea/runConfigurations.xml
.idea/deploymentTargetDropDown.xml

# OS-specific files
.DS_Store
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/

package dev.msfjarvis.aps.injection.coroutines

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Qualifier
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

@Module
@InstallIn(SingletonComponent::class)
object DispatcherModule {
@IODispatcher
@Provides
fun provideIODispatcher(): CoroutineDispatcher {
return Dispatchers.IO
}

@MainDispatcher
@Provides
fun provideMainDispatcher(): CoroutineDispatcher {
return Dispatchers.Main
}
}

@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class IODispatcher

@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class MainDispatcher
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import dev.msfjarvis.aps.data.crypto.GopenpgpCryptoHandler
@Module
@InstallIn(SingletonComponent::class)
object CryptoHandlerModule {

@Provides
@IntoSet
fun providePgpCryptoHandler(): CryptoHandler {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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.GPGKeyManager
import dev.msfjarvis.aps.data.crypto.GPGKeyPair
import dev.msfjarvis.aps.data.crypto.KeyManager
import dev.msfjarvis.aps.data.crypto.KeyPair
import dev.msfjarvis.aps.injection.context.FilesDirPath
import dev.msfjarvis.aps.injection.coroutines.IODispatcher
import kotlinx.coroutines.CoroutineDispatcher

/**
* This module adds all [KeyManager] 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 KeyManagerModule {

@Provides
fun providesGPGKeyManager(
@FilesDirPath filesDirPath: String,
@IODispatcher dispatcher: CoroutineDispatcher,
gpgKeyFactory: GPGKeyPair.Factory,
): GPGKeyManager {
return GPGKeyManager(
filesDirPath,
dispatcher,
gpgKeyFactory,
)
}

@Suppress("UNCHECKED_CAST")
@Provides
@IntoSet
fun provideKeyManager(
gpgKeyManager: GPGKeyManager,
): KeyManager<KeyPair> {
return gpgKeyManager as KeyManager<KeyPair>
}
}

/** Typealias for a [Set] of [KeyManager] instances injected by Dagger. */
typealias KeyManagerSet = Set<@JvmSuppressWildcards KeyManager<KeyPair>>
169 changes: 133 additions & 36 deletions app/src/main/java/dev/msfjarvis/aps/ui/crypto/GopenpgpDecryptActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,28 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.lifecycle.lifecycleScope
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.onSuccess
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import dev.msfjarvis.aps.R
import dev.msfjarvis.aps.data.crypto.CryptoHandler
import dev.msfjarvis.aps.data.crypto.KeyManager
import dev.msfjarvis.aps.data.crypto.KeyPair
import dev.msfjarvis.aps.data.passfile.PasswordEntry
import dev.msfjarvis.aps.data.password.FieldItem
import dev.msfjarvis.aps.data.repo.PasswordRepository
import dev.msfjarvis.aps.databinding.DecryptLayoutBinding
import dev.msfjarvis.aps.databinding.DialogPassphraseInputBinding
import dev.msfjarvis.aps.injection.crypto.CryptoSet
import dev.msfjarvis.aps.injection.crypto.KeyManagerSet
import dev.msfjarvis.aps.injection.password.PasswordEntryFactory
import dev.msfjarvis.aps.ui.adapters.FieldItemAdapter
import dev.msfjarvis.aps.ui.onboarding.activity.OnboardingActivity
import dev.msfjarvis.aps.util.FeatureFlags
import dev.msfjarvis.aps.util.extensions.findTillRoot
import dev.msfjarvis.aps.util.extensions.snackbar
import dev.msfjarvis.aps.util.extensions.unsafeLazy
import dev.msfjarvis.aps.util.extensions.viewBinding
import java.io.File
Expand All @@ -36,10 +50,9 @@ class GopenpgpDecryptActivity : BasePgpActivity() {
private val binding by viewBinding(DecryptLayoutBinding::inflate)
@Inject lateinit var passwordEntryFactory: PasswordEntryFactory
@Inject lateinit var cryptos: CryptoSet
@Inject lateinit var keyManagers: KeyManagerSet
private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) }

private var passwordEntry: PasswordEntry? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
Expand All @@ -53,7 +66,16 @@ class GopenpgpDecryptActivity : BasePgpActivity() {
true
}
}
decrypt()

lifecycleScope.launch {
val crypto = cryptos.first { it.canHandle(fullPath) }
val keyManager = keyManagers.first { it.canHandle(fullPath) }
val keyIds = getKeyIds(fullPath)

if (checkKeys(keyManager, keyIds)) {
showPassphraseDialog(crypto, keyManager, keyIds)
}
}
}

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
Expand Down Expand Up @@ -81,6 +103,29 @@ class GopenpgpDecryptActivity : BasePgpActivity() {
return true
}

private fun showPassphraseDialog(
crypto: CryptoHandler,
keyManager: KeyManager<KeyPair>,
keyIds: List<String>
) {
val view = layoutInflater.inflate(R.layout.dialog_passphrase_input, binding.root, false)
val dialogBinding = DialogPassphraseInputBinding.bind(view)

MaterialAlertDialogBuilder(this)
.setView(view)
.setPositiveButton("Unlock") { dialog, _ ->
dialog.dismiss()
val passphrase = dialogBinding.input.text.toString().toByteArray()
decrypt(crypto, keyManager, keyIds, passphrase)
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
finish()
}
.setOnCancelListener { finish() }
.show()
}

/**
* Automatically finishes the activity 60 seconds after decryption succeeded to prevent
* information leaks from stale activities.
Expand All @@ -98,7 +143,12 @@ class GopenpgpDecryptActivity : BasePgpActivity() {
* result triggers they can be repopulated with new data.
*/
private fun editPassword() {
val intent = Intent(this, PasswordCreationActivity::class.java)
val intent =
Intent(
this,
if (FeatureFlags.ENABLE_GOPENPGP) GopenpgpPasswordCreationActivity::class.java
else PasswordCreationActivity::class.java
)
intent.putExtra("FILE_PATH", relativeParentPath)
intent.putExtra("REPO_PATH", repoPath)
intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name)
Expand All @@ -122,54 +172,101 @@ class GopenpgpDecryptActivity : BasePgpActivity() {
)
}

private fun decrypt() {
private suspend fun checkKeys(keyManager: KeyManager<KeyPair>, keyIds: List<String>): Boolean {
if (keyIds.isEmpty()) {
// This probably means the store is not set correctly, it shouldn't happen but we will still
// show a snackbar
withContext(Dispatchers.Main) { snackbar(message = getString(R.string.gpg_id_not_found)) }
return false
}

if (keyManager.getKeyById(keyIds[0]) is Err) {
snackbar(
message = getString(R.string.snackbar_key_not_found),
actionText = getString(R.string.import_action_text)
) {
startActivity(OnboardingActivity.createKeyImportIntent(this@GopenpgpDecryptActivity))
finish()
}
return false
}

return true
}

private fun decrypt(
crypto: CryptoHandler,
keyManager: KeyManager<KeyPair>,
keyIds: List<String>,
passphrase: ByteArray
) {
// TODO: Binary GPG files do not work for now, need to fix that
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) }
withContext(Dispatchers.IO) {
keyManager
.getKeyById(keyIds[0])
.onSuccess { keyPair ->
val privateKey = keyPair.getPrivateKey().decodeToString()
val result = crypto.decrypt(privateKey, passphrase, message)
showPassword(result)
}
.onFailure {
snackbar(
message = getString(R.string.snackbar_key_not_found),
actionText = getString(R.string.import_action_text)
) {
startActivity(OnboardingActivity.createKeyImportIntent(this@GopenpgpDecryptActivity))
}
}
}
}
}
}

if (!entry.username.isNullOrBlank()) {
items.add(FieldItem.createUsernameField(entry.username!!))
}
private suspend fun showPassword(password: ByteArray) {
startAutoDismissTimer()
val entry = passwordEntryFactory.create(lifecycleScope, password)
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!!))
}

entry.extraContent.forEach { (key, value) ->
items.add(FieldItem(key, value, FieldItem.ActionType.COPY))
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))
}

withContext(Dispatchers.Main) {
binding.recyclerView.adapter = adapter
adapter.updateItems(items)
}
}

private fun getKeyIds(currentFilePath: String): List<String> {
val repoRoot = PasswordRepository.getRepositoryDirectory()
val directory = File(currentFilePath).parentFile ?: return emptyList()
val gpgIdentifierFile = directory.findTillRoot(".gpg-id", repoRoot) ?: return emptyList()

return gpgIdentifierFile.readText().split("\n")
}

companion object {

// TODO(msfjarvis): source these from storage and user input
const val PRIV_KEY = ""
const val PASS = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import dev.msfjarvis.aps.util.autofill.DirectoryStructure
import dev.msfjarvis.aps.util.crypto.GpgIdentifier
import dev.msfjarvis.aps.util.extensions.base64
import dev.msfjarvis.aps.util.extensions.commitChange
import dev.msfjarvis.aps.util.extensions.findTillRoot
import dev.msfjarvis.aps.util.extensions.getString
import dev.msfjarvis.aps.util.extensions.isInsideRepository
import dev.msfjarvis.aps.util.extensions.snackbar
Expand Down Expand Up @@ -139,22 +140,6 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
}
}

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)
Expand Down
Loading