DailyCue Security Review

OWASP Top 10 (2021) & OWASP Mobile Top 10 (2024) Assessment — Re-check v3

v1.2.12 · 9 Jun 2026 · Re-check v3 · Commit fde6edf
Application
DailyCue
Platform
Android (API 26–36)
Architecture
Kotlin + Jetpack Compose
Data Layer
Room + SQLCipher
Codebase
~125 source files
Review Scope
Source code, config, build
Review Timestamp
2026-06-09T14:00:00Z (Re-check v3)

Executive Summary

This is a full re-assessment (Re-check v3) of the DailyCue Android app against the OWASP Top 10 (2021) for web applications and the OWASP Mobile Top 10 (2024) for mobile-specific concerns. The application is a local-first, offline-capable reminder and routine tracker with no network communication — significantly reducing its attack surface.

Overall posture: Low. The app continues to maintain strong cryptographic controls (SQLCipher + Android Keystore), sensible permissions, and zero network attack surface. Since Re-check v2, the app has added routine schedule reconciliation (database v7 with generatedRoutineId tracking), checklist activation gating via RoutineTaskActivationPolicy, and a 5-minute notification lead option. Of 25 total findings, 12 have been remediated, with the remaining being medium, low, or informational.

Re-check v3 changes. All 12 prior remediations remain fully closed. The routine schedule reconciliation system introduced a new code path with several security-relevant observations. The most significant is a non-atomic reconciliation cycle (F20 — Medium) where multiple database operations (deletion, insertion, notification scheduling) execute without a wrapping @Transaction, leaving the database in a partially-updated state if interrupted. Additional findings include an unconditional checklist overwrite during reconciliation (F21 — Low), an uncaught LocalDate.parse exception path (F22 — Low), and incomplete test coverage for the activation policy time-gating edge cases including DST transitions (F23 — Low). Two informational findings document the still-silent SecurityException catch in NotificationHelper (F24) and timezone dependency in retention cleanup (F25). Dependencies remain current with no changes since v2.

12
Remediated
0
Critical
0
High
2
Medium
4
Low
7
Info / Positive

Findings

25 findings (0 critical, 0 high, 2 medium, 4 low, 7 informational, 12 remediated)

Remediated printStackTrace() Leaking Stack Traces to Logcat A09 — Security Logging & Monitoring Failures

NotificationBroadcastReceiver.kt (line 79) previously called e.printStackTrace() in the exception handler. This dumped the full stack trace to System.err, which appears in logcat on production devices.

✓ Remediated in commit 39a503d.
Replaced with Log.e("DailyCue", "Error processing notification broadcast", e), which properly routes through Android's logging framework. This follows standard Android logging conventions and can be stripped in release builds via ProGuard if desired (-assumenosideeffects).
Remediated Release Keystore Credentials Visible in Repository A02 — Cryptographic Failures / A05 — Security Misconfiguration

The keystore.properties file and release keystore binaries (dailycue-release.keystore, app/keystores/release.keystore) were present on disk in the project directory. A security review flagged these as potentially committed to version control.

✓ Verified — Never committed. Already gitignored.
Investigation confirmed:
  • All keystore artifacts are excluded by .gitignore entries (keystore.properties, *.keystore, *.jks, app/keystores/)
  • git log --all --full-history confirms none of these files have ever been tracked in git
  • build.gradle.kts reads signing credentials from these gitignored files, with a fallback to ~/.gradle/gradle.properties
No action needed — configuration was already correct.
Remediated Backup Import Lacks Input Validation A08 — Software & Data Integrity Failures

BackupManager.importFrom() previously read a user-provided JSON file via SAF and deserialized it directly with kotlinx.serialization with only a version number check. No size limits, no field validation, no integrity verification existed.

✓ Remediated in commit 3265b01.
Three layers of validation were added:
  1. File size check — rejects files > 10 MB before any parsing
  2. SHA-256 integrity checksum — computed on export and verified on import; optional field (checksum: String? = null) for backward compatibility
  3. Field-level sanitization — validates all IDs, string lengths, numeric ranges, and sort orders across routines, events, steps, and checklist items via validateBackupPayload()
Remediated Sensitive Data in SharedPreferences & DataStore A02 — Cryptographic Failures / M2 — Insecure Data Storage

Application settings were previously stored in both SharedPreferences (settings_prefs.xml) and Jetpack DataStore (settings.preferences_pb). Neither DataStore nor SharedPreferences are encrypted at rest. The owner name was stored in plaintext and could constitute personally identifiable information (PII).

The contactsJson column in the routines table stores phone numbers and contact names. This was already correctly stored inside the encrypted SQLCipher database, so it was never a concern.

✓ Remediated.
All application settings have been migrated from SharedPreferences and DataStore into the SQLCipher-encrypted Room database (app_settings table). The new RoomSettingsRepository stores every setting field (owner name, notification preferences, theme colors, etc.) in the encrypted database:
  • Owner name is now encrypted at rest via SQLCipher (AES-256 with hardware-backed Keystore key)
  • One-time migration reads legacy DataStore settings on first launch and deletes the DataStore files
  • Theme settings are also cached in SharedPreferences for cold-start access (before Room/SQLCipher is initialized), but the single source of truth is the encrypted database
  • DataStore can be fully removed as a dependency once migration is validated across all users
Remediated Dual-Write Inconsistency Risk (DataStore + SharedPreferences) A04 — Insecure Design

DataStoreSettingsRepository previously wrote theme-related and color settings to both SharedPreferences and DataStore in separate, non-atomic write operations. If the application crashed between writes, the two stores could become inconsistent.

✓ Remediated.
DataStoreSettingsRepository has been replaced with RoomSettingsRepository, which writes all settings to the SQLCipher-encrypted Room database as the single source of truth:
  • Theme settings (name, mode, all color overrides) are written to the encrypted database atomically via settingsDao.upsert()
  • For cold-start access before Room/SQLCipher is initialized, theme values are also cached to SharedPreferences — but this is a one-way cache, not a dual-write, and consistency is ensured by reading from the database on every normal access
  • All 14+ settings update methods now go through a single upsertField() helper that reads the current entity, applies the transform, and upserts atomically
Medium SMS Messages Sent Over Unencrypted Channel A02 — Cryptographic Failures / M3 — Insecure Communication

SendRoutineCompletionSmsUseCase opens the user's default SMS app via Intent.ACTION_SENDTO with a pre-composed message containing the owner name and routine name. SMS is an unencrypted protocol — messages are transmitted in plaintext over the cellular network and are readable by carriers, SS7 intermediaries, and anyone with access to the cellular infrastructure.

private fun buildMessage(ownerName: String, routineName: String): String { val sender = if (ownerName.isNotBlank()) ownerName else "You" return "DailyCue notification: $sender just completed the $routineName routine!" }

Improvements since the previous review:

  • SEND_SMS permission removed from manifest (commit 3ec7dd0) — the app no longer declares or requests this permission. SMS sending is handled entirely via Intent.ACTION_SENDTO, which uses the user's default SMS app without requiring the SEND_SMS permission.
  • Auto-send path removed (commit 79e41fe) — the "Disable Mobile Texting Confirmation" setting was removed. The app always shows a confirmation dialog before opening the SMS app. The user sees and approves every message.
  • SmsSentReceiver deleted — no longer needed since the app doesn't send SMS directly (commit 3ec7dd0).
  • SMS dialog deferred until after the reward animation completes, preventing the SMS dialog from interrupting the user's completion experience.

While this is an explicit user-facing feature (the user must enable SMS in settings, configure contacts with phone numbers, and confirm each message via dialog), the privacy implications should be documented. The message content is non-sensitive (routine completion notification), but SMS remains an unencrypted transport protocol by design.

Recommendation:
  • Document in the privacy policy that SMS messages are sent over unencrypted channels.
  • Consider offering an opt-in to send a generic message without the routine name if the routine name could be sensitive.
  • The current Intent.ACTION_SENDTO pattern is correct — it avoids the SEND_SMS permission entirely and works reliably on Android 15/16 for non-default SMS apps.
Remediated Weak Backup Integrity Protection A08 — Software & Data Integrity Failures

The JSON backup file previously had no integrity mechanism. The backup envelope contained version and appVersion metadata, but these were not cryptographically verified. A user could accidentally import a corrupted or malicious file without detection beyond the Kotlin Serialization parse step.

✓ Remediated in commit 3265b01.
A checksum: String? = null field was added to BackupEnvelope. During export, the payload is serialized to JSON and a SHA-256 hex digest is computed and stored in the envelope. During import, the digest is re-computed from the parsed payload and compared. A mismatch raises a SecurityException. The field is optional, so backups created before this change remain importable — they simply skip the checksum verification.
Remediated Outdated / Unmaintained Dependency Versions A06 — Vulnerable & Outdated Components

All previously outdated dependencies have been updated to current stable releases as part of the Kotlin 2.x migration (commit 479694d):

DependencyPreviousCurrent
Kotlin1.9.242.1.21
Compose BOM2024.06.002026.04.01
KSP1.9.24-1.0.202.1.21-2.0.2
AGP8.4.18.9.1
Gradle8.68.11.1
Room2.6.12.8.4
Hilt2.51.12.57.2
Navigation2.7.72.9.8
Lifecycle2.8.32.10.0
DataStore1.1.11.2.1
Kotlin Serialization1.6.31.7.3
Compose compiler1.5.14 (separate artifact)Built into Kotlin plugin

Key migration details:

  • Kotlin 1.9.24 → 2.1.21: Major compiler version upgrade. Compose compiler migrated from composeOptions { kotlinCompilerExtensionVersion } to the org.jetbrains.kotlin.plugin.compose Gradle plugin, which ships with Kotlin — eliminating version mismatch risk.
  • AGP 8.4.1 → 8.9.1: Full compatibility with compileSdk 36.
  • Room 2.6.1 → 2.8.4: Supports Kotlin 2.x metadata and latest SQLCipher.
  • Hilt 2.51.1 → 2.57.2: Supports Kotlin 2.x, fixes KSP2 integration.
✓ Remediated in commit 479694d.
All dependencies are now at current stable versions. The Kotlin 2.x migration resolved the primary blocker (Kotlin metadata incompatibility with Room 2.7+ and Hilt 2.56+). The Compose compiler is now a built-in Kotlin plugin, ensuring automatic version alignment.
Ongoing recommendation: Consider using Dependabot or Renovate to automate version update PRs and prevent future dependency drift. For now, dependencies are current as of June 2026.
Remediated Sensitive Data in File Name — notification_install_token.txt M2 — Insecure Data Storage

NotificationInstallTokenManager previously stored a UUID identifying this installation in noBackupFilesDir/notification_install_token.txt as plaintext.

✓ Remediated in commit 07b293e.
Replaced file-based storage with SharedPreferences. Token is now stored in the app's standard preferences file, which is encrypted at rest by Android's file-based encryption (FBE) on API 26+. The legacy plaintext file is no longer created.
Remediated exportSchema = false in Room Database A05 — Security Misconfiguration

DailyCueDatabase previously declared exportSchema = false, preventing Room from generating schema history files needed for writing and testing @Migration annotations.

✓ Remediated in commit 09ea055.
exportSchema set to true. Added KSP arg room.schemaLocation pointing to app/schemas/. Schema file 6.json (version 6, 425 lines) generated and verified. Output directory added to .gitignore.
Remediated ProGuard Over-Retention Rules Reduce Obfuscation A05 — Security Misconfiguration

The ProGuard configuration previously contained broad keep rules that reduced code obfuscation effectiveness and increased APK size:

-keep class androidx.compose.** { *; } -keep class androidx.navigation.** { *; } -keep class androidx.datastore.** { *; }

These rules were removed in commit 1925f52. R8 in Kotlin 2.1.21 + AGP 8.9.1 correctly auto-retains all needed classes:

  • 401 Compose classes auto-retained via R8 analysis
  • 15 Navigation classes auto-retained via R8 analysis
  • 46 DataStore classes auto-retained via R8 analysis

APK size reduction: 30.9 MB → 24.4 MB (21% decrease) due to improved R8 minification without the overly broad keep rules.

✓ Remediated in commit 1925f52.
Three broad keep rules removed. Verified via release build R8 pass, seeds file analysis, and 80-event monkey test on device with no crashes or ClassNotFoundException.
Remediated Room Database Schema History Not Tracked A06 — Vulnerable & Outdated Components

The database previously relied on fallbackToDestructiveMigration(), risking user data loss during upgrades. All version transitions now have proper @Migration annotations.

✓ Remediated in commit 13a9b48.
4 new @Migration annotations written (1→2 through 4→5), covering all schema transitions:
1→2: create checklist_items and routine_steps tables
2→3: create notifications table
3→4: add contactsJson column to routines (with PRAGMA guard for idempotency)
4→5: no-op (SQLCipher encryption only)
5→6: already existed (create app_settings table)
fallbackToDestructiveMigration() replaced with fallbackToDestructiveMigration(BuildConfig.DEBUG) — only allows destructive fallback in debug builds.
Remediated Broad Exception Handling Masks Errors A04 — Insecure Design / A09 — Logging & Monitoring

Four locations in the codebase used broad exception handlers that silently swallowed errors, making debugging difficult and potentially masking security-relevant failures.

  • DatabaseEncryptionManager.handlePreEncryptionMigration(): catch (_: Exception) { }
  • DailyCueApp.loadThemeSettings(): catch (e: Exception) { }
  • HomeViewModel.dismissReward(): catch (e: Exception) { }
  • BackupManager.importFrom() (step 12): runCatching { WalkthroughScreen.valueOf(screenName) }.getOrNull()
✓ Remediated in commit 39c88a4.
Log.w("DailyCue", ...) added to all 4 empty catch blocks. The BackupManager runCatching now uses .onFailure { Log.w(...) }. Failures are now observable in logcat during development and testing.
Positive Strong Database Encryption with Android Keystore A02 — Cryptographic Failures

DatabaseEncryptionManager implements a well-architected two-layer encryption scheme:

  • AES-256 key in hardware-backed Android Keystore (TEE/StrongBox compatible) — setKeySize(256)
  • AES-256-GCM with 128-bit authentication tag for passphrase encryption — GCM_TAG_LENGTH_BITS = 128
  • Random 32-byte passphrase generated via SecureRandom
  • Unique IV per encryption — prepended to ciphertext for storage, correctly extracted during decryption
  • No plaintext passphrase exposure — passphrase is only held in memory as a ByteArray and passed directly to SQLCipher
  • Pre-encryption migration handling — detects and removes plain SQLite databases before SQLCipher opens them

This is a model implementation of Android credential storage and the strongest aspect of the application's security posture.

Positive Zero Network Attack Surface A01 — Broken Access Control / M3 — Insecure Communication

The app contains zero HTTP, socket, or other network communication code. No INTERNET permission is declared. No WebView, no API calls, no third-party analytics, no crash reporting SDK. This eliminates the largest class of mobile app vulnerabilities entirely.

Recommendation: Maintain this property. If cloud features are added later, ensure they are optional and clearly communicated to users. Consider adding a network security config file to enforce TLS even if no network calls currently exist.
Positive SAF-Based Backup Requires Explicit User Consent A01 — Broken Access Control

The backup/restore system uses the Storage Access Framework (SAF), requiring the user to explicitly select a file location (export) or file (import) via the system file picker. This means:

  • No file path traversal vulnerabilities
  • No automatic file access without user consent
  • The app never requests WRITE_EXTERNAL_STORAGE or READ_EXTERNAL_STORAGE permissions

Additionally, the backup format now includes a SHA-256 integrity checksum and field-level validation, providing defense in depth even if a malicious file is selected through SAF.

Positive Secure Intent Handling — Immutable PendingIntents & Non-Exported Receivers A01 — Broken Access Control

Immutable PendingIntents: All PendingIntent instances across the codebase correctly use FLAG_IMMUTABLE, preventing intent injection attacks. Every PendingIntent reviewed (notification open, notification snooze, alarm scheduling, alarm cancellation) uses FLAG_IMMUTABLE.

Non-Exported Receivers: NotificationBroadcastReceiver and NotificationActionReceiver are both declared as android:exported="false" in the manifest. Only BootReceiver is exported (required for the BOOT_COMPLETED system broadcast), which is expected and correct.

Install Token Validation: Both the broadcast receiver and the WorkManager-based notification worker validate that incoming intents carry a token matching the current install via NotificationInstallTokenManager.isCurrent(), providing an additional layer of protection against stale or forged notification intents.

Unique Request Codes: PendingIntent request codes use event IDs and notification IDs, preventing accidental PendingIntent collisions.

Positive SQL Injection Prevention via Room Parameterized Queries A03 — Injection

All database queries across the codebase use Room's parameterized query syntax (:paramName). Room binds these as JDBC-style prepared statement parameters, making SQL injection impossible even if user-supplied data flows into queries.

// All Room DAO queries use parameterized bindings: @Query("SELECT * FROM events WHERE title = :title AND startTimeMillis > :currentTimeMillis") suspend fun getFutureEventsByTitle(title: String, currentTimeMillis: Long): List<EventEntity>

No dynamic SQL construction (rawQuery with string concatenation) exists anywhere in the codebase. The title field is used in WHERE clauses across several DAO methods (EventDao lines 24–61) — all correctly parameterized.

Recommendation: If future features require rawQuery, ensure all user-supplied values continue to use parameterized bindings rather than string interpolation.
Low Notification Permission Denial Silent Catch (Regression) A09 — Security Logging & Monitoring Failures

NotificationHelper.showNotification() (lines 227–234) wraps the NotificationManagerCompat.notify() call in a try/catch for SecurityException, but the catch block is empty:

try { NotificationManagerCompat.from(this).notify( (NOTIFICATION_ID_BASE + notificationId).toInt(), builder.build() ) } catch (e: SecurityException) { // Handle case where notification permission is not granted }

This is a regression of the pattern addressed in F13 (Remediated), where 4 similar empty catch blocks across the codebase were fixed by adding Log.w() calls. While a SecurityException from notify() is unlikely in normal operation (permission is requested via system dialog), the silent catch hides diagnostic information if the permission is unexpectedly revoked at runtime.

Recommendation: Add a Log.w("DailyCue", ...) call inside the catch block to log the SecurityException. This is consistent with the F13 remediation pattern and provides visibility into permission-related failures during development and testing.
Medium Non-Atomic Reconciliation Cycle in RoutineScheduleReconciler A04 — Insecure Design

RoutineScheduleReconciler.reconcileRoutine() performs multiple database operations per routine — stale event deletion, event refresh, checklist item save, new event insertion, notification scheduling — without wrapping them in a single Room @Transaction:

// RoutineScheduleReconciler.kt lines 76-114 (simplified) staleGeneratedEvents.forEach { deleteEventUseCase(event.id) } // no @Transaction refreshGeneratedEventIfNeeded(routine, event) // update + checklists scheduleRoutineUseCase(...) // insert new event scheduleNotificationUseCase(...) // schedule notification

If the process is killed mid-reconciliation, the database can be left in a partially-updated state: events may have been deleted but not re-inserted, checklist items may be orphaned, and notifications may not match the database state. On the next reconciliation run, the reconciler will attempt to self-heal by detecting duplicates and stale events, but in the interim the user may experience missing or duplicate events and notifications.

Recommendation:
  • Wrap the per-routine processing in a Room @Transaction annotation at the caller level (e.g., reconcileAll() and reconcileRoutine())
  • Alternatively, make reconcile() a suspend function that coordinates through a single transactional boundary, following the pattern used in BackupManager.restoreDatabase()
  • Consider adding a reconciliation state flag to detect incomplete runs on next startup
Low Unconditional Checklist Overwrite in refreshGeneratedEventIfNeeded A04 — Insecure Design

RoutineScheduleReconciler.refreshGeneratedEventIfNeeded() (line 208) unconditionally replaces all checklist items for a generated event with fresh copies from the routine definition, even when the event itself has not changed:

// Line 189 guards the event update: if (updated != event) { eventDao.update(updated) } // BUT line 208 runs unconditionally — outside the if-block: saveChecklistItemsUseCase(event.id, checklistItems)

Why this matters: Every time reconciliation runs (on every app launch, on every routine edit), the checklist items for every generated event are overwritten. Currently this is harmless because the app does not provide a UI for users to modify individual checklist items on generated routine events. However, if a future feature allows per-event checklist customization (e.g., checking off a step on a specific day), those modifications would be silently discarded on the next reconciliation run.

Recommendation:
  • Move the saveChecklistItemsUseCase call inside the if (updated != event) guard block
  • Or at minimum, add a check that routine steps have actually changed before overwriting checklist items on generated events
Low Uncaught LocalDate.parse Exception in Reconciliation A04 — Insecure Design / A09 — Logging & Monitoring

RoutineScheduleReconciler.refreshGeneratedEventIfNeeded() (line 169) calls LocalDate.parse(occurrenceDate) without error handling:

val startDate = LocalDate.parse(occurrenceDate) // If occurrenceDate contains a non-ISO-8601 string, this throws DateTimeParseException

If the generatedOccurrenceDate column in the database contains a non-ISO-8601 string (e.g., from data corruption, a failed migration, or manual editing), this parse call throws DateTimeParseException. Since refreshGeneratedEventIfNeeded() is called from reconcile() inside a forEach loop with no per-event error handling, a single corrupt date would crash the entire reconciliation batch, leaving the routine's schedule in an unreconciled state.

Compare with the safe patterns used in HistoryRetentionCleanup.kt and BackupManager.kt, which gracefully handle failures with runCatching or early returns.

Recommendation:
  • Wrap LocalDate.parse(occurrenceDate) in runCatching { ... }.getOrNull()
  • Log the failure with Log.w() and skip the event if parsing fails, allowing reconciliation to continue for other events
Low Incomplete Test Coverage for RoutineTaskActivationPolicy Time-Gating A04 — Insecure Design

RoutineTaskActivationPolicyTest.kt covers 5 scenarios but is missing important edge cases for time-based gating:

  • Zero notification lead: notificationMinutesBefore = 0 — should activate exactly at start time
  • Null start time for routine: startTimeMillis = null — should return false
  • DST spring-forward (23-hour day): startOfNextDayMillis calculation could be off by 1 hour
  • DST fall-back (25-hour day): Could allow an extra hour of activation
  • Midnight boundary: Exactly at startOfNextDayMillis — code uses strict less-than which is correct, but untested
  • Very large notification lead (480+ minutes): Could make activation start earlier than intended

All tests use America/New_York timezone. Adding DST transition tests would improve regression protection.

Recommendation:
  • Add test cases for notificationMinutesBefore = 0 and startTimeMillis = null
  • Add DST transition tests using known transition dates (e.g., March 8, 2026 for spring-forward, November 1, 2026 for fall-back)
  • Add exact boundary tests at midnight transitions
  • Consider testing with a UTC timezone as well
Positive NotificationHelper SecurityException Catch Remains Silent (F19 Follow-Up) A09 — Security Logging & Monitoring Failures

NotificationHelper.showNotification() (lines 227–234) continues to use an empty catch block for SecurityException:

try { NotificationManagerCompat.from(this).notify(...) } catch (e: SecurityException) { // Handle case where notification permission is not granted }

Re-check v3 assessment: This finding was originally classified as F19 (Low) in Re-check v2. After review, the empty catch is intentional by design: on API 33+, the notification permission can legitimately be denied by the user, and this is a graceful degradation path. The comment documents the intent. However, adding a Log.w() call would improve debuggability by distinguishing expected permission-denied scenarios from unexpected SecurityException causes (e.g., broadcast permission changes).

Downgraded to Informational from the original Low classification in Re-check v2, as the pattern is intentional and commented. The recommendation from F13 remains: add Log.w("DailyCue", "Notification permission denied", e) for consistency with the rest of the codebase.

Info Timezone Dependency in HistoryRetentionCleanup and Reconciliation Cutoff A04 — Insecure Design

HistoryRetentionCleanup.kt (lines 28, 32) and RoutineScheduleReconciler.kt (line 37) both use ZoneId.systemDefault() for date arithmetic:

// HistoryRetentionCleanup.kt private val zone: ZoneId = ZoneId.systemDefault() ... val cutoffMillis = cutoffDate.atStartOfDay(zone).toInstant().toEpochMilli() // RoutineScheduleReconciler.kt private val zone: ZoneId = ZoneId.systemDefault()

If the user changes their device timezone between runs, date boundaries shift. Events within the retention window in the old timezone could fall outside in the new timezone (or vice versa), causing either premature deletion or extended retention. For the reconciler, the scheduling horizon could shift by a day windowing events in or out unexpectedly.

Risk assessment: Device timezone changes are rare and the impact is bounded (at most a few hours of events affected). Events are stored as UTC epoch millis internally, so only the date boundary calculations are timezone-sensitive. This is a minor design fragility, not a security vulnerability.

Recommendation:
  • Document this as an acceptable limitation (timezone changes are rare)
  • Consider storing all event dates in UTC and computing cutoff/horizon boundaries in UTC to eliminate timezone dependency

Changes in Re-check v3

OWASP Top 10 (2021) Cross-Reference

CategoryRisk LevelKey Findings
A01 — Broken Access Control Low Positive controls (F16, F17). Immutable PendingIntents, SAF, non-exported receivers. allowBackup="false" prevents data exfiltration via ADB backup.
A02 — Cryptographic Failures Remediated F4 (plaintext owner name — remediated — now in encrypted Room DB), F6 (SMS plaintext — by design, SEND_SMS permission removed, always shows confirmation dialog), F14 (strong encryption — positive), F2 (keystore — remediated).
A03 — Injection Low No SQL injection (Room parameterizes all queries — F18). No WebView (no XSS). No OS command execution. No dynamic string evaluation.
A04 — Insecure Design Medium F5 (dual-write inconsistency — remediated — replaced by RoomSettingsRepository), F13 (broad exception handling — remediated — Log.w() added to all 4 catch blocks). F20 (non-atomic reconciliation — Medium). F21 (unconditional checklist overwrite — Low). F22 (uncaught LocalDate.parse — Low). F23 (incomplete test coverage — Low). F25 (timezone dependency — Info).
A05 — Security Misconfiguration Remediated F10 (exportSchema — remediated), F11 (ProGuard — remediated).
A06 — Vulnerable & Outdated Components Low F8 (outdated dependencies — remediated — all deps now current), F12 (destructive migrations — remediated — all @Migration annotations written).
A07 — Identification & Authentication Failures N/A No authentication required (local-only app with no user accounts).
A08 — Software & Data Integrity Failures Remediated F3 (backup validation — remediated), F7 (backup checksum — remediated). Both previously open findings resolved in commit 3265b01.
A09 — Security Logging & Monitoring Failures Low F1 (printStackTrace — remediated), F13 (empty catch blocks — remediated), F24 (NotificationHelper silent SecurityException catch — Info, intentional). No crash reporting or monitoring infrastructure.
A10 — SSRF N/A No server-side requests. No URL fetching. Zero network code (local-only app).

OWASP Mobile Top 10 (2024) Cross-Reference

CategoryRisk LevelNotes
M1 — Improper Platform Usage Low SEND_SMS has required="false". READ_CONTACTS is properly scoped to contact picker. SCHEDULE_EXACT_ALARM and USE_EXACT_ALARM are declared for notification scheduling. All runtime permissions are checked before use.
M2 — Insecure Data Storage Remediated Owner name stored in plaintext (F4 — remediated, now in encrypted Room DB). Notification token in plaintext file (F9 — remediated, now in FBE-protected SharedPreferences). Database is encrypted with SQLCipher (excellent — F14). Phone numbers stored in encrypted DB (correct).
M3 — Insecure Communication Low No network communication at all (F15 — positive). SMS messages are inherently unencrypted (F6 — medium, by design; SEND_SMS permission removed, always shows confirmation dialog, uses Intent.ACTION_SENDTO).
M4 — Insecure Authentication N/A No authentication in a local-only app.
M5 — Insufficient Cryptography Low SQLCipher + AES-256-GCM + Android Keystore is excellent (F14 — positive). All cryptographic choices are modern and appropriate.
M6 — Insecure Authorization N/A Single-user local app with no authorization model needed.
M7 — Client Code Quality Medium Clean architecture with proper separation of concerns (MVVM + Repository + UseCase). F20 (non-atomic reconciliation — Medium). F21 (unconditional checklist overwrite — Low). F22 (uncaught LocalDate.parse — Low). F23 (incomplete test coverage — Low). F25 (timezone dependency — Info).
M8 — Code Tampering Low ProGuard/R8 provides basic obfuscation and integrity checking. Keystore and signing credentials properly gitignored (F2 — remediated). No code integrity checks (e.g., SafetyNet Attestation) — acceptable for local app.
M9 — Reverse Engineering Low ProGuard/R8 enabled for release builds with minification and optimization. 21% APK size reduction from tightening rules (F11 — remediated). SQLCipher's native code layer adds natural obfuscation.
M10 — Extraneous Functionality Low No debug backdoors, test APIs, hidden activities, or developer-only functionality in release builds. The AndroidStartup provider is correctly non-exported and only used for WorkManager initialization.

Remediation Priority

PriorityFindingEffortStatus
Done Replace printStackTrace with Log.e Low Committed in 39a503d
Done Verify keystore secrets not in git Low Verified — already gitignored
Done Add backup import validation (size, schema, fields) Medium Committed in 3265b01
Done Add SHA-256 checksum to backup envelope Low Committed in 3265b01
Done Encrypt owner name — migrate all settings to encrypted Room DB Low Committed (current)
Done Eliminate dual-write in DataStoreSettingsRepository Medium Committed (current)
Done Update outdated dependencies (Kotlin 2.x, AGP, Room, Hilt, etc.) High Committed in 479694d
Done Tighten ProGuard rules — remove broad Compose/Navigation/DataStore keep rules Low Committed in 1925f52
P2 Document SMS privacy implications (unencrypted channel) Low Open — SEND_SMS permission removed, auto-send removed, always shows confirmation dialog (commits 3ec7dd0, 79e41fe)
Done Add logging to empty catch blocks Low Committed in 39c88a4
Done Set exportSchema = true for Room Low Committed in 09ea055
Done Write non-destructive @Migration annotations for Room Medium Committed in 13a9b48
Done Store notification token in SharedPreferences instead of plaintext file Low Committed in 07b293e
P3 Document SMS privacy implications in privacy policy Low Open — SEND_SMS permission removed, auto-send eliminated, always shows confirmation dialog (commits 3ec7dd0, 79e41fe)
P3 Add logging to NotificationHelper silent SecurityException catch (F24) Low Open — add Log.w() inside catch block, consistent with F13 pattern (F19 reclassified to Info)
P2 Wrap reconciliation cycle in @Transaction (F20) Medium Open — per-routine operations not atomic; wrap in Room @Transaction
P3 Guard checklist overwrite in reconciliation (F21) Low Open — move saveChecklistItemsUseCase inside if (updated != event) guard
P3 Handle LocalDate.parse exception gracefully (F22) Low Open — wrap in runCatching, log and skip on parse failure
P3 Add DST/timezone edge case tests for activation policy (F23) Low Open — add boundary tests for DST transitions, midnight, zero notification lead

Attack Surface Summary

VectorPresentNotes
Network (HTTP/WebSocket)NoneZero network code. No INTERNET permission.
WebView / JavaScriptNoneNo WebView usage anywhere in the app.
File I/O (outside SAF)MinimalOnly private app directories: noBackupFilesDir (token), getDatabasePath (SQLCipher), SharedPreferences, DataStore. All sandboxed by Android.
Content ProvidersNone exportedOnly androidx.startup.InitializationProvider (non-exported).
Broadcast Receivers1 exportedBootReceiver exported (required for BOOT_COMPLETED). Others non-exported and token-validated.
Deep Links / URL SchemesNoneNo intent filters with URL schemes or deep links.
Backup / Data ExportSAF-gatedUser must explicitly select file via system picker. SHA-256 integrity verified on import. Field-level validation applied.
SMS (opt-in)MediumExplicit user opt-in. No SEND_SMS permission. Always shows confirmation dialog. Uses Intent.ACTION_SENDTO. Unencrypted channel (by SMS protocol design).