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 android.os.Bundle
|
||||||
import com.google.firebase.analytics.FirebaseAnalytics
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
import com.google.firebase.analytics.logEvent
|
import com.google.firebase.analytics.logEvent
|
||||||
|
import java.security.MessageDigest
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
@ -11,6 +12,41 @@ class FirebaseAnalyticsTracker @Inject constructor(
|
||||||
private val analytics: FirebaseAnalytics
|
private val analytics: FirebaseAnalytics
|
||||||
) : AnalyticsTracker {
|
) : 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) {
|
override fun trackScreenViewed(screenName: String) {
|
||||||
analytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
|
analytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
|
||||||
param(FirebaseAnalytics.Param.SCREEN_NAME, screenName)
|
param(FirebaseAnalytics.Param.SCREEN_NAME, screenName)
|
||||||
|
|
@ -23,23 +59,23 @@ class FirebaseAnalyticsTracker @Inject constructor(
|
||||||
|
|
||||||
override fun trackQuestionViewed(questionId: String, category: String) {
|
override fun trackQuestionViewed(questionId: String, category: String) {
|
||||||
analytics.logEvent("question_viewed") {
|
analytics.logEvent("question_viewed") {
|
||||||
param("question_id", questionId)
|
param("question_hash", hashForAnalytics(questionId))
|
||||||
param("category", category)
|
param("category", coarseCategory(category))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun trackAnswerSubmitted(questionId: String, category: String, answerType: String) {
|
override fun trackAnswerSubmitted(questionId: String, category: String, answerType: String) {
|
||||||
analytics.logEvent("answer_submitted") {
|
analytics.logEvent("answer_submitted") {
|
||||||
param("question_id", questionId)
|
param("question_hash", hashForAnalytics(questionId))
|
||||||
param("category", category)
|
param("category", coarseCategory(category))
|
||||||
param("answer_type", answerType)
|
param("answer_type", answerType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun trackAnswerRevealed(questionId: String, category: String) {
|
override fun trackAnswerRevealed(questionId: String, category: String) {
|
||||||
analytics.logEvent("answer_revealed") {
|
analytics.logEvent("answer_revealed") {
|
||||||
param("question_id", questionId)
|
param("question_hash", hashForAnalytics(questionId))
|
||||||
param("category", category)
|
param("category", coarseCategory(category))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,13 +95,13 @@ class FirebaseAnalyticsTracker @Inject constructor(
|
||||||
|
|
||||||
override fun trackSpinWheelStarted(categoryId: String) {
|
override fun trackSpinWheelStarted(categoryId: String) {
|
||||||
analytics.logEvent("spin_wheel_started") {
|
analytics.logEvent("spin_wheel_started") {
|
||||||
param("category_id", categoryId)
|
param("category_hash", hashForAnalytics(categoryId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun trackSpinWheelCompleted(categoryId: String, answeredCount: Int) {
|
override fun trackSpinWheelCompleted(categoryId: String, answeredCount: Int) {
|
||||||
analytics.logEvent("spin_wheel_completed") {
|
analytics.logEvent("spin_wheel_completed") {
|
||||||
param("category_id", categoryId)
|
param("category_hash", hashForAnalytics(categoryId))
|
||||||
param("answered_count", answeredCount.toLong())
|
param("answered_count", answeredCount.toLong())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue