PasswordTreeKeyDerivationMigrationService.kt

package de.pflugradts.passbird.application.process.migration.passwordtree
import de.pflugradts.kotlinextensions.MutableOption.Companion.mutableOptionOf
import de.pflugradts.passbird.application.configuration.ReadableConfiguration
import de.pflugradts.passbird.application.configuration.ReadableConfiguration.Companion.PASSWORD_TREE_FILENAME
import de.pflugradts.passbird.application.passwordtree.LegacyPasswordTreePayloadReader
import de.pflugradts.passbird.application.passwordtree.PasswordTreeEnvelope
import de.pflugradts.passbird.application.passwordtree.PasswordTreePayloadWriter
import de.pflugradts.passbird.application.passwordtree.PasswordTreeSnapshot
import de.pflugradts.passbird.application.security.AesGcmCipher
import de.pflugradts.passbird.application.security.createLegacyAesGcmCipher
import de.pflugradts.passbird.application.toDirectory
import de.pflugradts.passbird.application.toFileName
import de.pflugradts.passbird.application.util.SystemOperation
import de.pflugradts.passbird.domain.model.egg.Egg
import de.pflugradts.passbird.domain.model.egg.Egg.Companion.createEgg
import de.pflugradts.passbird.domain.model.egg.EggIdMemory
import de.pflugradts.passbird.domain.model.egg.MemoryMap
import de.pflugradts.passbird.domain.model.egg.Protein
import de.pflugradts.passbird.domain.model.egg.Protein.Companion.createProtein
import de.pflugradts.passbird.domain.model.shell.EncryptedShell
import de.pflugradts.passbird.domain.model.shell.EncryptedShell.Companion.encryptedShellOf
import de.pflugradts.passbird.domain.model.shell.Shell
import de.pflugradts.passbird.domain.model.slot.Slots.Companion.slotIterator
import de.pflugradts.passbird.domain.service.password.encryption.CryptoProvider
import de.pflugradts.passbird.domain.service.password.tree.emptyMemory
class PasswordTreeKeyDerivationMigrationService constructor(
    private val configuration: ReadableConfiguration,
    private val passwordTreeEnvelope: PasswordTreeEnvelope,
    private val legacyPasswordTreePayloadReader: LegacyPasswordTreePayloadReader,
    private val passwordTreePayloadWriter: PasswordTreePayloadWriter,
    private val systemOperation: SystemOperation,
) {
    fun migrate(keyShell: Shell) {
        try {
            val legacyProvider = createLegacyAesGcmCipher(keyShell)
            val currentProvider = AesGcmCipher(keyShell)
            val decryptedShell = systemOperation.readBytesFromFile(filePath)
                .let { legacyProvider.decrypt(encryptedShellOf(it)) }
            val snapshot = try {
                legacyPasswordTreePayloadReader.read(decryptedShell)
            } finally {
                decryptedShell.scramble()
            }
            val migratedSnapshot = snapshot.migrate(legacyProvider, currentProvider)
            val payloadShell = passwordTreePayloadWriter.write(migratedSnapshot)
            val migratedBytes = try {
                passwordTreeEnvelope.wrap(currentProvider.encrypt(payloadShell).toByteArray())
            } finally {
                payloadShell.scramble()
            }
            systemOperation.writeBytesToSensitiveFile(filePath, migratedBytes)
        } finally {
            keyShell.scramble()
        }
    }
    private fun PasswordTreeSnapshot.migrate(legacyProvider: CryptoProvider, currentProvider: CryptoProvider) = PasswordTreeSnapshot(
        eggs = eggs.map { it.migrate(legacyProvider, currentProvider) },
        memory = memory.migrate(legacyProvider, currentProvider),
        nests = nests.map {
            try {
                it.copy()
            } finally {
                it.scramble()
            }
        },
    )
    private fun Egg.migrate(legacyProvider: CryptoProvider, currentProvider: CryptoProvider): Egg {
        val migratedEggId = viewEggId().migrate(legacyProvider, currentProvider)
        val migratedPassword = viewPassword().migrate(legacyProvider, currentProvider)
        try {
            return createEgg(
                slot = associatedNest(),
                eggIdShell = migratedEggId,
                passwordShell = migratedPassword,
                proteins = proteins.map { proteinOption ->
                    if (proteinOption.isPresent) {
                        proteinOption.get().migrate(legacyProvider, currentProvider)
                    } else {
                        mutableOptionOf()
                    }
                },
            )
        } finally {
            migratedEggId.scramble()
            migratedPassword.scramble()
        }
    }
    private fun Protein.migrate(legacyProvider: CryptoProvider, currentProvider: CryptoProvider) = run {
        val migratedType = viewType().migrate(legacyProvider, currentProvider)
        val migratedStructure = viewStructure().migrate(legacyProvider, currentProvider)
        try {
            mutableOptionOf(createProtein(migratedType, migratedStructure))
        } finally {
            migratedType.scramble()
            migratedStructure.scramble()
        }
    }
    private fun MemoryMap.migrate(legacyProvider: CryptoProvider, currentProvider: CryptoProvider) = emptyMemory().apply {
        slotIterator().forEach { nestSlot ->
            this[nestSlot].set(
                EggIdMemory().apply {
                    val source = this@migrate[nestSlot].get()
                    slotIterator().forEach { slot ->
                        source[slot].map { it.migrate(legacyProvider, currentProvider) }.ifPresent { this[slot].set(it) }
                    }
                },
            )
        }
    }
    private fun EncryptedShell.migrate(legacyProvider: CryptoProvider, currentProvider: CryptoProvider): EncryptedShell {
        try {
            val shell = legacyProvider.decrypt(this)
            try {
                return currentProvider.encrypt(shell)
            } finally {
                shell.scramble()
            }
        } finally {
            scramble()
        }
    }
    private val filePath get() = systemOperation.resolvePath(
        configuration.adapter.passwordTree.location.toDirectory(),
        PASSWORD_TREE_FILENAME.toFileName(),
    )
}