diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ead79571..7ecceb49 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -51,65 +51,3 @@ jobs: with: name: Test report path: app/build/reports - - instrumentation-tests: - runs-on: macos-11 - steps: - - name: Checkout repository - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f - with: - fetch-depth: 0 - - - name: Check if relevant files have changed - uses: actions/github-script@a3e7071a34d7e1f219a8a4de9a5e0a34d1ee1293 - id: service-changed - with: - result-encoding: string - script: | - const script = require('.github/check-changed-files.js') - return await script({github, context}) - - - uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 - if: ${{ steps.service-changed.outputs.result == 'true' }} - id: avd-cache - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-23 - - - uses: actions/setup-java@d202f5dbf7256730fb690ec59f6381650114feb2 - if: ${{ steps.service-changed.outputs.result == 'true' }} - with: - java-version: '11' - - - name: Copy CI gradle.properties - if: ${{ steps.service-changed.outputs.result == 'true' }} - run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - - - name: Create AVD and generate snapshot for caching - uses: reactivecircus/android-emulator-runner@5de26e4bd23bf523e8a4b7f077df8bfb8e52b50e - if: ${{ steps.avd-cache.outputs.cache-hit != 'true' }} - with: - api-level: 23 - force-avd-creation: false - emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: false - script: echo "Generated AVD snapshot for caching" - - - name: Run screenshot tests - uses: reactivecircus/android-emulator-runner@5de26e4bd23bf523e8a4b7f077df8bfb8e52b50e - if: ${{ steps.service-changed.outputs.result == 'true' }} - with: - api-level: 23 - force-avd-creation: false - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: true - script: ./gradlew connectedCheck -PslimTests - - - name: (Fail-only) upload test report - if: failure() - uses: actions/upload-artifact@27121b0bdffd731efa15d66772be8dc71245d074 - with: - name: Test report - path: app/build/reports diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 32207759..54042fb7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -64,7 +64,7 @@ dependencies { implementation(libs.androidx.annotation) coreLibraryDesugaring(libs.android.desugarJdkLibs) implementation(projects.autofillParser) - implementation(projects.cryptoPgp) + implementation(projects.cryptoPgpainless) implementation(projects.formatCommon) implementation(projects.openpgpKtx) implementation(libs.androidx.activity.ktx) diff --git a/app/src/main/java/dev/msfjarvis/aps/injection/crypto/CryptoHandlerModule.kt b/app/src/main/java/dev/msfjarvis/aps/injection/crypto/CryptoHandlerModule.kt index fdd37bf3..63a860d1 100644 --- a/app/src/main/java/dev/msfjarvis/aps/injection/crypto/CryptoHandlerModule.kt +++ b/app/src/main/java/dev/msfjarvis/aps/injection/crypto/CryptoHandlerModule.kt @@ -10,8 +10,8 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet -import dev.msfjarvis.aps.data.crypto.CryptoHandler -import dev.msfjarvis.aps.data.crypto.GopenpgpCryptoHandler +import dev.msfjarvis.aps.crypto.CryptoHandler +import dev.msfjarvis.aps.crypto.PGPainlessCryptoHandler /** * This module adds all [CryptoHandler] implementations into a Set which makes it easier to build @@ -23,7 +23,7 @@ object CryptoHandlerModule { @Provides @IntoSet fun providePgpCryptoHandler(): CryptoHandler { - return GopenpgpCryptoHandler() + return PGPainlessCryptoHandler() } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt index bb2e6492..5351c5d2 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt @@ -28,6 +28,7 @@ import dev.msfjarvis.aps.util.autofill.AutofillPreferences import dev.msfjarvis.aps.util.autofill.AutofillResponseBuilder import dev.msfjarvis.aps.util.autofill.DirectoryStructure import dev.msfjarvis.aps.util.extensions.asLog +import java.io.ByteArrayOutputStream import java.io.File import javax.inject.Inject import kotlinx.coroutines.Dispatchers @@ -130,11 +131,14 @@ class AutofillDecryptActivityV2 : AppCompatActivity() { runCatching { val crypto = cryptos.first { it.canHandle(file.absolutePath) } withContext(Dispatchers.IO) { + val outputStream = ByteArrayOutputStream() crypto.decrypt( DecryptActivityV2.PRIV_KEY, - DecryptActivityV2.PASS.toByteArray(charset = Charsets.UTF_8), - encryptedInput.readBytes() + DecryptActivityV2.PASS, + encryptedInput, + outputStream, ) + outputStream } } .onFailure { e -> @@ -143,7 +147,7 @@ class AutofillDecryptActivityV2 : AppCompatActivity() { } .onSuccess { result -> return runCatching { - val entry = passwordEntryFactory.create(lifecycleScope, result) + val entry = passwordEntryFactory.create(lifecycleScope, result.toByteArray()) AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure) } .getOrElse { e -> diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt index b000d21d..403b8191 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt @@ -20,6 +20,7 @@ import dev.msfjarvis.aps.injection.password.PasswordEntryFactory import dev.msfjarvis.aps.ui.adapters.FieldItemAdapter import dev.msfjarvis.aps.util.extensions.unsafeLazy import dev.msfjarvis.aps.util.extensions.viewBinding +import java.io.ByteArrayOutputStream import java.io.File import javax.inject.Inject import kotlin.time.Duration @@ -126,19 +127,22 @@ class DecryptActivityV2 : BasePgpActivity() { private fun decrypt() { lifecycleScope.launch { // TODO(msfjarvis): native methods are fallible, add error handling once out of testing - val message = withContext(Dispatchers.IO) { File(fullPath).readBytes() } + val message = withContext(Dispatchers.IO) { File(fullPath).inputStream() } val result = withContext(Dispatchers.IO) { val crypto = cryptos.first { it.canHandle(fullPath) } + val outputStream = ByteArrayOutputStream() crypto.decrypt( PRIV_KEY, - PASS.toByteArray(charset = Charsets.UTF_8), + PASS, message, + outputStream, ) + outputStream } startAutoDismissTimer() - val entry = passwordEntryFactory.create(lifecycleScope, result) + val entry = passwordEntryFactory.create(lifecycleScope, result.toByteArray()) passwordEntry = entry invalidateOptionsMenu() diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt index c2cc5b10..095ce53e 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt @@ -43,6 +43,7 @@ import dev.msfjarvis.aps.util.extensions.snackbar import dev.msfjarvis.aps.util.extensions.unsafeLazy import dev.msfjarvis.aps.util.extensions.viewBinding import dev.msfjarvis.aps.util.settings.PreferenceKeys +import java.io.ByteArrayOutputStream import java.io.File import java.io.IOException import javax.inject.Inject @@ -319,7 +320,15 @@ class PasswordCreationActivityV2 : BasePgpActivity() { runCatching { val crypto = cryptos.first { it.canHandle(path) } val result = - withContext(Dispatchers.IO) { crypto.encrypt(PUB_KEY, content.encodeToByteArray()) } + withContext(Dispatchers.IO) { + val outputStream = ByteArrayOutputStream() + crypto.encrypt( + listOf(PUB_KEY), + content.byteInputStream(), + outputStream, + ) + outputStream + } val file = File(path) // If we're not editing, this file should not already exist! // Additionally, if we were editing and the incoming and outgoing @@ -336,7 +345,7 @@ class PasswordCreationActivityV2 : BasePgpActivity() { return@runCatching } - withContext(Dispatchers.IO) { file.outputStream().use { it.write(result) } } + withContext(Dispatchers.IO) { file.writeBytes(result.toByteArray()) } // associate the new password name with the last name's timestamp in // history diff --git a/crypto-common/api/crypto-common.api b/crypto-common/api/crypto-common.api index 6468ea9b..6d137885 100644 --- a/crypto-common/api/crypto-common.api +++ b/crypto-common/api/crypto-common.api @@ -1,63 +1,63 @@ -public abstract class dev/msfjarvis/aps/data/crypto/CryptoException : java/lang/Exception { +public abstract class dev/msfjarvis/aps/crypto/CryptoException : java/lang/Exception { public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V } -public abstract interface class dev/msfjarvis/aps/data/crypto/CryptoHandler { +public abstract interface class dev/msfjarvis/aps/crypto/CryptoHandler { public abstract fun canHandle (Ljava/lang/String;)Z - public abstract fun decrypt (Ljava/lang/String;[B[B)[B - public abstract fun encrypt (Ljava/lang/String;[B)[B + public abstract fun decrypt (Ljava/lang/String;Ljava/lang/String;Ljava/io/InputStream;Ljava/io/OutputStream;)V + public abstract fun encrypt (Ljava/util/List;Ljava/io/InputStream;Ljava/io/OutputStream;)V } -public abstract interface class dev/msfjarvis/aps/data/crypto/KeyManager { - public abstract fun addKey (Ldev/msfjarvis/aps/data/crypto/KeyPair;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; +public abstract interface class dev/msfjarvis/aps/crypto/KeyManager { + public abstract fun addKey (Ldev/msfjarvis/aps/crypto/KeyPair;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun canHandle (Ljava/lang/String;)Z public abstract fun getAllKeys (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getKeyById (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun removeKey (Ldev/msfjarvis/aps/data/crypto/KeyPair;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun removeKey (Ldev/msfjarvis/aps/crypto/KeyPair;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class dev/msfjarvis/aps/data/crypto/KeyManager$DefaultImpls { - public static synthetic fun addKey$default (Ldev/msfjarvis/aps/data/crypto/KeyManager;Ldev/msfjarvis/aps/data/crypto/KeyPair;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +public final class dev/msfjarvis/aps/crypto/KeyManager$DefaultImpls { + public static synthetic fun addKey$default (Ldev/msfjarvis/aps/crypto/KeyManager;Ldev/msfjarvis/aps/crypto/KeyPair;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } -public abstract class dev/msfjarvis/aps/data/crypto/KeyManagerException : dev/msfjarvis/aps/data/crypto/CryptoException { +public abstract class dev/msfjarvis/aps/crypto/KeyManagerException : dev/msfjarvis/aps/crypto/CryptoException { public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V } -public final class dev/msfjarvis/aps/data/crypto/KeyManagerException$KeyAlreadyExistsException : dev/msfjarvis/aps/data/crypto/KeyManagerException { +public final class dev/msfjarvis/aps/crypto/KeyManagerException$KeyAlreadyExistsException : dev/msfjarvis/aps/crypto/KeyManagerException { public fun (Ljava/lang/String;)V } -public final class dev/msfjarvis/aps/data/crypto/KeyManagerException$KeyDeletionFailedException : dev/msfjarvis/aps/data/crypto/KeyManagerException { - public static final field INSTANCE Ldev/msfjarvis/aps/data/crypto/KeyManagerException$KeyDeletionFailedException; +public final class dev/msfjarvis/aps/crypto/KeyManagerException$KeyDeletionFailedException : dev/msfjarvis/aps/crypto/KeyManagerException { + public static final field INSTANCE Ldev/msfjarvis/aps/crypto/KeyManagerException$KeyDeletionFailedException; } -public final class dev/msfjarvis/aps/data/crypto/KeyManagerException$KeyDirectoryUnavailableException : dev/msfjarvis/aps/data/crypto/KeyManagerException { - public static final field INSTANCE Ldev/msfjarvis/aps/data/crypto/KeyManagerException$KeyDirectoryUnavailableException; +public final class dev/msfjarvis/aps/crypto/KeyManagerException$KeyDirectoryUnavailableException : dev/msfjarvis/aps/crypto/KeyManagerException { + public static final field INSTANCE Ldev/msfjarvis/aps/crypto/KeyManagerException$KeyDirectoryUnavailableException; } -public final class dev/msfjarvis/aps/data/crypto/KeyManagerException$KeyNotFoundException : dev/msfjarvis/aps/data/crypto/KeyManagerException { +public final class dev/msfjarvis/aps/crypto/KeyManagerException$KeyNotFoundException : dev/msfjarvis/aps/crypto/KeyManagerException { public fun (Ljava/lang/String;)V } -public final class dev/msfjarvis/aps/data/crypto/KeyManagerException$NoKeysAvailableException : dev/msfjarvis/aps/data/crypto/KeyManagerException { - public static final field INSTANCE Ldev/msfjarvis/aps/data/crypto/KeyManagerException$NoKeysAvailableException; +public final class dev/msfjarvis/aps/crypto/KeyManagerException$NoKeysAvailableException : dev/msfjarvis/aps/crypto/KeyManagerException { + public static final field INSTANCE Ldev/msfjarvis/aps/crypto/KeyManagerException$NoKeysAvailableException; } -public abstract interface class dev/msfjarvis/aps/data/crypto/KeyPair { +public abstract interface class dev/msfjarvis/aps/crypto/KeyPair { public abstract fun getKeyId ()Ljava/lang/String; public abstract fun getPrivateKey ()[B public abstract fun getPublicKey ()[B } -public abstract class dev/msfjarvis/aps/data/crypto/KeyPairException : dev/msfjarvis/aps/data/crypto/CryptoException { +public abstract class dev/msfjarvis/aps/crypto/KeyPairException : dev/msfjarvis/aps/crypto/CryptoException { public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V } -public final class dev/msfjarvis/aps/data/crypto/KeyPairException$PrivateKeyUnavailableException : dev/msfjarvis/aps/data/crypto/KeyPairException { - public static final field INSTANCE Ldev/msfjarvis/aps/data/crypto/KeyPairException$PrivateKeyUnavailableException; +public final class dev/msfjarvis/aps/crypto/KeyPairException$PrivateKeyUnavailableException : dev/msfjarvis/aps/crypto/KeyPairException { + public static final field INSTANCE Ldev/msfjarvis/aps/crypto/KeyPairException$PrivateKeyUnavailableException; } diff --git a/crypto-common/src/main/kotlin/dev/msfjarvis/aps/data/crypto/CryptoException.kt b/crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/CryptoException.kt similarity index 96% rename from crypto-common/src/main/kotlin/dev/msfjarvis/aps/data/crypto/CryptoException.kt rename to crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/CryptoException.kt index 6a73d381..34e64d5f 100644 --- a/crypto-common/src/main/kotlin/dev/msfjarvis/aps/data/crypto/CryptoException.kt +++ b/crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/CryptoException.kt @@ -1,4 +1,4 @@ -package dev.msfjarvis.aps.data.crypto +package dev.msfjarvis.aps.crypto public sealed class CryptoException(message: String? = null) : Exception(message) diff --git a/crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/CryptoHandler.kt b/crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/CryptoHandler.kt new file mode 100644 index 00000000..c64e9c9b --- /dev/null +++ b/crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/CryptoHandler.kt @@ -0,0 +1,37 @@ +/* + * 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.InputStream +import java.io.OutputStream + +/** Generic interface to implement cryptographic operations on top of. */ +public interface CryptoHandler { + + /** + * Decrypt the given [ciphertextStream] using a [privateKey] and [password], and writes the + * resultant plaintext to [outputStream]. + */ + public fun decrypt( + privateKey: String, + password: String, + ciphertextStream: InputStream, + outputStream: OutputStream, + ) + + /** + * Encrypt the given [plaintextStream] to the provided [pubKeys], and writes the encrypted + * ciphertext to [outputStream]. + */ + public fun encrypt( + pubKeys: List, + plaintextStream: InputStream, + outputStream: OutputStream, + ) + + /** Given a [fileName], return whether this instance can handle it. */ + public fun canHandle(fileName: String): Boolean +} diff --git a/crypto-common/src/main/kotlin/dev/msfjarvis/aps/data/crypto/KeyManager.kt b/crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/KeyManager.kt similarity index 94% rename from crypto-common/src/main/kotlin/dev/msfjarvis/aps/data/crypto/KeyManager.kt rename to crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/KeyManager.kt index b5ba881e..2f901354 100644 --- a/crypto-common/src/main/kotlin/dev/msfjarvis/aps/data/crypto/KeyManager.kt +++ b/crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/KeyManager.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-3.0-only */ -package dev.msfjarvis.aps.data.crypto +package dev.msfjarvis.aps.crypto import com.github.michaelbull.result.Result diff --git a/crypto-common/src/main/kotlin/dev/msfjarvis/aps/data/crypto/KeyPair.kt b/crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/KeyPair.kt similarity index 90% rename from crypto-common/src/main/kotlin/dev/msfjarvis/aps/data/crypto/KeyPair.kt rename to crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/KeyPair.kt index e2362612..b8dec216 100644 --- a/crypto-common/src/main/kotlin/dev/msfjarvis/aps/data/crypto/KeyPair.kt +++ b/crypto-common/src/main/kotlin/dev/msfjarvis/aps/crypto/KeyPair.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-3.0-only */ -package dev.msfjarvis.aps.data.crypto +package dev.msfjarvis.aps.crypto /** Defines expectations for a keypair used in public key cryptography. */ public interface KeyPair { diff --git a/crypto-common/src/main/kotlin/dev/msfjarvis/aps/data/crypto/CryptoHandler.kt b/crypto-common/src/main/kotlin/dev/msfjarvis/aps/data/crypto/CryptoHandler.kt deleted file mode 100644 index 453613a4..00000000 --- a/crypto-common/src/main/kotlin/dev/msfjarvis/aps/data/crypto/CryptoHandler.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package dev.msfjarvis.aps.data.crypto - -/** Generic interface to implement cryptographic operations on top of. */ -public interface CryptoHandler { - - /** - * Decrypt the given [ciphertext] using a [privateKey] and [passphrase], returning a [ByteArray] - * corresponding to the decrypted plaintext. - */ - public fun decrypt(privateKey: String, passphrase: ByteArray, ciphertext: ByteArray): ByteArray - - /** - * Encrypt the given [plaintext] to the provided [publicKey], returning the encrypted ciphertext - * as a [ByteArray] - */ - public fun encrypt(publicKey: String, plaintext: ByteArray): ByteArray - - /** Given a [fileName], return whether this instance can handle it. */ - public fun canHandle(fileName: String): Boolean -} diff --git a/crypto-pgp/api/crypto-pgp.api b/crypto-pgp/api/crypto-pgp.api deleted file mode 100644 index c9b2dde7..00000000 --- a/crypto-pgp/api/crypto-pgp.api +++ /dev/null @@ -1,25 +0,0 @@ -public final class dev/msfjarvis/aps/data/crypto/GPGKeyManager : dev/msfjarvis/aps/data/crypto/KeyManager { - public fun (Ljava/lang/String;Lkotlinx/coroutines/CoroutineDispatcher;)V - public fun addKey (Ldev/msfjarvis/aps/data/crypto/GPGKeyPair;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; - public synthetic fun addKey (Ldev/msfjarvis/aps/data/crypto/KeyPair;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun canHandle (Ljava/lang/String;)Z - public fun getAllKeys (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun getKeyById (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun removeKey (Ldev/msfjarvis/aps/data/crypto/GPGKeyPair;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public synthetic fun removeKey (Ldev/msfjarvis/aps/data/crypto/KeyPair;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class dev/msfjarvis/aps/data/crypto/GPGKeyPair : dev/msfjarvis/aps/data/crypto/KeyPair { - public fun (Lcom/proton/Gopenpgp/crypto/Key;)V - public fun getKeyId ()Ljava/lang/String; - public fun getPrivateKey ()[B - public fun getPublicKey ()[B -} - -public final class dev/msfjarvis/aps/data/crypto/GopenpgpCryptoHandler : dev/msfjarvis/aps/data/crypto/CryptoHandler { - public fun ()V - public fun canHandle (Ljava/lang/String;)Z - public fun decrypt (Ljava/lang/String;[B[B)[B - public fun encrypt (Ljava/lang/String;[B)[B -} - diff --git a/crypto-pgp/build.gradle.kts b/crypto-pgp/build.gradle.kts deleted file mode 100644 index 95542b1c..00000000 --- a/crypto-pgp/build.gradle.kts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -plugins { - id("com.android.library") - kotlin("android") - `aps-plugin` -} - -android { - defaultConfig { - testApplicationId = "dev.msfjarvis.aps.cryptopgp.test" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } -} - -dependencies { - api(projects.cryptoCommon) - implementation(libs.androidx.annotation) - implementation(libs.aps.gopenpgp) - implementation(libs.dagger.hilt.core) - implementation(libs.kotlin.coroutines.core) - implementation(libs.thirdparty.kotlinResult) - androidTestImplementation(libs.bundles.testDependencies) - androidTestImplementation(libs.kotlin.coroutines.test) - androidTestImplementation(libs.bundles.androidTestDependencies) -} diff --git a/crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/GPGKeyPairTest.kt b/crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/GPGKeyPairTest.kt deleted file mode 100644 index 2340d9a5..00000000 --- a/crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/GPGKeyPairTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.test.platform.app.InstrumentationRegistry -import com.proton.Gopenpgp.crypto.Key -import dev.msfjarvis.aps.crypto.utils.CryptoConstants -import dev.msfjarvis.aps.cryptopgp.test.R -import dev.msfjarvis.aps.data.crypto.GPGKeyPair -import dev.msfjarvis.aps.data.crypto.KeyPairException -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import org.junit.Test - -public class GPGKeyPairTest { - - @Test - public fun testIfKeyIdIsCorrect() { - val gpgKey = Key(getKey()) - val keyPair = GPGKeyPair(gpgKey) - - assertEquals(CryptoConstants.KEY_ID, keyPair.getKeyId()) - } - - @Test - public fun testBuildingKeyPairWithoutPrivateKey() { - assertFailsWith( - "GPGKeyPair does not have a private sub key" - ) { - // Get public key object from private key - val gpgKey = Key(getKey()).toPublic() - // Try creating a KeyPair from public key - val keyPair = GPGKeyPair(gpgKey) - - keyPair.getPrivateKey() - } - } - - private companion object { - - fun getKey(): String = - InstrumentationRegistry.getInstrumentation() - .context - .resources - .openRawResource(R.raw.private_key) - .readBytes() - .decodeToString() - } -} diff --git a/crypto-pgp/src/androidTest/res/raw/private_key b/crypto-pgp/src/androidTest/res/raw/private_key deleted file mode 100644 index 5a4f436c..00000000 --- a/crypto-pgp/src/androidTest/res/raw/private_key +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GopenPGP 2.1.9 -Comment: https://gopenpgp.org - -xYYEYN+EThYJKwYBBAHaRw8BAQdAh0d9GdVyJV6KbFynPz3sHkdi5RDnKYs+l0x0 -rEOEthX+CQMIfg7BTvTTe7pgvNERA1vLXRjSxXyi7tfSV13JRnrapp7YtNUSHLVS -PqbaLBd6+EXx7dJ9mUSUSWVga5mdtLZ/k6e+6dsygeHiJuwxfGbHnc0fSm9obiBE -b2UgPGpvaG4uZG9lQGV4YW1wbGUuY29tPsKIBBMWCAA6BQJg34ROCRAErOaZ1bFb -fhYhBJQ0DPsSHC5XfslyQwSs5pnVsVt+AhsDAh4BAhkBAwsJBwIVCAIiAQAAtgwB -AOa3rnipQPsxgxvOP1V+2kD6ssiwt6BZRWwPcyfeX1h4AP9ozBYr+PSmNbam9bnq -wgXwuQhPJeWTSgILMaiasugGCMeLBGDfhE4SCisGAQQBl1UBBQEBB0ClFQJX/L2G -EX9ucC5mvwj3X/7aDXDFAmIpQeWYSS1negMBCgn+CQMIF1uko+Ym3thgoDWUgM5e -MNmDG3rYkTa7h6mlhhrsYtE/GN78EJHP1ygFzOczU/YdbxSRTZCu697uPCZLWURV -1+b66KLTMNHNaAkoFb2JC8J4BBgWCAAqBQJg34ROCRAErOaZ1bFbfhYhBJQ0DPsS -HC5XfslyQwSs5pnVsVt+AhsMAAB1CgEApNcEivCSp0f8CnV4UCoSRRRekIbP1Ub2 -GJx6iRJR8xwA/jicDxdnl/Umfd3mWjGk04R47whiDOXdwjBmC1KVBaMH -=Sfsa ------END PGP PRIVATE KEY BLOCK----- \ No newline at end of file diff --git a/crypto-pgp/src/main/AndroidManifest.xml b/crypto-pgp/src/main/AndroidManifest.xml deleted file mode 100644 index f72b702d..00000000 --- a/crypto-pgp/src/main/AndroidManifest.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GPGKeyPair.kt b/crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GPGKeyPair.kt deleted file mode 100644 index 2dbe8689..00000000 --- a/crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GPGKeyPair.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package dev.msfjarvis.aps.data.crypto - -import com.proton.Gopenpgp.crypto.Key - -/** Wraps a Gopenpgp [Key] to implement [KeyPair]. */ -public class GPGKeyPair(private val key: Key) : KeyPair { - - init { - if (!key.isPrivate) throw KeyPairException.PrivateKeyUnavailableException - } - - override fun getPrivateKey(): ByteArray { - return key.armor().encodeToByteArray() - } - - override fun getPublicKey(): ByteArray { - return key.armoredPublicKey.encodeToByteArray() - } - - override fun getKeyId(): String { - return key.hexKeyID - } -} diff --git a/crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GopenpgpCryptoHandler.kt b/crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GopenpgpCryptoHandler.kt deleted file mode 100644 index 5d14b160..00000000 --- a/crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GopenpgpCryptoHandler.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package dev.msfjarvis.aps.data.crypto - -import com.proton.Gopenpgp.crypto.Crypto -import com.proton.Gopenpgp.helper.Helper -import javax.inject.Inject - -/** Gopenpgp backed implementation of [CryptoHandler]. */ -public class GopenpgpCryptoHandler @Inject constructor() : CryptoHandler { - - /** - * Decrypt the given [ciphertext] using the given PGP [privateKey] and corresponding [passphrase]. - */ - override fun decrypt( - privateKey: String, - passphrase: ByteArray, - ciphertext: ByteArray, - ): ByteArray { - // Decode the incoming cipher into a string and try to guess if it's armored. - val cipherString = ciphertext.decodeToString() - val isArmor = cipherString.startsWith("-----BEGIN PGP MESSAGE-----") - val message = - if (isArmor) { - Crypto.newPGPMessageFromArmored(cipherString) - } else { - Crypto.newPGPMessage(ciphertext) - } - return Helper.decryptBinaryMessageArmored( - privateKey, - passphrase, - message.armored, - ) - } - - override fun encrypt(publicKey: String, plaintext: ByteArray): ByteArray { - return Helper.encryptBinaryMessage( - publicKey, - plaintext, - ) - } - - override fun canHandle(fileName: String): Boolean { - return fileName.split('.').last() == "gpg" - } -} diff --git a/crypto-pgpainless/api/crypto-pgpainless.api b/crypto-pgpainless/api/crypto-pgpainless.api new file mode 100644 index 00000000..1c59ca1f --- /dev/null +++ b/crypto-pgpainless/api/crypto-pgpainless.api @@ -0,0 +1,31 @@ +public final class dev/msfjarvis/aps/crypto/PGPKeyManager : dev/msfjarvis/aps/crypto/KeyManager { + public static final field Companion Ldev/msfjarvis/aps/crypto/PGPKeyManager$Companion; + public fun (Ljava/lang/String;Lkotlinx/coroutines/CoroutineDispatcher;)V + public synthetic fun addKey (Ldev/msfjarvis/aps/crypto/KeyPair;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun addKey (Ldev/msfjarvis/aps/crypto/PGPKeyPair;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun canHandle (Ljava/lang/String;)Z + public fun getAllKeys (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun getKeyById (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun makeKey (Ljava/lang/String;)Ldev/msfjarvis/aps/crypto/PGPKeyPair; + public synthetic fun removeKey (Ldev/msfjarvis/aps/crypto/KeyPair;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun removeKey (Ldev/msfjarvis/aps/crypto/PGPKeyPair;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/msfjarvis/aps/crypto/PGPKeyManager$Companion { + public final fun makeKey (Ljava/lang/String;)Ldev/msfjarvis/aps/crypto/PGPKeyPair; +} + +public final class dev/msfjarvis/aps/crypto/PGPKeyPair : dev/msfjarvis/aps/crypto/KeyPair { + public fun (Lorg/bouncycastle/openpgp/PGPSecretKey;)V + public fun getKeyId ()Ljava/lang/String; + public fun getPrivateKey ()[B + public fun getPublicKey ()[B +} + +public final class dev/msfjarvis/aps/crypto/PGPainlessCryptoHandler : dev/msfjarvis/aps/crypto/CryptoHandler { + public fun ()V + public fun canHandle (Ljava/lang/String;)Z + public fun decrypt (Ljava/lang/String;Ljava/lang/String;Ljava/io/InputStream;Ljava/io/OutputStream;)V + public fun encrypt (Ljava/util/List;Ljava/io/InputStream;Ljava/io/OutputStream;)V +} + diff --git a/crypto-pgpainless/build.gradle.kts b/crypto-pgpainless/build.gradle.kts new file mode 100644 index 00000000..37040e80 --- /dev/null +++ b/crypto-pgpainless/build.gradle.kts @@ -0,0 +1,20 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +plugins { + kotlin("jvm") + `aps-plugin` +} + +dependencies { + api(projects.cryptoCommon) + implementation(libs.androidx.annotation) + implementation(libs.dagger.hilt.core) + implementation(libs.kotlin.coroutines.core) + implementation(libs.thirdparty.kotlinResult) + implementation(libs.thirdparty.pgpainless) + testImplementation(libs.bundles.testDependencies) + testImplementation(libs.kotlin.coroutines.test) +} diff --git a/crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GPGKeyManager.kt b/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManager.kt similarity index 70% rename from crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GPGKeyManager.kt rename to crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManager.kt index 478d2700..fd886843 100644 --- a/crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GPGKeyManager.kt +++ b/crypto-pgpainless/src/main/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManager.kt @@ -3,22 +3,24 @@ * SPDX-License-Identifier: GPL-3.0-only */ -package dev.msfjarvis.aps.data.crypto +package dev.msfjarvis.aps.crypto import androidx.annotation.VisibleForTesting import com.github.michaelbull.result.Result import com.github.michaelbull.result.runCatching -import com.proton.Gopenpgp.crypto.Crypto import java.io.File import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext +import org.pgpainless.PGPainless -public class GPGKeyManager(filesDir: String, private val dispatcher: CoroutineDispatcher) : - KeyManager { +public class PGPKeyManager( + filesDir: String, + private val dispatcher: CoroutineDispatcher, +) : KeyManager { private val keyDir = File(filesDir, KEY_DIR_NAME) - override suspend fun addKey(key: GPGKeyPair, replace: Boolean): Result = + override suspend fun addKey(key: PGPKeyPair, replace: Boolean): Result = withContext(dispatcher) { runCatching { if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException @@ -35,7 +37,7 @@ public class GPGKeyManager(filesDir: String, private val dispatcher: CoroutineDi } } - override suspend fun removeKey(key: GPGKeyPair): Result = + override suspend fun removeKey(key: PGPKeyPair): Result = withContext(dispatcher) { runCatching { if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException @@ -48,7 +50,7 @@ public class GPGKeyManager(filesDir: String, private val dispatcher: CoroutineDi } } - override suspend fun getKeyById(id: String): Result = + override suspend fun getKeyById(id: String): Result = withContext(dispatcher) { runCatching { if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException @@ -56,7 +58,9 @@ public class GPGKeyManager(filesDir: String, private val dispatcher: CoroutineDi if (keys.isNullOrEmpty()) throw KeyManagerException.NoKeysAvailableException for (keyFile in keys) { - val keyPair = GPGKeyPair(Crypto.newKeyFromArmored(keyFile.readText())) + val secretKeyRing = PGPainless.readKeyRing().secretKeyRing(keyFile.inputStream()) + val secretKey = secretKeyRing.secretKey + val keyPair = PGPKeyPair(secretKey) if (keyPair.getKeyId() == id) return@runCatching keyPair } @@ -64,14 +68,21 @@ public class GPGKeyManager(filesDir: String, private val dispatcher: CoroutineDi } } - override suspend fun getAllKeys(): Result, Throwable> = + override suspend fun getAllKeys(): Result, Throwable> = withContext(dispatcher) { runCatching { if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException val keys = keyDir.listFiles() if (keys.isNullOrEmpty()) return@runCatching listOf() - keys.map { GPGKeyPair(Crypto.newKeyFromArmored(it.readText())) }.toList() + keys + .map { keyFile -> + val secretKeyRing = PGPainless.readKeyRing().secretKeyRing(keyFile.inputStream()) + val secretKey = secretKeyRing.secretKey + + PGPKeyPair(secretKey) + } + .toList() } } @@ -85,11 +96,17 @@ public class GPGKeyManager(filesDir: String, private val dispatcher: CoroutineDi return keyDir.exists() || keyDir.mkdirs() } - internal companion object { + 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, + 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-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/utils/CryptoConstants.kt b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/CryptoConstants.kt similarity index 81% rename from crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/utils/CryptoConstants.kt rename to crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/CryptoConstants.kt index 873f7105..fad5308c 100644 --- a/crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/utils/CryptoConstants.kt +++ b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/CryptoConstants.kt @@ -3,12 +3,12 @@ * SPDX-License-Identifier: GPL-3.0-only */ -package dev.msfjarvis.aps.crypto.utils +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 = "04ace699d5b15b7e" + internal const val KEY_ID = "08edf7567183ce27" } diff --git a/crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/GPGKeyManagerTest.kt b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManagerTest.kt similarity index 54% rename from crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/GPGKeyManagerTest.kt rename to crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManagerTest.kt index 80a13eb5..12bb85ad 100644 --- a/crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/GPGKeyManagerTest.kt +++ b/crypto-pgpainless/src/test/kotlin/dev/msfjarvis/aps/crypto/PGPKeyManagerTest.kt @@ -1,59 +1,44 @@ package dev.msfjarvis.aps.crypto -import androidx.test.platform.app.InstrumentationRegistry import com.github.michaelbull.result.unwrap import com.github.michaelbull.result.unwrapError -import com.proton.Gopenpgp.crypto.Key -import dev.msfjarvis.aps.crypto.utils.CryptoConstants -import dev.msfjarvis.aps.cryptopgp.test.R -import dev.msfjarvis.aps.data.crypto.GPGKeyManager -import dev.msfjarvis.aps.data.crypto.GPGKeyPair -import dev.msfjarvis.aps.data.crypto.KeyManagerException 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.After -import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder @OptIn(ExperimentalCoroutinesApi::class) -public class GPGKeyManagerTest { +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 lateinit var gpgKeyManager: GPGKeyManager - private lateinit var key: GPGKeyPair - - @Before - public fun setup() { - gpgKeyManager = GPGKeyManager(getFilesDir().absolutePath, testCoroutineDispatcher) - key = GPGKeyPair(Key(getKey())) - } - - @After - public fun tearDown() { - val filesDir = getFilesDir() - val keysDir = File(filesDir, GPGKeyManager.KEY_DIR_NAME) - - keysDir.deleteRecursively() + 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 = gpgKeyManager.addKey(key).unwrap().getKeyId() + val keyId = keyManager.addKey(key).unwrap().getKeyId() assertEquals(CryptoConstants.KEY_ID, keyId) // Check if the keys directory have one file - val keysDir = File(getFilesDir(), GPGKeyManager.KEY_DIR_NAME) - assertEquals(1, keysDir.list()?.size) + assertEquals(1, filesDir.list()?.size) // Check if the file name is correct val keyFile = keysDir.listFiles()?.first() - assertEquals(keyFile?.name, "$keyId.${GPGKeyManager.KEY_EXTENSION}") + assertEquals(keyFile?.name, "$keyId.${PGPKeyManager.KEY_EXTENSION}") } } @@ -61,8 +46,8 @@ public class GPGKeyManagerTest { public fun testAddingKeyWithoutReplaceFlag() { runBlockingTest { // Check adding the keys twice - gpgKeyManager.addKey(key, false).unwrap() - val error = gpgKeyManager.addKey(key, false).unwrapError() + keyManager.addKey(key, false).unwrap() + val error = keyManager.addKey(key, false).unwrapError() assertIs(error) } @@ -72,8 +57,8 @@ public class GPGKeyManagerTest { public fun testAddingKeyWithReplaceFlag() { runBlockingTest { // Check adding the keys twice - gpgKeyManager.addKey(key, true).unwrap() - val keyId = gpgKeyManager.addKey(key, true).unwrap().getKeyId() + keyManager.addKey(key, true).unwrap() + val keyId = keyManager.addKey(key, true).unwrap().getKeyId() assertEquals(CryptoConstants.KEY_ID, keyId) } @@ -83,14 +68,14 @@ public class GPGKeyManagerTest { public fun testRemovingKey() { runBlockingTest { // Add key using KeyManager - gpgKeyManager.addKey(key).unwrap() + keyManager.addKey(key).unwrap() // Check if the key id returned is correct - val keyId = gpgKeyManager.removeKey(key).unwrap().getKeyId() + val keyId = keyManager.removeKey(key).unwrap().getKeyId() assertEquals(CryptoConstants.KEY_ID, keyId) // Check if the keys directory have 0 files - val keysDir = File(getFilesDir(), GPGKeyManager.KEY_DIR_NAME) + val keysDir = File(filesDir, PGPKeyManager.KEY_DIR_NAME) assertEquals(0, keysDir.list()?.size) } } @@ -99,10 +84,10 @@ public class GPGKeyManagerTest { public fun testGetExistingKey() { runBlockingTest { // Add key using KeyManager - gpgKeyManager.addKey(key).unwrap() + keyManager.addKey(key).unwrap() // Check returned key id matches the expected id and the created key id - val returnedKeyPair = gpgKeyManager.getKeyById(key.getKeyId()).unwrap() + val returnedKeyPair = keyManager.getKeyById(key.getKeyId()).unwrap() assertEquals(CryptoConstants.KEY_ID, key.getKeyId()) assertEquals(key.getKeyId(), returnedKeyPair.getKeyId()) } @@ -112,12 +97,12 @@ public class GPGKeyManagerTest { public fun testGetNonExistentKey() { runBlockingTest { // Add key using KeyManager - gpgKeyManager.addKey(key).unwrap() + keyManager.addKey(key).unwrap() val randomKeyId = "0x123456789" // Check returned key - val error = gpgKeyManager.getKeyById(randomKeyId).unwrapError() + val error = keyManager.getKeyById(randomKeyId).unwrapError() assertIs(error) assertEquals("No key found with id: $randomKeyId", error.message) } @@ -127,7 +112,7 @@ public class GPGKeyManagerTest { public fun testFindKeysWithoutAdding() { runBlockingTest { // Check returned key - val error = gpgKeyManager.getKeyById("0x123456789").unwrapError() + val error = keyManager.getKeyById("0x123456789").unwrapError() assertIs(error) assertEquals("No keys were found", error.message) } @@ -138,28 +123,17 @@ public class GPGKeyManagerTest { runBlockingTest { // TODO: Should we check for more than 1 keys? // Check if KeyManager returns no key - val noKeyList = gpgKeyManager.getAllKeys().unwrap() + val noKeyList = keyManager.getAllKeys().unwrap() assertEquals(0, noKeyList.size) // Add key using KeyManager - gpgKeyManager.addKey(key).unwrap() + keyManager.addKey(key).unwrap() // Check if KeyManager returns one key - val singleKeyList = gpgKeyManager.getAllKeys().unwrap() + val singleKeyList = keyManager.getAllKeys().unwrap() assertEquals(1, singleKeyList.size) } } - private companion object { - - fun getFilesDir(): File = InstrumentationRegistry.getInstrumentation().context.filesDir - - fun getKey(): String = - InstrumentationRegistry.getInstrumentation() - .context - .resources - .openRawResource(R.raw.private_key) - .readBytes() - .decodeToString() - } + 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 + +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----- diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ac87b21..8e22f4d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -61,7 +61,6 @@ dagger-hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt android-desugarJdkLibs = "com.android.tools:desugar_jdk_libs:1.1.5" # First-party libraries -aps-gopenpgp = "com.github.android-password-store:gopenpgp:0.1.5" aps-sublimeFuzzy = "com.github.android-password-store:sublime-fuzzy:1.0.0" aps-zxingAndroidEmbedded = "com.github.android-password-store:zxing-android-embedded:4.2.1" @@ -76,6 +75,7 @@ thirdparty-kotlinResult = "com.michael-bull.kotlin-result:kotlin-result:1.1.12" thirdparty-leakcanary = "com.squareup.leakcanary:leakcanary-android:2.7" thirdparty-logcat = "com.squareup.logcat:logcat:0.1" thirdparty-modernAndroidPrefs = "de.maxr1998:modernandroidpreferences:2.1.0" +thirdparty-pgpainless = "org.pgpainless:pgpainless-core:0.2.17" thirdparty-plumber = "com.squareup.leakcanary:plumber-android:2.7" thirdparty-sshj = "com.hierynomus:sshj:0.31.0" thirdparty-sshauth = "com.github.open-keychain.open-keychain:sshauthentication-api:5.7.5" diff --git a/settings.gradle.kts b/settings.gradle.kts index 56392b74..c8c641b8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,7 +10,7 @@ include(":autofill-parser") include(":crypto-common") -include(":crypto-pgp") +include(":crypto-pgpainless") include(":format-common")