aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2022-08-24 22:44:02 +0530
committerGitHub <noreply@github.com>2022-08-24 17:14:02 +0000
commit8129495608c1029cdb09071a80a73971d20f25d7 (patch)
treed2865f94ff382b045c0a5bb3016391b45e43fdf6
parent3178ec97632c5e9655cf3fc7de9fa1d2b4f30243 (diff)
Implement support for `.gpg-id` (#2080)
-rw-r--r--app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt21
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt8
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt47
-rw-r--r--app/src/main/res/values/strings.xml1
-rw-r--r--crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt8
-rw-r--r--crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt33
-rw-r--r--crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt4
7 files changed, 90 insertions, 32 deletions
diff --git a/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt b/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt
index 6c214fd1..297ada95 100644
--- a/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt
+++ b/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt
@@ -5,9 +5,10 @@
package app.passwordstore.data.crypto
+import app.passwordstore.crypto.GpgIdentifier
import app.passwordstore.crypto.PGPKeyManager
import app.passwordstore.crypto.PGPainlessCryptoHandler
-import app.passwordstore.util.extensions.isOk
+import com.github.michaelbull.result.getAll
import com.github.michaelbull.result.unwrap
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@@ -30,8 +31,12 @@ constructor(
withContext(Dispatchers.IO) { decryptPgp(password, message, out) }
}
- suspend fun encrypt(content: ByteArrayInputStream, out: ByteArrayOutputStream) {
- withContext(Dispatchers.IO) { encryptPgp(content, out) }
+ suspend fun encrypt(
+ identities: List<GpgIdentifier>,
+ content: ByteArrayInputStream,
+ out: ByteArrayOutputStream,
+ ) {
+ withContext(Dispatchers.IO) { encryptPgp(identities, content, out) }
}
private suspend fun decryptPgp(
@@ -41,11 +46,15 @@ constructor(
) {
val keys = pgpKeyManager.getAllKeys().unwrap()
// Iterates through the keys until the first successful decryption, then returns.
- keys.firstOrNull { key -> pgpCryptoHandler.decrypt(key, password, message, out).isOk() }
+ pgpCryptoHandler.decrypt(keys, password, message, out)
}
- private suspend fun encryptPgp(content: ByteArrayInputStream, out: ByteArrayOutputStream) {
- val keys = pgpKeyManager.getAllKeys().unwrap()
+ private suspend fun encryptPgp(
+ identities: List<GpgIdentifier>,
+ content: ByteArrayInputStream,
+ out: ByteArrayOutputStream,
+ ) {
+ val keys = identities.map { ident -> pgpKeyManager.getKeyById(ident) }.getAll()
pgpCryptoHandler.encrypt(
keys,
content,
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt
index 3fb0d52f..fa37f501 100644
--- a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt
@@ -21,6 +21,7 @@ import app.passwordstore.util.extensions.unsafeLazy
import app.passwordstore.util.extensions.viewBinding
import app.passwordstore.util.settings.PreferenceKeys
import com.github.michaelbull.result.runCatching
+import com.github.michaelbull.result.unwrapError
import dagger.hilt.android.AndroidEntryPoint
import java.io.ByteArrayOutputStream
import java.io.File
@@ -35,6 +36,8 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import logcat.LogPriority.ERROR
+import logcat.logcat
@OptIn(ExperimentalTime::class)
@AndroidEntryPoint
@@ -140,7 +143,9 @@ class DecryptActivity : BasePgpActivity() {
lifecycleScope.launch(Dispatchers.Main) {
dialog.password.collectLatest { value ->
if (value != null) {
- if (runCatching { decrypt(value) }.isErr()) {
+ val res = runCatching { decrypt(value) }
+ if (res.isErr()) {
+ logcat(ERROR) { res.unwrapError().stackTraceToString() }
decrypt(isError = true)
}
}
@@ -161,7 +166,6 @@ class DecryptActivity : BasePgpActivity() {
)
outputStream
}
- require(result.size() != 0) { "Incorrect password" }
startAutoDismissTimer()
val entry = passwordEntryFactory.create(result.toByteArray())
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt
index 2e3e48a9..fddc2943 100644
--- a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt
@@ -24,8 +24,10 @@ import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.lifecycleScope
import app.passwordstore.R
+import app.passwordstore.crypto.GpgIdentifier
import app.passwordstore.data.crypto.CryptoRepository
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
@@ -332,6 +334,32 @@ class PasswordCreationActivity : BasePgpActivity() {
copyPasswordToClipboard(editPass)
}
+ // 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()).not()) {
+ snackbar(message = resources.getString(R.string.invalid_gpg_id))
+ }
+ return@with
+ }
+ }
+ .filterIsInstance<GpgIdentifier>()
+ if (gpgIdentifiers.isEmpty()) {
+ error("Failed to parse identifiers from .gpg-id")
+ }
val content = "$editPass\n$editExtra"
val path =
when {
@@ -360,7 +388,7 @@ class PasswordCreationActivity : BasePgpActivity() {
val result =
withContext(Dispatchers.IO) {
val outputStream = ByteArrayOutputStream()
- repository.encrypt(content.byteInputStream(), outputStream)
+ repository.encrypt(gpgIdentifiers, content.byteInputStream(), outputStream)
outputStream
}
val file = File(path)
@@ -457,6 +485,23 @@ class PasswordCreationActivity : BasePgpActivity() {
}
}
+ @Suppress("ReturnCount")
+ 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
+ }
+ }
+
companion object {
private const val KEY_PWGEN_TYPE_CLASSIC = "classic"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d2c9ab06..9997f6aa 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -299,6 +299,7 @@
<string name="otp_import_success">Successfully imported TOTP configuration</string>
<string name="otp_import_failure">Failed to import TOTP configuration</string>
<string name="exporting_passwords">Exporting passwords…</string>
+ <string name="invalid_gpg_id">Found .gpg-id, but it contains an invalid key ID, fingerprint or user ID</string>
<string name="invalid_filename_text">File name must not contain \'/\', set directory above</string>
<string name="directory_hint">Directory</string>
<string name="new_folder_set_gpg_key">Set GPG key for directory</string>
diff --git a/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt b/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt
index f8995bf6..ea42af6d 100644
--- a/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt
+++ b/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt
@@ -14,13 +14,13 @@ import java.io.OutputStream
public interface CryptoHandler<Key> {
/**
- * Decrypt the given [ciphertextStream] using a [secretKey] and [passphrase], and writes the
- * resultant plaintext to [outputStream]. The returned [Result] should be checked to ensure it is
- * **not** an instance of [com.github.michaelbull.result.Err] before the contents of
+ * Decrypt the given [ciphertextStream] using a set of potential [keys] and [passphrase], and
+ * writes the resultant plaintext to [outputStream]. The returned [Result] should be checked to
+ * ensure it is **not** an instance of [com.github.michaelbull.result.Err] before the contents of
* [outputStream] are used.
*/
public fun decrypt(
- secretKey: Key,
+ keys: List<Key>,
passphrase: String,
ciphertextStream: InputStream,
outputStream: OutputStream,
diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt
index 64ad426f..74f880a2 100644
--- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt
+++ b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt
@@ -12,12 +12,12 @@ import app.passwordstore.crypto.errors.UnknownError
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.mapError
import com.github.michaelbull.result.runCatching
-import java.io.ByteArrayInputStream
import java.io.InputStream
import java.io.OutputStream
import javax.inject.Inject
import org.bouncycastle.openpgp.PGPPublicKeyRing
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection
+import org.bouncycastle.openpgp.PGPSecretKeyRing
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection
import org.pgpainless.PGPainless
import org.pgpainless.decryption_verification.ConsumerOptions
@@ -25,23 +25,25 @@ import org.pgpainless.encryption_signing.EncryptionOptions
import org.pgpainless.encryption_signing.ProducerOptions
import org.pgpainless.exception.WrongPassphraseException
import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector
-import org.pgpainless.key.util.KeyRingUtils
import org.pgpainless.util.Passphrase
public class PGPainlessCryptoHandler @Inject constructor() : CryptoHandler<PGPKey> {
public override fun decrypt(
- secretKey: PGPKey,
+ keys: List<PGPKey>,
passphrase: String,
ciphertextStream: InputStream,
outputStream: OutputStream,
): Result<Unit, CryptoHandlerException> =
runCatching {
- val pgpSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(secretKey.contents)
- val keyringCollection = PGPSecretKeyRingCollection(listOf(pgpSecretKeyRing))
+ if (keys.isEmpty()) throw NoKeysProvided("No keys provided for encryption")
+ val keyringCollection =
+ keys
+ .map { key -> PGPainless.readKeyRing().secretKeyRing(key.contents) }
+ .run(::PGPSecretKeyRingCollection)
val protector =
PasswordBasedSecretKeyRingProtector.forKey(
- pgpSecretKeyRing,
+ keyringCollection.first(),
Passphrase.fromPassword(passphrase)
)
PGPainless.decryptAndOrVerify()
@@ -68,17 +70,14 @@ public class PGPainlessCryptoHandler @Inject constructor() : CryptoHandler<PGPKe
): Result<Unit, CryptoHandlerException> =
runCatching {
if (keys.isEmpty()) throw NoKeysProvided("No keys provided for encryption")
- val publicKeyRings = arrayListOf<PGPPublicKeyRing>()
- val armoredKeys =
- keys.joinToString("\n") { key -> key.contents.decodeToString() }.toByteArray()
- val secKeysStream = ByteArrayInputStream(armoredKeys)
- publicKeyRings.addAll(
- KeyRingUtils.publicKeyRingCollectionFrom(
- PGPainless.readKeyRing().secretKeyRingCollection(secKeysStream)
- )
- )
- val pubKeysStream = ByteArrayInputStream(armoredKeys)
- publicKeyRings.addAll(PGPainless.readKeyRing().publicKeyRingCollection(pubKeysStream))
+ val publicKeyRings =
+ keys.mapNotNull(KeyUtils::tryParseKeyring).mapNotNull { keyRing ->
+ when (keyRing) {
+ is PGPPublicKeyRing -> keyRing
+ is PGPSecretKeyRing -> PGPainless.extractCertificate(keyRing)
+ else -> null
+ }
+ }
require(keys.size == publicKeyRings.size) {
"Failed to parse all keys: keys=${keys.size},parsed=${publicKeyRings.size}"
}
diff --git a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt b/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt
index 6a20fe25..80c8dc7c 100644
--- a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt
+++ b/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt
@@ -46,7 +46,7 @@ class PGPainlessCryptoHandlerTest {
val plaintextStream = ByteArrayOutputStream()
val decryptRes =
cryptoHandler.decrypt(
- secretKey,
+ listOf(secretKey),
CryptoConstants.KEY_PASSPHRASE,
ciphertextStream.toByteArray().inputStream(),
plaintextStream,
@@ -68,7 +68,7 @@ class PGPainlessCryptoHandlerTest {
val plaintextStream = ByteArrayOutputStream()
val result =
cryptoHandler.decrypt(
- secretKey,
+ listOf(secretKey),
"very incorrect passphrase",
ciphertextStream.toByteArray().inputStream(),
plaintextStream,