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 --- .../data/crypto/CryptoRepositoryTest.kt | 84 ++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 app/src/test/java/app/passwordstore/data/crypto/CryptoRepositoryTest.kt (limited to 'app/src/test') diff --git a/app/src/test/java/app/passwordstore/data/crypto/CryptoRepositoryTest.kt b/app/src/test/java/app/passwordstore/data/crypto/CryptoRepositoryTest.kt new file mode 100644 index 00000000..73349fd1 --- /dev/null +++ b/app/src/test/java/app/passwordstore/data/crypto/CryptoRepositoryTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2026 Danilo M. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.data.crypto + +import app.passwordstore.crypto.PGPIdentifier +import app.passwordstore.crypto.PGPKey +import app.passwordstore.crypto.PGPKeyManager +import app.passwordstore.crypto.PGPainlessCryptoHandler +import app.passwordstore.crypto.errors.KeyNotFoundException +import app.passwordstore.util.coroutines.DispatcherProvider +import com.github.ivanshafran.sharedpreferencesmock.SPMockBuilder +import com.github.michaelbull.result.getError +import com.github.michaelbull.result.unwrap +import java.io.ByteArrayOutputStream +import kotlin.test.Test +import kotlin.test.assertIs +import kotlin.test.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.pgpainless.PGPainless + +class CryptoRepositoryTest { + + @get:Rule val temporaryFolder = TemporaryFolder() + + private val dispatchers = + object : DispatcherProvider { + override fun io() = Dispatchers.Unconfined + } + + private fun newKey(uid: String) = + PGPKey(PGPainless.asciiArmor(PGPainless.generateKeyRing().modernKeyRing(uid)).toByteArray()) + + private suspend fun repository(keys: List): CryptoRepository { + val keyManager = PGPKeyManager(temporaryFolder.root.absolutePath, Dispatchers.Unconfined) + keys.forEach { keyManager.addKey(it).unwrap() } + return CryptoRepository( + keyManager, + PGPainlessCryptoHandler(), + dispatchers, + SPMockBuilder().createSharedPreferences(), + ) + } + + @Test + fun encryptFailsWhenARecipientKeyIsMissing() = runBlocking { + val repository = repository(listOf(newKey("present "))) + val out = ByteArrayOutputStream() + + val result = + repository.encrypt( + listOf( + PGPIdentifier.UserId("present@example.org"), + PGPIdentifier.UserId("missing@example.org"), + ), + "secret".byteInputStream(), + out, + ) + + assertIs(result.getError()) + assertTrue(out.size() == 0, "nothing may be written for a partial recipient set") + } + + @Test + fun encryptSucceedsWhenAllRecipientKeysArePresent() = runBlocking { + val repository = repository(listOf(newKey("a "), newKey("b "))) + val out = ByteArrayOutputStream() + + repository + .encrypt( + listOf(PGPIdentifier.UserId("a@example.org"), PGPIdentifier.UserId("b@example.org")), + "secret".byteInputStream(), + out, + ) + .unwrap() + + assertTrue(out.size() > 0) + } +} -- cgit v1.2.3