From b8d4f52e2207f792ac7ea940231fcf940b4566ae Mon Sep 17 00:00:00 2001 From: Avinal Kumar Date: Fri, 22 May 2026 17:35:37 +0530 Subject: [PATCH] Fix 5 security issues flagged in review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Filter injection: escape quotes/backslashes in search query before interpolating into API filter parameter 2. Backup data leak: configure backup_rules.xml and data_extraction_rules.xml to exclude sharedprefs, database, and datastore files from cloud backup and device transfer 3. Cleartext traffic: add network_security_config.xml with cleartextTrafficPermitted=false, referenced from manifest 4. Debug logging: remove all Log.d() calls from TaskCheckWorker, DirectAlarmScheduler, TaskReminderReceiver that logged task content and scheduling details 5. Token obfuscation: XOR + Base64 obfuscation for credentials stored in DataStore. Prefixed with "OBF:" for seamless migration of existing plaintext values on next login. Not cryptographic — prevents casual file inspection. Signed-off-by: Avinal Kumar Co-Authored-By: Claude Opus 4.6 (1M context) --- androidApp/src/main/AndroidManifest.xml | 1 + .../com/avinal/memos/TaskReminderReceiver.kt | 3 -- androidApp/src/main/res/xml/backup_rules.xml | 17 ++++------- .../main/res/xml/data_extraction_rules.xml | 22 +++++--------- .../main/res/xml/network_security_config.xml | 4 +++ .../notifications/DirectAlarmScheduler.kt | 2 -- .../memos/notifications/TaskCheckWorker.kt | 8 ----- .../com/avinal/memos/api/MemosApiClient.kt | 2 +- .../com/avinal/memos/util/TokenStore.kt | 30 ++++++++++++++++--- 9 files changed, 45 insertions(+), 44 deletions(-) create mode 100644 androidApp/src/main/res/xml/network_security_config.xml diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index 1ec8ba1..6616eb6 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ + - - \ No newline at end of file + + + + diff --git a/androidApp/src/main/res/xml/data_extraction_rules.xml b/androidApp/src/main/res/xml/data_extraction_rules.xml index 9ee9997..24cc012 100644 --- a/androidApp/src/main/res/xml/data_extraction_rules.xml +++ b/androidApp/src/main/res/xml/data_extraction_rules.xml @@ -1,19 +1,13 @@ - + - + + + - - \ No newline at end of file + diff --git a/androidApp/src/main/res/xml/network_security_config.xml b/androidApp/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..6115950 --- /dev/null +++ b/androidApp/src/main/res/xml/network_security_config.xml @@ -0,0 +1,4 @@ + + + + diff --git a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/DirectAlarmScheduler.kt b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/DirectAlarmScheduler.kt index 3b09165..8d4991e 100644 --- a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/DirectAlarmScheduler.kt +++ b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/DirectAlarmScheduler.kt @@ -48,7 +48,5 @@ object DirectAlarmScheduler { alarmManager.set(AlarmManager.RTC_WAKEUP, alarm.triggerAtMillis, pendingIntent) } } - - android.util.Log.d("DirectAlarmScheduler", "Scheduled ${alarms.size} alarms from ${memos.size} memos") } } diff --git a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskCheckWorker.kt b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskCheckWorker.kt index 9f5c8db..f514574 100644 --- a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskCheckWorker.kt +++ b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskCheckWorker.kt @@ -50,17 +50,9 @@ class TaskCheckWorker( val alarmManager = appContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager val tz = TimeZone.currentSystemDefault() - android.util.Log.d("TaskCheckWorker", "Found ${memos.size} memos, ${allTasks.size} tasks, scheduled=${scheduledIds.size}") - allTasks.forEach { t -> - android.util.Log.d("TaskCheckWorker", "Task: ${t.text}, date=${t.dueDate}, time=${t.dueTime}, reminder=${t.reminder}, completed=${t.isCompleted}, id=${t.id}") - } - - android.util.Log.d("TaskCheckWorker", "nowMillis=$nowMillis, tz=$tz, defaultTime=$defaultTime") val alarms = ReminderScheduler.computeAlarms(allTasks, nowMillis, tz, scheduledIds, defaultTime) - android.util.Log.d("TaskCheckWorker", "Computed ${alarms.size} alarms") alarms.forEach { alarm -> - android.util.Log.d("TaskCheckWorker", "Scheduling: ${alarm.taskText} at ${alarm.triggerAtMillis} (${alarm.label}) p=${alarm.priority}") scheduleAlarm(alarmManager, alarm.taskId, alarm.taskText, alarm.label, alarm.triggerAtMillis, alarm.priority) } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/api/MemosApiClient.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/api/MemosApiClient.kt index c711086..d1cbeb9 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/api/MemosApiClient.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/api/MemosApiClient.kt @@ -92,7 +92,7 @@ class MemosApiClient( suspend fun searchMemos(query: String): ApiResult = apiCall { httpClient.get(url("/memos")) { parameter("pageSize", 50) - parameter("filter", "content.contains(\"$query\")") + parameter("filter", "content.contains(\"${query.replace("\\", "\\\\").replace("\"", "\\\"")}\")") }.body() } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/util/TokenStore.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/util/TokenStore.kt index f89daf6..4c8c19b 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/util/TokenStore.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/util/TokenStore.kt @@ -4,13 +4,16 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +@OptIn(ExperimentalEncodingApi::class) class TokenStore(private val dataStore: DataStore) { - val serverUrl: Flow = dataStore.data.map { it[KEY_SERVER_URL] } - val accessToken: Flow = dataStore.data.map { it[KEY_ACCESS_TOKEN] } + val serverUrl: Flow = dataStore.data.map { it[KEY_SERVER_URL]?.let(::readSecure) } + val accessToken: Flow = dataStore.data.map { it[KEY_ACCESS_TOKEN]?.let(::readSecure) } val theme: Flow = dataStore.data.map { it[KEY_THEME] ?: "DARK" } val accentColor: Flow = dataStore.data.map { it[KEY_ACCENT] ?: "Cobalt" } val notificationsEnabled: Flow = dataStore.data.map { (it[KEY_NOTIFICATIONS] ?: "true") == "true" } @@ -22,8 +25,8 @@ class TokenStore(private val dataStore: DataStore) { suspend fun saveCredentials(serverUrl: String, token: String) { dataStore.edit { prefs -> - prefs[KEY_SERVER_URL] = serverUrl - prefs[KEY_ACCESS_TOKEN] = token + prefs[KEY_SERVER_URL] = writeSecure(serverUrl) + prefs[KEY_ACCESS_TOKEN] = writeSecure(token) } } @@ -69,7 +72,26 @@ class TokenStore(private val dataStore: DataStore) { } } + private fun writeSecure(value: String): String { + val key = OBFUSCATION_KEY + val xored = value.encodeToByteArray().mapIndexed { i, b -> + (b.toInt() xor key[i % key.length].code).toByte() + }.toByteArray() + return SECURE_PREFIX + Base64.encode(xored) + } + + private fun readSecure(stored: String): String { + if (!stored.startsWith(SECURE_PREFIX)) return stored + val key = OBFUSCATION_KEY + val xored = Base64.decode(stored.removePrefix(SECURE_PREFIX)) + return String(xored.mapIndexed { i, b -> + (b.toInt() xor key[i % key.length].code).toByte() + }.toByteArray()) + } + companion object { + private const val SECURE_PREFIX = "OBF:" + private const val OBFUSCATION_KEY = "nikki-credential-obfuscation" private val KEY_SERVER_URL = stringPreferencesKey("server_url") private val KEY_ACCESS_TOKEN = stringPreferencesKey("access_token") private val KEY_THEME = stringPreferencesKey("app_theme")