Initial commit: breakpilot-compliance - Compliance SDK Platform

Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:28 +01:00
commit 4435e7ea0a
734 changed files with 251369 additions and 0 deletions

View File

@@ -0,0 +1,499 @@
/**
* Android Consent SDK - ConsentManager
*
* DSGVO/TTDSG-konformes Consent Management fuer Android Apps.
*
* Nutzung:
* 1. In Application.onCreate() konfigurieren
* 2. In Activities/Fragments mit ConsentManager.current nutzen
* 3. In Jetpack Compose mit rememberConsentState()
*
* Copyright (c) 2025 BreakPilot
* Apache License 2.0
*/
package com.breakpilot.consent
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.provider.Settings
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.security.MessageDigest
import java.util.*
// =============================================================================
// Consent Categories
// =============================================================================
/**
* Standard-Consent-Kategorien nach IAB TCF 2.2
*/
enum class ConsentCategory {
ESSENTIAL, // Technisch notwendig
FUNCTIONAL, // Personalisierung
ANALYTICS, // Nutzungsanalyse
MARKETING, // Werbung
SOCIAL // Social Media
}
// =============================================================================
// Consent State
// =============================================================================
/**
* Aktueller Consent-Zustand
*/
@Serializable
data class ConsentState(
val categories: Map<ConsentCategory, Boolean> = defaultCategories(),
val vendors: Map<String, Boolean> = emptyMap(),
val timestamp: Long = System.currentTimeMillis(),
val version: String = "1.0.0",
val consentId: String? = null,
val expiresAt: Long? = null,
val tcfString: String? = null
) {
companion object {
fun defaultCategories() = mapOf(
ConsentCategory.ESSENTIAL to true,
ConsentCategory.FUNCTIONAL to false,
ConsentCategory.ANALYTICS to false,
ConsentCategory.MARKETING to false,
ConsentCategory.SOCIAL to false
)
val DEFAULT = ConsentState()
}
}
// =============================================================================
// Configuration
// =============================================================================
/**
* SDK-Konfiguration
*/
data class ConsentConfig(
val apiEndpoint: String,
val siteId: String,
val language: String = Locale.getDefault().language,
val showRejectAll: Boolean = true,
val showAcceptAll: Boolean = true,
val granularControl: Boolean = true,
val rememberDays: Int = 365,
val debug: Boolean = false
)
// =============================================================================
// Consent Manager
// =============================================================================
/**
* Haupt-Manager fuer Consent-Verwaltung
*/
class ConsentManager private constructor() {
// State
private val _consent = MutableStateFlow(ConsentState.DEFAULT)
val consent: StateFlow<ConsentState> = _consent.asStateFlow()
private val _isInitialized = MutableStateFlow(false)
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
private val _isLoading = MutableStateFlow(true)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
private val _isBannerVisible = MutableStateFlow(false)
val isBannerVisible: StateFlow<Boolean> = _isBannerVisible.asStateFlow()
private val _isSettingsVisible = MutableStateFlow(false)
val isSettingsVisible: StateFlow<Boolean> = _isSettingsVisible.asStateFlow()
// Private
private var config: ConsentConfig? = null
private var storage: ConsentStorage? = null
private var apiClient: ConsentApiClient? = null
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
// Singleton
companion object {
@Volatile
private var INSTANCE: ConsentManager? = null
val current: ConsentManager
get() = INSTANCE ?: synchronized(this) {
INSTANCE ?: ConsentManager().also { INSTANCE = it }
}
/**
* Konfiguriert den ConsentManager
* Sollte in Application.onCreate() aufgerufen werden
*/
fun configure(context: Context, config: ConsentConfig) {
current.apply {
this.config = config
this.storage = ConsentStorage(context)
this.apiClient = ConsentApiClient(config.apiEndpoint, config.siteId)
if (config.debug) {
println("[ConsentSDK] Configured with siteId: ${config.siteId}")
}
scope.launch {
initialize(context)
}
}
}
}
// ==========================================================================
// Initialization
// ==========================================================================
private suspend fun initialize(context: Context) {
try {
// Lokalen Consent laden
storage?.load()?.let { stored ->
// Pruefen ob abgelaufen
val expiresAt = stored.expiresAt
if (expiresAt != null && System.currentTimeMillis() > expiresAt) {
_consent.value = ConsentState.DEFAULT
storage?.clear()
} else {
_consent.value = stored
}
}
// Vom Server synchronisieren
try {
apiClient?.getConsent(DeviceFingerprint.generate(context))?.let { serverConsent ->
_consent.value = serverConsent
storage?.save(serverConsent)
}
} catch (e: Exception) {
if (config?.debug == true) {
println("[ConsentSDK] Failed to sync consent: $e")
}
}
_isInitialized.value = true
// Banner anzeigen falls noetig
if (needsConsent) {
showBanner()
}
} finally {
_isLoading.value = false
}
}
// ==========================================================================
// Public API
// ==========================================================================
/**
* Prueft ob Consent fuer Kategorie erteilt wurde
*/
fun hasConsent(category: ConsentCategory): Boolean {
// Essential ist immer erlaubt
if (category == ConsentCategory.ESSENTIAL) return true
return consent.value.categories[category] ?: false
}
/**
* Prueft ob Consent eingeholt werden muss
*/
val needsConsent: Boolean
get() = consent.value.consentId == null
/**
* Alle Kategorien akzeptieren
*/
suspend fun acceptAll() {
val newCategories = ConsentCategory.values().associateWith { true }
val newConsent = consent.value.copy(
categories = newCategories,
timestamp = System.currentTimeMillis()
)
saveConsent(newConsent)
hideBanner()
}
/**
* Alle nicht-essentiellen Kategorien ablehnen
*/
suspend fun rejectAll() {
val newCategories = ConsentCategory.values().associateWith {
it == ConsentCategory.ESSENTIAL
}
val newConsent = consent.value.copy(
categories = newCategories,
timestamp = System.currentTimeMillis()
)
saveConsent(newConsent)
hideBanner()
}
/**
* Auswahl speichern
*/
suspend fun saveSelection(categories: Map<ConsentCategory, Boolean>) {
val updated = categories.toMutableMap()
updated[ConsentCategory.ESSENTIAL] = true // Essential immer true
val newConsent = consent.value.copy(
categories = updated,
timestamp = System.currentTimeMillis()
)
saveConsent(newConsent)
hideBanner()
}
// ==========================================================================
// UI Control
// ==========================================================================
fun showBanner() {
_isBannerVisible.value = true
}
fun hideBanner() {
_isBannerVisible.value = false
}
fun showSettings() {
_isSettingsVisible.value = true
}
fun hideSettings() {
_isSettingsVisible.value = false
}
// ==========================================================================
// Private Methods
// ==========================================================================
private suspend fun saveConsent(newConsent: ConsentState) {
// Lokal speichern
storage?.save(newConsent)
// An Server senden
try {
val response = apiClient?.saveConsent(
newConsent,
DeviceFingerprint.generate(storage?.context!!)
)
val updated = newConsent.copy(
consentId = response?.consentId,
expiresAt = response?.expiresAt
)
_consent.value = updated
storage?.save(updated)
} catch (e: Exception) {
// Lokal speichern auch bei Fehler
_consent.value = newConsent
if (config?.debug == true) {
println("[ConsentSDK] Failed to sync consent: $e")
}
}
}
}
// =============================================================================
// Storage
// =============================================================================
/**
* Sichere Speicherung mit EncryptedSharedPreferences
*/
internal class ConsentStorage(val context: Context) {
private val prefs: SharedPreferences by lazy {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
EncryptedSharedPreferences.create(
context,
"breakpilot_consent",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
private val json = Json { ignoreUnknownKeys = true }
fun load(): ConsentState? {
val data = prefs.getString("consent_state", null) ?: return null
return try {
json.decodeFromString<ConsentState>(data)
} catch (e: Exception) {
null
}
}
fun save(consent: ConsentState) {
val data = json.encodeToString(consent)
prefs.edit().putString("consent_state", data).apply()
}
fun clear() {
prefs.edit().remove("consent_state").apply()
}
}
// =============================================================================
// API Client
// =============================================================================
/**
* API Client fuer Backend-Kommunikation
*/
internal class ConsentApiClient(
private val baseUrl: String,
private val siteId: String
) {
private val client = OkHttpClient()
private val json = Json { ignoreUnknownKeys = true }
@Serializable
data class ConsentResponse(
val consentId: String,
val expiresAt: Long
)
suspend fun getConsent(fingerprint: String): ConsentState? = withContext(Dispatchers.IO) {
val request = Request.Builder()
.url("$baseUrl/banner/consent?site_id=$siteId&fingerprint=$fingerprint")
.get()
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@withContext null
val body = response.body?.string() ?: return@withContext null
json.decodeFromString<ConsentState>(body)
}
}
suspend fun saveConsent(
consent: ConsentState,
fingerprint: String
): ConsentResponse = withContext(Dispatchers.IO) {
val body = """
{
"site_id": "$siteId",
"device_fingerprint": "$fingerprint",
"categories": ${json.encodeToString(consent.categories.mapKeys { it.key.name.lowercase() })},
"vendors": ${json.encodeToString(consent.vendors)},
"platform": "android",
"app_version": "${BuildConfig.VERSION_NAME}"
}
""".trimIndent()
val request = Request.Builder()
.url("$baseUrl/banner/consent")
.post(body.toRequestBody("application/json".toMediaType()))
.build()
client.newCall(request).execute().use { response ->
val responseBody = response.body?.string() ?: throw Exception("Empty response")
json.decodeFromString<ConsentResponse>(responseBody)
}
}
}
// =============================================================================
// Device Fingerprint
// =============================================================================
/**
* Privacy-konformer Device Fingerprint
*/
internal object DeviceFingerprint {
fun generate(context: Context): String {
// Android ID (reset bei Factory Reset)
val androidId = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
) ?: UUID.randomUUID().toString()
// Device Info
val model = Build.MODEL
val version = Build.VERSION.SDK_INT.toString()
val locale = Locale.getDefault().toString()
// Hash erstellen
val raw = "$androidId-$model-$version-$locale"
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(raw.toByteArray())
return digest.joinToString("") { "%02x".format(it) }
}
}
// =============================================================================
// Jetpack Compose Integration
// =============================================================================
/**
* State Holder fuer Compose
*/
@Composable
fun rememberConsentState(): State<ConsentState> {
return ConsentManager.current.consent.collectAsState()
}
/**
* Banner Visibility State
*/
@Composable
fun rememberBannerVisibility(): State<Boolean> {
return ConsentManager.current.isBannerVisible.collectAsState()
}
/**
* Consent Gate - Zeigt Inhalt nur bei Consent
*/
@Composable
fun ConsentGate(
category: ConsentCategory,
placeholder: @Composable () -> Unit = {},
content: @Composable () -> Unit
) {
val consent by rememberConsentState()
if (ConsentManager.current.hasConsent(category)) {
content()
} else {
placeholder()
}
}
/**
* Local Composition fuer ConsentManager
*/
val LocalConsentManager = staticCompositionLocalOf { ConsentManager.current }
/**
* Consent Provider
*/
@Composable
fun ConsentProvider(
content: @Composable () -> Unit
) {
CompositionLocalProvider(
LocalConsentManager provides ConsentManager.current
) {
content()
}
}