aboutsummaryrefslogtreecommitdiffstats
path: root/crypto
diff options
context:
space:
mode:
Diffstat (limited to 'crypto')
-rw-r--r--crypto/common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt4
-rw-r--r--crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt2
-rw-r--r--crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt4
-rw-r--r--crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt11
4 files changed, 21 insertions, 0 deletions
diff --git a/crypto/common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt b/crypto/common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt
index af6145d0..9589ceec 100644
--- a/crypto/common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt
+++ b/crypto/common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt
@@ -39,6 +39,10 @@ public class KeyNotFoundException(keyId: String) :
public class KeyAlreadyExistsException(keyId: String) :
KeyManagerException("Pre-existing key was found for $keyId")
+/** Attempting to replace a stored secret key with a public-only key of the same `keyId`. */
+public class KeyDowngradeException(keyId: String) :
+ KeyManagerException("A secret key is stored for $keyId; a public key cannot replace it")
+
/** Sealed exception types for [app.passwordstore.crypto.CryptoHandler]. */
public sealed class CryptoHandlerException(message: String? = null, cause: Throwable? = null) :
CryptoException(message, cause)
diff --git a/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt
index 311e9049..9acd0489 100644
--- a/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt
+++ b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt
@@ -41,6 +41,8 @@ public object KeyUtils {
}
/** True when [key] parses as a secret key ring, the only kind that can decrypt. */
+ // ponytail: GnuPG stub and smartcard keyrings (no usable secret material) count as secret too.
+ // Upgrade path: check for a secret subkey usable for decryption.
public fun isSecretKey(key: PGPKey): Boolean = tryParseKeyring(key) is PGPSecretKeyRing
/**
diff --git a/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt
index 6f852af4..ef58ad19 100644
--- a/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt
+++ b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt
@@ -14,6 +14,7 @@ import app.passwordstore.crypto.errors.InvalidKeyException
import app.passwordstore.crypto.errors.KeyAlreadyExistsException
import app.passwordstore.crypto.errors.KeyDeletionFailedException
import app.passwordstore.crypto.errors.KeyDirectoryUnavailableException
+import app.passwordstore.crypto.errors.KeyDowngradeException
import app.passwordstore.crypto.errors.KeyNotFoundException
import app.passwordstore.crypto.errors.NoKeysAvailableException
import app.passwordstore.crypto.errors.UnusableKeyException
@@ -59,6 +60,9 @@ constructor(filesDir: String, private val dispatcher: CoroutineDispatcher) :
keyFile.writeBytes(keyBytes)
return@runSuspendCatching key
}
+ // Never with replace either: it would delete the only copy of the private key.
+ existingKeyRing is PGPSecretKeyRing && incomingKeyRing is PGPPublicKeyRing ->
+ throw KeyDowngradeException(tryGetId(key)?.toString() ?: "Failed to retrieve key ID")
}
// Check for replace flag first and if it is false, throw an error
if (!replace)
diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt
index 0525c28a..81a666f4 100644
--- a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt
+++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt
@@ -9,6 +9,7 @@ import app.passwordstore.crypto.KeyUtils.tryGetId
import app.passwordstore.crypto.PGPIdentifier.KeyId
import app.passwordstore.crypto.PGPIdentifier.UserId
import app.passwordstore.crypto.errors.KeyAlreadyExistsException
+import app.passwordstore.crypto.errors.KeyDowngradeException
import app.passwordstore.crypto.errors.KeyNotFoundException
import app.passwordstore.crypto.errors.NoKeysAvailableException
import app.passwordstore.crypto.errors.UnusableKeyException
@@ -177,6 +178,16 @@ class PGPKeyManagerTest {
}
@Test
+ fun replaceSecretKeyWithPublicKeyIsRefusedEvenWithReplace() =
+ runTest(dispatcher) {
+ keyManager.addKey(secretKey).unwrap()
+ assertIs<KeyDowngradeException>(keyManager.addKey(publicKey, replace = true).unwrapError())
+ assertIs<KeyDowngradeException>(keyManager.addKey(publicKey, replace = false).unwrapError())
+ // The private key is still the stored one.
+ assertContentEquals(secretKey.contents, keyManager.getAllKeys().unwrap().single().contents)
+ }
+
+ @Test
fun replacePublicKeyWithSecretKey() =
runTest(dispatcher) {
assertTrue(keyManager.addKey(publicKey).isOk)