ChangeMasterPasswordCommandHandler.kt

package de.pflugradts.passbird.application.commandhandling.handler
import de.pflugradts.kotlinextensions.tryCatching
import de.pflugradts.passbird.application.KeyStoreAdapterPort
import de.pflugradts.passbird.application.SecureInputUnavailableException
import de.pflugradts.passbird.application.UserInterfaceAdapterPort
import de.pflugradts.passbird.application.commandhandling.CommandExecutionTracker
import de.pflugradts.passbird.application.commandhandling.command.ChangeMasterPasswordCommand
import de.pflugradts.passbird.application.failure.CommandFailure
import de.pflugradts.passbird.application.failure.reportFailure
import de.pflugradts.passbird.application.security.KeyStoreAuthenticationService
import de.pflugradts.passbird.domain.model.shell.PlainShell
import de.pflugradts.passbird.domain.model.shell.Shell.Companion.shellOf
import de.pflugradts.passbird.domain.model.transfer.Input
import de.pflugradts.passbird.domain.model.transfer.Output.Companion.outputOf
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.EVENT_HANDLED
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.OPERATION_ABORTED
class ChangeMasterPasswordCommandHandler constructor(
    private val keyStoreAdapterPort: KeyStoreAdapterPort,
    private val keyStoreAuthenticationService: KeyStoreAuthenticationService,
    private val userInterfaceAdapterPort: UserInterfaceAdapterPort,
    private val commandExecutionTracker: CommandExecutionTracker,
) : TypedCommandHandler<ChangeMasterPasswordCommand>(ChangeMasterPasswordCommand::class.java) {
    override fun handleCommand(@Suppress("UNUSED_PARAMETER") command: ChangeMasterPasswordCommand) {
        userInterfaceAdapterPort.sendLineBreak()
        userInterfaceAdapterPort.send(outputOf(shellOf(KEYSTORE_PREAMBLE)))
        userInterfaceAdapterPort.sendLineBreak()
        val authenticationResult = keyStoreAuthenticationService.authenticate(maxAttempts = 1, prompt = "Enter current key: ")
        val key = authenticationResult.getOrNull()
        if (key == null) {
            abort(
                if (authenticationResult.exceptionOrNull() is SecureInputUnavailableException) {
                    "Operation aborted."
                } else {
                    "Current key is incorrect - Operation aborted."
                },
            )
            return
        }
        try {
            val newPassword = receiveNewPassword()
            if (newPassword == null) {
                return
            }
            val storeResult = tryCatching {
                keyStoreAdapterPort.storeExistingKey(key, newPassword, keyStoreAuthenticationService.keyStorePath())
            }
            if (storeResult.failure) {
                commandExecutionTracker.markFailure()
                reportFailure(CommandFailure(storeResult.exceptionOrNull()!!))
                abort("Operation aborted.")
                return
            }
        } finally {
            key.scramble()
        }
        userInterfaceAdapterPort.send(outputOf(shellOf("Keystore successfully updated."), EVENT_HANDLED))
        userInterfaceAdapterPort.sendLineBreak()
    }
    private fun receiveNewPassword(): PlainShell? {
        var input: Input? = null
        return try {
            input = userInterfaceAdapterPort.receiveSecurely(outputOf(shellOf("Enter new key: ")))
            if (input.isEmpty) {
                input.invalidate()
                abort("Empty input - Operation aborted.")
                return null
            }
            val repeatedInput = userInterfaceAdapterPort.receiveSecurely(outputOf(shellOf("Enter new key again: ")))
            if (repeatedInput.isEmpty) {
                input.invalidate()
                repeatedInput.invalidate()
                abort("Empty input - Operation aborted.")
                return null
            }
            if (input != repeatedInput) {
                input.invalidate()
                repeatedInput.invalidate()
                abort("Your inputs do not match - Operation aborted.")
                return null
            }
            val password = input.toPlainShell()
            repeatedInput.invalidate()
            userInterfaceAdapterPort.sendLineBreak()
            password
        } catch (_: SecureInputUnavailableException) {
            input?.invalidate()
            abort("Operation aborted.")
            null
        }
    }
    private fun abort(message: String) {
        commandExecutionTracker.markAborted()
        userInterfaceAdapterPort.send(outputOf(shellOf(message), OPERATION_ABORTED))
        userInterfaceAdapterPort.sendLineBreak()
    }
    private companion object {
        const val KEYSTORE_PREAMBLE =
            "Your Passbird Keystore will be secured by a master password. This master password gives access to all " +
                "passwords stored in Passbird. If you lose this password, you will not be able to access any passwords " +
                "stored in Passbird. Choose your master password wisely. You have to input your master password twice. " +
                "Your input will be hidden unless secure input is disabled in your configuration."
    }
}