From 0992304e59e37d7e5cb34621c9e41dc9159378f0 Mon Sep 17 00:00:00 2001 From: oxidiert Date: Tue, 2 Sep 2025 13:01:41 +0200 Subject: [PATCH] change aes, now more secure, beacuse no hardcode anymore --- .../main/java/com/dano/test1/AES256Helper.kt | 101 ++++++----- .../java/com/dano/test1/DatabaseDownloader.kt | 49 +----- .../java/com/dano/test1/DatabaseUploader.kt | 158 +++++------------- .../main/java/com/dano/test1/LoginManager.kt | 30 +--- 4 files changed, 117 insertions(+), 221 deletions(-) diff --git a/app/src/main/java/com/dano/test1/AES256Helper.kt b/app/src/main/java/com/dano/test1/AES256Helper.kt index 1e10716..d2f7f5f 100644 --- a/app/src/main/java/com/dano/test1/AES256Helper.kt +++ b/app/src/main/java/com/dano/test1/AES256Helper.kt @@ -1,56 +1,73 @@ +// app/src/main/java/com/dano/test1/AES256Helper.kt +package com.dano.test1 + import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream +import java.security.SecureRandom import javax.crypto.Cipher -import javax.crypto.CipherInputStream -import javax.crypto.CipherOutputStream +import javax.crypto.Mac import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec -import kotlin.random.Random +import kotlin.math.min object AES256Helper { - private const val TRANSFORMATION = "AES/CBC/PKCS5Padding" - private const val ALGORITHM = "AES" - private const val IV_SIZE = 16 + // HKDF-SHA256: IKM = tokenHex->bytes, salt="", info="qdb-aes", len=32 + private fun hkdfFromToken(tokenHex: String, info: String = "qdb-aes", len: Int = 32): ByteArray { + val ikm = hexToBytes(tokenHex) + val mac = Mac.getInstance("HmacSHA256") + val zeroSalt = ByteArray(32) { 0 } + mac.init(SecretKeySpec(zeroSalt, "HmacSHA256")) + val prk = mac.doFinal(ikm) - // Beispiel-Key: 32 Bytes = 256 bit. Ersetze das durch deinen eigenen sicheren Schlüssel! - private val keyBytes = "12345678901234567890123456789012".toByteArray(Charsets.UTF_8) - private val secretKey = SecretKeySpec(keyBytes, ALGORITHM) - - // Verschlüsseln: InputFile -> OutputFile (mit zufälligem IV vorne in der Datei) - fun encryptFile(inputFile: File, outputFile: File) { - val iv = ByteArray(IV_SIZE) - Random.nextBytes(iv) - val ivSpec = IvParameterSpec(iv) - val cipher = Cipher.getInstance(TRANSFORMATION) - cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec) - - FileOutputStream(outputFile).use { fileOut -> - // IV vorne reinschreiben - fileOut.write(iv) - CipherOutputStream(fileOut, cipher).use { cipherOut -> - FileInputStream(inputFile).use { fileIn -> - fileIn.copyTo(cipherOut) - } - } + var previous = ByteArray(0) + val okm = ByteArray(len) + var generated = 0 + var counter = 1 + while (generated < len) { + mac.init(SecretKeySpec(prk, "HmacSHA256")) + mac.update(previous) + mac.update(info.toByteArray(Charsets.UTF_8)) + mac.update(counter.toByte()) + val t = mac.doFinal() + val toCopy = min(len - generated, t.size) + System.arraycopy(t, 0, okm, generated, toCopy) + previous = t + generated += toCopy + counter++ } + return okm } - // Entschlüsseln: InputFile (IV+Ciphertext) -> OutputFile (Klartext) - fun decryptFile(inputFile: File, outputFile: File) { - FileInputStream(inputFile).use { fileIn -> - val iv = ByteArray(IV_SIZE) - if (fileIn.read(iv) != IV_SIZE) throw IllegalArgumentException("Ungültige Datei oder IV fehlt") - val ivSpec = IvParameterSpec(iv) - val cipher = Cipher.getInstance(TRANSFORMATION) - cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec) - - CipherInputStream(fileIn, cipher).use { cipherIn -> - FileOutputStream(outputFile).use { fileOut -> - cipherIn.copyTo(fileOut) - } - } + private fun hexToBytes(hex: String): ByteArray { + val clean = hex.trim() + val len = clean.length + val out = ByteArray(len / 2) + var i = 0 + while (i < len) { + out[i / 2] = ((Character.digit(clean[i], 16) shl 4) + Character.digit(clean[i + 1], 16)).toByte() + i += 2 } + return out + } + + fun encryptFileWithToken(inFile: File, outFile: File, token: String) { + val key = hkdfFromToken(token) + val iv = ByteArray(16).also { SecureRandom().nextBytes(it) } + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + val plain = inFile.readBytes() + val enc = cipher.doFinal(plain) + outFile.writeBytes(iv + enc) + } + + fun decryptFileWithToken(inFile: File, token: String): ByteArray { + val key = hkdfFromToken(token) + val data = inFile.readBytes() + require(data.size >= 16) { "cipher too short" } + val iv = data.copyOfRange(0, 16) + val ct = data.copyOfRange(16, data.size) + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + return cipher.doFinal(ct) } } diff --git a/app/src/main/java/com/dano/test1/DatabaseDownloader.kt b/app/src/main/java/com/dano/test1/DatabaseDownloader.kt index ad5b4b7..5757df9 100644 --- a/app/src/main/java/com/dano/test1/DatabaseDownloader.kt +++ b/app/src/main/java/com/dano/test1/DatabaseDownloader.kt @@ -1,3 +1,4 @@ +// app/src/main/java/com/dano/test1/DatabaseDownloader.kt package com.dano.test1 import android.content.Context @@ -9,28 +10,17 @@ import okhttp3.OkHttpClient import okhttp3.Request import java.io.File import java.io.FileOutputStream -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec object DatabaseDownloader { private const val DB_NAME = "questionnaire_database" private const val SERVER_DOWNLOAD_URL = "http://49.13.157.44/downloadFull.php" - // AES-256 Key (muss exakt 32 Bytes lang sein) - private const val AES_KEY = "12345678901234567890123456789012" - private val client = OkHttpClient() - /** - * Startet den Download und Austausch der DB, benötigt gültiges Token - */ fun downloadAndReplaceDatabase(context: Context, token: String) { CoroutineScope(Dispatchers.IO).launch { try { - Log.d("DOWNLOAD", "Download gestartet: $SERVER_DOWNLOAD_URL") - val request = Request.Builder() .url(SERVER_DOWNLOAD_URL) .header("Authorization", "Bearer $token") @@ -38,47 +28,24 @@ object DatabaseDownloader { val response = client.newCall(request).execute() if (!response.isSuccessful) { - Log.e("DOWNLOAD", "Fehler beim Download: ${response.code}") + Log.e("DOWNLOAD", "HTTP ${response.code}") return@launch } - // Zwischenspeichern der verschlüsselten Datei - val downloadedFile = File(context.cacheDir, "downloaded_database.enc") + val encFile = File(context.cacheDir, "downloaded_database.enc") response.body?.byteStream()?.use { input -> - FileOutputStream(downloadedFile).use { output -> - input.copyTo(output) - } + FileOutputStream(encFile).use { output -> input.copyTo(output) } } - Log.d("DOWNLOAD", "Datei gespeichert: ${downloadedFile.absolutePath}") - // Entschlüsselung - val decryptedBytes = decryptFile(downloadedFile) + val decryptedBytes = AES256Helper.decryptFileWithToken(encFile, token) val dbFile = context.getDatabasePath(DB_NAME) if (dbFile.exists()) dbFile.delete() - FileOutputStream(dbFile).use { fos -> - fos.write(decryptedBytes) - } - - Log.d("DOWNLOAD", "Neue DB erfolgreich entschlüsselt und eingesetzt") + FileOutputStream(dbFile).use { it.write(decryptedBytes) } + Log.d("DOWNLOAD", "DB erfolgreich ersetzt") } catch (e: Exception) { - Log.e("DOWNLOAD", "Fehler beim Download oder Ersetzen der DB", e) + Log.e("DOWNLOAD", "Fehler", e) } } } - - private fun decryptFile(file: File): ByteArray { - val fileBytes = file.readBytes() - if (fileBytes.size < 16) throw IllegalArgumentException("Datei zu kurz, kein IV vorhanden") - - val iv = fileBytes.copyOfRange(0, 16) - val cipherBytes = fileBytes.copyOfRange(16, fileBytes.size) - - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - val keySpec = SecretKeySpec(AES_KEY.toByteArray(Charsets.UTF_8), "AES") - val ivSpec = IvParameterSpec(iv) - cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) - - return cipher.doFinal(cipherBytes) - } } diff --git a/app/src/main/java/com/dano/test1/DatabaseUploader.kt b/app/src/main/java/com/dano/test1/DatabaseUploader.kt index 370fafd..1af58fa 100644 --- a/app/src/main/java/com/dano/test1/DatabaseUploader.kt +++ b/app/src/main/java/com/dano/test1/DatabaseUploader.kt @@ -1,3 +1,4 @@ +// app/src/main/java/com/dano/test1/DatabaseUploader.kt package com.dano.test1 import android.content.Context @@ -25,181 +26,111 @@ object DatabaseUploader { private val client = OkHttpClient() - /** - * Startet den Upload mit Login über LoginManager. - * @param context Android Context - * @param password Vom User eingegebenes Passwort - */ fun uploadDatabaseWithLogin(context: Context, password: String) { LoginManager.loginUser(context, password, onSuccess = { token -> - Log.d("UPLOAD", "Login erfolgreich, Token erhalten") + Log.d("UPLOAD", "Login OK") uploadDatabase(context, token) }, - onError = { errorMsg -> - Log.e("UPLOAD", "Login fehlgeschlagen: $errorMsg") - } + onError = { msg -> Log.e("UPLOAD", "Login fehlgeschlagen: $msg") } ) } - /** - * Interner Upload, benötigt gültiges Token - */ private fun uploadDatabase(context: Context, token: String) { CoroutineScope(Dispatchers.IO).launch { try { val dbFile = context.getDatabasePath(DB_NAME) if (!dbFile.exists()) { - Log.e("UPLOAD", "Datenbankdatei existiert nicht: ${dbFile.absolutePath}") + Log.e("UPLOAD", "DB fehlt: ${dbFile.absolutePath}") return@launch } - // WAL-Checkpoint try { - val db = SQLiteDatabase.openDatabase( - dbFile.absolutePath, - null, - SQLiteDatabase.OPEN_READWRITE - ) - db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { cursor -> - if (cursor.moveToFirst()) { - try { Log.d("UPLOAD", "WAL-Checkpoint result: ${cursor.getInt(0)}") } catch (_: Exception) {} - } - } + val db = SQLiteDatabase.openDatabase(dbFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE) + db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { /* ignore */ } db.close() - Log.d("UPLOAD", "WAL-Checkpoint erfolgreich.") - } catch (e: Exception) { - Log.e("UPLOAD", "Fehler beim WAL-Checkpoint", e) - } - - val exists = checkDatabaseExists() - if (exists) { - Log.d("UPLOAD", "Server-Datenbank vorhanden → Delta-Upload") - } else { - Log.d("UPLOAD", "Keine Server-Datenbank → Delta-Upload") - } + } catch (_: Exception) { } + checkDatabaseExists() // nur Logging uploadPseudoDelta(context, dbFile, token) } catch (e: Exception) { - Log.e("UPLOAD", "Fehler beim Hochladen der DB", e) + Log.e("UPLOAD", "Fehler", e) } } } private fun checkDatabaseExists(): Boolean { return try { - val request = Request.Builder() - .url(SERVER_CHECK_URL) - .get() - .build() - - client.newCall(request).execute().use { response -> - if (!response.isSuccessful) { - Log.e("UPLOAD", "checkDatabaseExists HTTP error: ${response.code}") - return false - } - val body = response.body?.string() ?: return false - try { - val j = JSONObject(body) - j.optBoolean("exists", false) - } catch (e: Exception) { - body.contains("exists", ignoreCase = true) - } + val req = Request.Builder().url(SERVER_CHECK_URL).get().build() + client.newCall(req).execute().use { resp -> + if (!resp.isSuccessful) return false + val body = resp.body?.string() ?: return false + try { JSONObject(body).optBoolean("exists", false) } catch (_: Exception) { false } } - } catch (e: Exception) { - Log.e("UPLOAD", "Fehler bei Server-Prüfung", e) - false - } + } catch (e: Exception) { false } } private fun uploadPseudoDelta(context: Context, file: File, token: String) { try { val db = SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY) - val data = JSONObject().apply { put("clients", queryToJsonArray(db, "SELECT clientCode FROM clients")) put("questionnaires", queryToJsonArray(db, "SELECT id FROM questionnaires")) put("questions", queryToJsonArray(db, "SELECT questionId, questionnaireId, question FROM questions")) put("answers", queryToJsonArray(db, "SELECT clientCode, questionId, answerValue FROM answers")) - put( - "completed_questionnaires", - queryToJsonArray( - db, - "SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires" - ) - ) + put("completed_questionnaires", + queryToJsonArray(db, "SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires")) } - db.close() - val tmpJson = File(context.cacheDir, "payload.json") - tmpJson.writeText(data.toString()) - + val tmpJson = File(context.cacheDir, "payload.json").apply { writeText(data.toString()) } val tmpEnc = File(context.cacheDir, "payload.enc") try { - AES256Helper.encryptFile(tmpJson, tmpEnc) + AES256Helper.encryptFileWithToken(tmpJson, tmpEnc, token) } catch (e: Exception) { - Log.e("UPLOAD", "Fehler bei der Verschlüsselung der JSON-Datei", e) - tmpJson.delete() - return + Log.e("UPLOAD", "Verschlüsselung fehlgeschlagen", e) + tmpJson.delete(); return } - val requestBody = MultipartBody.Builder() + val body = MultipartBody.Builder() .setType(MultipartBody.FORM) - .addFormDataPart("token", token) // Token vom Login - .addFormDataPart( - "file", - "payload.enc", - tmpEnc.asRequestBody("application/octet-stream".toMediaType()) - ) - .build() - - val request = Request.Builder() - .url(SERVER_DELTA_URL) - .post(requestBody) + .addFormDataPart("token", token) + .addFormDataPart("file", "payload.enc", tmpEnc.asRequestBody("application/octet-stream".toMediaType())) .build() + val request = Request.Builder().url(SERVER_DELTA_URL).post(body).build() client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { - Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${e.message}") - tmpJson.delete() - tmpEnc.delete() + Log.e("UPLOAD", "Fehlgeschlagen: ${e.message}") + tmpJson.delete(); tmpEnc.delete() } - override fun onResponse(call: Call, response: Response) { - val body = try { response.body?.string() ?: "Keine Response" } catch (e: Exception) { - "Fehler beim Lesen der Response: ${e.message}" - } + val respBody = try { response.body?.string() ?: "" } catch (_: Exception) { "" } if (response.isSuccessful) { - Log.d("UPLOAD", "Delta-Upload erfolgreich: $body") - if (file.delete()) Log.d("UPLOAD", "Lokale DB gelöscht.") else Log.e("UPLOAD", "Löschen der lokalen DB fehlgeschlagen.") - val journalFile = File(file.parent, file.name + "-journal") - if (journalFile.exists() && journalFile.delete()) Log.d("UPLOAD", "Journal-Datei gelöscht.") - tmpJson.delete() - tmpEnc.delete() - exitProcess(0) + Log.d("UPLOAD", "OK: $respBody") + if (!file.delete()) Log.w("UPLOAD", "Lokale DB nicht gelöscht.") + File(file.parent, file.name + "-journal").delete() } else { - Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${response.code} $body") - tmpJson.delete() - tmpEnc.delete() + Log.e("UPLOAD", "HTTP ${response.code}: $respBody") } + tmpJson.delete(); tmpEnc.delete() + try { exitProcess(0) } catch (_: Exception) {} } }) - } catch (e: Exception) { - Log.e("UPLOAD", "Fehler beim Delta-Upload", e) + Log.e("UPLOAD", "Exception", e) } } private fun queryToJsonArray(db: SQLiteDatabase, query: String): JSONArray { - val cursor = db.rawQuery(query, null) - val jsonArray = JSONArray() - cursor.use { - val columnNames = it.columnNames + val c = db.rawQuery(query, null) + val arr = JSONArray() + c.use { + val cols = it.columnNames while (it.moveToNext()) { val obj = JSONObject() - for (col in columnNames) { + for (col in cols) { val idx = it.getColumnIndex(col) if (idx >= 0) { when (it.getType(idx)) { @@ -207,17 +138,14 @@ object DatabaseUploader { Cursor.FIELD_TYPE_FLOAT -> obj.put(col, it.getDouble(idx)) Cursor.FIELD_TYPE_STRING -> obj.put(col, it.getString(idx)) Cursor.FIELD_TYPE_NULL -> obj.put(col, JSONObject.NULL) - Cursor.FIELD_TYPE_BLOB -> { - val blob = it.getBlob(idx) - obj.put(col, Base64.encodeToString(blob, Base64.NO_WRAP)) - } + Cursor.FIELD_TYPE_BLOB -> obj.put(col, Base64.encodeToString(it.getBlob(idx), Base64.NO_WRAP)) else -> obj.put(col, it.getString(idx)) } } } - jsonArray.put(obj) + arr.put(obj) } } - return jsonArray + return arr } } diff --git a/app/src/main/java/com/dano/test1/LoginManager.kt b/app/src/main/java/com/dano/test1/LoginManager.kt index 34f8a0f..42186a5 100644 --- a/app/src/main/java/com/dano/test1/LoginManager.kt +++ b/app/src/main/java/com/dano/test1/LoginManager.kt @@ -1,3 +1,4 @@ +// app/src/main/java/com/dano/test1/LoginManager.kt package com.dano.test1 import android.content.Context @@ -13,18 +14,9 @@ import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONObject object LoginManager { - private const val SERVER_LOGIN_URL = "http://49.13.157.44/login.php" private val client = OkHttpClient() - /** - * Startet den Login-Prozess. - * - * @param context Android Context - * @param password Vom User eingegebenes Passwort - * @param onSuccess Callback mit dem Token wenn Login erfolgreich - * @param onError Callback mit Fehlermeldung - */ fun loginUser( context: Context, password: String, @@ -46,26 +38,18 @@ object LoginManager { if (response.isSuccessful && responseText != null) { val json = JSONObject(responseText) - if (json.getBoolean("success")) { + if (json.optBoolean("success")) { val token = json.getString("token") - withContext(Dispatchers.Main) { - onSuccess(token) - } + withContext(Dispatchers.Main) { onSuccess(token) } } else { - withContext(Dispatchers.Main) { - onError("Login fehlgeschlagen") - } + withContext(Dispatchers.Main) { onError("Login fehlgeschlagen") } } } else { - withContext(Dispatchers.Main) { - onError("Fehler beim Login (${response.code})") - } + withContext(Dispatchers.Main) { onError("Fehler beim Login (${response.code})") } } } catch (e: Exception) { - Log.e("LOGIN", "Exception beim Login", e) - withContext(Dispatchers.Main) { - onError("Exception: ${e.message}") - } + Log.e("LOGIN", "Exception", e) + withContext(Dispatchers.Main) { onError("Exception: ${e.message}") } } } }