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:
-2
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user