Make CryptoHandler use Key as the abstraction layer (#1651)

This commit is contained in:
Harsh Shandilya 2022-01-09 16:19:52 +05:30 committed by GitHub
parent ccb33af854
commit 799f1393e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 106 additions and 66 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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,
)

View file

@ -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<String>,
keys: List<Key>,
plaintextStream: InputStream,
outputStream: OutputStream,
)

View file

@ -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
}
}

View file

@ -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)

View file

@ -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<String>,
keys: List<Key>,
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 ->

View file

@ -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 <T> unsafeLazy(initializer: () -> T) =
lazy(LazyThreadSafetyMode.NONE) { initializer.invoke() }

View file

@ -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") }

View file

@ -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()
}

View file

@ -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 <john.doe@example.com>
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-----