mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-08-29 13:27:46 +00:00
Make CryptoHandler use Key as the abstraction layer (#1651)
This commit is contained in:
parent
ccb33af854
commit
799f1393e4
@ -21,6 +21,7 @@ import com.github.michaelbull.result.onFailure
|
|||||||
import com.github.michaelbull.result.onSuccess
|
import com.github.michaelbull.result.onSuccess
|
||||||
import com.github.michaelbull.result.runCatching
|
import com.github.michaelbull.result.runCatching
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dev.msfjarvis.aps.crypto.Key
|
||||||
import dev.msfjarvis.aps.data.passfile.PasswordEntry
|
import dev.msfjarvis.aps.data.passfile.PasswordEntry
|
||||||
import dev.msfjarvis.aps.injection.crypto.CryptoSet
|
import dev.msfjarvis.aps.injection.crypto.CryptoSet
|
||||||
import dev.msfjarvis.aps.ui.crypto.DecryptActivityV2
|
import dev.msfjarvis.aps.ui.crypto.DecryptActivityV2
|
||||||
@ -132,7 +133,7 @@ class AutofillDecryptActivityV2 : AppCompatActivity() {
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val outputStream = ByteArrayOutputStream()
|
val outputStream = ByteArrayOutputStream()
|
||||||
crypto.decrypt(
|
crypto.decrypt(
|
||||||
DecryptActivityV2.PRIV_KEY,
|
Key(DecryptActivityV2.PRIV_KEY.encodeToByteArray()),
|
||||||
DecryptActivityV2.PASS,
|
DecryptActivityV2.PASS,
|
||||||
encryptedInput,
|
encryptedInput,
|
||||||
outputStream,
|
outputStream,
|
||||||
|
@ -12,6 +12,7 @@ import android.view.MenuItem
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.msfjarvis.aps.R
|
import dev.msfjarvis.aps.R
|
||||||
|
import dev.msfjarvis.aps.crypto.Key
|
||||||
import dev.msfjarvis.aps.data.passfile.PasswordEntry
|
import dev.msfjarvis.aps.data.passfile.PasswordEntry
|
||||||
import dev.msfjarvis.aps.data.password.FieldItem
|
import dev.msfjarvis.aps.data.password.FieldItem
|
||||||
import dev.msfjarvis.aps.databinding.DecryptLayoutBinding
|
import dev.msfjarvis.aps.databinding.DecryptLayoutBinding
|
||||||
@ -134,7 +135,7 @@ class DecryptActivityV2 : BasePgpActivity() {
|
|||||||
val crypto = cryptos.first { it.canHandle(fullPath) }
|
val crypto = cryptos.first { it.canHandle(fullPath) }
|
||||||
val outputStream = ByteArrayOutputStream()
|
val outputStream = ByteArrayOutputStream()
|
||||||
crypto.decrypt(
|
crypto.decrypt(
|
||||||
PRIV_KEY,
|
Key(PRIV_KEY.encodeToByteArray()),
|
||||||
PASS,
|
PASS,
|
||||||
message,
|
message,
|
||||||
outputStream,
|
outputStream,
|
||||||
|
@ -36,6 +36,7 @@ import com.google.zxing.integration.android.IntentIntegrator.QR_CODE
|
|||||||
import com.google.zxing.qrcode.QRCodeReader
|
import com.google.zxing.qrcode.QRCodeReader
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.msfjarvis.aps.R
|
import dev.msfjarvis.aps.R
|
||||||
|
import dev.msfjarvis.aps.crypto.Key
|
||||||
import dev.msfjarvis.aps.data.passfile.PasswordEntry
|
import dev.msfjarvis.aps.data.passfile.PasswordEntry
|
||||||
import dev.msfjarvis.aps.databinding.PasswordCreationActivityBinding
|
import dev.msfjarvis.aps.databinding.PasswordCreationActivityBinding
|
||||||
import dev.msfjarvis.aps.injection.crypto.CryptoSet
|
import dev.msfjarvis.aps.injection.crypto.CryptoSet
|
||||||
@ -368,7 +369,7 @@ class PasswordCreationActivityV2 : BasePgpActivity() {
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val outputStream = ByteArrayOutputStream()
|
val outputStream = ByteArrayOutputStream()
|
||||||
crypto.encrypt(
|
crypto.encrypt(
|
||||||
listOf(PUB_KEY),
|
listOf(Key(PUB_KEY.encodeToByteArray())),
|
||||||
content.byteInputStream(),
|
content.byteInputStream(),
|
||||||
outputStream,
|
outputStream,
|
||||||
)
|
)
|
||||||
|
@ -12,22 +12,22 @@ import java.io.OutputStream
|
|||||||
public interface CryptoHandler {
|
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].
|
* resultant plaintext to [outputStream].
|
||||||
*/
|
*/
|
||||||
public fun decrypt(
|
public fun decrypt(
|
||||||
privateKey: String,
|
privateKey: Key,
|
||||||
password: String,
|
passphrase: String,
|
||||||
ciphertextStream: InputStream,
|
ciphertextStream: InputStream,
|
||||||
outputStream: OutputStream,
|
outputStream: OutputStream,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypt the given [plaintextStream] to the provided [pubKeys], and writes the encrypted
|
* Encrypt the given [plaintextStream] to the provided [keys], and writes the encrypted ciphertext
|
||||||
* ciphertext to [outputStream].
|
* to [outputStream].
|
||||||
*/
|
*/
|
||||||
public fun encrypt(
|
public fun encrypt(
|
||||||
pubKeys: List<String>,
|
keys: List<Key>,
|
||||||
plaintextStream: InputStream,
|
plaintextStream: InputStream,
|
||||||
outputStream: OutputStream,
|
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 androidx.annotation.VisibleForTesting
|
||||||
import com.github.michaelbull.result.Result
|
import com.github.michaelbull.result.Result
|
||||||
import com.github.michaelbull.result.get
|
|
||||||
import com.github.michaelbull.result.runCatching
|
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.io.File
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.bouncycastle.openpgp.PGPKeyRing
|
|
||||||
import org.pgpainless.PGPainless
|
|
||||||
import org.pgpainless.util.selection.userid.SelectUserId
|
import org.pgpainless.util.selection.userid.SelectUserId
|
||||||
|
|
||||||
public class PGPKeyManager
|
public class PGPKeyManager
|
||||||
@ -118,45 +116,6 @@ constructor(
|
|||||||
return keyDir.exists() || keyDir.mkdirs()
|
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 {
|
public companion object {
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
@ -21,34 +21,35 @@ import org.pgpainless.util.Passphrase
|
|||||||
public class PGPainlessCryptoHandler @Inject constructor() : CryptoHandler {
|
public class PGPainlessCryptoHandler @Inject constructor() : CryptoHandler {
|
||||||
|
|
||||||
public override fun decrypt(
|
public override fun decrypt(
|
||||||
privateKey: String,
|
privateKey: Key,
|
||||||
password: String,
|
passphrase: String,
|
||||||
ciphertextStream: InputStream,
|
ciphertextStream: InputStream,
|
||||||
outputStream: OutputStream,
|
outputStream: OutputStream,
|
||||||
) {
|
) {
|
||||||
val pgpSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(privateKey)
|
val pgpSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(privateKey.contents)
|
||||||
val keyringCollection = PGPSecretKeyRingCollection(listOf(pgpSecretKeyRing))
|
val keyringCollection = PGPSecretKeyRingCollection(listOf(pgpSecretKeyRing))
|
||||||
val protector =
|
val protector =
|
||||||
PasswordBasedSecretKeyRingProtector.forKey(
|
PasswordBasedSecretKeyRingProtector.forKey(
|
||||||
pgpSecretKeyRing,
|
pgpSecretKeyRing,
|
||||||
Passphrase.fromPassword(password)
|
Passphrase.fromPassword(passphrase)
|
||||||
)
|
)
|
||||||
PGPainless.decryptAndOrVerify()
|
PGPainless.decryptAndOrVerify()
|
||||||
.onInputStream(ciphertextStream)
|
.onInputStream(ciphertextStream)
|
||||||
.withOptions(
|
.withOptions(
|
||||||
ConsumerOptions()
|
ConsumerOptions()
|
||||||
.addDecryptionKeys(keyringCollection, protector)
|
.addDecryptionKeys(keyringCollection, protector)
|
||||||
.addDecryptionPassphrase(Passphrase.fromPassword(password))
|
.addDecryptionPassphrase(Passphrase.fromPassword(passphrase))
|
||||||
)
|
)
|
||||||
.use { decryptionStream -> decryptionStream.copyTo(outputStream) }
|
.use { decryptionStream -> decryptionStream.copyTo(outputStream) }
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun encrypt(
|
public override fun encrypt(
|
||||||
pubKeys: List<String>,
|
keys: List<Key>,
|
||||||
plaintextStream: InputStream,
|
plaintextStream: InputStream,
|
||||||
outputStream: OutputStream,
|
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 =
|
val publicKeyRingCollection =
|
||||||
pubKeysStream.use {
|
pubKeysStream.use {
|
||||||
ArmoredInputStream(it).use { armoredInputStream ->
|
ArmoredInputStream(it).use { armoredInputStream ->
|
||||||
|
@ -28,7 +28,7 @@ class PGPKeyManagerTest {
|
|||||||
private val dispatcher = StandardTestDispatcher()
|
private val dispatcher = StandardTestDispatcher()
|
||||||
private val scope = TestScope(dispatcher)
|
private val scope = TestScope(dispatcher)
|
||||||
private val keyManager by unsafeLazy { PGPKeyManager(filesDir.absolutePath, 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) =
|
private fun <T> unsafeLazy(initializer: () -> T) =
|
||||||
lazy(LazyThreadSafetyMode.NONE) { initializer.invoke() }
|
lazy(LazyThreadSafetyMode.NONE) { initializer.invoke() }
|
||||||
|
@ -14,19 +14,20 @@ import kotlin.test.assertTrue
|
|||||||
class PGPainlessCryptoHandlerTest {
|
class PGPainlessCryptoHandlerTest {
|
||||||
|
|
||||||
private val cryptoHandler = PGPainlessCryptoHandler()
|
private val cryptoHandler = PGPainlessCryptoHandler()
|
||||||
|
private val privateKey = Key(TestUtils.getArmoredPrivateKey())
|
||||||
|
private val publicKey = Key(TestUtils.getArmoredPublicKey())
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun encrypt_and_decrypt() {
|
fun encryptAndDecrypt() {
|
||||||
val key = TestUtils.getArmoredPrivateKey()
|
|
||||||
val ciphertextStream = ByteArrayOutputStream()
|
val ciphertextStream = ByteArrayOutputStream()
|
||||||
cryptoHandler.encrypt(
|
cryptoHandler.encrypt(
|
||||||
listOf(key),
|
listOf(publicKey),
|
||||||
CryptoConstants.PLAIN_TEXT.byteInputStream(Charsets.UTF_8),
|
CryptoConstants.PLAIN_TEXT.byteInputStream(Charsets.UTF_8),
|
||||||
ciphertextStream,
|
ciphertextStream,
|
||||||
)
|
)
|
||||||
val plaintextStream = ByteArrayOutputStream()
|
val plaintextStream = ByteArrayOutputStream()
|
||||||
cryptoHandler.decrypt(
|
cryptoHandler.decrypt(
|
||||||
key,
|
privateKey,
|
||||||
CryptoConstants.KEY_PASSPHRASE,
|
CryptoConstants.KEY_PASSPHRASE,
|
||||||
ciphertextStream.toByteArray().inputStream(),
|
ciphertextStream.toByteArray().inputStream(),
|
||||||
plaintextStream,
|
plaintextStream,
|
||||||
@ -35,7 +36,7 @@ class PGPainlessCryptoHandlerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun can_handle_filters_formats() {
|
fun canHandleFiltersFormats() {
|
||||||
assertFalse { cryptoHandler.canHandle("example.com") }
|
assertFalse { cryptoHandler.canHandle("example.com") }
|
||||||
assertTrue { cryptoHandler.canHandle("example.com.gpg") }
|
assertTrue { cryptoHandler.canHandle("example.com.gpg") }
|
||||||
assertFalse { cryptoHandler.canHandle("example.com.asc") }
|
assertFalse { cryptoHandler.canHandle("example.com.asc") }
|
||||||
|
@ -6,5 +6,6 @@
|
|||||||
package dev.msfjarvis.aps.crypto
|
package dev.msfjarvis.aps.crypto
|
||||||
|
|
||||||
object TestUtils {
|
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…
x
Reference in New Issue
Block a user