diff options
author | Harsh Shandilya <me@msfjarvis.dev> | 2023-08-10 03:21:47 +0530 |
---|---|---|
committer | Harsh Shandilya <me@msfjarvis.dev> | 2023-08-10 03:31:08 +0530 |
commit | 959a56d7ffbc20db60721d7a09f399c8bdefe07e (patch) | |
tree | 0d9b14e0bb770fd6da2e04295f2c22c087142439 /crypto-pgpainless/src/main | |
parent | efef72b6327c8e683c8844146e23d12104f12dd1 (diff) |
refactor: un-flatten module structure
Diffstat (limited to 'crypto-pgpainless/src/main')
7 files changed, 0 insertions, 541 deletions
diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt deleted file mode 100644 index 5b23dc18..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt +++ /dev/null @@ -1,52 +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 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 -import org.pgpainless.PGPainless -import org.pgpainless.key.parsing.KeyRingReader - -/** Utility methods to deal with [PGPKey]s. */ -public object KeyUtils { - /** - * Attempts to parse a [PGPKeyRing] from a given [key]. The key is first tried as a secret key and - * then as a public one before the method gives up and returns null. - */ - public fun tryParseKeyring(key: PGPKey): PGPKeyRing? { - return runCatching { KeyRingReader.readKeyRing(key.contents.inputStream()) }.get() - } - - /** Parses a [PGPKeyRing] from the given [key] and calculates its long key ID */ - public fun tryGetId(key: PGPKey): KeyId? { - val keyRing = tryParseKeyring(key) ?: return null - return KeyId(keyRing.publicKey.keyID) - } - - /** - * Attempts to parse the given [PGPKey] into a [PGPKeyRing] and obtains the [UserId] of the - * corresponding public key. - */ - public fun tryGetEmail(key: PGPKey): UserId? { - val keyRing = tryParseKeyring(key) ?: return null - return UserId(keyRing.publicKey.userIDs.next()) - } - - /** - * Tests if the given [key] can be used for encryption, which is a bare minimum necessity for the - * app. - */ - public fun isKeyUsable(key: PGPKey): Boolean { - return runCatching { - val keyRing = tryParseKeyring(key) ?: return false - PGPainless.inspectKeyRing(keyRing).isUsableForEncryption - } - .get() != null - } -} diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPDecryptOptions.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPDecryptOptions.kt deleted file mode 100644 index 15ce92f0..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPDecryptOptions.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.passwordstore.crypto - -/** [CryptoOptions] implementation for PGPainless decrypt operations. */ -public class PGPDecryptOptions -private constructor( - private val values: Map<String, Boolean>, -) : CryptoOptions { - - override fun isOptionEnabled(option: String): Boolean { - return values.getOrDefault(option, false) - } - - /** Implementation of a builder pattern for [PGPDecryptOptions]. */ - public class Builder { - - /** Build the final [PGPDecryptOptions] object. */ - public fun build(): PGPDecryptOptions { - return PGPDecryptOptions(emptyMap()) - } - } -} diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPEncryptOptions.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPEncryptOptions.kt deleted file mode 100644 index 90de6b51..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPEncryptOptions.kt +++ /dev/null @@ -1,35 +0,0 @@ -package app.passwordstore.crypto - -/** [CryptoOptions] implementation for PGPainless encrypt operations. */ -public class PGPEncryptOptions -private constructor( - private val values: Map<String, Boolean>, -) : CryptoOptions { - - internal companion object { - const val ASCII_ARMOR = "ASCII_ARMOR" - } - - override fun isOptionEnabled(option: String): Boolean { - return values.getOrDefault(option, false) - } - - /** Implementation of a builder pattern for [PGPEncryptOptions]. */ - public class Builder { - private val optionsMap = mutableMapOf<String, Boolean>() - - /** - * Toggle whether the encryption operation output will be ASCII armored or in OpenPGP's binary - * format. - */ - public fun withAsciiArmor(enabled: Boolean): Builder { - optionsMap[ASCII_ARMOR] = enabled - return this - } - - /** Build the final [PGPEncryptOptions] object. */ - public fun build(): PGPEncryptOptions { - return PGPEncryptOptions(optionsMap) - } - } -} diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt deleted file mode 100644 index 98a1de2e..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.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 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) <Email>' 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/PGPKey.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKey.kt deleted file mode 100644 index a33655d4..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKey.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.crypto - -/** - * A simple value class wrapping over a [ByteArray] that can represent a PGP key. The implementation - * details of public/ private parts as well as identities are deferred to [PGPKeyManager]. - */ -@JvmInline public value class PGPKey(public val contents: ByteArray) diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt deleted file mode 100644 index aed1acf2..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -@file:Suppress("BlockingMethodInNonBlockingContext") - -package app.passwordstore.crypto - -import androidx.annotation.VisibleForTesting -import app.passwordstore.crypto.KeyUtils.isKeyUsable -import app.passwordstore.crypto.KeyUtils.tryGetId -import app.passwordstore.crypto.KeyUtils.tryParseKeyring -import app.passwordstore.crypto.errors.InvalidKeyException -import app.passwordstore.crypto.errors.KeyAlreadyExistsException -import app.passwordstore.crypto.errors.KeyDeletionFailedException -import app.passwordstore.crypto.errors.KeyDirectoryUnavailableException -import app.passwordstore.crypto.errors.KeyNotFoundException -import app.passwordstore.crypto.errors.NoKeysAvailableException -import app.passwordstore.crypto.errors.UnusableKeyException -import app.passwordstore.util.coroutines.runSuspendCatching -import com.github.michaelbull.result.Result -import com.github.michaelbull.result.unwrap -import java.io.File -import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.withContext -import org.bouncycastle.openpgp.PGPPublicKeyRing -import org.bouncycastle.openpgp.PGPSecretKeyRing -import org.pgpainless.PGPainless -import org.pgpainless.util.selection.userid.SelectUserId - -public class PGPKeyManager -@Inject -constructor( - filesDir: String, - private val dispatcher: CoroutineDispatcher, -) : KeyManager<PGPKey, PGPIdentifier> { - - private val keyDir = File(filesDir, KEY_DIR_NAME) - - /** @see KeyManager.addKey */ - override suspend fun addKey(key: PGPKey, replace: Boolean): Result<PGPKey, Throwable> = - withContext(dispatcher) { - runSuspendCatching { - if (!keyDirExists()) throw KeyDirectoryUnavailableException - val incomingKeyRing = tryParseKeyring(key) ?: throw InvalidKeyException - if (!isKeyUsable(key)) throw UnusableKeyException - val keyFile = File(keyDir, "${tryGetId(key)}.$KEY_EXTENSION") - if (keyFile.exists()) { - val existingKeyBytes = keyFile.readBytes() - val existingKeyRing = - tryParseKeyring(PGPKey(existingKeyBytes)) ?: throw InvalidKeyException - when { - existingKeyRing is PGPPublicKeyRing && incomingKeyRing is PGPSecretKeyRing -> { - keyFile.writeBytes(key.contents) - return@runSuspendCatching key - } - existingKeyRing is PGPPublicKeyRing && incomingKeyRing is PGPPublicKeyRing -> { - val updatedPublicKey = PGPainless.mergeCertificate(existingKeyRing, incomingKeyRing) - val keyBytes = PGPainless.asciiArmor(updatedPublicKey).encodeToByteArray() - keyFile.writeBytes(keyBytes) - return@runSuspendCatching key - } - } - // Check for replace flag first and if it is false, throw an error - if (!replace) - throw KeyAlreadyExistsException( - tryGetId(key)?.toString() ?: "Failed to retrieve key ID" - ) - if (!keyFile.delete()) throw KeyDeletionFailedException - } - - keyFile.writeBytes(key.contents) - - key - } - } - - /** @see KeyManager.removeKey */ - override suspend fun removeKey(identifier: PGPIdentifier): Result<Unit, Throwable> = - withContext(dispatcher) { - runSuspendCatching { - if (!keyDirExists()) throw KeyDirectoryUnavailableException - val key = getKeyById(identifier).unwrap() - val keyFile = File(keyDir, "${tryGetId(key)}.$KEY_EXTENSION") - if (keyFile.exists()) { - if (!keyFile.delete()) throw KeyDeletionFailedException - } - } - } - - /** @see KeyManager.getKeyById */ - override suspend fun getKeyById(id: PGPIdentifier): Result<PGPKey, Throwable> = - withContext(dispatcher) { - runSuspendCatching { - if (!keyDirExists()) throw KeyDirectoryUnavailableException - val keyFiles = keyDir.listFiles() - if (keyFiles.isNullOrEmpty()) throw NoKeysAvailableException - val keys = keyFiles.map { file -> PGPKey(file.readBytes()) } - - val matchResult = - when (id) { - is PGPIdentifier.KeyId -> { - val keyIdMatch = - keys - .map { key -> key to tryGetId(key) } - .firstOrNull { (_, keyId) -> keyId?.id == id.id } - keyIdMatch?.first - } - is PGPIdentifier.UserId -> { - val selector = SelectUserId.byEmail(id.email) - val userIdMatch = - keys - .map { key -> key to tryParseKeyring(key) } - .firstOrNull { (_, keyRing) -> selector.firstMatch(keyRing) != null } - userIdMatch?.first - } - } - - if (matchResult != null) { - return@runSuspendCatching matchResult - } - - throw KeyNotFoundException("$id") - } - } - - /** @see KeyManager.getAllKeys */ - override suspend fun getAllKeys(): Result<List<PGPKey>, Throwable> = - withContext(dispatcher) { - runSuspendCatching { - if (!keyDirExists()) throw KeyDirectoryUnavailableException - val keyFiles = keyDir.listFiles() - if (keyFiles.isNullOrEmpty()) return@runSuspendCatching emptyList() - keyFiles.map { keyFile -> PGPKey(keyFile.readBytes()) }.toList() - } - } - - /** @see KeyManager.getKeyById */ - override suspend fun getKeyId(key: PGPKey): PGPIdentifier? = tryGetId(key) - - /** Checks if [keyDir] exists and attempts to create it if not. */ - private fun keyDirExists(): Boolean { - return keyDir.exists() || keyDir.mkdirs() - } - - public 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-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt deleted file mode 100644 index a7087acf..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.crypto - -import app.passwordstore.crypto.errors.CryptoHandlerException -import app.passwordstore.crypto.errors.IncorrectPassphraseException -import app.passwordstore.crypto.errors.NoKeysProvidedException -import app.passwordstore.crypto.errors.UnknownError -import com.github.michaelbull.result.Result -import com.github.michaelbull.result.mapError -import com.github.michaelbull.result.runCatching -import java.io.InputStream -import java.io.OutputStream -import javax.inject.Inject -import org.bouncycastle.openpgp.PGPPublicKeyRing -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection -import org.bouncycastle.openpgp.PGPSecretKeyRing -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection -import org.bouncycastle.util.io.Streams -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.exception.WrongPassphraseException -import org.pgpainless.key.protection.SecretKeyRingProtector -import org.pgpainless.util.Passphrase - -public class PGPainlessCryptoHandler @Inject constructor() : - CryptoHandler<PGPKey, PGPEncryptOptions, PGPDecryptOptions> { - - /** - * Decrypts the given [ciphertextStream] using [PGPainless] and writes the decrypted output to - * [outputStream]. The provided [passphrase] is wrapped in a [SecretKeyRingProtector] and the - * [keys] argument is defensively checked to ensure it has at least one key present. - * - * @see CryptoHandler.decrypt - */ - public override fun decrypt( - keys: List<PGPKey>, - passphrase: String, - ciphertextStream: InputStream, - outputStream: OutputStream, - options: PGPDecryptOptions, - ): Result<Unit, CryptoHandlerException> = - runCatching { - if (keys.isEmpty()) { - throw NoKeysProvidedException - } - val keyringCollection = - keys - .map { key -> PGPainless.readKeyRing().secretKeyRing(key.contents) } - .run(::PGPSecretKeyRingCollection) - val protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(passphrase)) - val decryptionStream = - PGPainless.decryptAndOrVerify() - .onInputStream(ciphertextStream) - .withOptions( - ConsumerOptions() - .addDecryptionKeys(keyringCollection, protector) - .addDecryptionPassphrase(Passphrase.fromPassword(passphrase)) - ) - Streams.pipeAll(decryptionStream, outputStream) - decryptionStream.close() - keyringCollection.forEach { keyRing -> - check(decryptionStream.metadata.isEncryptedFor(keyRing)) { - "Stream should be encrypted for ${keyRing.secretKey.keyID} but wasn't" - } - } - return@runCatching - } - .mapError { error -> - when (error) { - is WrongPassphraseException -> IncorrectPassphraseException(error) - is CryptoHandlerException -> error - else -> UnknownError(error) - } - } - - /** - * Encrypts the provided [plaintextStream] and writes the encrypted output to [outputStream]. The - * [keys] argument is defensively checked to contain at least one key. - * - * @see CryptoHandler.encrypt - */ - public override fun encrypt( - keys: List<PGPKey>, - plaintextStream: InputStream, - outputStream: OutputStream, - options: PGPEncryptOptions, - ): Result<Unit, CryptoHandlerException> = - runCatching { - if (keys.isEmpty()) { - throw NoKeysProvidedException - } - val publicKeyRings = - keys.mapNotNull(KeyUtils::tryParseKeyring).mapNotNull { keyRing -> - when (keyRing) { - is PGPPublicKeyRing -> keyRing - is PGPSecretKeyRing -> PGPainless.extractCertificate(keyRing) - else -> null - } - } - require(keys.size == publicKeyRings.size) { - "Failed to parse all keys: ${keys.size} keys were provided but only ${publicKeyRings.size} were valid" - } - if (publicKeyRings.isEmpty()) { - throw NoKeysProvidedException - } - val publicKeyRingCollection = PGPPublicKeyRingCollection(publicKeyRings) - val encryptionOptions = EncryptionOptions().addRecipients(publicKeyRingCollection) - val producerOptions = - ProducerOptions.encrypt(encryptionOptions) - .setAsciiArmor(options.isOptionEnabled(PGPEncryptOptions.ASCII_ARMOR)) - val encryptionStream = - PGPainless.encryptAndOrSign().onOutputStream(outputStream).withOptions(producerOptions) - Streams.pipeAll(plaintextStream, encryptionStream) - encryptionStream.close() - val result = encryptionStream.result - publicKeyRingCollection.forEach { keyRing -> - require(result.isEncryptedFor(keyRing)) { - "Stream should be encrypted for ${keyRing.publicKey.keyID} but wasn't" - } - } - } - .mapError { error -> - when (error) { - is CryptoHandlerException -> error - else -> UnknownError(error) - } - } - - /** Runs a naive check on the extension for the given [fileName] to check if it is a PGP file. */ - public override fun canHandle(fileName: String): Boolean { - return fileName.substringAfterLast('.', "") == "gpg" - } -} |