aboutsummaryrefslogtreecommitdiff
path: root/crypto-pgp/src
diff options
context:
space:
mode:
authorAditya Wasan <adityawasan55@gmail.com>2021-08-17 04:14:43 +0530
committerGitHub <noreply@github.com>2021-08-17 04:14:43 +0530
commitb7abd561f561af451ec717746e198a8686d10868 (patch)
tree16af9397d205d6501624ba4e98389e21764d9dd3 /crypto-pgp/src
parent9982562dc4e1ab4dbd058cf9d3c3c46fc598dec8 (diff)
Add `KeyPair` and `KeyManager` to manage keys in the app (#1487)
Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'crypto-pgp/src')
-rw-r--r--crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/GPGKeyManagerTest.kt165
-rw-r--r--crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/GPGKeyPairTest.kt52
-rw-r--r--crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/utils/CryptoConstants.kt14
-rw-r--r--crypto-pgp/src/androidTest/res/raw/private_key18
-rw-r--r--crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GPGKeyManager.kt95
-rw-r--r--crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GPGKeyPair.kt28
6 files changed, 372 insertions, 0 deletions
diff --git a/crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/GPGKeyManagerTest.kt b/crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/GPGKeyManagerTest.kt
new file mode 100644
index 00000000..80a13eb5
--- /dev/null
+++ b/crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/GPGKeyManagerTest.kt
@@ -0,0 +1,165 @@
+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.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+public class GPGKeyManagerTest {
+
+ 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()
+ }
+
+ @Test
+ public fun testAddingKey() {
+ runBlockingTest {
+ // Check if the key id returned is correct
+ val keyId = gpgKeyManager.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)
+
+ // Check if the file name is correct
+ val keyFile = keysDir.listFiles()?.first()
+ assertEquals(keyFile?.name, "$keyId.${GPGKeyManager.KEY_EXTENSION}")
+ }
+ }
+
+ @Test
+ public fun testAddingKeyWithoutReplaceFlag() {
+ runBlockingTest {
+ // Check adding the keys twice
+ gpgKeyManager.addKey(key, false).unwrap()
+ val error = gpgKeyManager.addKey(key, false).unwrapError()
+
+ assertIs<KeyManagerException.KeyAlreadyExistsException>(error)
+ }
+ }
+
+ @Test
+ public fun testAddingKeyWithReplaceFlag() {
+ runBlockingTest {
+ // Check adding the keys twice
+ gpgKeyManager.addKey(key, true).unwrap()
+ val keyId = gpgKeyManager.addKey(key, true).unwrap().getKeyId()
+
+ assertEquals(CryptoConstants.KEY_ID, keyId)
+ }
+ }
+
+ @Test
+ public fun testRemovingKey() {
+ runBlockingTest {
+ // Add key using KeyManager
+ gpgKeyManager.addKey(key).unwrap()
+
+ // Check if the key id returned is correct
+ val keyId = gpgKeyManager.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)
+ assertEquals(0, keysDir.list()?.size)
+ }
+ }
+
+ @Test
+ public fun testGetExistingKey() {
+ runBlockingTest {
+ // Add key using KeyManager
+ gpgKeyManager.addKey(key).unwrap()
+
+ // Check returned key id matches the expected id and the created key id
+ val returnedKeyPair = gpgKeyManager.getKeyById(key.getKeyId()).unwrap()
+ assertEquals(CryptoConstants.KEY_ID, key.getKeyId())
+ assertEquals(key.getKeyId(), returnedKeyPair.getKeyId())
+ }
+ }
+
+ @Test
+ public fun testGetNonExistentKey() {
+ runBlockingTest {
+ // Add key using KeyManager
+ gpgKeyManager.addKey(key).unwrap()
+
+ val randomKeyId = "0x123456789"
+
+ // Check returned key
+ val error = gpgKeyManager.getKeyById(randomKeyId).unwrapError()
+ assertIs<KeyManagerException.KeyNotFoundException>(error)
+ assertEquals("No key found with id: $randomKeyId", error.message)
+ }
+ }
+
+ @Test
+ public fun testFindKeysWithoutAdding() {
+ runBlockingTest {
+ // Check returned key
+ val error = gpgKeyManager.getKeyById("0x123456789").unwrapError()
+ assertIs<KeyManagerException.NoKeysAvailableException>(error)
+ assertEquals("No keys were found", error.message)
+ }
+ }
+
+ @Test
+ public fun testGettingAllKeys() {
+ runBlockingTest {
+ // TODO: Should we check for more than 1 keys?
+ // Check if KeyManager returns no key
+ val noKeyList = gpgKeyManager.getAllKeys().unwrap()
+ assertEquals(0, noKeyList.size)
+
+ // Add key using KeyManager
+ gpgKeyManager.addKey(key).unwrap()
+
+ // Check if KeyManager returns one key
+ val singleKeyList = gpgKeyManager.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()
+ }
+}
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
new file mode 100644
index 00000000..2340d9a5
--- /dev/null
+++ b/crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/GPGKeyPairTest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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()
+ }
+}
diff --git a/crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/utils/CryptoConstants.kt b/crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/utils/CryptoConstants.kt
new file mode 100644
index 00000000..873f7105
--- /dev/null
+++ b/crypto-pgp/src/androidTest/kotlin/dev/msfjarvis/aps/crypto/utils/CryptoConstants.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.crypto.utils
+
+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"
+}
diff --git a/crypto-pgp/src/androidTest/res/raw/private_key b/crypto-pgp/src/androidTest/res/raw/private_key
new file mode 100644
index 00000000..5a4f436c
--- /dev/null
+++ b/crypto-pgp/src/androidTest/res/raw/private_key
@@ -0,0 +1,18 @@
+-----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/kotlin/dev/msfjarvis/aps/data/crypto/GPGKeyManager.kt b/crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GPGKeyManager.kt
new file mode 100644
index 00000000..478d2700
--- /dev/null
+++ b/crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GPGKeyManager.kt
@@ -0,0 +1,95 @@
+/*
+ * 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 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
+
+public class GPGKeyManager(filesDir: String, private val dispatcher: CoroutineDispatcher) :
+ KeyManager<GPGKeyPair> {
+
+ private val keyDir = File(filesDir, KEY_DIR_NAME)
+
+ override suspend fun addKey(key: GPGKeyPair, replace: Boolean): Result<GPGKeyPair, Throwable> =
+ withContext(dispatcher) {
+ runCatching {
+ if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException
+ val keyFile = File(keyDir, "${key.getKeyId()}.$KEY_EXTENSION")
+ if (keyFile.exists()) {
+ // Check for replace flag first and if it is false, throw an error
+ if (!replace) throw KeyManagerException.KeyAlreadyExistsException(key.getKeyId())
+ if (!keyFile.delete()) throw KeyManagerException.KeyDeletionFailedException
+ }
+
+ keyFile.writeBytes(key.getPrivateKey())
+
+ key
+ }
+ }
+
+ override suspend fun removeKey(key: GPGKeyPair): Result<GPGKeyPair, Throwable> =
+ withContext(dispatcher) {
+ runCatching {
+ if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException
+ val keyFile = File(keyDir, "${key.getKeyId()}.$KEY_EXTENSION")
+ if (keyFile.exists()) {
+ if (!keyFile.delete()) throw KeyManagerException.KeyDeletionFailedException
+ }
+
+ key
+ }
+ }
+
+ override suspend fun getKeyById(id: String): Result<GPGKeyPair, Throwable> =
+ withContext(dispatcher) {
+ runCatching {
+ if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException
+ val keys = keyDir.listFiles()
+ if (keys.isNullOrEmpty()) throw KeyManagerException.NoKeysAvailableException
+
+ for (keyFile in keys) {
+ val keyPair = GPGKeyPair(Crypto.newKeyFromArmored(keyFile.readText()))
+ if (keyPair.getKeyId() == id) return@runCatching keyPair
+ }
+
+ throw KeyManagerException.KeyNotFoundException(id)
+ }
+ }
+
+ override suspend fun getAllKeys(): Result<List<GPGKeyPair>, 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()
+ }
+ }
+
+ override fun canHandle(fileName: String): Boolean {
+ // TODO: This is a temp hack for now and in future it should check that the GPGKeyManager can
+ // decrypt the file
+ return fileName.endsWith(KEY_EXTENSION)
+ }
+
+ private fun keyDirExists(): Boolean {
+ return keyDir.exists() || keyDir.mkdirs()
+ }
+
+ internal 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"
+ }
+}
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
new file mode 100644
index 00000000..2dbe8689
--- /dev/null
+++ b/crypto-pgp/src/main/kotlin/dev/msfjarvis/aps/data/crypto/GPGKeyPair.kt
@@ -0,0 +1,28 @@
+/*
+ * 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
+ }
+}