security: hash question IDs and coarsen categories in Firebase Analytics (SecurityReport #9)
This commit is contained in:
parent
a412247bf3
commit
e28a08e5f1
|
|
@ -3,6 +3,7 @@ package app.closer.core.analytics
|
|||
import android.os.Bundle
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.logEvent
|
||||
import java.security.MessageDigest
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -11,6 +12,41 @@ class FirebaseAnalyticsTracker @Inject constructor(
|
|||
private val analytics: FirebaseAnalytics
|
||||
) : AnalyticsTracker {
|
||||
|
||||
/**
|
||||
* Hashes an identifier for analytics logging.
|
||||
*
|
||||
* Uses SHA-256 and truncates to the first 16 hex characters. This is a one-way,
|
||||
* deterministic transform — enough for aggregate analytics correlation while making
|
||||
* it infeasible to recover the original question content from the logged value.
|
||||
*/
|
||||
private fun hashForAnalytics(value: String): String {
|
||||
if (value.isBlank()) return "empty"
|
||||
return try {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.toByteArray(Charsets.UTF_8))
|
||||
digest.joinToString("", limit = 8, truncated = "") { "%02x".format(it) }
|
||||
} catch (e: Exception) {
|
||||
"hash_error"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps fine-grained category identifiers to a coarse-grained set safe for analytics.
|
||||
*
|
||||
* This preserves useful aggregate behavioral data (e.g. which coarse areas drive
|
||||
* engagement) without leaking the specific, often intimate, category names.
|
||||
*/
|
||||
private fun coarseCategory(category: String): String {
|
||||
return when (category.lowercase().trim()) {
|
||||
"communication", "conflict", "trust", "intimacy", "emotional_intimacy",
|
||||
"physical_intimacy", "sex", "desire", "rebuilding_trust" -> "intimacy"
|
||||
"fun", "play", "adventure", "date_night", "travel", "creativity" -> "fun"
|
||||
"money", "future", "goals", "family", "values", "lifestyle" -> "life"
|
||||
"daily_check_in", "app_usage" -> "meta"
|
||||
else -> "other"
|
||||
}
|
||||
}
|
||||
|
||||
override fun trackScreenViewed(screenName: String) {
|
||||
analytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
|
||||
param(FirebaseAnalytics.Param.SCREEN_NAME, screenName)
|
||||
|
|
@ -23,23 +59,23 @@ class FirebaseAnalyticsTracker @Inject constructor(
|
|||
|
||||
override fun trackQuestionViewed(questionId: String, category: String) {
|
||||
analytics.logEvent("question_viewed") {
|
||||
param("question_id", questionId)
|
||||
param("category", category)
|
||||
param("question_hash", hashForAnalytics(questionId))
|
||||
param("category", coarseCategory(category))
|
||||
}
|
||||
}
|
||||
|
||||
override fun trackAnswerSubmitted(questionId: String, category: String, answerType: String) {
|
||||
analytics.logEvent("answer_submitted") {
|
||||
param("question_id", questionId)
|
||||
param("category", category)
|
||||
param("question_hash", hashForAnalytics(questionId))
|
||||
param("category", coarseCategory(category))
|
||||
param("answer_type", answerType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun trackAnswerRevealed(questionId: String, category: String) {
|
||||
analytics.logEvent("answer_revealed") {
|
||||
param("question_id", questionId)
|
||||
param("category", category)
|
||||
param("question_hash", hashForAnalytics(questionId))
|
||||
param("category", coarseCategory(category))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,13 +95,13 @@ class FirebaseAnalyticsTracker @Inject constructor(
|
|||
|
||||
override fun trackSpinWheelStarted(categoryId: String) {
|
||||
analytics.logEvent("spin_wheel_started") {
|
||||
param("category_id", categoryId)
|
||||
param("category_hash", hashForAnalytics(categoryId))
|
||||
}
|
||||
}
|
||||
|
||||
override fun trackSpinWheelCompleted(categoryId: String, answeredCount: Int) {
|
||||
analytics.logEvent("spin_wheel_completed") {
|
||||
param("category_id", categoryId)
|
||||
param("category_hash", hashForAnalytics(categoryId))
|
||||
param("answered_count", answeredCount.toLong())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue