From 931eb40e8e4deb41edcb1c725e74586ff9f918b2 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 2 Nov 2026 18:27:09 +0100 Subject: fix(crypto): refuse to encrypt when a .gpg-id recipient key is missing CryptoRepository.encrypt looked up every .gpg-id recipient and dropped the ones it could not find, so an entry saved on a device that lacked one recipient's key was encrypted to the others only. With a store shared between a phone key and a smartcard key, entries created on the phone could no longer be decrypted with the smartcard alone. Fail with the lookup error instead (KeyNotFoundException names the key), so the user imports the missing public key and saves again. PasswordCreationActivity also ignored the Result of encrypt and wrote the output buffer regardless: on any encryption failure it wrote an empty .gpg file, overwriting the entry when editing, and committed it. Show the error and leave the file untouched. CryptoRepositoryTest covers both the missing-recipient failure and the all-present success path, with keys generated by PGPainless (added as an app test dependency). Co-Authored-By: Claude Opus 5.5 --- .../app/passwordstore/data/crypto/CryptoRepository.kt | 11 +++++++++-- .../ui/crypto/PasswordCreationActivity.kt | 19 +++++++++++++++---- app/src/main/res/values/strings.xml | 5 +++-- 3 files changed, 27 insertions(+), 8 deletions(-) (limited to 'app/src/main') 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 eb858006..61b63ea7 100644 --- a/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt +++ b/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt @@ -14,6 +14,8 @@ import app.passwordstore.crypto.PGPainlessCryptoHandler import app.passwordstore.injection.prefs.SettingsPreferences import app.passwordstore.util.coroutines.DispatcherProvider import app.passwordstore.util.settings.PreferenceKeys +import com.github.michaelbull.result.andThen +import com.github.michaelbull.result.combine import com.github.michaelbull.result.filterValues import com.github.michaelbull.result.map import com.github.michaelbull.result.mapBoth @@ -64,7 +66,12 @@ constructor( PGPEncryptOptions.Builder() .withAsciiArmor(settings.getBoolean(PreferenceKeys.ASCII_ARMOR, false)) .build() - val keys = identities.map { id -> pgpKeyManager.getKeyById(id) }.filterValues() - pgpCryptoHandler.encrypt(keys, content, out, encryptionOptions).map { out } + // Every recipient in .gpg-id must be present: silently dropping one would write an entry + // that the missing key's owner can no longer decrypt. + identities + .map { id -> pgpKeyManager.getKeyById(id) } + .combine() + .andThen { keys -> pgpCryptoHandler.encrypt(keys, content, out, encryptionOptions) } + .map { out } } } 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 b4fd21e5..6971c2e3 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt @@ -40,6 +40,7 @@ import app.passwordstore.util.extensions.unsafeLazy import app.passwordstore.util.extensions.viewBinding import app.passwordstore.util.settings.DirectoryStructure import app.passwordstore.util.settings.PreferenceKeys +import com.github.michaelbull.result.getOrElse import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess import com.github.michaelbull.result.runCatching @@ -378,10 +379,20 @@ class PasswordCreationActivity : BasePGPActivity() { runCatching { val result = withContext(dispatcherProvider.io()) { - val outputStream = ByteArrayOutputStream() - repository.encrypt(gpgIdentifiers, content.byteInputStream(), outputStream) - outputStream - } + val outputStream = ByteArrayOutputStream() + repository.encrypt(gpgIdentifiers, content.byteInputStream(), outputStream) + } + .getOrElse { e -> + logcat(ERROR) { e.asLog("Failed to encrypt password file") } + MaterialAlertDialogBuilder(this@PasswordCreationActivity) + .setTitle(R.string.password_creation_file_fail_title) + .setMessage( + getString(R.string.password_creation_encrypt_fail_message, e.message) + ) + .setPositiveButton(android.R.string.ok, null) + .show() + return@runCatching + } val passwordFile = Paths.get(path) // If we're not editing, this file should not already exist! // Additionally, if we were editing and the incoming and outgoing diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0584a1f2..61596e58 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -38,8 +38,8 @@ Failed to move %1$s to %2$s - Add generated password for %1$s using Android Password Store. - Edit password for %1$s using Android Password Store. + Add generated password for %1$s using HardenedPass. + Edit password for %1$s using HardenedPass. Remove %1$s from store. Rename %1$s to %2$s. Move multiple passwords to %1$s. @@ -304,6 +304,7 @@ Error + Could not encrypt the password, nothing was saved: %1$s Failed to write password file to the store, please try again. Failed to delete password file %1$s from the store, please delete it manually. File already exists, please use a different name -- cgit v1.2.3