Make CryptoHandler use Key as the abstraction layer (#1651)
This commit is contained in:
parent
ccb33af854
commit
799f1393e4
11 changed files with 106 additions and 66 deletions
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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") }
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
21
crypto-pgpainless/src/test/resources/public_key
Normal file
21
crypto-pgpainless/src/test/resources/public_key
Normal 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-----
|
Loading…
Reference in a new issue