Checklist voor veilige opslag in Kotlin voor tokens, sleutels en PII
Checklist voor veilige opslag in Kotlin: kies tussen Android Keystore, EncryptedSharedPreferences en database-encryptie voor tokens, sleutels en PII.

Wat je probeert te beschermen (in gewone taal)
Veilige opslag in een zakelijke app betekent één ding: als iemand de telefoon (of de app-bestanden) krijgt, mogen ze niet kunnen lezen of hergebruiken wat je hebt opgeslagen. Dat geldt voor data in rust (op schijf) en voor geheimen die lekken via backups, logs, crashrapporten of debugtools.
Een eenvoudige mentale test: wat kan een vreemdeling doen als die de opslagmap van je app opent? In veel apps zijn de meest waardevolle items geen foto's of instellingen. Het zijn korte strings die toegang ontgrendelen.
Opslag op het apparaat bevat vaak sessietokens (zodat gebruikers ingelogd blijven), refresh tokens, API-sleutels, encryptiesleutels, persoonlijke gegevens (PII) zoals namen en e-mails, en gecachte bedrijfsrecords voor offline gebruik (orders, tickets, klantnotities).
Hier zijn veelvoorkomende echte foutscenario's:
- Een verloren of gestolen apparaat wordt onderzocht en tokens worden gekopieerd om een gebruiker te imiteren.
- Malware of een “helper”-app leest lokale bestanden op een geroot apparaat of via toegankelijkheidstrucs.
- Automatische device-backups verplaatsen je app-data naar een plek die je niet had gepland.
- Debug-builds loggen tokens, schrijven ze naar crashrapporten of schakelen beveiligingschecks uit.
Daarom is “gewoon in SharedPreferences opslaan” niet oké voor alles wat toegang verleent (tokens) of schade kan toebrengen aan gebruikers en je bedrijf (PII). Plain SharedPreferences is als geheimen op een plakbriefje in de app schrijven: handig, en makkelijk te lezen als iemand de kans krijgt.
Het beste startpunt is elk opgeslagen item te benoemen en twee vragen te stellen: ontgrendelt het iets, en zou het een probleem zijn als het openbaar wordt? De rest (Keystore, encrypted preferences, versleutelde database) volgt daaruit.
Classificeer je data: tokens, sleutels en PII
Veilige opslag wordt makkelijker als je stopt met alle “gevoelige data” hetzelfde te behandelen. Begin met opsommen wat de app opslaat en wat er gebeurt als het lekt.
Tokens zijn niet hetzelfde als wachtwoorden. Access- en refresh-tokens zijn bedoeld om opgeslagen te worden zodat de gebruiker ingelogd blijft, maar ze zijn nog steeds waardevolle geheimen. Wachtwoorden mogen niet opgeslagen worden. Als je inloggen support, bewaar alleen wat strikt nodig is voor de sessie (meestal tokens) en vertrouw op de server voor wachtwoordverificatie.
Sleutels zijn een andere klasse. API-sleutels, signing keys en encryptiesleutels kunnen hele systemen ontgrendelen, niet alleen één gebruikersaccount. Als iemand ze uit een apparaat haalt, kunnen ze misbruik op schaal automatiseren. Een goede vuistregel: als een waarde buiten de app gebruikt kan worden om de app te imiteren of data te ontsleutelen, behandel het als hoger risico dan een gebruikerstoken.
PII is alles dat een persoon kan identificeren: e-mail, telefoon, thuisadres, klantnotities, overheids-ID's, gezondheidsdata. Zelfs velden die onschuldig lijken worden gevoelig in combinatie.
Een snelle labelindeling die goed werkt in de praktijk:
- Sessiegeheimen: access token, refresh token, sessiecookie
- App-geheimen: API-sleutels, signing keys, encryptiesleutels (vermijd deze op apparaten als het kan)
- Gebruikersdata (PII): profielgegevens, identificatoren, documenten, medische of financiële info
- Apparaat- en analytics-ID's: advertising ID, device ID, install ID (nog steeds gevoelig onder veel policies)
Android Keystore: wanneer het te gebruiken
Android Keystore is het beste wanneer je geheimen moet beschermen die nooit in platte tekst van het apparaat weg mogen. Het is een kluis voor cryptografische sleutels, geen database voor je daadwerkelijke data.
Waar het goed in is: sleutels genereren en bewaren die gebruikt worden voor encryptie/decryptie, signeren of verifiëren. Gewoonlijk versleutel je een token of offline data elders, en een Keystore-sleutel is wat het ontgrendelt.
Hardware-backed sleutels: wat dat echt betekent
Op veel apparaten kunnen Keystore-sleutels hardware-backed zijn. Dat betekent dat sleutelbewerkingen plaatsvinden binnen een beschermde omgeving en het sleutelmateriaal niet te extraheren is. Het verlaagt het risico van malware die appbestanden kan lezen.
Hardware-backed is niet gegarandeerd op elk apparaat en het gedrag verschilt per model en Android-versie. Bouw alsof sleutelbewerkingen kunnen falen.
Gebruikersauthenticatie als poort
Keystore kan vereisen dat de gebruiker aanwezig is voordat een sleutel gebruikt mag worden. Zo koppel je toegang aan biometrie of apparaatreferenties. Bijvoorbeeld: je kunt een exporttoken versleutelen en het alleen ontsleutelen nadat de gebruiker bevestigt met vingerafdruk of PIN.
Keystore is een sterke keuze wanneer je een niet-exporteerbare sleutel wilt, wanneer je biometrische of apparaat-credential goedkeuring wilt voor gevoelige acties, en wanneer je per-apparaat geheimen wilt die niet synchroniseren of met backups meegaan.
Plan voor valkuilen: sleutels kunnen ongeldig worden na veranderingen in het vergrendelscherm, biometrische wijzigingen of security-events. Verwacht fouten en implementeer een nette fallback: detecteer ongeldige sleutels, wis versleutelde blobs en vraag de gebruiker opnieuw in te loggen.
EncryptedSharedPreferences: wanneer het genoeg is
EncryptedSharedPreferences is een goede default voor een klein aantal geheimen in key-value vorm. Het is “SharedPreferences, maar versleuteld”, zodat iemand niet zomaar een bestand kan openen en waarden kan lezen.
Onder de motorkap gebruikt het een master key om waarden te versleutelen en ontsleutelen. Die master key wordt beschermd door Android Keystore, dus je app slaat de ruwe encryptiesleutel niet in platte tekst op.
Het is meestal genoeg voor een paar kleine items die je vaak leest, zoals access- en refresh-tokens, sessie-IDs, apparaat-ID's, environment-flags of kleine staatjes zoals de laatste synchronisatietijd. Het is ook acceptabel voor piepkleine stukjes gebruikersdata als je ze echt moet bewaren, maar het moet geen stortplaats voor PII worden.
Het is geen goede keuze voor iets groots of gestructureerd. Als je offline lijsten, zoeken of query's nodig hebt (klanten, tickets, orders), wordt EncryptedSharedPreferences traag en onhandig. Dan wil je een versleutelde database.
Een eenvoudige regel: als je alle opgeslagen sleutels op één scherm kunt opsommen, is EncryptedSharedPreferences waarschijnlijk goed genoeg. Als je rijen en query's nodig hebt, ga dan door naar een database.
Database-encryptie: wanneer je het nodig hebt
Database-encryptie is belangrijk wanneer je meer dan een klein instellingetje of één token opslaat. Als je app bedrijfsdata op het apparaat bewaart, ga ervan uit dat het uit een verloren telefoon gehaald kan worden tenzij je het beschermt.
Een database is logisch wanneer je offline toegang tot records nodig hebt, lokale caching voor performance, historie/audit-trails of lange notities en bijlagen.
Twee veelvoorkomende encryptie-benaderingen
Volledige database-encryptie (vaak SQLCipher-stijl) versleutelt het hele bestand in rust. Je app opent het met een sleutel. Dit is eenvoudig te redeneren omdat je je niet hoeft te herinneren welke kolommen beschermd zijn.
App-laag veldencryptie versleutelt alleen bepaalde velden voordat je schrijft, en ontsleutelt na het lezen. Dit kan werken als de meeste records niet gevoelig zijn, of als je een specifieke databasestructuur wilt behouden zonder het bestandsformaat te veranderen.
Afwegingen: vertrouwelijkheid versus zoeken en sorteren
Volledige database-encryptie verbergt alles op schijf, maar zodra de database ontgrendeld is, kan je app normaal query's uitvoeren.
Veldencryptie beschermt specifieke kolommen, maar je verliest makkelijk zoeken en sorteren op versleutelde waarden. Sorteren op een versleutelde achternaam werkt niet betrouwbaar en zoeken wordt ofwel “zoeken na ontsleuteling” (traag) of “extra indexen opslaan” (meer complexiteit en potentiële lekken).
Basisprincipes van key-management
De databasesleutel mag nooit hardcoded of met de app worden meegeleverd. Een veelgebruikt patroon is een willekeurige databasesleutel genereren en die vervolgens ingepakt (versleuteld) opslaan met een sleutel in Android Keystore. Bij logout kun je de ingepakte sleutel verwijderen en de lokale database als wegwerp behandelen, of hem behouden als de app offline moet blijven werken tussen sessies.
Hoe te kiezen: een praktische vergelijking
Je kiest niet “de meest veilige” optie in het algemeen. Je kiest de veiligste optie die past bij hoe je app de data gebruikt.
Vragen die echt het juiste besluit sturen:
- Hoe vaak wordt de data gelezen (bij elke start of zelden)?
- Hoeveel data is het (een paar bytes of duizenden records)?
- Wat gebeurt er als het lekt (irritant, kostbaar, juridisch meldingsplichtig)?
- Heb je offline toegang, zoek- of sorteervermogen nodig?
- Heb je compliance-eisen (retentie, audit, encryptieregels)?
Een werkbare mapping:
- Tokens (OAuth access en refresh tokens) horen meestal in EncryptedSharedPreferences omdat ze klein zijn en vaak gelezen worden.
- Sleutelmateriaal hoort waar mogelijk in Android Keystore om de kans te verkleinen dat het van het apparaat gekopieerd kan worden.
- PII en offline bedrijfsdata hebben meestal database-encryptie nodig zodra je meer dan een paar velden opslaat of offline lijsten en filtering nodig hebt.
Gemengde data is normaal in zakelijke apps. Een praktisch patroon is een random data-encryptiesleutel (DEK) genereren voor je lokale database of bestand, alleen de ingepakte DEK bewaren met een Keystore-backed sleutel, en roteren wanneer nodig.
Als je twijfelt, kies het eenvoudigere veilige pad: bewaar minder. Vermijd offline PII tenzij het echt nodig is en houd sleutels in Keystore.
Stappenplan: implementeer veilige opslag in een Kotlin-app
Begin met het opschrijven van elke waarde die je van plan bent op het apparaat op te slaan en de exacte reden waarom het daar moet zijn. Dat is de snelste manier om “voor het geval” opslag te voorkomen.
Voordat je code schrijft, bepaal je regels: hoe lang elk item moet blijven, wanneer het vervangen moet worden en wat “logout” echt betekent. Een access token kan 15 minuten geldig zijn, een refresh token langer, en offline PII kan een strikte “verwijder na 30 dagen” regel nodig hebben.
Implementatie die onderhoudbaar blijft:
- Maak een enkele “SecureStorage” wrapper zodat de rest van de app nooit direct SharedPreferences, Keystore of de database aanraakt.
- Zet elk item op de juiste plek: tokens in EncryptedSharedPreferences, encryptiesleutels beschermd door Android Keystore, en grotere offline datasets in een versleutelde database.
- Handel fouten doelbewust af. Als veilige opslag faalt, faal gesloten. Val niet stilletjes terug op platte opslag.
- Voeg diagnostiek toe zonder data te lekken: log gebeurtenistypen en foutcodes, nooit tokens, sleutels of gebruikersgegevens.
- Koppel deletiepaden: logout, accountverwijdering en “data wissen” moeten door dezelfde wis-routine lopen.
Test vervolgens de saaie gevallen die in productie beveiliging breken: herstellen uit backup, upgraden vanaf een oudere appversie, veranderingen in apparaatvergrendeling, migreren naar een nieuwe telefoon. Zorg dat gebruikers niet vastlopen in een loop waarbij opgeslagen data niet kan worden ontsleuteld maar de app blijft proberen.
Schrijf tenslotte de beslissingen op één pagina zodat het hele team ze kan volgen: wat wordt opgeslagen, waar, retentieperiodes en wat er moet gebeuren wanneer ontsleuteling faalt.
Veelgemaakte fouten die veilige opslag breken
De meeste fouten gaan niet over het kiezen van de verkeerde bibliotheek. Ze gebeuren wanneer één klein snelkoppeltje geheimen stilletjes kopieert naar plekken waar je ze niet wilde opslaan.
Het grootste alarm is een refresh token (of een langlevend sessietoken) opgeslagen in platte tekst ergens: SharedPreferences, een bestand, een “tijdelijke” cache of een kolom in een lokale database. Als iemand een backup krijgt, een geroot apparaat dump of een debug build-artifact, kan dat token het wachtwoord overleven.
Geheimen lekken ook via zichtbaarheid, niet opslag. Logs met volledige request-headers, tokens printen tijdens debugging, of het toevoegen van “nuttige” context aan crashrapporten en analytics kan credentials buiten het apparaat blootleggen. Behandel logs als publiek.
Sleutelhanding is een andere veelvoorkomende kloof. Eén sleutel voor alles vergroten de blast radius. Geen rotatie betekent dat oude compromitteringen geldig blijven. Neem een plan op voor key versioning, rotatie en wat er met oude versleutelde data gebeurt.
Vergeet de paden “buiten de kluis” niet
Encryptie stopt backups er niet van om lokale app-data te kopiëren. Het stopt screenshots of schermopnames niet die PII vastleggen. Het stopt debug-builds met versoepelde instellingen niet, of exportfuncties (CSV/share sheets) die gevoelige velden lekken. Klembordgebruik kan ook eenmalige codes of rekeningnummers lekken.
Ook lost encryptie geen autorisatieproblemen op. Als je app PII blijft tonen na logout, of gecachte data toegankelijk houdt zonder opnieuw te authenticeren, is dat een access-control bug. Vergrendel de UI, wis gevoelige caches bij logout en controleer permissies opnieuw voordat je beschermde data toont.
Operationele details: lifecycle, logout en randgevallen
Veilige opslag is niet alleen waar je geheimen neerzet. Het is hoe ze zich gedragen in de tijd: wanneer de app slaapt, wanneer een gebruiker uitlogt en wanneer het apparaat vergrendeld is.
Voor tokens: plan de volledige lifecycle. Access tokens moeten kortlevend zijn. Refresh tokens behandel je als wachtwoorden. Als een token verlopen is, vernieuw het stilletjes. Als een refresh faalt (revoked, wachtwoord gewijzigd, apparaat verwijderd), stop met retry-loops en forceer een schone aanmelding. Ondersteun ook server-side revocatie. Lokale opslag kan niet helpen als je gestolen credentials nooit ongeldig maakt.
Gebruik biometrie voor re-auth, niet voor alles. Vraag erom wanneer de actie echt risico heeft (PII bekijken, data exporteren, uitbetalingsgegevens wijzigen, een eenmalige sleutel tonen). Vraag niet bij elke app-open.
Bij logout: wees streng en voorspelbaar:
- Maak eerst in-memory kopieën leeg (tokens gecached in singletons, interceptors of ViewModels).
- Wis opgeslagen tokens en sessiestaat (inclusief refresh tokens).
- Verwijder of invalideer lokale encryptiesleutels als je ontwerp dat ondersteunt.
- Verwijder offline PII en gecachte API-responses.
- Schakel achtergrondtaken uit die data opnieuw zouden kunnen ophalen.
Randgevallen zijn belangrijk in zakelijke apps: meerdere accounts op één apparaat, work profiles, backup/restore, apparaat-naar-apparaat overdracht en gedeeltelijke uitlogprocedures (van bedrijf/workspace wisselen in plaats van volledig uitloggen). Test force stop, OS-upgrades en klokwijzigingen omdat tijdsafwijking expiratie-logica kan breken.
Detectie van manipulatie is een afweging. Basischecks (debuggable builds, emulatorflags, simpele root-signalen, Play Integrity verdicts) beperken occasionele misbruikers, maar vastberaden aanvallers kunnen ze omzeilen. Behandel tamper-signalen als risicofactoren: beperk offline toegang, eis re-auth en log het event.
Snelle checklist voordat je uitrolt
Gebruik dit voorafgaand aan release. Het richt zich op plekken waar veilige opslag in echte zakelijke apps faalt.
- Ga uit van een vijandig apparaat. Als een aanvaller een geroot apparaat of volledige device-image heeft, kunnen ze tokens, sleutels of PII lezen uit appbestanden, preferences, logs of screenshots? Als het antwoord “misschien” is, verplaats geheimen naar Keystore-backed bescherming en houd de payload versleuteld.
- Controleer backups en apparaattransfers. Houd gevoelige bestanden buiten Android Auto Backup, cloudbackups en device-to-device transfers. Als het verliezen van een sleutel herstel zou breken, plan de recovery (herauthenticatie en opnieuw downloaden in plaats van proberen te ontsleutelen).
- Zoek naar per ongeluk platte tekst op schijf. Kijk naar temp-bestanden, HTTP-caches, crashrapporten, analytics events en image-caches die PII of tokens kunnen bevatten. Controleer debug-logging en JSON-dumps.
- Verlopen en roteer. Access tokens kort houden, refresh tokens beschermen en server-side sessies intrekbaar maken. Definieer key-rotatie en wat de app doet wanneer een token wordt geweigerd (clear, re-auth, één retry).
- Gedrag bij herinstallatie en apparaatwissel. Test uninstall en reinstall, en open offline daarna. Als Keystore-sleutels weg zijn, moet de app veilig falen (wis versleutelde data, toon sign-in, voorkom gedeeltelijke reads die staat corrupt maken).
Een snelle validatie is een “bad day” test: een gebruiker logt uit, verandert het wachtwoord, herstelt een backup naar een nieuw toestel en opent de app in een vliegtuig. Het resultaat moet voorspelbaar zijn: ofwel decrypt data voor de juiste gebruiker, of het wordt gewist en na inloggen opnieuw opgehaald.
Voorbeeldscenario: een zakelijke app die PII offline opslaat
Stel je een field sales-app voor die gebruikt wordt in gebieden met slechte dekking. Vertegenwoordigers loggen ’s ochtends één keer in, bladeren offline door toegewezen klanten, voegen aantekeningen toe en synchroniseren later. Dit is waar een opslagchecklist geen theorie meer is maar echt lekken voorkomt.
Een praktische verdeling:
- Access token: houd kortlevend en bewaar in EncryptedSharedPreferences.
- Refresh token: bescherm strenger en poort toegang via Android Keystore.
- Klant-PII (namen, telefoons, adressen): bewaar in een versleutelde lokale database.
- Offline notities en bijlagen: bewaar in de versleutelde database met extra voorzichtigheid bij export en delen.
Voeg nu twee features toe en het risico verandert.
Als je “remember me” toevoegt, wordt de refresh token de hoofddeur terug naar het account. Behandel het als een wachtwoord. Afhankelijk van je gebruikers kun je vereisen dat apparaatontgrendeling (PIN/patroon/biometrie) nodig is voordat je het ontsleutelt.
Als je offline modus toevoegt, bescherm je niet langer alleen een sessie. Je beschermt een volledige klantenlijst die op zichzelf waardevol is. Dat duwt je meestal richting database-encryptie plus duidelijke logout-regels: wis lokale PII, houd alleen wat nodig is voor de volgende login en annuleer achtergrondsync.
Test op echte apparaten, niet alleen emulators. Verifieer minimaal vergrendel-/ontgrendelgedrag, herinstallatiegedrag, backup/restore en scheiding tussen meerdere gebruikers of work profiles.
Volgende stappen: maak het een herhaalbare teamgewoonte
Veilige opslag werkt alleen als het een gewoonte is. Schrijf een korte opslagpolicy die je team kan volgen: wat waar hoort (Keystore, EncryptedSharedPreferences, versleutelde database), wat nooit opgeslagen wordt en wat bij logout moet worden gewist.
Maak het onderdeel van dagelijkse levering: definition of done, code review en releasechecks.
Een lichte reviewer-checklist:
- Elk opgeslagen item is gelabeld (token, sleutelmateriaal of PII).
- De opslagkeuze is gerechtvaardigd in codecommentaar.
- Logout en accountwissel verwijderen de juiste data (en alleen die data).
- Fouten en logs printen nooit geheimen of volledige PII.
- Iemand is eigenaar van de policy en houdt deze actueel.
Als je team AppMaster (appmaster.io) gebruikt om zakelijke apps te bouwen en Kotlin-source voor de Android-client te exporteren, behoud dan dezelfde SecureStorage-wrapper-aanpak zodat gegenereerde en aangepaste code één consistente policy volgen.
Begin met een klein proof-of-concept
Bouw een kleine POC die één auth-token en één PII-record opslaat (bijv. een klanttelefoonnummer dat offline nodig is). Test daarna fresh install, upgrade, logout, wijziging van vergrendelscherminstellingen en data wissen. Breid pas uit als wis-gedrag correct en herhaalbaar is.
FAQ
Begin met precies op te sommen wat je opslaat en waarom. Plaats kleine sessiegeheimen zoals access- en refresh-tokens in EncryptedSharedPreferences, bewaar cryptografische sleutels in Android Keystore en gebruik een versleutelde database voor offline zakelijke gegevens en PII zodra je meer dan een paar velden hebt of zoek-/filterfunctionaliteit nodig hebt.
Plain SharedPreferences schrijft waarden naar een bestand dat vaak gelezen kan worden via devicebackups, toegang op een geroot apparaat of debug-artifacten. Als de waarde een token of PII is, maakt het behandelen als een normale instelling het veel makkelijker om die buiten de app te kopiëren en hergebruiken.
Gebruik Android Keystore om cryptografische sleutels te genereren en vast te houden die niet exporteerbaar mogen zijn. Je gebruikt die sleutels doorgaans om andere data te versleutelen (tokens, database-sleutels, bestanden) en je kunt optioneel gebruikersauthenticatie (biometrie of apparaatreferentie) vereisen voordat de sleutel gebruikt mag worden.
Het betekent dat sleutelbewerkingen in beschermde hardware kunnen plaatsvinden, zodat de sleutelmaterialen moeilijker te extraheren zijn, zelfs als een aanvaller appbestanden kan lezen. Verwacht niet dat dit altijd beschikbaar is of overal hetzelfde werkt; ontwerp voor fouten en implementeer een herstelstroom als sleutels onbeschikbaar of ongeldig worden.
Meestal is het genoeg voor een klein setje vaak gelezen sleutel-waardegeheimen zoals access/refresh-tokens, sessie-ID's en kleine staatjes. Het is geen goede keuze voor grote data, gestructureerde offlinerecords of iets dat je moet doorzoeken en filteren zoals klanten, tickets of orders.
Kies een versleutelde database wanneer je offline zakelijke data of PII op schaal opslaat, zoek-/filterfunctionaliteit nodig hebt, of historie offline wilt bewaren. Het verlaagt het risico dat een verloren apparaat volledige klantlijsten of notities blootgeeft, terwijl de app toch offline kan werken met een duidelijke sleutelstrategie.
Volledige database-encryptie beschermt het hele bestand in rust en is makkelijker te overzien omdat je niet hoeft bij te houden welke kolommen gevoelig zijn. Veldencryptie kan werken voor een paar kolommen, maar maakt zoeken en sorteren lastig en het is makkelijk om per ongeluk data te lekken via indexen of afgeleide velden.
Genereer een willekeurige database-sleutel en bewaar die alleen in ingepakte vorm (versleuteld) met een Keystore-ondersteunde sleutel. Hardcode nooit sleutels of stuur ze mee in de app, en bepaal wat er bij logout of key-invalidatie moet gebeuren (vaak: de ingepakte sleutel verwijderen en lokale data als wegwerp behandelen).
Sleutels kunnen ongeldig worden gemaakt door wijzigingen in het vergrendelscherm of biometrie, OS-security events, of restore/migraties. Handel dit expliciet af: detecteer decrypt-fouten, wis veilig de versleutelde blobs of lokale database en vraag de gebruiker opnieuw in te loggen in plaats van te blijven proberen of terug te vallen naar platte opslag.
De meeste lekken gebeuren “buiten de kluis”: logs, crashreports, analytics events, debug-prints, HTTP-caches, screenshots, klembordgebruik en backup/restore-paden. Behandel logs als publiek, registreer nooit tokens of volledige PII, zet exportpaden uit en zorg dat logout zowel opgeslagen data als in-memory kopieën wist.


