summaryrefslogtreecommitdiff
path: root/crypto-pgpainless/src
diff options
context:
space:
mode:
Diffstat (limited to 'crypto-pgpainless/src')
-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
-rw-r--r--crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/CryptoConstants.kt14
-rw-r--r--crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManagerTest.kt139
-rw-r--r--crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPKeyPairTest.kt23
-rw-r--r--crypto-pgpainless/src/test/resources/private_key26
7 files changed, 414 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"
+ }
+}
diff --git a/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/CryptoConstants.kt b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/CryptoConstants.kt
new file mode 100644
index 00000000..fad5308c
--- /dev/null
+++ b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/CryptoConstants.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.crypto
+
+internal object CryptoConstants {
+ internal const val KEY_PASSPHRASE = "hunter2"
+ internal const val PLAIN_TEXT = "encryption worthy content"
+ internal const val KEY_NAME = "John Doe"
+ internal const val KEY_EMAIL = "john.doe@example.com"
+ internal const val KEY_ID = "08edf7567183ce27"
+}
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
new file mode 100644
index 00000000..12bb85ad
--- /dev/null
+++ b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManagerTest.kt
@@ -0,0 +1,139 @@
+package dev.msfjarvis.aps.crypto
+
+import com.github.michaelbull.result.unwrap
+import com.github.michaelbull.result.unwrapError
+import java.io.File
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+
+@OptIn(ExperimentalCoroutinesApi::class)
+public class PGPKeyManagerTest {
+
+ @get:Rule public val temporaryFolder: TemporaryFolder = TemporaryFolder()
+ private val filesDir by lazy(LazyThreadSafetyMode.NONE) { temporaryFolder.root }
+ private val keysDir by lazy(LazyThreadSafetyMode.NONE) {
+ File(filesDir, PGPKeyManager.KEY_DIR_NAME)
+ }
+ private val testCoroutineDispatcher = TestCoroutineDispatcher()
+ private val keyManager by lazy(LazyThreadSafetyMode.NONE) {
+ PGPKeyManager(filesDir.absolutePath, testCoroutineDispatcher)
+ }
+ private val key = PGPKeyManager.makeKey(getArmoredKey())
+
+ @Test
+ public fun testAddingKey() {
+ runBlockingTest {
+ // Check if the key id returned is correct
+ val keyId = keyManager.addKey(key).unwrap().getKeyId()
+ assertEquals(CryptoConstants.KEY_ID, keyId)
+
+ // Check if the keys directory have one file
+ assertEquals(1, filesDir.list()?.size)
+
+ // Check if the file name is correct
+ val keyFile = keysDir.listFiles()?.first()
+ assertEquals(keyFile?.name, "$keyId.${PGPKeyManager.KEY_EXTENSION}")
+ }
+ }
+
+ @Test
+ public fun testAddingKeyWithoutReplaceFlag() {
+ runBlockingTest {
+ // Check adding the keys twice
+ keyManager.addKey(key, false).unwrap()
+ val error = keyManager.addKey(key, false).unwrapError()
+
+ assertIs<KeyManagerException.KeyAlreadyExistsException>(error)
+ }
+ }
+
+ @Test
+ public fun testAddingKeyWithReplaceFlag() {
+ runBlockingTest {
+ // Check adding the keys twice
+ keyManager.addKey(key, true).unwrap()
+ val keyId = keyManager.addKey(key, true).unwrap().getKeyId()
+
+ assertEquals(CryptoConstants.KEY_ID, keyId)
+ }
+ }
+
+ @Test
+ public fun testRemovingKey() {
+ runBlockingTest {
+ // Add key using KeyManager
+ keyManager.addKey(key).unwrap()
+
+ // Check if the key id returned is correct
+ val keyId = keyManager.removeKey(key).unwrap().getKeyId()
+ assertEquals(CryptoConstants.KEY_ID, keyId)
+
+ // Check if the keys directory have 0 files
+ val keysDir = File(filesDir, PGPKeyManager.KEY_DIR_NAME)
+ assertEquals(0, keysDir.list()?.size)
+ }
+ }
+
+ @Test
+ public fun testGetExistingKey() {
+ runBlockingTest {
+ // Add key using KeyManager
+ keyManager.addKey(key).unwrap()
+
+ // Check returned key id matches the expected id and the created key id
+ val returnedKeyPair = keyManager.getKeyById(key.getKeyId()).unwrap()
+ assertEquals(CryptoConstants.KEY_ID, key.getKeyId())
+ assertEquals(key.getKeyId(), returnedKeyPair.getKeyId())
+ }
+ }
+
+ @Test
+ public fun testGetNonExistentKey() {
+ runBlockingTest {
+ // Add key using KeyManager
+ keyManager.addKey(key).unwrap()
+
+ val randomKeyId = "0x123456789"
+
+ // Check returned key
+ val error = keyManager.getKeyById(randomKeyId).unwrapError()
+ assertIs<KeyManagerException.KeyNotFoundException>(error)
+ assertEquals("No key found with id: $randomKeyId", error.message)
+ }
+ }
+
+ @Test
+ public fun testFindKeysWithoutAdding() {
+ runBlockingTest {
+ // Check returned key
+ val error = keyManager.getKeyById("0x123456789").unwrapError()
+ assertIs<KeyManagerException.NoKeysAvailableException>(error)
+ assertEquals("No keys were found", error.message)
+ }
+ }
+
+ @Test
+ public fun testGettingAllKeys() {
+ runBlockingTest {
+ // TODO: Should we check for more than 1 keys?
+ // Check if KeyManager returns no key
+ val noKeyList = keyManager.getAllKeys().unwrap()
+ assertEquals(0, noKeyList.size)
+
+ // Add key using KeyManager
+ keyManager.addKey(key).unwrap()
+
+ // Check if KeyManager returns one key
+ val singleKeyList = keyManager.getAllKeys().unwrap()
+ assertEquals(1, singleKeyList.size)
+ }
+ }
+
+ private fun getArmoredKey() = this::class.java.classLoader.getResource("private_key").readText()
+}
diff --git a/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPKeyPairTest.kt b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPKeyPairTest.kt
new file mode 100644
index 00000000..9fc3ed69
--- /dev/null
+++ b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPKeyPairTest.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.crypto
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import org.pgpainless.PGPainless
+
+public class PGPKeyPairTest {
+
+ @Test
+ public fun testIfKeyIdIsCorrect() {
+ val secretKey = PGPainless.readKeyRing().secretKeyRing(getKey()).secretKey
+ val keyPair = PGPKeyPair(secretKey)
+
+ assertEquals(CryptoConstants.KEY_ID, keyPair.getKeyId())
+ }
+
+ private fun getKey(): String = this::class.java.classLoader.getResource("private_key").readText()
+}
diff --git a/crypto-pgpainless/src/test/resources/private_key b/crypto-pgpainless/src/test/resources/private_key
new file mode 100644
index 00000000..61334b01
--- /dev/null
+++ b/crypto-pgpainless/src/test/resources/private_key
@@ -0,0 +1,26 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: PGPainless
+Comment: BC98 82EF 93DC 22F8 D7D4 47AD 08ED F756 7183 CE27
+Comment: John Doe <john.doe@example.com>
+
+lIYEYT33+BYJKwYBBAHaRw8BAQdAoofwCvOfKJ4pGxEO4s64wFD+QnePpNY5zXgW
+TTOFb2/+CQMCh3Bp60ThtX9g8u+uxtuLdeeU5UC14Ox4zVD/x2L7sUzN94XVocOn
+WVJTIgeZ1CBhrsSOMg5grj0Zwf1YODlBpZ85V8stPebpjZ2mCZUz1rQfSm9obiBE
+b2UgPGpvaG4uZG9lQGV4YW1wbGUuY29tPoh4BBMWCgAgBQJhPff4AhsBBRYCAwEA
+BAsJCAcFFQoJCAsCHgECGQEACgkQCO33VnGDzifl1gD8CIAGoF23Yi1aAM8sI0Sq
+33AgyBGmQOsAy1dfLItKRawBAKijCl6cayrl/GG5FxLfDpCz79DDUaqeiJ3GGKhH
+0n4AnIsEYT33+BIKKwYBBAGXVQEFAQEHQLt4VWwVSJ/ir1K1oEjokDCwj6FBICjc
+jpXiNTeuLHxfAwEIB/4JAwKHcGnrROG1f2AcnEUWhC2rDrztJB3JK7pe+PVJbMaK
+O2eYKLiBZOT6Dy1rexMi0vS19IMYLf1V2qgsO9phoglOD+m95tr8Ha9FhfbpJjua
+iHUEGBYKAB0FAmE99/gCGwwFFgIDAQAECwkIBwUVCgkICwIeAQAKCRAI7fdWcYPO
+J5p+AQC5g/FmMU3ayalGVBNU3Bb8xua9P/6zzPFbreV/isFF4wEA1lT9timgPFV6
+Xr0sZEt5/7YtCo0FShBcxm5sAdnU0wmchgRhPff4FgkrBgEEAdpHDwEBB0CV36g4
+wjvS+Kgbutv1D6UOatOt/JBvPgBn/4SR9qtgU/4JAwKHcGnrROG1f2A1hnm2UXZL
+Go/tPJo3pJCJDLClIKi7I5RoHruafuQ2ODvznLbCnbuft9B2cA5MZUMFCk6nBvoU
+k6hwGWxOSNJIOmrCx+PMiNUEGBYKAH0FAmE99/gCGwIFFgIDAQAECwkIBwUVCgkI
+CwIeAV8gBBkWCgAGBQJhPff4AAoJEGSLoii3QC8mrhcBALzpJQTHF8cJJRA9+DQ3
+qZ85Eu217MJix1aYA1i0zyP5AQD/jN/aBsSTqAHF+zU8/ezzHeoilyBYgxLS9Q2q
+elDeDAAKCRAI7fdWcYPOJ7aHAP9EBq0rzV3c6GtVl8bPnk+llpV/1aodxTSnijQt
+VSMuMAD+JMUDJd2bimlhuVwpu0DFiF7IF64SAxmVifTwsTWYiQs=
+=/dDf
+-----END PGP PRIVATE KEY BLOCK-----