aboutsummaryrefslogtreecommitdiff
path: root/crypto-pgpainless/src/main
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2021-10-23 17:02:50 +0530
committerGitHub <noreply@github.com>2021-10-23 17:02:50 +0530
commitaac74ae4515aa1d746f46287029441f5a945c98e (patch)
tree9d23e06592ecd884d6b58dd089692d9e4224a3f9 /crypto-pgpainless/src/main
parent21c8653e6815ca34574e783a5ce7ac783b188228 (diff)
Switch new PGP backend to use PGPainless (#1522)
* crypto-pgpainless: init * crypto-pgpainless: add an opinionated CryptoHandler impl * app: migrate to crypto-pgpainless * crypto-pgp: remove * github: remove now unused instrumentation tests job * crypto-common: fixup package names * wip(crypto-pgpainless): add `PGPKeyPair` and `PGPKeyManager` Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> (cherry picked from commit 02d07e9e797a8600cc8c534a731dfffcc44cfdde) * crypto-pgpainless: use hex-encoded key IDs * crypto-pgpainless: replace legacy Gopenpgp-generated key file * crypto-pgpainless: fix CryptoConstants source set * crypto-pgpainless: fix tests * crypto-pgpainless: reinstate PGPKeyManager tests Co-authored-by: Aditya Wasan <adityawasan55@gmail.com>
Diffstat (limited to 'crypto-pgpainless/src/main')
-rw-r--r--crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManager.kt112
-rw-r--r--crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPKeyPair.kt31
-rw-r--r--crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPainlessCryptoHandler.kt69
3 files changed, 212 insertions, 0 deletions
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
new file mode 100644
index 00000000..fd886843
--- /dev/null
+++ b/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManager.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.crypto
+
+import androidx.annotation.VisibleForTesting
+import com.github.michaelbull.result.Result
+import com.github.michaelbull.result.runCatching
+import java.io.File
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+import org.pgpainless.PGPainless
+
+public class PGPKeyManager(
+ filesDir: String,
+ private val dispatcher: CoroutineDispatcher,
+) : KeyManager<PGPKeyPair> {
+
+ private val keyDir = File(filesDir, KEY_DIR_NAME)
+
+ override suspend fun addKey(key: PGPKeyPair, replace: Boolean): Result<PGPKeyPair, Throwable> =
+ withContext(dispatcher) {
+ runCatching {
+ if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException
+ val keyFile = File(keyDir, "${key.getKeyId()}.$KEY_EXTENSION")
+ if (keyFile.exists()) {
+ // Check for replace flag first and if it is false, throw an error
+ if (!replace) throw KeyManagerException.KeyAlreadyExistsException(key.getKeyId())
+ if (!keyFile.delete()) throw KeyManagerException.KeyDeletionFailedException
+ }
+
+ keyFile.writeBytes(key.getPrivateKey())
+
+ key
+ }
+ }
+
+ override suspend fun removeKey(key: PGPKeyPair): Result<PGPKeyPair, Throwable> =
+ withContext(dispatcher) {
+ runCatching {
+ if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException
+ val keyFile = File(keyDir, "${key.getKeyId()}.$KEY_EXTENSION")
+ if (keyFile.exists()) {
+ if (!keyFile.delete()) throw KeyManagerException.KeyDeletionFailedException
+ }
+
+ key
+ }
+ }
+
+ override suspend fun getKeyById(id: String): Result<PGPKeyPair, Throwable> =
+ withContext(dispatcher) {
+ runCatching {
+ if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException
+ val keys = keyDir.listFiles()
+ if (keys.isNullOrEmpty()) throw KeyManagerException.NoKeysAvailableException
+
+ for (keyFile in keys) {
+ val secretKeyRing = PGPainless.readKeyRing().secretKeyRing(keyFile.inputStream())
+ val secretKey = secretKeyRing.secretKey
+ val keyPair = PGPKeyPair(secretKey)
+ if (keyPair.getKeyId() == id) return@runCatching keyPair
+ }
+
+ throw KeyManagerException.KeyNotFoundException(id)
+ }
+ }
+
+ override suspend fun getAllKeys(): Result<List<PGPKeyPair>, Throwable> =
+ withContext(dispatcher) {
+ runCatching {
+ if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException
+ val keys = keyDir.listFiles()
+ if (keys.isNullOrEmpty()) return@runCatching listOf()
+
+ keys
+ .map { keyFile ->
+ val secretKeyRing = PGPainless.readKeyRing().secretKeyRing(keyFile.inputStream())
+ val secretKey = secretKeyRing.secretKey
+
+ PGPKeyPair(secretKey)
+ }
+ .toList()
+ }
+ }
+
+ override fun canHandle(fileName: String): Boolean {
+ // TODO: This is a temp hack for now and in future it should check that the GPGKeyManager can
+ // decrypt the file
+ return fileName.endsWith(KEY_EXTENSION)
+ }
+
+ private fun keyDirExists(): Boolean {
+ return keyDir.exists() || keyDir.mkdirs()
+ }
+
+ public companion object {
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val KEY_DIR_NAME: String = "keys"
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val KEY_EXTENSION: String = "key"
+
+ @JvmStatic
+ public fun makeKey(armoredKey: String): PGPKeyPair {
+ val secretKey = PGPainless.readKeyRing().secretKeyRing(armoredKey).secretKey
+ return PGPKeyPair(secretKey)
+ }
+ }
+}
diff --git a/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPKeyPair.kt b/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPKeyPair.kt
new file mode 100644
index 00000000..03bcf515
--- /dev/null
+++ b/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPKeyPair.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.crypto
+
+import org.bouncycastle.openpgp.PGPSecretKey
+
+public class PGPKeyPair(private val secretKey: PGPSecretKey) : KeyPair {
+
+ init {
+ if (secretKey.isPrivateKeyEmpty) throw KeyPairException.PrivateKeyUnavailableException
+ }
+
+ override fun getPrivateKey(): ByteArray {
+ return secretKey.encoded
+ }
+ override fun getPublicKey(): ByteArray {
+ return secretKey.publicKey.encoded
+ }
+ override fun getKeyId(): String {
+ var keyId = secretKey.keyID.toString(radix = 16)
+ if (keyId.length < KEY_ID_LENGTH) keyId = "0$keyId"
+ return keyId
+ }
+
+ private companion object {
+ private const val KEY_ID_LENGTH = 16
+ }
+}
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
new file mode 100644
index 00000000..3276b995
--- /dev/null
+++ b/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPainlessCryptoHandler.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.crypto
+
+import java.io.ByteArrayInputStream
+import java.io.InputStream
+import java.io.OutputStream
+import javax.inject.Inject
+import org.bouncycastle.bcpg.ArmoredInputStream
+import org.bouncycastle.openpgp.PGPSecretKeyRingCollection
+import org.pgpainless.PGPainless
+import org.pgpainless.decryption_verification.ConsumerOptions
+import org.pgpainless.encryption_signing.EncryptionOptions
+import org.pgpainless.encryption_signing.ProducerOptions
+import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector
+import org.pgpainless.util.Passphrase
+
+public class PGPainlessCryptoHandler @Inject constructor() : CryptoHandler {
+
+ public override fun decrypt(
+ privateKey: String,
+ password: String,
+ ciphertextStream: InputStream,
+ outputStream: OutputStream,
+ ) {
+ val pgpSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(privateKey)
+ val keyringCollection = PGPSecretKeyRingCollection(listOf(pgpSecretKeyRing))
+ val protector =
+ PasswordBasedSecretKeyRingProtector.forKey(
+ pgpSecretKeyRing,
+ Passphrase.fromPassword(password)
+ )
+ PGPainless.decryptAndOrVerify()
+ .onInputStream(ciphertextStream)
+ .withOptions(
+ ConsumerOptions()
+ .addDecryptionKeys(keyringCollection, protector)
+ .addDecryptionPassphrase(Passphrase.fromPassword(password))
+ )
+ .use { decryptionStream -> decryptionStream.copyTo(outputStream) }
+ }
+
+ public override fun encrypt(
+ pubKeys: List<String>,
+ plaintextStream: InputStream,
+ outputStream: OutputStream,
+ ) {
+ val pubKeysStream = ByteArrayInputStream(pubKeys.joinToString("\n").toByteArray())
+ val publicKeyRingCollection =
+ pubKeysStream.use {
+ 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)
+ }
+ }
+
+ public override fun canHandle(fileName: String): Boolean {
+ return fileName.split('.').lastOrNull() == "gpg"
+ }
+}