diff options
author | Aditya Wasan <adityawasan55@gmail.com> | 2021-08-17 04:14:43 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-17 04:14:43 +0530 |
commit | b7abd561f561af451ec717746e198a8686d10868 (patch) | |
tree | 16af9397d205d6501624ba4e98389e21764d9dd3 /crypto-pgp/src | |
parent | 9982562dc4e1ab4dbd058cf9d3c3c46fc598dec8 (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')
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 + } +} |