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>
This commit is contained in:
Harsh Shandilya 2021-10-23 17:02:50 +05:30 committed by GitHub
parent 21c8653e68
commit aac74ae451
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 353 additions and 402 deletions

View file

@ -51,65 +51,3 @@ jobs:
with: with:
name: Test report name: Test report
path: app/build/reports 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

View file

@ -64,7 +64,7 @@ dependencies {
implementation(libs.androidx.annotation) implementation(libs.androidx.annotation)
coreLibraryDesugaring(libs.android.desugarJdkLibs) coreLibraryDesugaring(libs.android.desugarJdkLibs)
implementation(projects.autofillParser) implementation(projects.autofillParser)
implementation(projects.cryptoPgp) implementation(projects.cryptoPgpainless)
implementation(projects.formatCommon) implementation(projects.formatCommon)
implementation(projects.openpgpKtx) implementation(projects.openpgpKtx)
implementation(libs.androidx.activity.ktx) implementation(libs.androidx.activity.ktx)

View file

@ -10,8 +10,8 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet import dagger.multibindings.IntoSet
import dev.msfjarvis.aps.data.crypto.CryptoHandler import dev.msfjarvis.aps.crypto.CryptoHandler
import dev.msfjarvis.aps.data.crypto.GopenpgpCryptoHandler import dev.msfjarvis.aps.crypto.PGPainlessCryptoHandler
/** /**
* This module adds all [CryptoHandler] implementations into a Set which makes it easier to build * This module adds all [CryptoHandler] implementations into a Set which makes it easier to build
@ -23,7 +23,7 @@ object CryptoHandlerModule {
@Provides @Provides
@IntoSet @IntoSet
fun providePgpCryptoHandler(): CryptoHandler { fun providePgpCryptoHandler(): CryptoHandler {
return GopenpgpCryptoHandler() return PGPainlessCryptoHandler()
} }
} }

View file

@ -28,6 +28,7 @@ import dev.msfjarvis.aps.util.autofill.AutofillPreferences
import dev.msfjarvis.aps.util.autofill.AutofillResponseBuilder import dev.msfjarvis.aps.util.autofill.AutofillResponseBuilder
import dev.msfjarvis.aps.util.autofill.DirectoryStructure import dev.msfjarvis.aps.util.autofill.DirectoryStructure
import dev.msfjarvis.aps.util.extensions.asLog import dev.msfjarvis.aps.util.extensions.asLog
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -130,11 +131,14 @@ class AutofillDecryptActivityV2 : AppCompatActivity() {
runCatching { runCatching {
val crypto = cryptos.first { it.canHandle(file.absolutePath) } val crypto = cryptos.first { it.canHandle(file.absolutePath) }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val outputStream = ByteArrayOutputStream()
crypto.decrypt( crypto.decrypt(
DecryptActivityV2.PRIV_KEY, DecryptActivityV2.PRIV_KEY,
DecryptActivityV2.PASS.toByteArray(charset = Charsets.UTF_8), DecryptActivityV2.PASS,
encryptedInput.readBytes() encryptedInput,
outputStream,
) )
outputStream
} }
} }
.onFailure { e -> .onFailure { e ->
@ -143,7 +147,7 @@ class AutofillDecryptActivityV2 : AppCompatActivity() {
} }
.onSuccess { result -> .onSuccess { result ->
return runCatching { return runCatching {
val entry = passwordEntryFactory.create(lifecycleScope, result) val entry = passwordEntryFactory.create(lifecycleScope, result.toByteArray())
AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure) AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure)
} }
.getOrElse { e -> .getOrElse { e ->

View file

@ -20,6 +20,7 @@ import dev.msfjarvis.aps.injection.password.PasswordEntryFactory
import dev.msfjarvis.aps.ui.adapters.FieldItemAdapter import dev.msfjarvis.aps.ui.adapters.FieldItemAdapter
import dev.msfjarvis.aps.util.extensions.unsafeLazy import dev.msfjarvis.aps.util.extensions.unsafeLazy
import dev.msfjarvis.aps.util.extensions.viewBinding import dev.msfjarvis.aps.util.extensions.viewBinding
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration import kotlin.time.Duration
@ -126,19 +127,22 @@ class DecryptActivityV2 : BasePgpActivity() {
private fun decrypt() { private fun decrypt() {
lifecycleScope.launch { lifecycleScope.launch {
// TODO(msfjarvis): native methods are fallible, add error handling once out of testing // 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 = val result =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val crypto = cryptos.first { it.canHandle(fullPath) } val crypto = cryptos.first { it.canHandle(fullPath) }
val outputStream = ByteArrayOutputStream()
crypto.decrypt( crypto.decrypt(
PRIV_KEY, PRIV_KEY,
PASS.toByteArray(charset = Charsets.UTF_8), PASS,
message, message,
outputStream,
) )
outputStream
} }
startAutoDismissTimer() startAutoDismissTimer()
val entry = passwordEntryFactory.create(lifecycleScope, result) val entry = passwordEntryFactory.create(lifecycleScope, result.toByteArray())
passwordEntry = entry passwordEntry = entry
invalidateOptionsMenu() invalidateOptionsMenu()

View file

@ -43,6 +43,7 @@ import dev.msfjarvis.aps.util.extensions.snackbar
import dev.msfjarvis.aps.util.extensions.unsafeLazy import dev.msfjarvis.aps.util.extensions.unsafeLazy
import dev.msfjarvis.aps.util.extensions.viewBinding import dev.msfjarvis.aps.util.extensions.viewBinding
import dev.msfjarvis.aps.util.settings.PreferenceKeys import dev.msfjarvis.aps.util.settings.PreferenceKeys
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@ -319,7 +320,15 @@ class PasswordCreationActivityV2 : BasePgpActivity() {
runCatching { runCatching {
val crypto = cryptos.first { it.canHandle(path) } val crypto = cryptos.first { it.canHandle(path) }
val result = 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) val file = File(path)
// If we're not editing, this file should not already exist! // If we're not editing, this file should not already exist!
// Additionally, if we were editing and the incoming and outgoing // Additionally, if we were editing and the incoming and outgoing
@ -336,7 +345,7 @@ class PasswordCreationActivityV2 : BasePgpActivity() {
return@runCatching 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 // associate the new password name with the last name's timestamp in
// history // history

View file

@ -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 <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (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 canHandle (Ljava/lang/String;)Z
public abstract fun decrypt (Ljava/lang/String;[B[B)[B public abstract fun decrypt (Ljava/lang/String;Ljava/lang/String;Ljava/io/InputStream;Ljava/io/OutputStream;)V
public abstract fun encrypt (Ljava/lang/String;[B)[B 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 interface class dev/msfjarvis/aps/crypto/KeyManager {
public abstract fun addKey (Ldev/msfjarvis/aps/data/crypto/KeyPair;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; 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 canHandle (Ljava/lang/String;)Z
public abstract fun getAllKeys (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 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 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 final class dev/msfjarvis/aps/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 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 <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (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 <init> (Ljava/lang/String;)V public fun <init> (Ljava/lang/String;)V
} }
public final class dev/msfjarvis/aps/data/crypto/KeyManagerException$KeyDeletionFailedException : dev/msfjarvis/aps/data/crypto/KeyManagerException { public final class dev/msfjarvis/aps/crypto/KeyManagerException$KeyDeletionFailedException : dev/msfjarvis/aps/crypto/KeyManagerException {
public static final field INSTANCE Ldev/msfjarvis/aps/data/crypto/KeyManagerException$KeyDeletionFailedException; 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 final class dev/msfjarvis/aps/crypto/KeyManagerException$KeyDirectoryUnavailableException : dev/msfjarvis/aps/crypto/KeyManagerException {
public static final field INSTANCE Ldev/msfjarvis/aps/data/crypto/KeyManagerException$KeyDirectoryUnavailableException; 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 <init> (Ljava/lang/String;)V public fun <init> (Ljava/lang/String;)V
} }
public final class dev/msfjarvis/aps/data/crypto/KeyManagerException$NoKeysAvailableException : dev/msfjarvis/aps/data/crypto/KeyManagerException { public final class dev/msfjarvis/aps/crypto/KeyManagerException$NoKeysAvailableException : dev/msfjarvis/aps/crypto/KeyManagerException {
public static final field INSTANCE Ldev/msfjarvis/aps/data/crypto/KeyManagerException$NoKeysAvailableException; 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 getKeyId ()Ljava/lang/String;
public abstract fun getPrivateKey ()[B public abstract fun getPrivateKey ()[B
public abstract fun getPublicKey ()[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 <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (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 final class dev/msfjarvis/aps/crypto/KeyPairException$PrivateKeyUnavailableException : dev/msfjarvis/aps/crypto/KeyPairException {
public static final field INSTANCE Ldev/msfjarvis/aps/data/crypto/KeyPairException$PrivateKeyUnavailableException; public static final field INSTANCE Ldev/msfjarvis/aps/crypto/KeyPairException$PrivateKeyUnavailableException;
} }

View file

@ -1,4 +1,4 @@
package dev.msfjarvis.aps.data.crypto package dev.msfjarvis.aps.crypto
public sealed class CryptoException(message: String? = null) : Exception(message) public sealed class CryptoException(message: String? = null) : Exception(message)

View file

@ -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<String>,
plaintextStream: InputStream,
outputStream: OutputStream,
)
/** Given a [fileName], return whether this instance can handle it. */
public fun canHandle(fileName: String): Boolean
}

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package dev.msfjarvis.aps.data.crypto package dev.msfjarvis.aps.crypto
import com.github.michaelbull.result.Result import com.github.michaelbull.result.Result

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-3.0-only * 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. */ /** Defines expectations for a keypair used in public key cryptography. */
public interface KeyPair { public interface KeyPair {

View file

@ -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
}

View file

@ -1,25 +0,0 @@
public final class dev/msfjarvis/aps/data/crypto/GPGKeyManager : dev/msfjarvis/aps/data/crypto/KeyManager {
public fun <init> (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 <init> (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 <init> ()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
}

View file

@ -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)
}

View file

@ -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<KeyPairException.PrivateKeyUnavailableException>(
"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()
}
}

View file

@ -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-----

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
~ SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
-->
<manifest package="dev.msfjarvis.aps.cryptopgp"></manifest>

View file

@ -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
}
}

View file

@ -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"
}
}

View file

@ -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 <init> (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 <init> (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 <init> ()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
}

View file

@ -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)
}

View file

@ -3,22 +3,24 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package dev.msfjarvis.aps.data.crypto 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.runCatching import com.github.michaelbull.result.runCatching
import com.proton.Gopenpgp.crypto.Crypto
import java.io.File import java.io.File
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.pgpainless.PGPainless
public class GPGKeyManager(filesDir: String, private val dispatcher: CoroutineDispatcher) : public class PGPKeyManager(
KeyManager<GPGKeyPair> { filesDir: String,
private val dispatcher: CoroutineDispatcher,
) : KeyManager<PGPKeyPair> {
private val keyDir = File(filesDir, KEY_DIR_NAME) private val keyDir = File(filesDir, KEY_DIR_NAME)
override suspend fun addKey(key: GPGKeyPair, replace: Boolean): Result<GPGKeyPair, Throwable> = override suspend fun addKey(key: PGPKeyPair, replace: Boolean): Result<PGPKeyPair, Throwable> =
withContext(dispatcher) { withContext(dispatcher) {
runCatching { runCatching {
if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException 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<GPGKeyPair, Throwable> = override suspend fun removeKey(key: PGPKeyPair): Result<PGPKeyPair, Throwable> =
withContext(dispatcher) { withContext(dispatcher) {
runCatching { runCatching {
if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException 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<GPGKeyPair, Throwable> = override suspend fun getKeyById(id: String): Result<PGPKeyPair, Throwable> =
withContext(dispatcher) { withContext(dispatcher) {
runCatching { runCatching {
if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException
@ -56,7 +58,9 @@ public class GPGKeyManager(filesDir: String, private val dispatcher: CoroutineDi
if (keys.isNullOrEmpty()) throw KeyManagerException.NoKeysAvailableException if (keys.isNullOrEmpty()) throw KeyManagerException.NoKeysAvailableException
for (keyFile in keys) { 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 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<List<GPGKeyPair>, Throwable> = override suspend fun getAllKeys(): Result<List<PGPKeyPair>, Throwable> =
withContext(dispatcher) { withContext(dispatcher) {
runCatching { runCatching {
if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException
val keys = keyDir.listFiles() val keys = keyDir.listFiles()
if (keys.isNullOrEmpty()) return@runCatching listOf() 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() return keyDir.exists() || keyDir.mkdirs()
} }
internal companion object { public companion object {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal const val KEY_DIR_NAME: String = "keys" internal const val KEY_DIR_NAME: String = "keys"
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal const val KEY_EXTENSION: String = "key" internal const val KEY_EXTENSION: String = "key"
@JvmStatic
public fun makeKey(armoredKey: String): PGPKeyPair {
val secretKey = PGPainless.readKeyRing().secretKeyRing(armoredKey).secretKey
return PGPKeyPair(secretKey)
}
} }
} }

View file

@ -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
}
}

View file

@ -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"
}
}

View file

@ -3,12 +3,12 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package dev.msfjarvis.aps.crypto.utils package dev.msfjarvis.aps.crypto
internal object CryptoConstants { internal object CryptoConstants {
internal const val KEY_PASSPHRASE = "hunter2" internal const val KEY_PASSPHRASE = "hunter2"
internal const val PLAIN_TEXT = "encryption worthy content" internal const val PLAIN_TEXT = "encryption worthy content"
internal const val KEY_NAME = "John Doe" internal const val KEY_NAME = "John Doe"
internal const val KEY_EMAIL = "john.doe@example.com" internal const val KEY_EMAIL = "john.doe@example.com"
internal const val KEY_ID = "04ace699d5b15b7e" internal const val KEY_ID = "08edf7567183ce27"
} }

View file

@ -1,59 +1,44 @@
package dev.msfjarvis.aps.crypto package dev.msfjarvis.aps.crypto
import androidx.test.platform.app.InstrumentationRegistry
import com.github.michaelbull.result.unwrap import com.github.michaelbull.result.unwrap
import com.github.michaelbull.result.unwrapError 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 java.io.File
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertIs import kotlin.test.assertIs
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import org.junit.After import org.junit.Rule
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
@OptIn(ExperimentalCoroutinesApi::class) @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 val testCoroutineDispatcher = TestCoroutineDispatcher()
private lateinit var gpgKeyManager: GPGKeyManager private val keyManager by lazy(LazyThreadSafetyMode.NONE) {
private lateinit var key: GPGKeyPair PGPKeyManager(filesDir.absolutePath, testCoroutineDispatcher)
@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 key = PGPKeyManager.makeKey(getArmoredKey())
@Test @Test
public fun testAddingKey() { public fun testAddingKey() {
runBlockingTest { runBlockingTest {
// Check if the key id returned is correct // 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) assertEquals(CryptoConstants.KEY_ID, keyId)
// Check if the keys directory have one file // Check if the keys directory have one file
val keysDir = File(getFilesDir(), GPGKeyManager.KEY_DIR_NAME) assertEquals(1, filesDir.list()?.size)
assertEquals(1, keysDir.list()?.size)
// Check if the file name is correct // Check if the file name is correct
val keyFile = keysDir.listFiles()?.first() 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() { public fun testAddingKeyWithoutReplaceFlag() {
runBlockingTest { runBlockingTest {
// Check adding the keys twice // Check adding the keys twice
gpgKeyManager.addKey(key, false).unwrap() keyManager.addKey(key, false).unwrap()
val error = gpgKeyManager.addKey(key, false).unwrapError() val error = keyManager.addKey(key, false).unwrapError()
assertIs<KeyManagerException.KeyAlreadyExistsException>(error) assertIs<KeyManagerException.KeyAlreadyExistsException>(error)
} }
@ -72,8 +57,8 @@ public class GPGKeyManagerTest {
public fun testAddingKeyWithReplaceFlag() { public fun testAddingKeyWithReplaceFlag() {
runBlockingTest { runBlockingTest {
// Check adding the keys twice // Check adding the keys twice
gpgKeyManager.addKey(key, true).unwrap() keyManager.addKey(key, true).unwrap()
val keyId = gpgKeyManager.addKey(key, true).unwrap().getKeyId() val keyId = keyManager.addKey(key, true).unwrap().getKeyId()
assertEquals(CryptoConstants.KEY_ID, keyId) assertEquals(CryptoConstants.KEY_ID, keyId)
} }
@ -83,14 +68,14 @@ public class GPGKeyManagerTest {
public fun testRemovingKey() { public fun testRemovingKey() {
runBlockingTest { runBlockingTest {
// Add key using KeyManager // Add key using KeyManager
gpgKeyManager.addKey(key).unwrap() keyManager.addKey(key).unwrap()
// Check if the key id returned is correct // 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) assertEquals(CryptoConstants.KEY_ID, keyId)
// Check if the keys directory have 0 files // 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) assertEquals(0, keysDir.list()?.size)
} }
} }
@ -99,10 +84,10 @@ public class GPGKeyManagerTest {
public fun testGetExistingKey() { public fun testGetExistingKey() {
runBlockingTest { runBlockingTest {
// Add key using KeyManager // 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 // 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(CryptoConstants.KEY_ID, key.getKeyId())
assertEquals(key.getKeyId(), returnedKeyPair.getKeyId()) assertEquals(key.getKeyId(), returnedKeyPair.getKeyId())
} }
@ -112,12 +97,12 @@ public class GPGKeyManagerTest {
public fun testGetNonExistentKey() { public fun testGetNonExistentKey() {
runBlockingTest { runBlockingTest {
// Add key using KeyManager // Add key using KeyManager
gpgKeyManager.addKey(key).unwrap() keyManager.addKey(key).unwrap()
val randomKeyId = "0x123456789" val randomKeyId = "0x123456789"
// Check returned key // Check returned key
val error = gpgKeyManager.getKeyById(randomKeyId).unwrapError() val error = keyManager.getKeyById(randomKeyId).unwrapError()
assertIs<KeyManagerException.KeyNotFoundException>(error) assertIs<KeyManagerException.KeyNotFoundException>(error)
assertEquals("No key found with id: $randomKeyId", error.message) assertEquals("No key found with id: $randomKeyId", error.message)
} }
@ -127,7 +112,7 @@ public class GPGKeyManagerTest {
public fun testFindKeysWithoutAdding() { public fun testFindKeysWithoutAdding() {
runBlockingTest { runBlockingTest {
// Check returned key // Check returned key
val error = gpgKeyManager.getKeyById("0x123456789").unwrapError() val error = keyManager.getKeyById("0x123456789").unwrapError()
assertIs<KeyManagerException.NoKeysAvailableException>(error) assertIs<KeyManagerException.NoKeysAvailableException>(error)
assertEquals("No keys were found", error.message) assertEquals("No keys were found", error.message)
} }
@ -138,28 +123,17 @@ public class GPGKeyManagerTest {
runBlockingTest { runBlockingTest {
// TODO: Should we check for more than 1 keys? // TODO: Should we check for more than 1 keys?
// Check if KeyManager returns no key // Check if KeyManager returns no key
val noKeyList = gpgKeyManager.getAllKeys().unwrap() val noKeyList = keyManager.getAllKeys().unwrap()
assertEquals(0, noKeyList.size) assertEquals(0, noKeyList.size)
// Add key using KeyManager // Add key using KeyManager
gpgKeyManager.addKey(key).unwrap() keyManager.addKey(key).unwrap()
// Check if KeyManager returns one key // Check if KeyManager returns one key
val singleKeyList = gpgKeyManager.getAllKeys().unwrap() val singleKeyList = keyManager.getAllKeys().unwrap()
assertEquals(1, singleKeyList.size) assertEquals(1, singleKeyList.size)
} }
} }
private companion object { private fun getArmoredKey() = this::class.java.classLoader.getResource("private_key").readText()
fun getFilesDir(): File = InstrumentationRegistry.getInstrumentation().context.filesDir
fun getKey(): String =
InstrumentationRegistry.getInstrumentation()
.context
.resources
.openRawResource(R.raw.private_key)
.readBytes()
.decodeToString()
}
} }

View file

@ -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()
}

View file

@ -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-----

View file

@ -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" android-desugarJdkLibs = "com.android.tools:desugar_jdk_libs:1.1.5"
# First-party libraries # 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-sublimeFuzzy = "com.github.android-password-store:sublime-fuzzy:1.0.0"
aps-zxingAndroidEmbedded = "com.github.android-password-store:zxing-android-embedded:4.2.1" 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-leakcanary = "com.squareup.leakcanary:leakcanary-android:2.7"
thirdparty-logcat = "com.squareup.logcat:logcat:0.1" thirdparty-logcat = "com.squareup.logcat:logcat:0.1"
thirdparty-modernAndroidPrefs = "de.maxr1998:modernandroidpreferences:2.1.0" 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-plumber = "com.squareup.leakcanary:plumber-android:2.7"
thirdparty-sshj = "com.hierynomus:sshj:0.31.0" thirdparty-sshj = "com.hierynomus:sshj:0.31.0"
thirdparty-sshauth = "com.github.open-keychain.open-keychain:sshauthentication-api:5.7.5" thirdparty-sshauth = "com.github.open-keychain.open-keychain:sshauthentication-api:5.7.5"

View file

@ -10,7 +10,7 @@ include(":autofill-parser")
include(":crypto-common") include(":crypto-common")
include(":crypto-pgp") include(":crypto-pgpainless")
include(":format-common") include(":format-common")