change aes, now more secure, beacuse no hardcode anymore

This commit is contained in:
oxidiert
2025-09-02 13:01:41 +02:00
parent 073f33a9bb
commit 0992304e59
4 changed files with 117 additions and 221 deletions

View File

@ -1,56 +1,73 @@
// app/src/main/java/com/dano/test1/AES256Helper.kt
package com.dano.test1
import java.io.File import java.io.File
import java.io.FileInputStream import java.security.SecureRandom
import java.io.FileOutputStream
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherInputStream import javax.crypto.Mac
import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import kotlin.random.Random import kotlin.math.min
object AES256Helper { object AES256Helper {
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding" // HKDF-SHA256: IKM = tokenHex->bytes, salt="", info="qdb-aes", len=32
private const val ALGORITHM = "AES" private fun hkdfFromToken(tokenHex: String, info: String = "qdb-aes", len: Int = 32): ByteArray {
private const val IV_SIZE = 16 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! var previous = ByteArray(0)
private val keyBytes = "12345678901234567890123456789012".toByteArray(Charsets.UTF_8) val okm = ByteArray(len)
private val secretKey = SecretKeySpec(keyBytes, ALGORITHM) var generated = 0
var counter = 1
// Verschlüsseln: InputFile -> OutputFile (mit zufälligem IV vorne in der Datei) while (generated < len) {
fun encryptFile(inputFile: File, outputFile: File) { mac.init(SecretKeySpec(prk, "HmacSHA256"))
val iv = ByteArray(IV_SIZE) mac.update(previous)
Random.nextBytes(iv) mac.update(info.toByteArray(Charsets.UTF_8))
val ivSpec = IvParameterSpec(iv) mac.update(counter.toByte())
val cipher = Cipher.getInstance(TRANSFORMATION) val t = mac.doFinal()
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec) val toCopy = min(len - generated, t.size)
System.arraycopy(t, 0, okm, generated, toCopy)
FileOutputStream(outputFile).use { fileOut -> previous = t
// IV vorne reinschreiben generated += toCopy
fileOut.write(iv) counter++
CipherOutputStream(fileOut, cipher).use { cipherOut ->
FileInputStream(inputFile).use { fileIn ->
fileIn.copyTo(cipherOut)
}
}
} }
return okm
} }
// Entschlüsseln: InputFile (IV+Ciphertext) -> OutputFile (Klartext) private fun hexToBytes(hex: String): ByteArray {
fun decryptFile(inputFile: File, outputFile: File) { val clean = hex.trim()
FileInputStream(inputFile).use { fileIn -> val len = clean.length
val iv = ByteArray(IV_SIZE) val out = ByteArray(len / 2)
if (fileIn.read(iv) != IV_SIZE) throw IllegalArgumentException("Ungültige Datei oder IV fehlt") var i = 0
val ivSpec = IvParameterSpec(iv) while (i < len) {
val cipher = Cipher.getInstance(TRANSFORMATION) out[i / 2] = ((Character.digit(clean[i], 16) shl 4) + Character.digit(clean[i + 1], 16)).toByte()
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec) i += 2
}
return out
}
CipherInputStream(fileIn, cipher).use { cipherIn -> fun encryptFileWithToken(inFile: File, outFile: File, token: String) {
FileOutputStream(outputFile).use { fileOut -> val key = hkdfFromToken(token)
cipherIn.copyTo(fileOut) 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)
} }
} }

View File

@ -1,3 +1,4 @@
// app/src/main/java/com/dano/test1/DatabaseDownloader.kt
package com.dano.test1 package com.dano.test1
import android.content.Context import android.content.Context
@ -9,28 +10,17 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object DatabaseDownloader { object DatabaseDownloader {
private const val DB_NAME = "questionnaire_database" private const val DB_NAME = "questionnaire_database"
private const val SERVER_DOWNLOAD_URL = "http://49.13.157.44/downloadFull.php" 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() private val client = OkHttpClient()
/**
* Startet den Download und Austausch der DB, benötigt gültiges Token
*/
fun downloadAndReplaceDatabase(context: Context, token: String) { fun downloadAndReplaceDatabase(context: Context, token: String) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
Log.d("DOWNLOAD", "Download gestartet: $SERVER_DOWNLOAD_URL")
val request = Request.Builder() val request = Request.Builder()
.url(SERVER_DOWNLOAD_URL) .url(SERVER_DOWNLOAD_URL)
.header("Authorization", "Bearer $token") .header("Authorization", "Bearer $token")
@ -38,47 +28,24 @@ object DatabaseDownloader {
val response = client.newCall(request).execute() val response = client.newCall(request).execute()
if (!response.isSuccessful) { if (!response.isSuccessful) {
Log.e("DOWNLOAD", "Fehler beim Download: ${response.code}") Log.e("DOWNLOAD", "HTTP ${response.code}")
return@launch return@launch
} }
// Zwischenspeichern der verschlüsselten Datei val encFile = File(context.cacheDir, "downloaded_database.enc")
val downloadedFile = File(context.cacheDir, "downloaded_database.enc")
response.body?.byteStream()?.use { input -> response.body?.byteStream()?.use { input ->
FileOutputStream(downloadedFile).use { output -> FileOutputStream(encFile).use { output -> input.copyTo(output) }
input.copyTo(output)
} }
}
Log.d("DOWNLOAD", "Datei gespeichert: ${downloadedFile.absolutePath}")
// Entschlüsselung val decryptedBytes = AES256Helper.decryptFileWithToken(encFile, token)
val decryptedBytes = decryptFile(downloadedFile)
val dbFile = context.getDatabasePath(DB_NAME) val dbFile = context.getDatabasePath(DB_NAME)
if (dbFile.exists()) dbFile.delete() if (dbFile.exists()) dbFile.delete()
FileOutputStream(dbFile).use { fos -> FileOutputStream(dbFile).use { it.write(decryptedBytes) }
fos.write(decryptedBytes)
}
Log.d("DOWNLOAD", "Neue DB erfolgreich entschlüsselt und eingesetzt")
Log.d("DOWNLOAD", "DB erfolgreich ersetzt")
} catch (e: Exception) { } 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)
}
} }

View File

@ -1,3 +1,4 @@
// app/src/main/java/com/dano/test1/DatabaseUploader.kt
package com.dano.test1 package com.dano.test1
import android.content.Context import android.content.Context
@ -25,181 +26,111 @@ object DatabaseUploader {
private val client = OkHttpClient() 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) { fun uploadDatabaseWithLogin(context: Context, password: String) {
LoginManager.loginUser(context, password, LoginManager.loginUser(context, password,
onSuccess = { token -> onSuccess = { token ->
Log.d("UPLOAD", "Login erfolgreich, Token erhalten") Log.d("UPLOAD", "Login OK")
uploadDatabase(context, token) uploadDatabase(context, token)
}, },
onError = { errorMsg -> onError = { msg -> Log.e("UPLOAD", "Login fehlgeschlagen: $msg") }
Log.e("UPLOAD", "Login fehlgeschlagen: $errorMsg")
}
) )
} }
/**
* Interner Upload, benötigt gültiges Token
*/
private fun uploadDatabase(context: Context, token: String) { private fun uploadDatabase(context: Context, token: String) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val dbFile = context.getDatabasePath(DB_NAME) val dbFile = context.getDatabasePath(DB_NAME)
if (!dbFile.exists()) { if (!dbFile.exists()) {
Log.e("UPLOAD", "Datenbankdatei existiert nicht: ${dbFile.absolutePath}") Log.e("UPLOAD", "DB fehlt: ${dbFile.absolutePath}")
return@launch return@launch
} }
// WAL-Checkpoint
try { try {
val db = SQLiteDatabase.openDatabase( val db = SQLiteDatabase.openDatabase(dbFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE)
dbFile.absolutePath, db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { /* ignore */ }
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) {}
}
}
db.close() db.close()
Log.d("UPLOAD", "WAL-Checkpoint erfolgreich.") } catch (_: Exception) { }
} 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")
}
checkDatabaseExists() // nur Logging
uploadPseudoDelta(context, dbFile, token) uploadPseudoDelta(context, dbFile, token)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("UPLOAD", "Fehler beim Hochladen der DB", e) Log.e("UPLOAD", "Fehler", e)
} }
} }
} }
private fun checkDatabaseExists(): Boolean { private fun checkDatabaseExists(): Boolean {
return try { return try {
val request = Request.Builder() val req = Request.Builder().url(SERVER_CHECK_URL).get().build()
.url(SERVER_CHECK_URL) client.newCall(req).execute().use { resp ->
.get() if (!resp.isSuccessful) return false
.build() val body = resp.body?.string() ?: return false
try { JSONObject(body).optBoolean("exists", false) } catch (_: Exception) { false }
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)
}
}
} 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) { private fun uploadPseudoDelta(context: Context, file: File, token: String) {
try { try {
val db = SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY) val db = SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY)
val data = JSONObject().apply { val data = JSONObject().apply {
put("clients", queryToJsonArray(db, "SELECT clientCode FROM clients")) put("clients", queryToJsonArray(db, "SELECT clientCode FROM clients"))
put("questionnaires", queryToJsonArray(db, "SELECT id FROM questionnaires")) put("questionnaires", queryToJsonArray(db, "SELECT id FROM questionnaires"))
put("questions", queryToJsonArray(db, "SELECT questionId, questionnaireId, question FROM questions")) put("questions", queryToJsonArray(db, "SELECT questionId, questionnaireId, question FROM questions"))
put("answers", queryToJsonArray(db, "SELECT clientCode, questionId, answerValue FROM answers")) put("answers", queryToJsonArray(db, "SELECT clientCode, questionId, answerValue FROM answers"))
put( put("completed_questionnaires",
"completed_questionnaires", queryToJsonArray(db, "SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires"))
queryToJsonArray(
db,
"SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires"
)
)
} }
db.close() db.close()
val tmpJson = File(context.cacheDir, "payload.json") val tmpJson = File(context.cacheDir, "payload.json").apply { writeText(data.toString()) }
tmpJson.writeText(data.toString())
val tmpEnc = File(context.cacheDir, "payload.enc") val tmpEnc = File(context.cacheDir, "payload.enc")
try { try {
AES256Helper.encryptFile(tmpJson, tmpEnc) AES256Helper.encryptFileWithToken(tmpJson, tmpEnc, token)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("UPLOAD", "Fehler bei der Verschlüsselung der JSON-Datei", e) Log.e("UPLOAD", "Verschlüsselung fehlgeschlagen", e)
tmpJson.delete() tmpJson.delete(); return
return
} }
val requestBody = MultipartBody.Builder() val body = MultipartBody.Builder()
.setType(MultipartBody.FORM) .setType(MultipartBody.FORM)
.addFormDataPart("token", token) // Token vom Login .addFormDataPart("token", token)
.addFormDataPart( .addFormDataPart("file", "payload.enc", tmpEnc.asRequestBody("application/octet-stream".toMediaType()))
"file",
"payload.enc",
tmpEnc.asRequestBody("application/octet-stream".toMediaType())
)
.build()
val request = Request.Builder()
.url(SERVER_DELTA_URL)
.post(requestBody)
.build() .build()
val request = Request.Builder().url(SERVER_DELTA_URL).post(body).build()
client.newCall(request).enqueue(object : Callback { client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${e.message}") Log.e("UPLOAD", "Fehlgeschlagen: ${e.message}")
tmpJson.delete() tmpJson.delete(); tmpEnc.delete()
tmpEnc.delete()
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
val body = try { response.body?.string() ?: "Keine Response" } catch (e: Exception) { val respBody = try { response.body?.string() ?: "" } catch (_: Exception) { "" }
"Fehler beim Lesen der Response: ${e.message}"
}
if (response.isSuccessful) { if (response.isSuccessful) {
Log.d("UPLOAD", "Delta-Upload erfolgreich: $body") Log.d("UPLOAD", "OK: $respBody")
if (file.delete()) Log.d("UPLOAD", "Lokale DB gelöscht.") else Log.e("UPLOAD", "Löschen der lokalen DB fehlgeschlagen.") if (!file.delete()) Log.w("UPLOAD", "Lokale DB nicht gelöscht.")
val journalFile = File(file.parent, file.name + "-journal") File(file.parent, file.name + "-journal").delete()
if (journalFile.exists() && journalFile.delete()) Log.d("UPLOAD", "Journal-Datei gelöscht.")
tmpJson.delete()
tmpEnc.delete()
exitProcess(0)
} else { } else {
Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${response.code} $body") Log.e("UPLOAD", "HTTP ${response.code}: $respBody")
tmpJson.delete()
tmpEnc.delete()
} }
tmpJson.delete(); tmpEnc.delete()
try { exitProcess(0) } catch (_: Exception) {}
} }
}) })
} catch (e: 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 { private fun queryToJsonArray(db: SQLiteDatabase, query: String): JSONArray {
val cursor = db.rawQuery(query, null) val c = db.rawQuery(query, null)
val jsonArray = JSONArray() val arr = JSONArray()
cursor.use { c.use {
val columnNames = it.columnNames val cols = it.columnNames
while (it.moveToNext()) { while (it.moveToNext()) {
val obj = JSONObject() val obj = JSONObject()
for (col in columnNames) { for (col in cols) {
val idx = it.getColumnIndex(col) val idx = it.getColumnIndex(col)
if (idx >= 0) { if (idx >= 0) {
when (it.getType(idx)) { when (it.getType(idx)) {
@ -207,17 +138,14 @@ object DatabaseUploader {
Cursor.FIELD_TYPE_FLOAT -> obj.put(col, it.getDouble(idx)) Cursor.FIELD_TYPE_FLOAT -> obj.put(col, it.getDouble(idx))
Cursor.FIELD_TYPE_STRING -> obj.put(col, it.getString(idx)) Cursor.FIELD_TYPE_STRING -> obj.put(col, it.getString(idx))
Cursor.FIELD_TYPE_NULL -> obj.put(col, JSONObject.NULL) Cursor.FIELD_TYPE_NULL -> obj.put(col, JSONObject.NULL)
Cursor.FIELD_TYPE_BLOB -> { Cursor.FIELD_TYPE_BLOB -> obj.put(col, Base64.encodeToString(it.getBlob(idx), Base64.NO_WRAP))
val blob = it.getBlob(idx)
obj.put(col, Base64.encodeToString(blob, Base64.NO_WRAP))
}
else -> obj.put(col, it.getString(idx)) else -> obj.put(col, it.getString(idx))
} }
} }
} }
jsonArray.put(obj) arr.put(obj)
} }
} }
return jsonArray return arr
} }
} }

View File

@ -1,3 +1,4 @@
// app/src/main/java/com/dano/test1/LoginManager.kt
package com.dano.test1 package com.dano.test1
import android.content.Context import android.content.Context
@ -13,18 +14,9 @@ import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject import org.json.JSONObject
object LoginManager { object LoginManager {
private const val SERVER_LOGIN_URL = "http://49.13.157.44/login.php" private const val SERVER_LOGIN_URL = "http://49.13.157.44/login.php"
private val client = OkHttpClient() 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( fun loginUser(
context: Context, context: Context,
password: String, password: String,
@ -46,26 +38,18 @@ object LoginManager {
if (response.isSuccessful && responseText != null) { if (response.isSuccessful && responseText != null) {
val json = JSONObject(responseText) val json = JSONObject(responseText)
if (json.getBoolean("success")) { if (json.optBoolean("success")) {
val token = json.getString("token") val token = json.getString("token")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) { onSuccess(token) }
onSuccess(token) } else {
withContext(Dispatchers.Main) { onError("Login fehlgeschlagen") }
} }
} else { } else {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) { onError("Fehler beim Login (${response.code})") }
onError("Login fehlgeschlagen")
}
}
} else {
withContext(Dispatchers.Main) {
onError("Fehler beim Login (${response.code})")
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("LOGIN", "Exception beim Login", e) Log.e("LOGIN", "Exception", e)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) { onError("Exception: ${e.message}") }
onError("Exception: ${e.message}")
}
} }
} }
} }