crypto-pgpainless: prepare for error handling (#1877)
This commit is contained in:
parent
b8b0693642
commit
d4a4ac06ed
6 changed files with 98 additions and 44 deletions
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
package dev.msfjarvis.aps.data.crypto
|
package dev.msfjarvis.aps.data.crypto
|
||||||
|
|
||||||
import com.github.michaelbull.result.runCatching
|
|
||||||
import com.github.michaelbull.result.unwrap
|
import com.github.michaelbull.result.unwrap
|
||||||
import dev.msfjarvis.aps.crypto.PGPKeyManager
|
import dev.msfjarvis.aps.crypto.PGPKeyManager
|
||||||
import dev.msfjarvis.aps.crypto.PGPainlessCryptoHandler
|
import dev.msfjarvis.aps.crypto.PGPainlessCryptoHandler
|
||||||
|
@ -42,9 +41,7 @@ constructor(
|
||||||
) {
|
) {
|
||||||
val keys = pgpKeyManager.getAllKeys().unwrap()
|
val keys = pgpKeyManager.getAllKeys().unwrap()
|
||||||
// Iterates through the keys until the first successful decryption, then returns.
|
// Iterates through the keys until the first successful decryption, then returns.
|
||||||
keys.firstOrNull { key ->
|
keys.firstOrNull { key -> pgpCryptoHandler.decrypt(key, password, message, out).isOk() }
|
||||||
runCatching { pgpCryptoHandler.decrypt(key, password, message, out) }.isOk()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun encryptPgp(content: ByteArrayInputStream, out: ByteArrayOutputStream) {
|
private suspend fun encryptPgp(content: ByteArrayInputStream, out: ByteArrayOutputStream) {
|
||||||
|
|
|
@ -10,10 +10,8 @@ plugins { id("com.github.android-password-store.kotlin-common") }
|
||||||
|
|
||||||
tasks.withType<KotlinCompile>().configureEach {
|
tasks.withType<KotlinCompile>().configureEach {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
if (project.providers.gradleProperty("android.injected.invoked.from.ide").orNull != "true") {
|
if (!name.contains("test", ignoreCase = true)) {
|
||||||
if (!name.contains("test", ignoreCase = true)) {
|
freeCompilerArgs += listOf("-Xexplicit-api=strict")
|
||||||
freeCompilerArgs = freeCompilerArgs + listOf("-Xexplicit-api=strict")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
package dev.msfjarvis.aps.crypto
|
package dev.msfjarvis.aps.crypto
|
||||||
|
|
||||||
|
import com.github.michaelbull.result.Result
|
||||||
|
import dev.msfjarvis.aps.crypto.errors.CryptoHandlerException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
@ -13,24 +15,27 @@ public interface CryptoHandler<Key> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt the given [ciphertextStream] using a [privateKey] and [passphrase], and writes the
|
* Decrypt the given [ciphertextStream] using a [privateKey] and [passphrase], and writes the
|
||||||
* resultant plaintext to [outputStream].
|
* 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(
|
public fun decrypt(
|
||||||
privateKey: Key,
|
privateKey: Key,
|
||||||
passphrase: String,
|
passphrase: String,
|
||||||
ciphertextStream: InputStream,
|
ciphertextStream: InputStream,
|
||||||
outputStream: OutputStream,
|
outputStream: OutputStream,
|
||||||
)
|
): Result<Unit, CryptoHandlerException>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypt the given [plaintextStream] to the provided [keys], and writes the encrypted ciphertext
|
* Encrypt the given [plaintextStream] to the provided [keys], and writes the encrypted ciphertext
|
||||||
* to [outputStream].
|
* 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 encrypt(
|
public fun encrypt(
|
||||||
keys: List<Key>,
|
keys: List<Key>,
|
||||||
plaintextStream: InputStream,
|
plaintextStream: InputStream,
|
||||||
outputStream: OutputStream,
|
outputStream: OutputStream,
|
||||||
)
|
): Result<Unit, CryptoHandlerException>
|
||||||
|
|
||||||
/** Given a [fileName], return whether this instance can handle it. */
|
/** Given a [fileName], return whether this instance can handle it. */
|
||||||
public fun canHandle(fileName: String): Boolean
|
public fun canHandle(fileName: String): Boolean
|
||||||
|
|
|
@ -2,7 +2,8 @@ package dev.msfjarvis.aps.crypto.errors
|
||||||
|
|
||||||
import dev.msfjarvis.aps.crypto.KeyManager
|
import dev.msfjarvis.aps.crypto.KeyManager
|
||||||
|
|
||||||
public sealed class CryptoException(message: String? = null) : Exception(message)
|
public sealed class CryptoException(message: String? = null, cause: Throwable? = null) :
|
||||||
|
Exception(message, cause)
|
||||||
|
|
||||||
/** Sealed exception types for [KeyManager]. */
|
/** Sealed exception types for [KeyManager]. */
|
||||||
public sealed class KeyManagerException(message: String? = null) : CryptoException(message)
|
public sealed class KeyManagerException(message: String? = null) : CryptoException(message)
|
||||||
|
@ -28,3 +29,13 @@ public class KeyNotFoundException(keyId: String) :
|
||||||
/** Attempting to add another key for [keyId] without requesting a replace. */
|
/** Attempting to add another key for [keyId] without requesting a replace. */
|
||||||
public class KeyAlreadyExistsException(keyId: String) :
|
public class KeyAlreadyExistsException(keyId: String) :
|
||||||
KeyManagerException("Pre-existing key was found for $keyId")
|
KeyManagerException("Pre-existing key was found for $keyId")
|
||||||
|
|
||||||
|
/** Sealed exception types for [CryptoHandler]. */
|
||||||
|
public sealed class CryptoHandlerException(message: String? = null, cause: Throwable? = null) :
|
||||||
|
CryptoException(message, cause)
|
||||||
|
|
||||||
|
/** The passphrase provided for decryption was incorrect. */
|
||||||
|
public class IncorrectPassphraseException(cause: Throwable) : CryptoHandlerException(null, cause)
|
||||||
|
|
||||||
|
/** An unexpected error that cannot be mapped to a known type. */
|
||||||
|
public class UnknownError(cause: Throwable) : CryptoHandlerException(null, cause)
|
||||||
|
|
|
@ -5,6 +5,12 @@
|
||||||
|
|
||||||
package dev.msfjarvis.aps.crypto
|
package dev.msfjarvis.aps.crypto
|
||||||
|
|
||||||
|
import com.github.michaelbull.result.Result
|
||||||
|
import com.github.michaelbull.result.mapError
|
||||||
|
import com.github.michaelbull.result.runCatching
|
||||||
|
import dev.msfjarvis.aps.crypto.errors.CryptoHandlerException
|
||||||
|
import dev.msfjarvis.aps.crypto.errors.IncorrectPassphraseException
|
||||||
|
import dev.msfjarvis.aps.crypto.errors.UnknownError
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
@ -15,6 +21,7 @@ import org.pgpainless.PGPainless
|
||||||
import org.pgpainless.decryption_verification.ConsumerOptions
|
import org.pgpainless.decryption_verification.ConsumerOptions
|
||||||
import org.pgpainless.encryption_signing.EncryptionOptions
|
import org.pgpainless.encryption_signing.EncryptionOptions
|
||||||
import org.pgpainless.encryption_signing.ProducerOptions
|
import org.pgpainless.encryption_signing.ProducerOptions
|
||||||
|
import org.pgpainless.exception.WrongPassphraseException
|
||||||
import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector
|
import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector
|
||||||
import org.pgpainless.util.Passphrase
|
import org.pgpainless.util.Passphrase
|
||||||
|
|
||||||
|
@ -25,44 +32,56 @@ public class PGPainlessCryptoHandler @Inject constructor() : CryptoHandler<PGPKe
|
||||||
passphrase: String,
|
passphrase: String,
|
||||||
ciphertextStream: InputStream,
|
ciphertextStream: InputStream,
|
||||||
outputStream: OutputStream,
|
outputStream: OutputStream,
|
||||||
) {
|
): Result<Unit, CryptoHandlerException> =
|
||||||
val pgpSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(privateKey.contents)
|
runCatching {
|
||||||
val keyringCollection = PGPSecretKeyRingCollection(listOf(pgpSecretKeyRing))
|
val pgpSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(privateKey.contents)
|
||||||
val protector =
|
val keyringCollection = PGPSecretKeyRingCollection(listOf(pgpSecretKeyRing))
|
||||||
PasswordBasedSecretKeyRingProtector.forKey(
|
val protector =
|
||||||
pgpSecretKeyRing,
|
PasswordBasedSecretKeyRingProtector.forKey(
|
||||||
Passphrase.fromPassword(passphrase)
|
pgpSecretKeyRing,
|
||||||
)
|
Passphrase.fromPassword(passphrase)
|
||||||
PGPainless.decryptAndOrVerify()
|
)
|
||||||
.onInputStream(ciphertextStream)
|
PGPainless.decryptAndOrVerify()
|
||||||
.withOptions(
|
.onInputStream(ciphertextStream)
|
||||||
ConsumerOptions()
|
.withOptions(
|
||||||
.addDecryptionKeys(keyringCollection, protector)
|
ConsumerOptions()
|
||||||
.addDecryptionPassphrase(Passphrase.fromPassword(passphrase))
|
.addDecryptionKeys(keyringCollection, protector)
|
||||||
)
|
.addDecryptionPassphrase(Passphrase.fromPassword(passphrase))
|
||||||
.use { decryptionStream -> decryptionStream.copyTo(outputStream) }
|
)
|
||||||
}
|
.use { decryptionStream -> decryptionStream.copyTo(outputStream) }
|
||||||
|
return@runCatching
|
||||||
|
}
|
||||||
|
.mapError { error ->
|
||||||
|
when (error) {
|
||||||
|
is WrongPassphraseException -> IncorrectPassphraseException(error)
|
||||||
|
else -> UnknownError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override fun encrypt(
|
public override fun encrypt(
|
||||||
keys: List<PGPKey>,
|
keys: List<PGPKey>,
|
||||||
plaintextStream: InputStream,
|
plaintextStream: InputStream,
|
||||||
outputStream: OutputStream,
|
outputStream: OutputStream,
|
||||||
) {
|
): Result<Unit, CryptoHandlerException> =
|
||||||
val armoredKeys = keys.map { key -> key.contents.decodeToString() }
|
runCatching {
|
||||||
val pubKeysStream = ByteArrayInputStream(armoredKeys.joinToString("\n").toByteArray())
|
val armoredKeys = keys.map { key -> key.contents.decodeToString() }
|
||||||
val publicKeyRingCollection =
|
val pubKeysStream = ByteArrayInputStream(armoredKeys.joinToString("\n").toByteArray())
|
||||||
pubKeysStream.use {
|
val publicKeyRingCollection =
|
||||||
ArmoredInputStream(it).use { armoredInputStream ->
|
pubKeysStream.use {
|
||||||
PGPainless.readKeyRing().publicKeyRingCollection(armoredInputStream)
|
ArmoredInputStream(it).use { armoredInputStream ->
|
||||||
|
PGPainless.readKeyRing().publicKeyRingCollection(armoredInputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val encOpt =
|
||||||
|
EncryptionOptions().apply { publicKeyRingCollection.forEach { addRecipient(it) } }
|
||||||
|
val prodOpt = ProducerOptions.encrypt(encOpt).setAsciiArmor(true)
|
||||||
|
PGPainless.encryptAndOrSign().onOutputStream(outputStream).withOptions(prodOpt).use {
|
||||||
|
encryptionStream ->
|
||||||
|
plaintextStream.copyTo(encryptionStream)
|
||||||
}
|
}
|
||||||
|
return@runCatching
|
||||||
}
|
}
|
||||||
val encOpt = EncryptionOptions().apply { publicKeyRingCollection.forEach { addRecipient(it) } }
|
.mapError { error -> UnknownError(error) }
|
||||||
val prodOpt = ProducerOptions.encrypt(encOpt).setAsciiArmor(true)
|
|
||||||
PGPainless.encryptAndOrSign().onOutputStream(outputStream).withOptions(prodOpt).use {
|
|
||||||
encryptionStream ->
|
|
||||||
plaintextStream.copyTo(encryptionStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override fun canHandle(fileName: String): Boolean {
|
public override fun canHandle(fileName: String): Boolean {
|
||||||
return fileName.split('.').lastOrNull() == "gpg"
|
return fileName.split('.').lastOrNull() == "gpg"
|
||||||
|
|
|
@ -5,10 +5,14 @@
|
||||||
|
|
||||||
package dev.msfjarvis.aps.crypto
|
package dev.msfjarvis.aps.crypto
|
||||||
|
|
||||||
|
import com.github.michaelbull.result.Err
|
||||||
|
import com.github.michaelbull.result.getError
|
||||||
|
import dev.msfjarvis.aps.crypto.errors.IncorrectPassphraseException
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertIs
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class PGPainlessCryptoHandlerTest {
|
class PGPainlessCryptoHandlerTest {
|
||||||
|
@ -35,6 +39,26 @@ class PGPainlessCryptoHandlerTest {
|
||||||
assertEquals(CryptoConstants.PLAIN_TEXT, plaintextStream.toString(Charsets.UTF_8))
|
assertEquals(CryptoConstants.PLAIN_TEXT, plaintextStream.toString(Charsets.UTF_8))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun decryptWithWrongPassphrase() {
|
||||||
|
val ciphertextStream = ByteArrayOutputStream()
|
||||||
|
cryptoHandler.encrypt(
|
||||||
|
listOf(publicKey),
|
||||||
|
CryptoConstants.PLAIN_TEXT.byteInputStream(Charsets.UTF_8),
|
||||||
|
ciphertextStream,
|
||||||
|
)
|
||||||
|
val plaintextStream = ByteArrayOutputStream()
|
||||||
|
val result =
|
||||||
|
cryptoHandler.decrypt(
|
||||||
|
privateKey,
|
||||||
|
"very incorrect passphrase",
|
||||||
|
ciphertextStream.toByteArray().inputStream(),
|
||||||
|
plaintextStream,
|
||||||
|
)
|
||||||
|
assertIs<Err<Throwable>>(result)
|
||||||
|
assertIs<IncorrectPassphraseException>(result.getError())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun canHandleFiltersFormats() {
|
fun canHandleFiltersFormats() {
|
||||||
assertFalse { cryptoHandler.canHandle("example.com") }
|
assertFalse { cryptoHandler.canHandle("example.com") }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue