Secrets im Networking sicher behandeln
Secrets gehören nicht fest in die App. Du lernst, wie du API-Keys, Tokens und Leakage im Networking einordnest.
Secrets im Networking sind vertrauliche Werte, die deine App beim Sprechen mit Servern verwendet: API-Keys, Zugriffstokens, Refresh-Tokens, Client-Secrets oder interne Endpunkte. In Android ist der wichtigste Grundsatz klar: Alles, was du in die App packst, kann mit genug Aufwand aus APK, AAB, Speicher, Logs oder Netzwerkverkehr gelesen werden. Deine Aufgabe ist deshalb nicht, ein Secret im Client perfekt zu verstecken. Deine Aufgabe ist, keine sensiblen Secrets fest einzubauen, Tokens sauber zu begrenzen und Leakage früh zu erkennen.
Was ist das?
Ein Secret ist ein Wert, der einem Angreifer mehr Rechte gibt, als er ohne diesen Wert hätte. Ein API-Key kann etwa Zugriff auf einen Kartendienst erlauben. Ein Bearer-Token kann eine konkrete Nutzeranfrage autorisieren. Ein Refresh-Token kann neue Zugriffstokens erzeugen. Ein internes Service-Secret kann sogar Backend-Funktionen öffnen, die nie für mobile Clients gedacht waren.
Im Android-Kontext ist diese Unterscheidung wichtig, weil eine App an Nutzer ausgeliefert wird. Der Code läuft nicht in deiner kontrollierten Serverumgebung, sondern auf Geräten, die du nicht kontrollierst. Nutzer können die App installieren, die APK extrahieren, Strings suchen, Bytecode analysieren, Netzwerkaufrufe beobachten oder ein gerootetes Gerät verwenden. Ob du Kotlin, Jetpack Compose, Retrofit, Ktor oder eine andere Bibliothek nutzt, ändert daran nichts.
Ein gutes mentales Modell lautet: Der Android-Client ist kein Tresor. Er ist ein Teilnehmer in deinem System, dem du nur die Berechtigungen gibst, die für die aktuelle Aufgabe nötig sind. Ein API-Key, der nur die Nutzung eines öffentlichen SDKs identifiziert und zusätzlich per Paketname, Signatur, Quota und Serverregeln eingeschränkt ist, ist anders zu bewerten als ein Backend-Secret mit Vollzugriff. Der erste Wert ist nicht ideal, aber kontrollierbar. Der zweite Wert gehört nie in die App.
Das Thema passt direkt in die Data-Layer-Phase einer Android-Roadmap. Der Data Layer kapselt Datenquellen, Repositories, Netzwerkclients und Speicher. Genau dort entscheidest du, wie Authentifizierung in deine Architektur eingebaut wird: Wo wird ein Token gelesen? Wer darf ihn speichern? Welche Klasse hängt ihn an Requests? Welche Logs sind erlaubt? Welche Fehler landen in Crash-Reports? Secrets sind also kein Detail am Rand, sondern Teil deiner Architektur- und Release-Qualität.
Für Lernende ist besonders wichtig: Security beginnt nicht erst bei Spezialthemen wie Verschlüsselung oder Root-Erkennung. Sie beginnt bei einfachen Entscheidungen im Alltag. Schreibst du einen Key direkt in eine Kotlin-Datei? Gibst du einen Token im Logcat aus? Commitest du eine Beispiel-Konfiguration mit echten Werten? Speichert dein Offline-Cache komplette Antworten mit sensiblen Daten? Jede dieser Entscheidungen kann zu Leakage führen.
Leakage bedeutet, dass ein vertraulicher Wert an eine Stelle gelangt, an der er nicht sein darf. Das kann öffentlich sein, etwa in GitHub. Es kann aber auch intern passieren, etwa in Logs, Analytics-Events, Screenshots, Testreports oder Support-Tickets. In Android-Projekten ist Leakage oft unspektakulär: Ein Entwickler loggt einen Request-Header, um einen Bug zu finden, und der Header enthält einen Bearer-Token. Später wird der Log in einem Issue kopiert. Der Schaden entsteht nicht durch eine komplizierte Attacke, sondern durch eine fehlende Grenze.
Wie funktioniert es?
Networking-Secrets tauchen meist an vier Stellen auf: beim Build, im Netzwerkclient, im lokalen Speicher und in Logs oder Diagnosedaten. Für jede Stelle brauchst du eine klare Regel.
Beim Build ist die wichtigste Regel: Keine sensiblen Secrets in Quellcode, Ressourcen oder BuildConfig, wenn diese Secrets echten Schutz bieten sollen. BuildConfig.API_KEY wirkt auf den ersten Blick sauber, weil der Wert nicht direkt in der Kotlin-Datei steht. Im fertigen Artefakt ist er trotzdem vorhanden. Wer die App analysiert, kann ihn finden. Dasselbe gilt für strings.xml, native Libraries, verschleierte Konstanten oder Werte aus Gradle-Dateien. Ob der String direkt sichtbar ist oder erst nach etwas Arbeit, ändert die Sicherheitsannahme nicht.
Es gibt Fälle, in denen ein API-Key technisch im Client stehen muss, etwa bei bestimmten SDKs. Dann behandelst du ihn nicht als starkes Secret. Du schränkst ihn ein: nur für deine App-Signatur, nur für bestimmte Paketnamen, nur für bestimmte APIs, mit Quotas, Monitoring und Sperrmöglichkeit. Zusätzlich sollte dein Backend keine sicherheitskritischen Entscheidungen allein auf diesen Key stützen. Ein solcher Key identifiziert die App, aber er beweist nicht, dass ein vertrauenswürdiger Nutzer eine erlaubte Aktion ausführt.
Im Netzwerkclient geht es um Token-Fluss. Ein typisches modernes Android-Projekt hat eine Data-Layer-Struktur mit Repository, Remote Data Source und HTTP-Client. Der Token sollte nicht quer durch ViewModels, Composables und einzelne Feature-Klassen gereicht werden. Besser ist eine zentrale Stelle, die Requests ergänzt, zum Beispiel ein OkHttp-Interceptor. Das ViewModel sagt dann nur: “Lade mein Profil.” Es kennt den Auth-Header nicht. Compose zeigt Zustand an und triggert Aktionen, aber es verwaltet keine Secrets.
Diese Trennung hilft dir auch beim Testen. Du kannst prüfen, dass dein Repository bei fehlender Authentifizierung einen passenden Fehler liefert. Du kannst den Interceptor isoliert testen. Du kannst verhindern, dass UI-Code Tokens aus Versehen in State-Objekte oder Navigation-Argumente schreibt. Besonders bei Compose ist das relevant, weil UI-State oft leicht sichtbar gemacht, gespeichert oder in Previews nachgebaut wird. Ein Token hat in einem data class UiState fast nie etwas zu suchen.
Beim lokalen Speicher musst du unterscheiden, was du speicherst und warum. Zugriffstokens sollten kurzlebig sein. Refresh-Tokens sind sensibler, weil sie neue Tokens erzeugen können. Wenn du sie auf dem Gerät speichern musst, nutze dafür geeignete Android-Mechanismen, etwa verschlüsselte Speicherung über Jetpack Security oder eine Architektur, bei der der Schaden bei Diebstahl begrenzt bleibt. Wichtiger als der konkrete Wrapper ist die Systementscheidung: Token kurz halten, Rechte begrenzen, Rotation ermöglichen und serverseitiges Widerrufen unterstützen.
Offline-first-Apps machen das Thema etwas anspruchsvoller. Sie speichern Daten lokal, synchronisieren später und müssen auch ohne Netzwerk sinnvoll arbeiten. Das heißt aber nicht, dass Auth-Daten oder sensible Netzwerkantworten ungeprüft in die lokale Datenbank gehören. Der Offline-Cache sollte nach Datenarten getrennt werden. Produktlisten, öffentliche Einstellungen oder bereits freigegebene Inhalte sind anders zu behandeln als private Nachrichten, Zahlungsdaten oder Auth-Header. Der Data Layer sollte hier bewusst entscheiden, welche Daten persistiert werden, wie lange sie gültig sind und wann sie gelöscht werden.
Logs sind eine häufige Schwachstelle. Viele HTTP-Logging-Interceptors sind im Debug-Build hilfreich, aber gefährlich, wenn sie Header oder Bodies mit sensiblen Daten ausgeben. In Release-Builds gehören solche Logs abgeschaltet oder streng redigiert. Auch in Debug-Builds solltest du nicht automatisch alles loggen, weil Screenshots, Pairing-Sessions, CI-Ausgaben oder Bugreports weitergegeben werden können. Eine gute Regel lautet: Logge IDs, Statuscodes und technische Zustände, aber keine Tokens, keine Passwörter, keine Session-Cookies und keine vollständigen privaten Payloads.
Der Lebenszyklus von Tokens ist ebenfalls Teil des Verständnisses. Ein Zugriffstoken wird erzeugt, gespeichert, an Requests gehängt, bei Ablauf erneuert und bei Logout gelöscht. Jeder Schritt kann falsch implementiert werden. Wenn du bei Ablauf einfach denselben Request zehnmal parallel wiederholst, erzeugst du Race Conditions. Wenn du beim Logout nur den UI-State leerst, aber den Token im Speicher lässt, bleibt die Session technisch bestehen. Wenn du einen 401-Fehler pauschal als Netzwerkproblem behandelst, versteckst du Auth-Bugs.
Seniorere Android-Entwicklung zeigt sich hier nicht in komplizierter Verschlüsselung, sondern in klaren Grenzen. UI kennt keine Secrets. Der Data Layer kapselt Auth. Der Netzwerkclient hängt Tokens zentral an. Logs redigieren sensible Werte. Der Server betrachtet mobile Clients als nicht vollständig vertrauenswürdig. Release-Checks prüfen reale Artefakte, nicht nur Quellcode. Das ist die Grundlage, auf der du später fortgeschrittene Themen sauber aufbauen kannst.
In der Praxis
Stell dir vor, du baust eine App mit Login und einem Profil-Endpunkt. Das ViewModel soll nicht wissen, wie der Authorization-Header entsteht. Es ruft ein Repository auf. Der OkHttp-Client bekommt einen Interceptor, der bei Bedarf den aktuellen Token liest und den Header ergänzt. Gleichzeitig wird ein Logging-Interceptor so konfiguriert, dass er in Release-Builds keine sensiblen Inhalte ausgibt.
class AuthInterceptor(
private val tokenStore: TokenStore
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val accessToken = tokenStore.currentAccessToken()
val requestWithAuth = if (accessToken != null) {
originalRequest.newBuilder()
.header("Authorization", "Bearer $accessToken")
.build()
} else {
originalRequest
}
return chain.proceed(requestWithAuth)
}
}
interface TokenStore {
fun currentAccessToken(): String?
suspend fun saveTokens(accessToken: String, refreshToken: String?)
suspend fun clear()
}
fun buildHttpClient(
tokenStore: TokenStore,
isDebug: Boolean
): OkHttpClient {
val builder = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor(tokenStore))
if (isDebug) {
val logging = HttpLoggingInterceptor { message ->
val redacted = message
.replace(Regex("Authorization: Bearer .+"), "Authorization: Bearer <redacted>")
Log.d("Network", redacted)
}.apply {
level = HttpLoggingInterceptor.Level.BASIC
}
builder.addInterceptor(logging)
}
return builder.build()
}
Das Beispiel zeigt keine vollständige Auth-Lösung, aber die entscheidende Richtung: Der Token wird nicht in Composables, Navigation-Routen oder Feature-Parametern verteilt. Der Netzwerkclient ergänzt ihn an einer technischen Grenze. Logs werden begrenzt. Der Store ist ein Interface, damit du in Tests eine Fake-Implementierung nutzen kannst.
Eine typische Stolperfalle ist der Gedanke: “Der Key steht nur in local.properties, also ist er sicher.” Das stimmt nur für dein Repository. Sobald Gradle den Wert in BuildConfig, Ressourcen oder ein Manifest schreibt, landet er im Artefakt. local.properties schützt also vor versehentlichen Commits, nicht vor Extraktion aus der ausgelieferten App. Für echte Secrets brauchst du einen Server, der die geschützte Aktion ausführt, oder ein Token-Konzept mit kurzer Gültigkeit und begrenzten Rechten.
Eine zweite Stolperfalle ist übermäßiges Vertrauen in Obfuscation. R8 und ProGuard erschweren Analyse und reduzieren Code, aber sie machen aus einem eingebetteten Secret kein sicheres Secret. Ein String, der zur Laufzeit für einen Request gebraucht wird, muss irgendwann im Speicher oder im Netzwerk auftauchen. Obfuscation ist sinnvoll, aber sie ersetzt kein passendes Sicherheitsmodell.
Eine praktische Entscheidungsregel lautet:
Entscheidungsregel für API-Keys und Tokens
Wenn der Wert einem Angreifer dauerhaften oder breiten Zugriff geben würde, gehört er nicht in die App. Wenn der Wert im Client technisch nötig ist, muss er serverseitig eingeschränkt, überwacht und widerrufbar sein. Wenn der Wert nutzerbezogen ist, sollte er kurzlebig, zweckgebunden und beim Logout zuverlässig entfernt werden.
Diese Regel hilft dir im Alltag bei Code-Reviews. Sie zwingt dich, für jeden Wert zu fragen: Was kann jemand damit tun? Wie lange ist der Wert gültig? Kann ich ihn sperren? Ist er an Nutzer, App, Gerät, Signatur oder Scope gebunden? Wird er geloggt? Taucht er in Crash-Reports auf? Wird er lokal gecacht? Diese Fragen sind oft wichtiger als die konkrete Bibliothek.
Auch Tests können das Verständnis sichern. Du kannst Unit-Tests schreiben, die prüfen, dass der Interceptor bei vorhandenem Token den Header setzt und ohne Token keinen Header erzeugt. Du kannst Tests für Logout ergänzen, die sicherstellen, dass der TokenStore geleert wird. In Integrationstests kannst du MockWebServer verwenden und prüfen, welche Header deine App wirklich sendet. Für Logging lässt sich zumindest strukturell testen, dass Redaction-Funktionen bekannte Token-Muster ersetzen.
Zusätzlich solltest du reale Build-Artefakte prüfen. Suche in Debug- und Release-Artefakten nach bekannten Test-Keys, internen Hostnamen und verdächtigen Begriffen wie secret, token, password oder Authorization. Das ist kein vollständiger Sicherheitscheck, aber ein guter Lernschritt. Viele Leaks fallen schon auf, wenn du nicht nur den Quellcode ansiehst, sondern das Ergebnis deines Builds.
Für Offline-first-Apps ergänzt du eine weitere Prüfung: Welche Daten liegen nach einer erfolgreichen Synchronisation lokal vor? Öffne die Datenbank im Debug-Build, prüfe gespeicherte Tabellen und kontrolliere, ob Auth-Header, Tokens oder private Rohantworten dort landen. Wenn sensible Nutzerdaten gespeichert werden müssen, dokumentiere die Entscheidung und baue Löschpfade ein, etwa bei Logout, Kontowechsel oder Widerruf der Session.
In einem Compose-Projekt solltest du außerdem auf UI-State achten. Ein Fehler wäre zum Beispiel, den Token in einem rememberSaveable oder in einem Navigation-Argument zu transportieren. Dadurch kann der Wert in gespeicherten Zuständen, Logs oder Debug-Ausgaben auftauchen. Die UI braucht normalerweise nur fachliche Zustände wie isLoggedIn, isLoading, userName oder errorMessage. Der geheime Wert bleibt im Data Layer.
Fazit
Secrets im Networking behandelst du sicher, indem du die Grenze des Android-Clients akzeptierst: Was in der App steckt, kann aus der App herausgeholt werden. Baue deshalb keine dauerhaften Backend-Secrets ein, schränke unvermeidbare API-Keys konsequent ein, verwalte Tokens zentral im Data Layer und halte Logs frei von vertraulichen Werten. Prüfe dein Verständnis praktisch: Lies deinen Netzwerkcode im Code-Review mit der Frage “Wo könnte ein Token sichtbar werden?”, teste Interceptor und Logout-Verhalten, kontrolliere Debug-Logs und suche in echten Build-Artefakten nach sensiblen Strings. So entwickelst du eine Routine, die nicht nur theoretisch korrekt ist, sondern im Android-Alltag hilft.