diff options
Diffstat (limited to 'crypto/pgpainless/src/test/kotlin')
6 files changed, 506 insertions, 0 deletions
diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/CryptoConstants.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/CryptoConstants.kt new file mode 100644 index 00000000..d827e169 --- /dev/null +++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/CryptoConstants.kt @@ -0,0 +1,14 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.crypto + +object CryptoConstants { + const val KEY_PASSPHRASE = "hunter2" + const val PLAIN_TEXT = "encryption worthy content" + const val KEY_NAME = "John Doe" + const val KEY_EMAIL = "john.doe@example.com" + const val KEY_ID = 0x08edf7567183ce27 +} diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt new file mode 100644 index 00000000..a0f84402 --- /dev/null +++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt @@ -0,0 +1,35 @@ +package app.passwordstore.crypto + +import app.passwordstore.crypto.KeyUtils.isKeyUsable +import app.passwordstore.crypto.KeyUtils.tryGetId +import app.passwordstore.crypto.KeyUtils.tryParseKeyring +import app.passwordstore.crypto.TestUtils.AllKeys +import app.passwordstore.crypto.TestUtils.getArmoredSecretKeyWithMultipleIdentities +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import org.bouncycastle.openpgp.PGPSecretKeyRing + +class KeyUtilsTest { + @Test + fun parseKeyWithMultipleIdentities() { + val key = PGPKey(getArmoredSecretKeyWithMultipleIdentities()) + val keyring = tryParseKeyring(key) + assertNotNull(keyring) + assertIs<PGPSecretKeyRing>(keyring) + val keyId = tryGetId(key) + assertNotNull(keyId) + assertIs<PGPIdentifier.KeyId>(keyId) + assertEquals("b950ae2813841585", keyId.toString()) + } + + @Test + fun isKeyUsable() { + val params = AllKeys.entries.map { it to (it != AllKeys.AEAD_PUB && it != AllKeys.AEAD_SEC) } + params.forEach { (allKeys, isUsable) -> + val key = PGPKey(allKeys.keyMaterial) + assertEquals(isUsable, isKeyUsable(key), "${allKeys.name} failed expectation:") + } + } +} diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPIdentifierTest.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPIdentifierTest.kt new file mode 100644 index 00000000..efc6e0ba --- /dev/null +++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPIdentifierTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.crypto + +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class PGPIdentifierTest { + + @Test + fun parseHexKeyIdWithout0xPrefix() { + val identifier = PGPIdentifier.fromString("79E8208280490C77") + assertNotNull(identifier) + assertTrue { identifier is PGPIdentifier.KeyId } + } + + @Test + fun parseHexKeyId() { + val identifier = PGPIdentifier.fromString("0x79E8208280490C77") + assertNotNull(identifier) + assertTrue { identifier is PGPIdentifier.KeyId } + } + + @Test + fun parseValidEmail() { + val identifier = PGPIdentifier.fromString("john.doe@example.org") + assertNotNull(identifier) + assertTrue { identifier is PGPIdentifier.UserId } + } + + @Test + fun parseEmailWithoutTLD() { + val identifier = PGPIdentifier.fromString("john.doe@example") + assertNotNull(identifier) + assertTrue { identifier is PGPIdentifier.UserId } + } +} diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt new file mode 100644 index 00000000..85cf8e1b --- /dev/null +++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt @@ -0,0 +1,236 @@ +package app.passwordstore.crypto + +import app.passwordstore.crypto.KeyUtils.tryGetId +import app.passwordstore.crypto.PGPIdentifier.KeyId +import app.passwordstore.crypto.PGPIdentifier.UserId +import app.passwordstore.crypto.errors.KeyAlreadyExistsException +import app.passwordstore.crypto.errors.KeyNotFoundException +import app.passwordstore.crypto.errors.NoKeysAvailableException +import app.passwordstore.crypto.errors.UnusableKeyException +import com.github.michaelbull.result.Err +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.unwrap +import com.github.michaelbull.result.unwrapError +import java.io.File +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.rules.TemporaryFolder + +class PGPKeyManagerTest { + + @get:Rule val temporaryFolder: TemporaryFolder = TemporaryFolder() + private val dispatcher = StandardTestDispatcher() + private val scope = TestScope(dispatcher) + private val filesDir by unsafeLazy { temporaryFolder.root } + private val keysDir by unsafeLazy { File(filesDir, PGPKeyManager.KEY_DIR_NAME) } + private val keyManager by unsafeLazy { PGPKeyManager(filesDir.absolutePath, dispatcher) } + private val secretKey = PGPKey(TestUtils.getArmoredSecretKey()) + private val publicKey = PGPKey(TestUtils.getArmoredPublicKey()) + + private fun <T> unsafeLazy(initializer: () -> T) = + lazy(LazyThreadSafetyMode.NONE) { initializer.invoke() } + + @Test + fun addKey() = + runTest(dispatcher) { + // Check if the key id returned is correct + val keyId = keyManager.getKeyId(keyManager.addKey(secretKey).unwrap()) + assertEquals(KeyId(CryptoConstants.KEY_ID), keyId) + // Check if the keys directory have one file + assertEquals(1, filesDir.list()?.size) + // Check if the file name is correct + val keyFile = keysDir.listFiles()?.first() + assertEquals(keyFile?.name, "$keyId.${PGPKeyManager.KEY_EXTENSION}") + } + + @Test + fun addKeyWithoutReplaceFlag() = + runTest(dispatcher) { + // Check adding the keys twice + keyManager.addKey(secretKey, false).unwrap() + val error = keyManager.addKey(secretKey, false).unwrapError() + + assertIs<KeyAlreadyExistsException>(error) + } + + @Test + fun addKeyWithReplaceFlag() = + runTest(dispatcher) { + // Check adding the keys twice + keyManager.addKey(secretKey, true).unwrap() + val keyId = keyManager.getKeyId(keyManager.addKey(secretKey, true).unwrap()) + + assertEquals(KeyId(CryptoConstants.KEY_ID), keyId) + } + + @Test + fun addKeyWithUnusableKey() = + runTest(dispatcher) { + val error = keyManager.addKey(PGPKey(TestUtils.getAEADSecretKey())).unwrapError() + assertEquals(UnusableKeyException, error) + } + + @Test + fun removeKey() = + runTest(dispatcher) { + // Add key using KeyManager + keyManager.addKey(secretKey).unwrap() + // Remove key + keyManager.removeKey(tryGetId(secretKey)!!).unwrap() + // Check that no keys remain + val keys = keyManager.getAllKeys().unwrap() + assertEquals(0, keys.size) + } + + @Test + fun getKeyById() = + runTest(dispatcher) { + // Add key using KeyManager + keyManager.addKey(secretKey).unwrap() + val keyId = keyManager.getKeyId(secretKey) + assertNotNull(keyId) + assertEquals(KeyId(CryptoConstants.KEY_ID), keyManager.getKeyId(secretKey)) + // Check returned key id matches the expected id and the created key id + val returnedKey = keyManager.getKeyById(keyId).unwrap() + assertEquals(keyManager.getKeyId(secretKey), keyManager.getKeyId(returnedKey)) + } + + @Test + fun getKeyByFullUserId() = + runTest(dispatcher) { + keyManager.addKey(secretKey).unwrap() + val keyId = "${CryptoConstants.KEY_NAME} <${CryptoConstants.KEY_EMAIL}>" + val returnedKey = keyManager.getKeyById(UserId(keyId)).unwrap() + assertEquals(keyManager.getKeyId(secretKey), keyManager.getKeyId(returnedKey)) + } + + @Test + fun getKeyByEmailUserId() = + runTest(dispatcher) { + keyManager.addKey(secretKey).unwrap() + val keyId = CryptoConstants.KEY_EMAIL + val returnedKey = keyManager.getKeyById(UserId(keyId)).unwrap() + assertEquals(keyManager.getKeyId(secretKey), keyManager.getKeyId(returnedKey)) + } + + @Test + fun getNonExistentKey() = + runTest(dispatcher) { + // Add key using KeyManager + keyManager.addKey(secretKey).unwrap() + val keyId = KeyId(0x08edf7567183ce44) + // Check returned key + val error = keyManager.getKeyById(keyId).unwrapError() + assertIs<KeyNotFoundException>(error) + assertEquals("No key found with id: $keyId", error.message) + } + + @Test + fun findNonExistentKey() = + runTest(dispatcher) { + // Check returned key + val error = keyManager.getKeyById(KeyId(0x08edf7567183ce44)).unwrapError() + assertIs<NoKeysAvailableException>(error) + assertEquals("No keys were found", error.message) + } + + @Test + fun getAllKeys() = + runTest(dispatcher) { + // Check if KeyManager returns no key + val noKeyList = keyManager.getAllKeys().unwrap() + assertEquals(0, noKeyList.size) + // Add key using KeyManager + keyManager.addKey(secretKey).unwrap() + keyManager.addKey(PGPKey(TestUtils.getArmoredSecretKeyWithMultipleIdentities())).unwrap() + // Check if KeyManager returns one key + val singleKeyList = keyManager.getAllKeys().unwrap() + assertEquals(2, singleKeyList.size) + } + + @Test + fun getMultipleIdentityKeyWithAllIdentities() = + runTest(dispatcher) { + val key = PGPKey(TestUtils.getArmoredSecretKeyWithMultipleIdentities()) + keyManager.addKey(key).unwrap() + val johnKey = keyManager.getKeyById(UserId("john@doe.org")).unwrap() + val janeKey = keyManager.getKeyById(UserId("jane@doe.org")).unwrap() + + assertContentEquals(johnKey.contents, janeKey.contents) + } + + @Test + fun replaceSecretKeyWithPublicKey() = + runTest(dispatcher) { + assertIs<Ok<PGPKey>>(keyManager.addKey(secretKey)) + assertIs<Err<KeyAlreadyExistsException>>(keyManager.addKey(publicKey)) + } + + @Test + fun replacePublicKeyWithSecretKey() = + runTest(dispatcher) { + assertIs<Ok<PGPKey>>(keyManager.addKey(publicKey)) + assertIs<Ok<PGPKey>>(keyManager.addKey(secretKey)) + } + + @Test + fun replacePublicKeyWithPublicKey() = + runTest(dispatcher) { + assertIs<Ok<PGPKey>>(keyManager.addKey(publicKey)) + assertIs<Ok<PGPKey>>(keyManager.addKey(publicKey)) + val allKeys = keyManager.getAllKeys() + assertIs<Ok<List<PGPKey>>>(allKeys) + assertEquals(1, allKeys.value.size) + val key = allKeys.value[0] + assertContentEquals(publicKey.contents, key.contents) + } + + @Test + fun replaceSecretKeyWithSecretKey() = + runTest(dispatcher) { + assertIs<Ok<PGPKey>>(keyManager.addKey(secretKey)) + assertIs<Err<KeyAlreadyExistsException>>(keyManager.addKey(secretKey)) + } + + @Test + fun addMultipleKeysWithSameEmail() = + runTest(dispatcher) { + val alice = + PGPKey(this::class.java.classLoader.getResource("alice_owner@example_com")!!.readBytes()) + val bobby = + PGPKey(this::class.java.classLoader.getResource("bobby_owner@example_com")!!.readBytes()) + assertIs<Ok<PGPKey>>(keyManager.addKey(alice)) + assertIs<Ok<PGPKey>>(keyManager.addKey(bobby)) + + keyManager.getAllKeys().apply { + assertIs<Ok<List<PGPKey>>>(this) + assertEquals(2, this.value.size) + } + val longKeyIds = + arrayOf( + KeyId(-7087927403306410599), // Alice + KeyId(-961222705095032109), // Bobby + ) + val userIds = + arrayOf( + UserId("Alice <owner@example.com>"), + UserId("Bobby <owner@example.com>"), + ) + + for (idCollection in arrayOf(longKeyIds, userIds)) { + val alice1 = keyManager.getKeyById(idCollection[0]) + val bobby1 = keyManager.getKeyById(idCollection[1]) + assertIs<Ok<PGPKey>>(alice1) + assertIs<Ok<PGPKey>>(bobby1) + assertNotEquals(alice1.value.contents, bobby1.value.contents) + } + } +} diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt new file mode 100644 index 00000000..8bf6ba1e --- /dev/null +++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +@file:Suppress("JUnitMalformedDeclaration") // The test runner takes care of it + +package app.passwordstore.crypto + +import app.passwordstore.crypto.CryptoConstants.KEY_PASSPHRASE +import app.passwordstore.crypto.CryptoConstants.PLAIN_TEXT +import app.passwordstore.crypto.errors.IncorrectPassphraseException +import com.github.michaelbull.result.Err +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.getError +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import java.io.ByteArrayOutputStream +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertTrue +import org.junit.runner.RunWith +import org.pgpainless.PGPainless +import org.pgpainless.decryption_verification.MessageInspector + +@Suppress("Unused") // Test runner handles it internally +enum class EncryptionKey(val keySet: List<PGPKey>) { + PUBLIC(listOf(PGPKey(TestUtils.getArmoredPublicKey()))), + SECRET(listOf(PGPKey(TestUtils.getArmoredSecretKey()))), + ALL(listOf(PGPKey(TestUtils.getArmoredPublicKey()), PGPKey(TestUtils.getArmoredSecretKey()))), +} + +@RunWith(TestParameterInjector::class) +class PGPainlessCryptoHandlerTest { + + private val cryptoHandler = PGPainlessCryptoHandler() + private val secretKey = PGPKey(TestUtils.getArmoredSecretKey()) + + @Test + fun encryptAndDecrypt(@TestParameter encryptionKey: EncryptionKey) { + val ciphertextStream = ByteArrayOutputStream() + val encryptRes = + cryptoHandler.encrypt( + encryptionKey.keySet, + PLAIN_TEXT.byteInputStream(Charsets.UTF_8), + ciphertextStream, + PGPEncryptOptions.Builder().build(), + ) + assertIs<Ok<Unit>>(encryptRes) + val plaintextStream = ByteArrayOutputStream() + val decryptRes = + cryptoHandler.decrypt( + listOf(secretKey), + KEY_PASSPHRASE, + ciphertextStream.toByteArray().inputStream(), + plaintextStream, + PGPDecryptOptions.Builder().build(), + ) + assertIs<Ok<Unit>>(decryptRes) + assertEquals(PLAIN_TEXT, plaintextStream.toString(Charsets.UTF_8)) + } + + @Test + fun decryptWithWrongPassphrase(@TestParameter encryptionKey: EncryptionKey) { + val ciphertextStream = ByteArrayOutputStream() + val encryptRes = + cryptoHandler.encrypt( + encryptionKey.keySet, + PLAIN_TEXT.byteInputStream(Charsets.UTF_8), + ciphertextStream, + PGPEncryptOptions.Builder().build(), + ) + assertIs<Ok<Unit>>(encryptRes) + val plaintextStream = ByteArrayOutputStream() + val result = + cryptoHandler.decrypt( + listOf(secretKey), + "very incorrect passphrase", + ciphertextStream.toByteArray().inputStream(), + plaintextStream, + PGPDecryptOptions.Builder().build(), + ) + assertIs<Err<Throwable>>(result) + assertIs<IncorrectPassphraseException>(result.getError()) + } + + @Test + fun encryptAsciiArmored(@TestParameter encryptionKey: EncryptionKey) { + val ciphertextStream = ByteArrayOutputStream() + val encryptRes = + cryptoHandler.encrypt( + encryptionKey.keySet, + PLAIN_TEXT.byteInputStream(Charsets.UTF_8), + ciphertextStream, + PGPEncryptOptions.Builder().withAsciiArmor(true).build(), + ) + assertIs<Ok<Unit>>(encryptRes) + val ciphertext = ciphertextStream.toString(Charsets.UTF_8) + assertContains(ciphertext, "Version: PGPainless") + assertContains(ciphertext, "-----BEGIN PGP MESSAGE-----") + assertContains(ciphertext, "-----END PGP MESSAGE-----") + } + + @Test + fun encryptMultiple() { + val alice = + PGPainless.generateKeyRing().modernKeyRing("Alice <owner@example.com>", KEY_PASSPHRASE) + val bob = PGPainless.generateKeyRing().modernKeyRing("Bob <owner@example.com>", KEY_PASSPHRASE) + val aliceKey = PGPKey(PGPainless.asciiArmor(alice).encodeToByteArray()) + val bobKey = PGPKey(PGPainless.asciiArmor(bob).encodeToByteArray()) + val ciphertextStream = ByteArrayOutputStream() + val encryptRes = + cryptoHandler.encrypt( + listOf(aliceKey, bobKey), + PLAIN_TEXT.byteInputStream(Charsets.UTF_8), + ciphertextStream, + PGPEncryptOptions.Builder().withAsciiArmor(true).build(), + ) + assertIs<Ok<Unit>>(encryptRes) + val message = ciphertextStream.toByteArray().decodeToString() + val info = MessageInspector.determineEncryptionInfoForMessage(message) + assertTrue(info.isEncrypted) + assertEquals(2, info.keyIds.size) + assertFalse(info.isSignedOnly) + for (key in listOf(aliceKey, bobKey)) { + val ciphertextStreamCopy = message.byteInputStream() + val plaintextStream = ByteArrayOutputStream() + val res = + cryptoHandler.decrypt( + listOf(key), + KEY_PASSPHRASE, + ciphertextStreamCopy, + plaintextStream, + PGPDecryptOptions.Builder().build(), + ) + assertIs<Ok<Unit>>(res) + } + } + + @Test + fun canHandleFiltersFormats() { + assertFalse { cryptoHandler.canHandle("example.com") } + assertTrue { cryptoHandler.canHandle("example.com.gpg") } + assertFalse { cryptoHandler.canHandle("example.com.asc") } + } +} diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/TestUtils.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/TestUtils.kt new file mode 100644 index 00000000..90b98ac9 --- /dev/null +++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/TestUtils.kt @@ -0,0 +1,32 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +@file:Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + +package app.passwordstore.crypto + +object TestUtils { + fun getArmoredSecretKey() = this::class.java.classLoader.getResource("secret_key").readBytes() + + fun getArmoredPublicKey() = this::class.java.classLoader.getResource("public_key").readBytes() + + fun getArmoredSecretKeyWithMultipleIdentities() = + this::class.java.classLoader.getResource("secret_key_multiple_identities").readBytes() + + fun getArmoredPublicKeyWithMultipleIdentities() = + this::class.java.classLoader.getResource("public_key_multiple_identities").readBytes() + + fun getAEADPublicKey() = this::class.java.classLoader.getResource("aead_pub").readBytes() + + fun getAEADSecretKey() = this::class.java.classLoader.getResource("aead_sec").readBytes() + + enum class AllKeys(val keyMaterial: ByteArray) { + ARMORED_SEC(getArmoredSecretKey()), + ARMORED_PUB(getArmoredPublicKey()), + MULTIPLE_IDENTITIES_SEC(getArmoredSecretKeyWithMultipleIdentities()), + MULTIPLE_IDENTITIES_PUB(getArmoredPublicKeyWithMultipleIdentities()), + AEAD_SEC(getAEADSecretKey()), + AEAD_PUB(getAEADPublicKey()), + } +} |