From 799f1393e49955d05f68b81af26d6cfaf9beadfd Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sun, 9 Jan 2022 16:19:52 +0530 Subject: Make CryptoHandler use Key as the abstraction layer (#1651) --- .../aps/ui/autofill/AutofillDecryptActivityV2.kt | 3 +- .../msfjarvis/aps/ui/crypto/DecryptActivityV2.kt | 3 +- .../aps/ui/crypto/PasswordCreationActivityV2.kt | 3 +- .../dev/msfjarvis/aps/crypto/CryptoHandler.kt | 12 ++--- .../kotlin/dev/msfjarvis/aps/crypto/KeyUtils.kt | 54 ++++++++++++++++++++++ .../dev/msfjarvis/aps/crypto/PGPKeyManager.kt | 45 +----------------- .../aps/crypto/PGPainlessCryptoHandler.kt | 15 +++--- .../dev/msfjarvis/aps/crypto/PGPKeyManagerTest.kt | 2 +- .../aps/crypto/PGPainlessCryptoHandlerTest.kt | 11 +++-- .../kotlin/dev/msfjarvis/aps/crypto/TestUtils.kt | 3 +- crypto-pgpainless/src/test/resources/public_key | 21 +++++++++ 11 files changed, 106 insertions(+), 66 deletions(-) create mode 100644 crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/KeyUtils.kt create mode 100644 crypto-pgpainless/src/test/resources/public_key diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt index 5e722a54..8f4578cf 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt @@ -21,6 +21,7 @@ import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess import com.github.michaelbull.result.runCatching import dagger.hilt.android.AndroidEntryPoint +import dev.msfjarvis.aps.crypto.Key import dev.msfjarvis.aps.data.passfile.PasswordEntry import dev.msfjarvis.aps.injection.crypto.CryptoSet import dev.msfjarvis.aps.ui.crypto.DecryptActivityV2 @@ -132,7 +133,7 @@ class AutofillDecryptActivityV2 : AppCompatActivity() { withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() crypto.decrypt( - DecryptActivityV2.PRIV_KEY, + Key(DecryptActivityV2.PRIV_KEY.encodeToByteArray()), DecryptActivityV2.PASS, encryptedInput, outputStream, diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt index d7d376cc..799ca32d 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt @@ -12,6 +12,7 @@ import android.view.MenuItem import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.crypto.Key import dev.msfjarvis.aps.data.passfile.PasswordEntry import dev.msfjarvis.aps.data.password.FieldItem import dev.msfjarvis.aps.databinding.DecryptLayoutBinding @@ -134,7 +135,7 @@ class DecryptActivityV2 : BasePgpActivity() { val crypto = cryptos.first { it.canHandle(fullPath) } val outputStream = ByteArrayOutputStream() crypto.decrypt( - PRIV_KEY, + Key(PRIV_KEY.encodeToByteArray()), PASS, message, outputStream, diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt index 7bea5e49..ab5b6371 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt @@ -36,6 +36,7 @@ import com.google.zxing.integration.android.IntentIntegrator.QR_CODE import com.google.zxing.qrcode.QRCodeReader import dagger.hilt.android.AndroidEntryPoint import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.crypto.Key import dev.msfjarvis.aps.data.passfile.PasswordEntry import dev.msfjarvis.aps.databinding.PasswordCreationActivityBinding import dev.msfjarvis.aps.injection.crypto.CryptoSet @@ -368,7 +369,7 @@ class PasswordCreationActivityV2 : BasePgpActivity() { withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() crypto.encrypt( - listOf(PUB_KEY), + listOf(Key(PUB_KEY.encodeToByteArray())), content.byteInputStream(), outputStream, ) diff --git a/crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/CryptoHandler.kt b/crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/CryptoHandler.kt index c64e9c9b..eb04ee7a 100644 --- a/crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/CryptoHandler.kt +++ b/crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/CryptoHandler.kt @@ -12,22 +12,22 @@ import java.io.OutputStream public interface CryptoHandler { /** - * Decrypt the given [ciphertextStream] using a [privateKey] and [password], and writes the + * Decrypt the given [ciphertextStream] using a [privateKey] and [passphrase], and writes the * resultant plaintext to [outputStream]. */ public fun decrypt( - privateKey: String, - password: String, + privateKey: Key, + passphrase: String, ciphertextStream: InputStream, outputStream: OutputStream, ) /** - * Encrypt the given [plaintextStream] to the provided [pubKeys], and writes the encrypted - * ciphertext to [outputStream]. + * Encrypt the given [plaintextStream] to the provided [keys], and writes the encrypted ciphertext + * to [outputStream]. */ public fun encrypt( - pubKeys: List, + keys: List, plaintextStream: InputStream, outputStream: OutputStream, ) diff --git a/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/KeyUtils.kt b/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/KeyUtils.kt new file mode 100644 index 00000000..e7a3c387 --- /dev/null +++ b/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/KeyUtils.kt @@ -0,0 +1,54 @@ +/* + * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.crypto + +import com.github.michaelbull.result.get +import com.github.michaelbull.result.runCatching +import java.util.Locale +import org.bouncycastle.openpgp.PGPKeyRing +import org.pgpainless.PGPainless + +/** Utility methods to deal with PGP [Key]s. */ +public object KeyUtils { + /** + * Attempts to parse a [PGPKeyRing] from a given [key]. The key is first tried as a secret key and + * then as a public one before the method gives up and returns null. + */ + public fun tryParseKeyring(key: Key): PGPKeyRing? { + val secKeyRing = runCatching { PGPainless.readKeyRing().secretKeyRing(key.contents) }.get() + if (secKeyRing != null) { + return secKeyRing + } + val pubKeyRing = runCatching { PGPainless.readKeyRing().publicKeyRing(key.contents) }.get() + if (pubKeyRing != null) { + return pubKeyRing + } + return null + } + + /** Parses a [PGPKeyRing] from the given [key] and returns its hex-formatted key ID. */ + public fun tryGetId(key: Key): String? { + val keyRing = tryParseKeyring(key) ?: return null + return convertKeyIdToHex(keyRing.publicKey.keyID) + } + + /** Convert a [Long] key ID to a formatted string. */ + private fun convertKeyIdToHex(keyId: Long): String { + return "0x" + convertKeyIdToHex32bit(keyId shr 32) + convertKeyIdToHex32bit(keyId) + } + + /** + * Converts [keyId] to an unsigned [Long] then uses [java.lang.Long.toHexString] to convert it to + * a lowercase hex ID. + */ + private fun convertKeyIdToHex32bit(keyId: Long): String { + var hexString = java.lang.Long.toHexString(keyId and 0xffffffffL).lowercase(Locale.ENGLISH) + while (hexString.length < 8) { + hexString = "0$hexString" + } + return hexString + } +} diff --git a/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManager.kt b/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManager.kt index f1c53721..2053ecd7 100644 --- a/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManager.kt +++ b/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManager.kt @@ -8,15 +8,13 @@ package dev.msfjarvis.aps.crypto import androidx.annotation.VisibleForTesting import com.github.michaelbull.result.Result -import com.github.michaelbull.result.get import com.github.michaelbull.result.runCatching +import dev.msfjarvis.aps.crypto.KeyUtils.tryGetId +import dev.msfjarvis.aps.crypto.KeyUtils.tryParseKeyring import java.io.File -import java.util.Locale import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import org.bouncycastle.openpgp.PGPKeyRing -import org.pgpainless.PGPainless import org.pgpainless.util.selection.userid.SelectUserId public class PGPKeyManager @@ -118,45 +116,6 @@ constructor( return keyDir.exists() || keyDir.mkdirs() } - /** - * Attempts to parse a [PGPKeyRing] from a given [key]. The key is first tried as a secret key and - * then as a public one before the method gives up and returns null. - */ - private fun tryParseKeyring(key: Key): PGPKeyRing? { - val secKeyRing = runCatching { PGPainless.readKeyRing().secretKeyRing(key.contents) }.get() - if (secKeyRing != null) { - return secKeyRing - } - val pubKeyRing = runCatching { PGPainless.readKeyRing().publicKeyRing(key.contents) }.get() - if (pubKeyRing != null) { - return pubKeyRing - } - return null - } - - /** Parses a [PGPKeyRing] from the given [key] and returns its hex-formatted key ID. */ - private fun tryGetId(key: Key): String? { - val keyRing = tryParseKeyring(key) ?: return null - return convertKeyIdToHex(keyRing.publicKey.keyID) - } - - /** Convert a [Long] key ID to a formatted string. */ - private fun convertKeyIdToHex(keyId: Long): String { - return "0x" + convertKeyIdToHex32bit(keyId shr 32) + convertKeyIdToHex32bit(keyId) - } - - /** - * Converts [keyId] to an unsigned [Long] then uses [java.lang.Long.toHexString] to convert it to - * a lowercase hex ID. - */ - private fun convertKeyIdToHex32bit(keyId: Long): String { - var hexString = java.lang.Long.toHexString(keyId and 0xffffffffL).lowercase(Locale.ENGLISH) - while (hexString.length < 8) { - hexString = "0$hexString" - } - return hexString - } - public companion object { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) diff --git a/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPainlessCryptoHandler.kt b/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPainlessCryptoHandler.kt index 3276b995..427cd555 100644 --- a/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPainlessCryptoHandler.kt +++ b/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPainlessCryptoHandler.kt @@ -21,34 +21,35 @@ import org.pgpainless.util.Passphrase public class PGPainlessCryptoHandler @Inject constructor() : CryptoHandler { public override fun decrypt( - privateKey: String, - password: String, + privateKey: Key, + passphrase: String, ciphertextStream: InputStream, outputStream: OutputStream, ) { - val pgpSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(privateKey) + val pgpSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(privateKey.contents) val keyringCollection = PGPSecretKeyRingCollection(listOf(pgpSecretKeyRing)) val protector = PasswordBasedSecretKeyRingProtector.forKey( pgpSecretKeyRing, - Passphrase.fromPassword(password) + Passphrase.fromPassword(passphrase) ) PGPainless.decryptAndOrVerify() .onInputStream(ciphertextStream) .withOptions( ConsumerOptions() .addDecryptionKeys(keyringCollection, protector) - .addDecryptionPassphrase(Passphrase.fromPassword(password)) + .addDecryptionPassphrase(Passphrase.fromPassword(passphrase)) ) .use { decryptionStream -> decryptionStream.copyTo(outputStream) } } public override fun encrypt( - pubKeys: List, + keys: List, plaintextStream: InputStream, outputStream: OutputStream, ) { - val pubKeysStream = ByteArrayInputStream(pubKeys.joinToString("\n").toByteArray()) + val armoredKeys = keys.map { key -> key.contents.decodeToString() } + val pubKeysStream = ByteArrayInputStream(armoredKeys.joinToString("\n").toByteArray()) val publicKeyRingCollection = pubKeysStream.use { ArmoredInputStream(it).use { armoredInputStream -> diff --git a/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManagerTest.kt b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManagerTest.kt index c547bdd4..ed2b00c3 100644 --- a/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManagerTest.kt +++ b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManagerTest.kt @@ -28,7 +28,7 @@ class PGPKeyManagerTest { private val dispatcher = StandardTestDispatcher() private val scope = TestScope(dispatcher) private val keyManager by unsafeLazy { PGPKeyManager(filesDir.absolutePath, dispatcher) } - private val key = Key(TestUtils.getArmoredPrivateKey().encodeToByteArray()) + private val key = Key(TestUtils.getArmoredPrivateKey()) private fun unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE) { initializer.invoke() } diff --git a/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPainlessCryptoHandlerTest.kt b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPainlessCryptoHandlerTest.kt index c5afeaa2..6ec6ccee 100644 --- a/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPainlessCryptoHandlerTest.kt +++ b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPainlessCryptoHandlerTest.kt @@ -14,19 +14,20 @@ import kotlin.test.assertTrue class PGPainlessCryptoHandlerTest { private val cryptoHandler = PGPainlessCryptoHandler() + private val privateKey = Key(TestUtils.getArmoredPrivateKey()) + private val publicKey = Key(TestUtils.getArmoredPublicKey()) @Test - fun encrypt_and_decrypt() { - val key = TestUtils.getArmoredPrivateKey() + fun encryptAndDecrypt() { val ciphertextStream = ByteArrayOutputStream() cryptoHandler.encrypt( - listOf(key), + listOf(publicKey), CryptoConstants.PLAIN_TEXT.byteInputStream(Charsets.UTF_8), ciphertextStream, ) val plaintextStream = ByteArrayOutputStream() cryptoHandler.decrypt( - key, + privateKey, CryptoConstants.KEY_PASSPHRASE, ciphertextStream.toByteArray().inputStream(), plaintextStream, @@ -35,7 +36,7 @@ class PGPainlessCryptoHandlerTest { } @Test - fun can_handle_filters_formats() { + fun canHandleFiltersFormats() { assertFalse { cryptoHandler.canHandle("example.com") } assertTrue { cryptoHandler.canHandle("example.com.gpg") } assertFalse { cryptoHandler.canHandle("example.com.asc") } diff --git a/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/TestUtils.kt b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/TestUtils.kt index 1e01da51..ab9b6bf1 100644 --- a/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/TestUtils.kt +++ b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/TestUtils.kt @@ -6,5 +6,6 @@ package dev.msfjarvis.aps.crypto object TestUtils { - fun getArmoredPrivateKey() = this::class.java.classLoader.getResource("private_key").readText() + fun getArmoredPrivateKey() = this::class.java.classLoader.getResource("private_key").readBytes() + fun getArmoredPublicKey() = this::class.java.classLoader.getResource("public_key").readBytes() } diff --git a/crypto-pgpainless/src/test/resources/public_key b/crypto-pgpainless/src/test/resources/public_key new file mode 100644 index 00000000..987bac6f --- /dev/null +++ b/crypto-pgpainless/src/test/resources/public_key @@ -0,0 +1,21 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: PGPainless +Comment: BC98 82EF 93DC 22F8 D7D4 47AD 08ED F756 7183 CE27 +Comment: John Doe + +mDMEYT33+BYJKwYBBAHaRw8BAQdAoofwCvOfKJ4pGxEO4s64wFD+QnePpNY5zXgW +TTOFb2+0H0pvaG4gRG9lIDxqb2huLmRvZUBleGFtcGxlLmNvbT6IeAQTFgoAIAUC +YT33+AIbAQUWAgMBAAQLCQgHBRUKCQgLAh4BAhkBAAoJEAjt91Zxg84n5dYA/AiA +BqBdt2ItWgDPLCNEqt9wIMgRpkDrAMtXXyyLSkWsAQCoowpenGsq5fxhuRcS3w6Q +s+/Qw1GqnoidxhioR9J+ALg4BGE99/gSCisGAQQBl1UBBQEBB0C7eFVsFUif4q9S +taBI6JAwsI+hQSAo3I6V4jU3rix8XwMBCAeIdQQYFgoAHQUCYT33+AIbDAUWAgMB +AAQLCQgHBRUKCQgLAh4BAAoJEAjt91Zxg84nmn4BALmD8WYxTdrJqUZUE1TcFvzG +5r0//rPM8Vut5X+KwUXjAQDWVP22KaA8VXpevSxkS3n/ti0KjQVKEFzGbmwB2dTT +CbgzBGE99/gWCSsGAQQB2kcPAQEHQJXfqDjCO9L4qBu62/UPpQ5q0638kG8+AGf/ +hJH2q2BTiNUEGBYKAH0FAmE99/gCGwIFFgIDAQAECwkIBwUVCgkICwIeAV8gBBkW +CgAGBQJhPff4AAoJEGSLoii3QC8mrhcBALzpJQTHF8cJJRA9+DQ3qZ85Eu217MJi +x1aYA1i0zyP5AQD/jN/aBsSTqAHF+zU8/ezzHeoilyBYgxLS9Q2qelDeDAAKCRAI +7fdWcYPOJ7aHAP9EBq0rzV3c6GtVl8bPnk+llpV/1aodxTSnijQtVSMuMAD+JMUD +Jd2bimlhuVwpu0DFiF7IF64SAxmVifTwsTWYiQs= +=jGlC +-----END PGP PUBLIC KEY BLOCK----- -- cgit v1.2.3