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

Commit 73d92c8

Browse files
committed
app: Add option to import key during decryption
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
1 parent ef7fa37 commit 73d92c8

File tree

3 files changed

+122
-46
lines changed

3 files changed

+122
-46
lines changed

app/src/main/java/dev/msfjarvis/aps/ui/crypto/GopenpgpDecryptActivity.kt

Lines changed: 100 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@ import android.os.Bundle
1010
import android.view.Menu
1111
import android.view.MenuItem
1212
import androidx.lifecycle.lifecycleScope
13-
import com.github.michaelbull.result.unwrap
13+
import com.github.michaelbull.result.Err
14+
import com.github.michaelbull.result.onFailure
15+
import com.github.michaelbull.result.onSuccess
1416
import com.google.android.material.dialog.MaterialAlertDialogBuilder
1517
import dagger.hilt.android.AndroidEntryPoint
1618
import dev.msfjarvis.aps.R
19+
import dev.msfjarvis.aps.data.crypto.CryptoHandler
20+
import dev.msfjarvis.aps.data.crypto.KeyManager
21+
import dev.msfjarvis.aps.data.crypto.KeyPair
1722
import dev.msfjarvis.aps.data.passfile.PasswordEntry
1823
import dev.msfjarvis.aps.data.password.FieldItem
1924
import dev.msfjarvis.aps.data.repo.PasswordRepository
@@ -23,7 +28,10 @@ import dev.msfjarvis.aps.injection.crypto.CryptoSet
2328
import dev.msfjarvis.aps.injection.crypto.KeyManagerSet
2429
import dev.msfjarvis.aps.injection.password.PasswordEntryFactory
2530
import dev.msfjarvis.aps.ui.adapters.FieldItemAdapter
31+
import dev.msfjarvis.aps.ui.onboarding.activity.OnboardingActivity
32+
import dev.msfjarvis.aps.util.FeatureFlags
2633
import dev.msfjarvis.aps.util.extensions.findTillRoot
34+
import dev.msfjarvis.aps.util.extensions.snackbar
2735
import dev.msfjarvis.aps.util.extensions.unsafeLazy
2836
import dev.msfjarvis.aps.util.extensions.viewBinding
2937
import java.io.File
@@ -59,7 +67,15 @@ class GopenpgpDecryptActivity : BasePgpActivity() {
5967
}
6068
}
6169

62-
showPassphraseDialog()
70+
lifecycleScope.launch {
71+
val crypto = cryptos.first { it.canHandle(fullPath) }
72+
val keyManager = keyManagers.first { it.canHandle(fullPath) }
73+
val keyIds = getKeyIds(fullPath)
74+
75+
if (checkKeys(keyManager, keyIds)) {
76+
showPassphraseDialog(crypto, keyManager, keyIds)
77+
}
78+
}
6379
}
6480

6581
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
@@ -87,7 +103,11 @@ class GopenpgpDecryptActivity : BasePgpActivity() {
87103
return true
88104
}
89105

90-
private fun showPassphraseDialog() {
106+
private fun showPassphraseDialog(
107+
crypto: CryptoHandler,
108+
keyManager: KeyManager<KeyPair>,
109+
keyIds: List<String>
110+
) {
91111
val view = layoutInflater.inflate(R.layout.dialog_passphrase_input, binding.root, false)
92112
val dialogBinding = DialogPassphraseInputBinding.bind(view)
93113

@@ -96,7 +116,7 @@ class GopenpgpDecryptActivity : BasePgpActivity() {
96116
.setPositiveButton("Unlock") { dialog, _ ->
97117
dialog.dismiss()
98118
val passphrase = dialogBinding.input.text.toString().toByteArray()
99-
decrypt(passphrase)
119+
decrypt(crypto, keyManager, keyIds, passphrase)
100120
}
101121
.setNegativeButton("Cancel") { dialog, _ ->
102122
dialog.dismiss()
@@ -123,7 +143,12 @@ class GopenpgpDecryptActivity : BasePgpActivity() {
123143
* result triggers they can be repopulated with new data.
124144
*/
125145
private fun editPassword() {
126-
val intent = Intent(this, PasswordCreationActivity::class.java)
146+
val intent =
147+
Intent(
148+
this,
149+
if (FeatureFlags.ENABLE_GOPENPGP) GopenpgpPasswordCreationActivity::class.java
150+
else PasswordCreationActivity::class.java
151+
)
127152
intent.putExtra("FILE_PATH", relativeParentPath)
128153
intent.putExtra("REPO_PATH", repoPath)
129154
intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name)
@@ -147,56 +172,86 @@ class GopenpgpDecryptActivity : BasePgpActivity() {
147172
)
148173
}
149174

150-
private fun decrypt(passphrase: ByteArray) {
175+
private suspend fun checkKeys(keyManager: KeyManager<KeyPair>, keyIds: List<String>): Boolean {
176+
if (keyIds.isEmpty()) {
177+
// This probably means the store is not set correctly, it shouldn't happen but we will still
178+
// show a snackbar
179+
withContext(Dispatchers.Main) { snackbar(message = getString(R.string.gpg_id_not_found)) }
180+
return false
181+
}
182+
183+
if (keyManager.getKeyById(keyIds[0]) is Err) {
184+
snackbar(
185+
message = getString(R.string.snackbar_key_not_found),
186+
actionText = getString(R.string.import_action_text)
187+
) {
188+
startActivity(OnboardingActivity.createKeyImportIntent(this@GopenpgpDecryptActivity))
189+
finish()
190+
}
191+
return false
192+
}
193+
194+
return true
195+
}
196+
197+
private fun decrypt(
198+
crypto: CryptoHandler,
199+
keyManager: KeyManager<KeyPair>,
200+
keyIds: List<String>,
201+
passphrase: ByteArray
202+
) {
203+
// TODO: Binary GPG files do not work for now, need to fix that
151204
lifecycleScope.launch {
152205
// TODO(msfjarvis): native methods are fallible, add error handling once out of testing
153206
val message = withContext(Dispatchers.IO) { File(fullPath).readBytes() }
154-
val crypto = cryptos.first { it.canHandle(fullPath) }
155-
val keyManager = keyManagers.first { it.canHandle(fullPath) }
156-
val keyIds = getKeyIds(fullPath)
157-
if (keyIds.isEmpty()) {
158-
// TODO: Show option to import gpg key here
159-
return@launch
160-
}
161-
val result =
162-
withContext(Dispatchers.IO) {
163-
val privateKey =
164-
keyManager.getKeyById(keyIds[0]).unwrap().getPrivateKey().decodeToString()
165-
166-
// TODO: this throws an error if passphrase is incorrect
167-
crypto.decrypt(
168-
privateKey,
169-
passphrase,
170-
message,
171-
)
172-
}
173-
startAutoDismissTimer()
174-
val entry = passwordEntryFactory.create(lifecycleScope, result)
175-
passwordEntry = entry
176-
invalidateOptionsMenu()
177-
val items = arrayListOf<FieldItem>()
178-
val adapter = FieldItemAdapter(emptyList(), true) { text -> copyTextToClipboard(text) }
179-
if (!entry.password.isNullOrBlank()) {
180-
items.add(FieldItem.createPasswordField(entry.password!!))
181-
}
182207

183-
if (entry.hasTotp()) {
184-
lifecycleScope.launch {
185-
items.add(FieldItem.createOtpField(entry.totp.value))
186-
entry.totp.collect { code ->
187-
withContext(Dispatchers.Main) { adapter.updateOTPCode(code) }
208+
withContext(Dispatchers.IO) {
209+
keyManager
210+
.getKeyById(keyIds[0])
211+
.onSuccess { keyPair ->
212+
val privateKey = keyPair.getPrivateKey().decodeToString()
213+
val result = crypto.decrypt(privateKey, passphrase, message)
214+
showPassword(result)
215+
}
216+
.onFailure {
217+
snackbar(
218+
message = getString(R.string.snackbar_key_not_found),
219+
actionText = getString(R.string.import_action_text)
220+
) {
221+
startActivity(OnboardingActivity.createKeyImportIntent(this@GopenpgpDecryptActivity))
222+
}
188223
}
189-
}
190224
}
225+
}
226+
}
191227

192-
if (!entry.username.isNullOrBlank()) {
193-
items.add(FieldItem.createUsernameField(entry.username!!))
194-
}
228+
private suspend fun showPassword(password: ByteArray) {
229+
startAutoDismissTimer()
230+
val entry = passwordEntryFactory.create(lifecycleScope, password)
231+
passwordEntry = entry
232+
invalidateOptionsMenu()
233+
val items = arrayListOf<FieldItem>()
234+
val adapter = FieldItemAdapter(emptyList(), true) { text -> copyTextToClipboard(text) }
235+
if (!entry.password.isNullOrBlank()) {
236+
items.add(FieldItem.createPasswordField(entry.password!!))
237+
}
195238

196-
entry.extraContent.forEach { (key, value) ->
197-
items.add(FieldItem(key, value, FieldItem.ActionType.COPY))
239+
if (entry.hasTotp()) {
240+
lifecycleScope.launch {
241+
items.add(FieldItem.createOtpField(entry.totp.value))
242+
entry.totp.collect { code -> withContext(Dispatchers.Main) { adapter.updateOTPCode(code) } }
198243
}
244+
}
245+
246+
if (!entry.username.isNullOrBlank()) {
247+
items.add(FieldItem.createUsernameField(entry.username!!))
248+
}
249+
250+
entry.extraContent.forEach { (key, value) ->
251+
items.add(FieldItem(key, value, FieldItem.ActionType.COPY))
252+
}
199253

254+
withContext(Dispatchers.Main) {
200255
binding.recyclerView.adapter = adapter
201256
adapter.updateItems(items)
202257
}

app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,24 @@ fun FragmentActivity.snackbar(
141141
return snackbar
142142
}
143143

144+
/**
145+
* Show a [Snackbar] with action in a [FragmentActivity] and correctly anchor it to a
146+
* [com.google.android.material.floatingactionbutton.FloatingActionButton] if one exists in the
147+
* [view]
148+
*/
149+
fun FragmentActivity.snackbar(
150+
view: View = findViewById(android.R.id.content),
151+
message: String,
152+
length: Int = Snackbar.LENGTH_SHORT,
153+
actionText: String,
154+
onClickListener: View.OnClickListener,
155+
): Snackbar {
156+
val snackbar = Snackbar.make(view, message, length).setAction(actionText, onClickListener)
157+
snackbar.anchorView = findViewById(R.id.fab)
158+
snackbar.show()
159+
return snackbar
160+
}
161+
144162
/** Simplifies the common `getString(key, null) ?: defaultValue` case slightly */
145163
fun SharedPreferences.getString(key: String): String? = getString(key, null)
146164

app/src/main/res/values/strings.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
~ SPDX-License-Identifier: GPL-3.0-only
44
-->
55

6-
<resources>
6+
<resources xmlns:tools="http://schemas.android.com/tools">
77

88
<plurals name="delete_title">
99
<item quantity="one">%d item selected</item>
@@ -405,5 +405,8 @@
405405
<!-- Gopenpgp Decrypt activity -->
406406
<string name="passphrase_input_title">Enter Key Passphrase</string>
407407
<string name="passphrase_input_hint">Passphrase</string>
408+
<string name="snackbar_key_not_found">Cannot find a GPG key associated to this file, do you wanna import one now?</string>
409+
<string name="gpg_id_not_found">No key ids were found, check your .gpg-id and make sure it contains the key id</string>
410+
<string name="import_action_text">Import</string>
408411

409412
</resources>

0 commit comments

Comments
 (0)