1
0
mirror of https://github.com/avinal/nikki.git synced 2026-07-03 21:40:09 +05:30

Fix 5 security issues flagged in review

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 <avinal.xlvii@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context)
This commit is contained in:
2026-05-22 17:35:37 +05:30
parent 44b00736db
commit b8d4f52e22
9 changed files with 45 additions and 44 deletions
+1
View File
@@ -12,6 +12,7 @@
<application
android:allowBackup="true"
android:networkSecurityConfig="@xml/network_security_config"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
@@ -7,13 +7,10 @@ import com.avinal.memos.notifications.TaskNotificationManager
class TaskReminderReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
android.util.Log.d("TaskReminderReceiver", "onReceive fired!")
val taskText = intent.getStringExtra("task_text") ?: "Task reminder"
val dueLabel = intent.getStringExtra("due_label") ?: "due"
val notificationId = intent.getIntExtra("notification_id", 0)
val priority = intent.getIntExtra("priority", 0)
android.util.Log.d("TaskReminderReceiver", "Showing: $taskText - $dueLabel (p=$priority, id=$notificationId)")
TaskNotificationManager.createChannels(context)
TaskNotificationManager.showTaskNotification(
context = context,
+5 -12
View File
@@ -1,13 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
<exclude domain="sharedpref" path="." />
<exclude domain="database" path="." />
<exclude domain="file" path="datastore/" />
</full-backup-content>
@@ -1,19 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
<exclude domain="sharedpref" path="." />
<exclude domain="database" path="." />
<exclude domain="file" path="datastore/" />
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
<exclude domain="sharedpref" path="." />
<exclude domain="database" path="." />
<exclude domain="file" path="datastore/" />
</device-transfer>
-->
</data-extraction-rules>
</data-extraction-rules>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
</network-security-config>
@@ -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")
}
}
@@ -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)
}
@@ -92,7 +92,7 @@ class MemosApiClient(
suspend fun searchMemos(query: String): ApiResult<ListMemosResponse> = apiCall {
httpClient.get(url("/memos")) {
parameter("pageSize", 50)
parameter("filter", "content.contains(\"$query\")")
parameter("filter", "content.contains(\"${query.replace("\\", "\\\\").replace("\"", "\\\"")}\")")
}.body()
}
@@ -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<Preferences>) {
val serverUrl: Flow<String?> = dataStore.data.map { it[KEY_SERVER_URL] }
val accessToken: Flow<String?> = dataStore.data.map { it[KEY_ACCESS_TOKEN] }
val serverUrl: Flow<String?> = dataStore.data.map { it[KEY_SERVER_URL]?.let(::readSecure) }
val accessToken: Flow<String?> = dataStore.data.map { it[KEY_ACCESS_TOKEN]?.let(::readSecure) }
val theme: Flow<String> = dataStore.data.map { it[KEY_THEME] ?: "DARK" }
val accentColor: Flow<String> = dataStore.data.map { it[KEY_ACCENT] ?: "Cobalt" }
val notificationsEnabled: Flow<Boolean> = dataStore.data.map { (it[KEY_NOTIFICATIONS] ?: "true") == "true" }
@@ -22,8 +25,8 @@ class TokenStore(private val dataStore: DataStore<Preferences>) {
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<Preferences>) {
}
}
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")