refactor: add missing docs and add explicit NoKeysProvidedException
This commit is contained in:
parent
6b0e4feed6
commit
a24d02052f
6 changed files with 49 additions and 14 deletions
|
@ -38,7 +38,4 @@ public interface KeyManager<Key, KeyIdentifier> {
|
|||
* as an identifier for the cryptographic identity tied to this key.
|
||||
*/
|
||||
public suspend fun getKeyId(key: Key): KeyIdentifier?
|
||||
|
||||
/** Given a [fileName], return whether this instance can handle it. */
|
||||
public fun canHandle(fileName: String): Boolean
|
||||
}
|
||||
|
|
|
@ -37,5 +37,8 @@ public sealed class CryptoHandlerException(message: String? = null, cause: Throw
|
|||
/** The passphrase provided for decryption was incorrect. */
|
||||
public class IncorrectPassphraseException(cause: Throwable) : CryptoHandlerException(null, cause)
|
||||
|
||||
/** No keys were passed to the encrypt/decrypt operation. */
|
||||
public object NoKeysProvidedException : CryptoHandlerException(null, null)
|
||||
|
||||
/** An unexpected error that cannot be mapped to a known type. */
|
||||
public class UnknownError(cause: Throwable) : CryptoHandlerException(null, cause)
|
||||
|
|
|
@ -8,7 +8,10 @@ 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)
|
||||
|
@ -32,6 +35,10 @@ public sealed class GpgIdentifier {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -45,6 +52,9 @@ public sealed class GpgIdentifier {
|
|||
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
|
||||
|
|
|
@ -28,6 +28,10 @@ public object KeyUtils {
|
|||
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())
|
||||
|
|
|
@ -36,6 +36,7 @@ constructor(
|
|||
|
||||
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 {
|
||||
|
@ -72,6 +73,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/** @see KeyManager.removeKey */
|
||||
override suspend fun removeKey(identifier: GpgIdentifier): Result<Unit, Throwable> =
|
||||
withContext(dispatcher) {
|
||||
runSuspendCatching {
|
||||
|
@ -84,6 +86,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/** @see KeyManager.getKeyById */
|
||||
override suspend fun getKeyById(id: GpgIdentifier): Result<PGPKey, Throwable> =
|
||||
withContext(dispatcher) {
|
||||
runSuspendCatching {
|
||||
|
@ -119,6 +122,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/** @see KeyManager.getAllKeys */
|
||||
override suspend fun getAllKeys(): Result<List<PGPKey>, Throwable> =
|
||||
withContext(dispatcher) {
|
||||
runSuspendCatching {
|
||||
|
@ -129,14 +133,9 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/** @see KeyManager.getKeyById */
|
||||
override suspend fun getKeyId(key: PGPKey): GpgIdentifier? = tryGetId(key)
|
||||
|
||||
// TODO: This is a temp hack for now and in future it should check that the GPGKeyManager can
|
||||
// decrypt the file.
|
||||
override fun canHandle(fileName: String): Boolean {
|
||||
return fileName.endsWith(KEY_EXTENSION)
|
||||
}
|
||||
|
||||
/** Checks if [keyDir] exists and attempts to create it if not. */
|
||||
private fun keyDirExists(): Boolean {
|
||||
return keyDir.exists() || keyDir.mkdirs()
|
||||
|
|
|
@ -7,6 +7,7 @@ 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
|
||||
|
@ -29,6 +30,13 @@ 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,
|
||||
|
@ -37,7 +45,9 @@ public class PGPainlessCryptoHandler @Inject constructor() :
|
|||
options: PGPDecryptOptions,
|
||||
): Result<Unit, CryptoHandlerException> =
|
||||
runCatching {
|
||||
require(keys.isNotEmpty())
|
||||
if (keys.isEmpty()) {
|
||||
throw NoKeysProvidedException
|
||||
}
|
||||
val keyringCollection =
|
||||
keys
|
||||
.map { key -> PGPainless.readKeyRing().secretKeyRing(key.contents) }
|
||||
|
@ -56,10 +66,17 @@ public class PGPainlessCryptoHandler @Inject constructor() :
|
|||
.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,
|
||||
|
@ -67,7 +84,9 @@ public class PGPainlessCryptoHandler @Inject constructor() :
|
|||
options: PGPEncryptOptions,
|
||||
): Result<Unit, CryptoHandlerException> =
|
||||
runCatching {
|
||||
require(keys.isNotEmpty())
|
||||
if (keys.isEmpty()) {
|
||||
throw NoKeysProvidedException
|
||||
}
|
||||
val publicKeyRings =
|
||||
keys.mapNotNull(KeyUtils::tryParseKeyring).mapNotNull { keyRing ->
|
||||
when (keyRing) {
|
||||
|
@ -77,9 +96,11 @@ public class PGPainlessCryptoHandler @Inject constructor() :
|
|||
}
|
||||
}
|
||||
require(keys.size == publicKeyRings.size) {
|
||||
"Failed to parse all keys: keys=${keys.size},parsed=${publicKeyRings.size}"
|
||||
"Failed to parse all keys: ${keys.size} keys were provided but only ${publicKeyRings.size} were valid"
|
||||
}
|
||||
if (publicKeyRings.isEmpty()) {
|
||||
throw NoKeysProvidedException
|
||||
}
|
||||
require(publicKeyRings.isNotEmpty()) { "No public keys to encrypt message to" }
|
||||
val publicKeyRingCollection = PGPPublicKeyRingCollection(publicKeyRings)
|
||||
val encryptionOptions = EncryptionOptions().addRecipients(publicKeyRingCollection)
|
||||
val producerOptions =
|
||||
|
@ -103,7 +124,8 @@ public class PGPainlessCryptoHandler @Inject constructor() :
|
|||
}
|
||||
}
|
||||
|
||||
/** 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.split('.').lastOrNull() == "gpg"
|
||||
return fileName.substringAfterLast('.', "") == "gpg"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue