feat(device-initialization, core): Plan-USB-Backup hinzugefügt, BackupService implementiert und UI-Export-Button ergänzt

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-29 15:08:11 +02:00
parent b94984043c
commit 62f9472695
8 changed files with 153 additions and 4 deletions
@@ -0,0 +1,23 @@
package at.mocode.frontend.core.network.backup
import kotlinx.serialization.Serializable
@Serializable
data class BackupPayload(
val timestamp: Long,
val deviceName: String,
val data: String,
val checksum: String
)
interface BackupService {
/**
* Schreibt Daten verschlüsselt in das Backup-Verzeichnis.
*/
fun exportDelta(data: String, targetPath: String, sharedKey: String): Result<String>
/**
* Liest Daten aus einer verschlüsselten Datei ein.
*/
fun importDelta(filePath: String, sharedKey: String): Result<String>
}
@@ -3,7 +3,7 @@ package at.mocode.frontend.core.network.discovery
import org.koin.core.module.Module
/**
* Erwartetes Koin-Modul für die Netzwerk-Discovery.
* Erwartetes Koin-Modul für die Netzwerk-Discovery und Backup.
* Plattform-spezifische Implementierungen (JVM mit JmDNS, JS/Wasm evtl. No-op)
* müssen hier injiziert werden.
*/
@@ -0,0 +1,87 @@
package at.mocode.frontend.core.network.backup
import kotlinx.serialization.json.Json
import java.io.File
import java.security.MessageDigest
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class FileBackupService(private val deviceName: String) : BackupService {
private val json = Json { prettyPrint = true }
override fun exportDelta(data: String, targetPath: String, sharedKey: String): Result<String> {
return try {
val timestamp = System.currentTimeMillis()
val checksum = calculateChecksum(data)
val payload = BackupPayload(timestamp, deviceName, data, checksum)
val jsonContent = json.encodeToString(payload)
val encryptedData = encrypt(jsonContent, sharedKey)
val dir = File(targetPath)
if (!dir.exists()) dir.mkdirs()
val fileName = "delta_${timestamp}_${deviceName}.msbackup"
val file = File(dir, fileName)
file.writeText(encryptedData)
println("[Plan-USB] Export erfolgreich: ${file.absolutePath}")
Result.success(file.absoluteName)
} catch (e: Exception) {
println("[Plan-USB] Export fehlgeschlagen: ${e.message}")
Result.failure(e)
}
}
override fun importDelta(filePath: String, sharedKey: String): Result<String> {
return try {
val file = File(filePath)
val encryptedData = file.readText()
val jsonContent = decrypt(encryptedData, sharedKey)
val payload = json.decodeFromString<BackupPayload>(jsonContent)
if (calculateChecksum(payload.data) != payload.checksum) {
throw Exception("Checksummenfehler: Daten wurden möglicherweise manipuliert.")
}
println("[Plan-USB] Import erfolgreich von ${payload.deviceName}")
Result.success(payload.data)
} catch (e: Exception) {
println("[Plan-USB] Import fehlgeschlagen: ${e.message}")
Result.failure(e)
}
}
private fun calculateChecksum(data: String): String {
val bytes = MessageDigest.getInstance("SHA-256").digest(data.toByteArray())
return bytes.joinToString("") { "%02x".format(it) }
}
private fun encrypt(data: String, key: String): String {
val secretKey = generateKey(key)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val iv = IvParameterSpec(ByteArray(16)) // Vereinfacht für PoC
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv)
val encrypted = cipher.doFinal(data.toByteArray())
return Base64.getEncoder().encodeToString(encrypted)
}
private fun decrypt(encrypted: String, key: String): String {
val secretKey = generateKey(key)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val iv = IvParameterSpec(ByteArray(16))
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv)
val decrypted = cipher.doFinal(Base64.getDecoder().decode(encrypted))
return String(decrypted)
}
private fun generateKey(key: String): SecretKeySpec {
val sha = MessageDigest.getInstance("SHA-256")
val keyBytes = sha.digest(key.toByteArray()).copyOf(16) // AES-128 für Kompatibilität
return SecretKeySpec(keyBytes, "AES")
}
}
private val File.absoluteName: String get() = this.name
@@ -1,5 +1,7 @@
package at.mocode.frontend.core.network.discovery
import at.mocode.frontend.core.network.backup.BackupService
import at.mocode.frontend.core.network.backup.FileBackupService
import org.koin.core.module.Module
import org.koin.dsl.module
@@ -8,4 +10,5 @@ import org.koin.dsl.module
*/
actual val discoveryModule: Module = module {
single<NetworkDiscoveryService> { JmDnsDiscoveryService() }
single<BackupService> { (deviceName: String) -> FileBackupService(deviceName) }
}