From 5dac84c3c882ee42bc541042684de18d7166fdd3 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Thu, 15 Jun 2023 15:49:32 +0530 Subject: refactor: consistently adopt PGP over GPG for naming PGP is the standard, GPG is an implementation of it. We're adhering to PGP, and not using GPG. --- .../app/passwordstore/crypto/GpgIdentifier.kt | 128 --------------------- .../kotlin/app/passwordstore/crypto/KeyUtils.kt | 4 +- .../app/passwordstore/crypto/PGPIdentifier.kt | 128 +++++++++++++++++++++ .../app/passwordstore/crypto/PGPKeyManager.kt | 12 +- .../app/passwordstore/crypto/GpgIdentifierTest.kt | 41 ------- .../app/passwordstore/crypto/KeyUtilsTest.kt | 2 +- .../app/passwordstore/crypto/PGPIdentifierTest.kt | 41 +++++++ .../app/passwordstore/crypto/PGPKeyManagerTest.kt | 4 +- 8 files changed, 180 insertions(+), 180 deletions(-) delete mode 100644 crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/GpgIdentifier.kt create mode 100644 crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt delete mode 100644 crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/GpgIdentifierTest.kt create mode 100644 crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPIdentifierTest.kt (limited to 'crypto-pgpainless/src') diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/GpgIdentifier.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/GpgIdentifier.kt deleted file mode 100644 index 96b9cc88..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/GpgIdentifier.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.crypto - -import java.util.Locale -import java.util.regex.Pattern - -/** Supertype for valid identifiers of GPG keys. */ -public sealed class GpgIdentifier { - - /** A [GpgIdentifier] that represents either a long key ID or a fingerprint. */ - public data class KeyId(val id: Long) : GpgIdentifier() { - override fun toString(): String { - return convertKeyIdToHex(id) - } - - /** Convert a [Long] key ID to a formatted string. */ - private fun convertKeyIdToHex(keyId: Long): String { - return convertKeyIdToHex32bit(keyId shr HEX_32_BIT_COUNT) + convertKeyIdToHex32bit(keyId) - } - - /** - * Converts [keyId] to an unsigned [Long] then uses [java.lang.Long.toHexString] to convert it - * to a lowercase hex ID. - */ - private fun convertKeyIdToHex32bit(keyId: Long): String { - var hexString = java.lang.Long.toHexString(keyId and HEX_32_BITMASK).lowercase(Locale.ENGLISH) - while (hexString.length < HEX_32_STRING_LENGTH) { - hexString = "0$hexString" - } - return hexString - } - } - - /** - * A [GpgIdentifier] that represents the textual name/email combination corresponding to a key. - * Despite the [email] property in this class, the value is not guaranteed to be a valid email. - */ - public data class UserId(val email: String) : GpgIdentifier() { - override fun toString(): String { - return email - } - } - - public companion object { - private const val HEX_RADIX = 16 - private const val HEX_32_BIT_COUNT = 32 - private const val HEX_32_BITMASK = 0xffffffffL - private const val HEX_32_STRING_LENGTH = 8 - private const val TRUNCATED_FINGERPRINT_LENGTH = 16 - - /** - * Attempts to parse an untyped String identifier into a concrete subtype of [GpgIdentifier]. - */ - @Suppress("ReturnCount") - public fun fromString(identifier: String): GpgIdentifier? { - if (identifier.isEmpty()) return null - // Match long key IDs: - // FF22334455667788 or 0xFF22334455667788 - val maybeLongKeyId = - identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F\\d]{16}".toRegex()) } - if (maybeLongKeyId != null) { - val keyId = maybeLongKeyId.toULong(HEX_RADIX) - return KeyId(keyId.toLong()) - } - - // Match fingerprints: - // FF223344556677889900112233445566778899 or 0xFF223344556677889900112233445566778899 - val maybeFingerprint = - identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F\\d]{40}".toRegex()) } - if (maybeFingerprint != null) { - // Truncating to the long key ID is not a security issue since OpenKeychain only - // accepts - // non-ambiguous key IDs. - val keyId = maybeFingerprint.takeLast(TRUNCATED_FINGERPRINT_LENGTH).toULong(HEX_RADIX) - return KeyId(keyId.toLong()) - } - - return splitUserId(identifier)?.let { UserId(it) } - } - - private object UserIdRegex { - val PATTERN: Pattern = Pattern.compile("^(.*?)(?: \\((.*)\\))?(?: <(.*)>)?$") - const val NAME = 1 - const val EMAIL = 3 - } - - private object EmailRegex { - val PATTERN: Pattern = Pattern.compile("^\"]*@[^<>\"]*[.]?[^<>\"]*)\"?>?$") - const val EMAIL = 1 - } - - /** - * Takes a 'Name (Comment) ' user ID in any of its permutations and attempts to extract - * an email from it. - */ - @Suppress("NestedBlockDepth") - private fun splitUserId(userId: String): String? { - if (userId.isNotEmpty()) { - val matcher = UserIdRegex.PATTERN.matcher(userId) - if (matcher.matches()) { - var name = - if (matcher.group(UserIdRegex.NAME)?.isEmpty() == true) null - else matcher.group(UserIdRegex.NAME) - var email = matcher.group(UserIdRegex.EMAIL) - if (email != null && name != null) { - val emailMatcher = EmailRegex.PATTERN.matcher(name) - if (emailMatcher.matches() && email == emailMatcher.group(EmailRegex.EMAIL)) { - email = emailMatcher.group(EmailRegex.EMAIL) - name = null - } - } - if (email == null && name != null) { - val emailMatcher = EmailRegex.PATTERN.matcher(name) - if (emailMatcher.matches()) { - email = emailMatcher.group(EmailRegex.EMAIL) - } - } - return email - } - } - return null - } - } -} diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt index da77bf33..3c36060f 100644 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt +++ b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt @@ -5,8 +5,8 @@ package app.passwordstore.crypto -import app.passwordstore.crypto.GpgIdentifier.KeyId -import app.passwordstore.crypto.GpgIdentifier.UserId +import app.passwordstore.crypto.PGPIdentifier.KeyId +import app.passwordstore.crypto.PGPIdentifier.UserId import com.github.michaelbull.result.get import com.github.michaelbull.result.runCatching import org.bouncycastle.openpgp.PGPKeyRing diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt new file mode 100644 index 00000000..98a1de2e --- /dev/null +++ b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt @@ -0,0 +1,128 @@ +/* + * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.crypto + +import java.util.Locale +import java.util.regex.Pattern + +/** Supertype for valid identifiers of PGP keys. */ +public sealed class PGPIdentifier { + + /** A [PGPIdentifier] that represents either a long key ID or a fingerprint. */ + public data class KeyId(val id: Long) : PGPIdentifier() { + override fun toString(): String { + return convertKeyIdToHex(id) + } + + /** Convert a [Long] key ID to a formatted string. */ + private fun convertKeyIdToHex(keyId: Long): String { + return convertKeyIdToHex32bit(keyId shr HEX_32_BIT_COUNT) + convertKeyIdToHex32bit(keyId) + } + + /** + * Converts [keyId] to an unsigned [Long] then uses [java.lang.Long.toHexString] to convert it + * to a lowercase hex ID. + */ + private fun convertKeyIdToHex32bit(keyId: Long): String { + var hexString = java.lang.Long.toHexString(keyId and HEX_32_BITMASK).lowercase(Locale.ENGLISH) + while (hexString.length < HEX_32_STRING_LENGTH) { + hexString = "0$hexString" + } + return hexString + } + } + + /** + * A [PGPIdentifier] that represents the textual name/email combination corresponding to a key. + * Despite the [email] property in this class, the value is not guaranteed to be a valid email. + */ + public data class UserId(val email: String) : PGPIdentifier() { + override fun toString(): String { + return email + } + } + + public companion object { + private const val HEX_RADIX = 16 + private const val HEX_32_BIT_COUNT = 32 + private const val HEX_32_BITMASK = 0xffffffffL + private const val HEX_32_STRING_LENGTH = 8 + private const val TRUNCATED_FINGERPRINT_LENGTH = 16 + + /** + * Attempts to parse an untyped String identifier into a concrete subtype of [PGPIdentifier]. + */ + @Suppress("ReturnCount") + public fun fromString(identifier: String): PGPIdentifier? { + if (identifier.isEmpty()) return null + // Match long key IDs: + // FF22334455667788 or 0xFF22334455667788 + val maybeLongKeyId = + identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F\\d]{16}".toRegex()) } + if (maybeLongKeyId != null) { + val keyId = maybeLongKeyId.toULong(HEX_RADIX) + return KeyId(keyId.toLong()) + } + + // Match fingerprints: + // FF223344556677889900112233445566778899 or 0xFF223344556677889900112233445566778899 + val maybeFingerprint = + identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F\\d]{40}".toRegex()) } + if (maybeFingerprint != null) { + // Truncating to the long key ID is not a security issue since OpenKeychain only + // accepts + // non-ambiguous key IDs. + val keyId = maybeFingerprint.takeLast(TRUNCATED_FINGERPRINT_LENGTH).toULong(HEX_RADIX) + return KeyId(keyId.toLong()) + } + + return splitUserId(identifier)?.let { UserId(it) } + } + + private object UserIdRegex { + val PATTERN: Pattern = Pattern.compile("^(.*?)(?: \\((.*)\\))?(?: <(.*)>)?$") + const val NAME = 1 + const val EMAIL = 3 + } + + private object EmailRegex { + val PATTERN: Pattern = Pattern.compile("^\"]*@[^<>\"]*[.]?[^<>\"]*)\"?>?$") + const val EMAIL = 1 + } + + /** + * Takes a 'Name (Comment) ' user ID in any of its permutations and attempts to extract + * an email from it. + */ + @Suppress("NestedBlockDepth") + private fun splitUserId(userId: String): String? { + if (userId.isNotEmpty()) { + val matcher = UserIdRegex.PATTERN.matcher(userId) + if (matcher.matches()) { + var name = + if (matcher.group(UserIdRegex.NAME)?.isEmpty() == true) null + else matcher.group(UserIdRegex.NAME) + var email = matcher.group(UserIdRegex.EMAIL) + if (email != null && name != null) { + val emailMatcher = EmailRegex.PATTERN.matcher(name) + if (emailMatcher.matches() && email == emailMatcher.group(EmailRegex.EMAIL)) { + email = emailMatcher.group(EmailRegex.EMAIL) + name = null + } + } + if (email == null && name != null) { + val emailMatcher = EmailRegex.PATTERN.matcher(name) + if (emailMatcher.matches()) { + email = emailMatcher.group(EmailRegex.EMAIL) + } + } + return email + } + } + return null + } + } +} diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt index f6ef50b3..a34d0379 100644 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt +++ b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt @@ -32,7 +32,7 @@ public class PGPKeyManager constructor( filesDir: String, private val dispatcher: CoroutineDispatcher, -) : KeyManager { +) : KeyManager { private val keyDir = File(filesDir, KEY_DIR_NAME) @@ -74,7 +74,7 @@ constructor( } /** @see KeyManager.removeKey */ - override suspend fun removeKey(identifier: GpgIdentifier): Result = + override suspend fun removeKey(identifier: PGPIdentifier): Result = withContext(dispatcher) { runSuspendCatching { if (!keyDirExists()) throw KeyDirectoryUnavailableException @@ -87,7 +87,7 @@ constructor( } /** @see KeyManager.getKeyById */ - override suspend fun getKeyById(id: GpgIdentifier): Result = + override suspend fun getKeyById(id: PGPIdentifier): Result = withContext(dispatcher) { runSuspendCatching { if (!keyDirExists()) throw KeyDirectoryUnavailableException @@ -97,14 +97,14 @@ constructor( val matchResult = when (id) { - is GpgIdentifier.KeyId -> { + is PGPIdentifier.KeyId -> { val keyIdMatch = keys .map { key -> key to tryGetId(key) } .firstOrNull { (_, keyId) -> keyId?.id == id.id } keyIdMatch?.first } - is GpgIdentifier.UserId -> { + is PGPIdentifier.UserId -> { val selector = SelectUserId.byEmail(id.email) val userIdMatch = keys @@ -134,7 +134,7 @@ constructor( } /** @see KeyManager.getKeyById */ - override suspend fun getKeyId(key: PGPKey): GpgIdentifier? = tryGetId(key) + override suspend fun getKeyId(key: PGPKey): PGPIdentifier? = tryGetId(key) /** Checks if [keyDir] exists and attempts to create it if not. */ private fun keyDirExists(): Boolean { diff --git a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/GpgIdentifierTest.kt b/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/GpgIdentifierTest.kt deleted file mode 100644 index 009976a2..00000000 --- a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/GpgIdentifierTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 GpgIdentifierTest { - - @Test - fun parseHexKeyIdWithout0xPrefix() { - val identifier = GpgIdentifier.fromString("79E8208280490C77") - assertNotNull(identifier) - assertTrue { identifier is GpgIdentifier.KeyId } - } - - @Test - fun parseHexKeyId() { - val identifier = GpgIdentifier.fromString("0x79E8208280490C77") - assertNotNull(identifier) - assertTrue { identifier is GpgIdentifier.KeyId } - } - - @Test - fun parseValidEmail() { - val identifier = GpgIdentifier.fromString("john.doe@example.org") - assertNotNull(identifier) - assertTrue { identifier is GpgIdentifier.UserId } - } - - @Test - fun parseEmailWithoutTLD() { - val identifier = GpgIdentifier.fromString("john.doe@example") - assertNotNull(identifier) - assertTrue { identifier is GpgIdentifier.UserId } - } -} diff --git a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt b/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt index f7bf46d9..39af53b1 100644 --- a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt +++ b/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt @@ -18,7 +18,7 @@ class KeyUtilsTest { assertIs(keyring) val keyId = tryGetId(key) assertNotNull(keyId) - assertIs(keyId) + assertIs(keyId) assertEquals("b950ae2813841585", keyId.toString()) } } 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 index c8bd66c5..43a62bd7 100644 --- a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt +++ b/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt @@ -1,8 +1,8 @@ package app.passwordstore.crypto -import app.passwordstore.crypto.GpgIdentifier.KeyId -import app.passwordstore.crypto.GpgIdentifier.UserId 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 -- cgit v1.2.3