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

Fix security issues from review

Notifications:
- VISIBILITY_PRIVATE on all channels and notifications
  (hides task text from lockscreen)
- Remove setFullScreenIntent (requires USE_FULL_SCREEN_INTENT
  permission; p1 channel already bypasses DND)

Auth:
- Clear cached token and server URL in memory on logout
  via AuthRepository.onLogout callback

Offline queue:
- Replace manual JSON string interpolation with
  kotlinx.serialization JsonObject/JsonPrimitive
  (prevents JSON injection from memo content)

CI/CD:
- Pin all GitHub Actions to commit SHAs
- Add permissions: contents: read to build workflow
- Decode keystore via env var instead of inline expansion
- Sanitize tag name through env var in release upload
- Fix test task name: testAndroidHostTest

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context)
This commit is contained in:
2026-06-05 15:08:16 +05:30
parent 6b1d798c95
commit e4c19c2d7c
11 changed files with 55 additions and 21 deletions
+9 -2
View File
@@ -4,6 +4,13 @@ on:
pull_request: pull_request:
branches: [main] branches: [main]
concurrency:
group: build-${{ github.head_ref || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -14,12 +21,12 @@ jobs:
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 21
- uses: gradle/actions/setup-gradle@v4 - uses: gradle/actions/setup-gradle@v4
- name: Run tests - name: Run tests
run: ./gradlew :composeApp:testDebugUnitTest run: ./gradlew :composeApp:testAndroidHostTest
- name: Build debug APK - name: Build debug APK
run: ./gradlew :androidApp:assembleDebug run: ./gradlew :androidApp:assembleDebug
+11 -4
View File
@@ -4,6 +4,10 @@ on:
release: release:
types: [created] types: [created]
concurrency:
group: release-${{ github.event.release.tag_name }}
cancel-in-progress: false
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -16,12 +20,14 @@ jobs:
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 21
- uses: gradle/actions/setup-gradle@v4 - uses: gradle/actions/setup-gradle@v4
- name: Decode keystore - name: Decode keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > ${{ runner.temp }}/keystore.jks env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
run: echo "$KEYSTORE_BASE64" | base64 -d > "${{ runner.temp }}/keystore.jks"
- name: Build signed release APK - name: Build signed release APK
env: env:
@@ -34,6 +40,7 @@ jobs:
- name: Upload release APK - name: Upload release APK
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
TAG: ${{ github.event.release.tag_name }}
run: | run: |
mv androidApp/build/outputs/apk/release/androidApp-release.apk nikki-${{ github.event.release.tag_name }}.apk mv androidApp/build/outputs/apk/release/androidApp-release.apk "nikki-${TAG}.apk"
gh release upload ${{ github.event.release.tag_name }} nikki-${{ github.event.release.tag_name }}.apk gh release upload "${TAG}" "nikki-${TAG}.apk"
+6
View File
@@ -36,11 +36,17 @@ kotlin {
sourceSets { sourceSets {
commonMain.dependencies { commonMain.dependencies {
@Suppress("DEPRECATION")
implementation(compose.runtime) implementation(compose.runtime)
@Suppress("DEPRECATION")
implementation(compose.foundation) implementation(compose.foundation)
@Suppress("DEPRECATION")
implementation(compose.material3) implementation(compose.material3)
@Suppress("DEPRECATION")
implementation(compose.materialIconsExtended) implementation(compose.materialIconsExtended)
@Suppress("DEPRECATION")
implementation(compose.ui) implementation(compose.ui)
@Suppress("DEPRECATION")
implementation(compose.components.resources) implementation(compose.components.resources)
implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.viewmodel)
@@ -28,17 +28,17 @@ object TaskNotificationManager {
manager.createNotificationChannel(NotificationChannel(CHANNEL_P1, "P1 — Urgent", NotificationManager.IMPORTANCE_HIGH).apply { manager.createNotificationChannel(NotificationChannel(CHANNEL_P1, "P1 — Urgent", NotificationManager.IMPORTANCE_HIGH).apply {
description = "High priority — alarm sound, strong vibration, wakes screen" description = "High priority — alarm sound, strong vibration, wakes screen"
enableVibration(true); vibrationPattern = longArrayOf(0, 500, 200, 500, 200, 500) enableVibration(true); vibrationPattern = longArrayOf(0, 500, 200, 500, 200, 500)
setSound(alarmUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC; setShowBadge(true); setBypassDnd(true) setSound(alarmUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PRIVATE; setShowBadge(true); setBypassDnd(true)
}) })
manager.createNotificationChannel(NotificationChannel(CHANNEL_P2, "P2 — Medium", NotificationManager.IMPORTANCE_HIGH).apply { manager.createNotificationChannel(NotificationChannel(CHANNEL_P2, "P2 — Medium", NotificationManager.IMPORTANCE_HIGH).apply {
description = "Medium priority — notification sound, vibration" description = "Medium priority — notification sound, vibration"
enableVibration(true); vibrationPattern = longArrayOf(0, 300, 200, 300) enableVibration(true); vibrationPattern = longArrayOf(0, 300, 200, 300)
setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC; setShowBadge(true) setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PRIVATE; setShowBadge(true)
}) })
manager.createNotificationChannel(NotificationChannel(CHANNEL_P3, "P3 — Low", NotificationManager.IMPORTANCE_DEFAULT).apply { manager.createNotificationChannel(NotificationChannel(CHANNEL_P3, "P3 — Low", NotificationManager.IMPORTANCE_DEFAULT).apply {
description = "Low priority — notification sound, short vibration" description = "Low priority — notification sound, short vibration"
enableVibration(true); vibrationPattern = longArrayOf(0, 200) enableVibration(true); vibrationPattern = longArrayOf(0, 200)
setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PRIVATE
}) })
manager.createNotificationChannel(NotificationChannel(CHANNEL_DEFAULT, "No Priority", NotificationManager.IMPORTANCE_LOW).apply { manager.createNotificationChannel(NotificationChannel(CHANNEL_DEFAULT, "No Priority", NotificationManager.IMPORTANCE_LOW).apply {
description = "No priority — silent notification" description = "No priority — silent notification"
@@ -89,7 +89,7 @@ object TaskNotificationManager {
.setShowWhen(true) .setShowWhen(true)
.setPriority(notifPriority) .setPriority(notifPriority)
.setCategory(NotificationCompat.CATEGORY_REMINDER) .setCategory(NotificationCompat.CATEGORY_REMINDER)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.setAutoCancel(true) .setAutoCancel(true)
.setOnlyAlertOnce(false) .setOnlyAlertOnce(false)
.setContentIntent(pendingOpen) .setContentIntent(pendingOpen)
@@ -99,7 +99,6 @@ object TaskNotificationManager {
when (priority) { when (priority) {
1 -> { 1 -> {
builder.setFullScreenIntent(pendingOpen, true)
builder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)) builder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM))
builder.setVibrate(longArrayOf(0, 500, 200, 500, 200, 500)) builder.setVibrate(longArrayOf(0, 500, 200, 500, 200, 500))
} }
@@ -2,12 +2,14 @@ package com.avinal.memos
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import coil3.ImageLoader import coil3.ImageLoader
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.setSingletonImageLoaderFactory import coil3.compose.setSingletonImageLoaderFactory
import coil3.network.ktor3.KtorNetworkFetcherFactory import coil3.network.ktor3.KtorNetworkFetcherFactory
import com.avinal.memos.ui.navigation.AppNavHost import com.avinal.memos.ui.navigation.AppNavHost
import com.avinal.memos.ui.theme.NikkiTheme import com.avinal.memos.ui.theme.NikkiTheme
import com.avinal.memos.util.LocalAppDependencies import com.avinal.memos.util.LocalAppDependencies
@OptIn(ExperimentalCoilApi::class)
@Composable @Composable
fun App(sharedText: String? = null) { fun App(sharedText: String? = null) {
val deps = LocalAppDependencies.current val deps = LocalAppDependencies.current
@@ -39,7 +39,11 @@ class AppDependencies(
) )
} }
val authRepository: AuthRepository by lazy { AuthRepository(apiClient, tokenStore) } val authRepository: AuthRepository by lazy {
AuthRepository(apiClient, tokenStore).also {
it.onLogout = { cachedToken = null; cachedServerUrl = null }
}
}
val memoRepository: MemoRepository by lazy { val memoRepository: MemoRepository by lazy {
MemoRepository(apiClient, database.memoDao()) { MemoRepository(apiClient, database.memoDao()) {
com.avinal.memos.util.triggerReminderCheck() com.avinal.memos.util.triggerReminderCheck()
@@ -52,5 +52,8 @@ class AuthRepository(
suspend fun logout() { suspend fun logout() {
tokenStore.clear() tokenStore.clear()
_currentUser.value = null _currentUser.value = null
onLogout?.invoke()
} }
var onLogout: (() -> Unit)? = null
} }
@@ -127,7 +127,12 @@ class MemoRepository(
is ApiResult.NetworkError -> { is ApiResult.NetworkError -> {
pendingSyncDao?.insert(PendingSyncEntity( pendingSyncDao?.insert(PendingSyncEntity(
memoId = null, action = "CREATE", memoId = null, action = "CREATE",
payload = """{"content":"${content.replace("\"", "\\\"")}","visibility":"${visibility.toApiString()}"}""", payload = kotlinx.serialization.json.Json.encodeToString(
kotlinx.serialization.json.JsonObject(mapOf(
"content" to kotlinx.serialization.json.JsonPrimitive(content),
"visibility" to kotlinx.serialization.json.JsonPrimitive(visibility.toApiString()),
))
),
createdAt = nowMillis(), createdAt = nowMillis(),
)) ))
result result
@@ -155,14 +160,14 @@ class MemoRepository(
} }
is ApiResult.Error -> result is ApiResult.Error -> result
is ApiResult.NetworkError -> { is ApiResult.NetworkError -> {
val payloadParts = buildList { val fields = buildMap<String, kotlinx.serialization.json.JsonElement> {
if (content != null) add(""""content":"${content.replace("\"", "\\\"")}"""") if (content != null) put("content", kotlinx.serialization.json.JsonPrimitive(content))
if (visibility != null) add(""""visibility":"${visibility.toApiString()}"""") if (visibility != null) put("visibility", kotlinx.serialization.json.JsonPrimitive(visibility.toApiString()))
if (pinned != null) add(""""pinned":$pinned""") if (pinned != null) put("pinned", kotlinx.serialization.json.JsonPrimitive(pinned))
} }
pendingSyncDao?.insert(PendingSyncEntity( pendingSyncDao?.insert(PendingSyncEntity(
memoId = id, action = "UPDATE", memoId = id, action = "UPDATE",
payload = "{${payloadParts.joinToString(",")}}", payload = kotlinx.serialization.json.Json.encodeToString(kotlinx.serialization.json.JsonObject(fields)),
createdAt = nowMillis(), createdAt = nowMillis(),
)) ))
result result
@@ -1,3 +1,4 @@
@file:Suppress("DEPRECATION")
package com.avinal.memos.ui.components package com.avinal.memos.ui.components
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
@@ -236,7 +237,7 @@ private fun InlineEditor(
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
dateState.selectedDateMillis?.let { ms -> dateState.selectedDateMillis?.let { ms ->
val d = kotlinx.datetime.Instant.fromEpochMilliseconds(ms) val d = Instant.fromEpochMilliseconds(ms)
.toLocalDateTime(kotlinx.datetime.TimeZone.UTC).date .toLocalDateTime(kotlinx.datetime.TimeZone.UTC).date
onContentChange(content.trimEnd() + " $d") onContentChange(content.trimEnd() + " $d")
} }
@@ -375,7 +375,7 @@ fun MemoListScreen(
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
dateState.selectedDateMillis?.let { ms -> dateState.selectedDateMillis?.let { ms ->
val d = kotlinx.datetime.Instant.fromEpochMilliseconds(ms) val d = kotlin.time.Instant.fromEpochMilliseconds(ms)
.toLocalDateTime(kotlinx.datetime.TimeZone.UTC).date .toLocalDateTime(kotlinx.datetime.TimeZone.UTC).date
val r = composeField.text.trimEnd() + " $d"; composeField = TextFieldValue(r, TextRange(r.length)) val r = composeField.text.trimEnd() + " $d"; composeField = TextFieldValue(r, TextRange(r.length))
} }
@@ -41,7 +41,7 @@ import com.avinal.memos.domain.ReminderUnit
import com.avinal.memos.domain.Task import com.avinal.memos.domain.Task
import com.avinal.memos.parser.TaskParser import com.avinal.memos.parser.TaskParser
import com.avinal.memos.ui.theme.LocalAccentColor import com.avinal.memos.ui.theme.LocalAccentColor
import kotlinx.datetime.Instant import kotlin.time.Instant
import kotlinx.datetime.LocalTime import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.atStartOfDayIn