Checklist di archiviazione sicura in Kotlin per token, chiavi e PII
Checklist di archiviazione sicura in Kotlin per scegliere tra Android Keystore, EncryptedSharedPreferences e crittografia del database per token, chiavi e PII.

Cosa stai cercando di proteggere (in termini semplici)
La storage sicura in un'app business significa una cosa: se qualcuno prende il telefono (o i file dell'app), non dovrebbe comunque poter leggere o riutilizzare ciò che hai salvato. Questo include i dati a riposo (su disco) e anche i segreti che possono fuoriuscire tramite backup, log, report di crash o strumenti di debug.
Un semplice test mentale: cosa potrebbe fare uno sconosciuto se aprisse la cartella di storage della tua app? In molte app, gli oggetti più preziosi non sono foto o impostazioni. Sono piccole stringhe che sbloccano l'accesso.
Lo storage sul dispositivo spesso include token di sessione (per mantenere l'utente connesso), refresh token, API key, chiavi di crittografia, dati personali (PII) come nomi e email, e record business in cache usati offline (ordini, ticket, note cliente).
Ecco modalità di fallimento comuni nella realtà:
- Un dispositivo smarrito o rubato viene esaminato e i token vengono copiati per impersonare un utente.
- Malware o un'app “helper” leggono file locali su un dispositivo rootato o tramite trucchi di accessibilità.
- I backup automatici spostano i dati della tua app in posti che non avevi previsto.
- Build di debug loggano token, li inseriscono nei report di crash o disabilitano controlli di sicurezza.
Per questo motivo "mettilo in SharedPreferences" non va bene per nulla che conceda accesso (token) o possa danneggiare utenti e azienda (PII). Le SharedPreferences normali sono come scrivere segreti su un post-it dentro l'app: comodo, e facile da leggere se qualcuno ci mette le mani.
Il punto di partenza utile è nominare ogni elemento salvato e farsi due domande: sblocca qualcosa? E sarebbe un problema se diventasse pubblico? Il resto (Keystore, preferenze cifrate, database cifrato) segue da lì.
Classifica i tuoi dati: token, chiavi e PII
La storage sicura diventa più semplice quando smetti di trattare tutti i "dati sensibili" allo stesso modo. Inizia elencando ciò che l'app salva e cosa succederebbe se trapelasse.
Token non sono la stessa cosa delle password. Access token e refresh token sono pensati per essere salvati così l'utente resta connesso, ma restano segreti di alto valore. Le password non dovrebbero essere salvate affatto. Se ti serve il login, conserva solo ciò che è necessario per mantenere la sessione (di solito i token) e fai affidamento sul server per le verifiche della password.
Chiavi sono una classe diversa. API key, chiavi di firma e chiavi di crittografia possono sbloccare interi sistemi, non solo un singolo account utente. Se qualcuno le estrae da un dispositivo, può automatizzare abusi su larga scala. Una buona regola: se un valore può essere usato fuori dall'app per impersonare l'app o decriptare dati, trattalo come più rischioso di un token utente.
PII è tutto ciò che può identificare una persona: email, telefono, indirizzo, note cliente, documenti d'identità, dati sanitari. Anche campi che sembrano innocui diventano sensibili quando combinati.
Un sistema di etichettatura rapido e pratico:
- Segreti di sessione: access token, refresh token, cookie di sessione
- Segreti dell'app: API key, chiavi di firma, chiavi di crittografia (evita di metterle sui dispositivi dove possibile)
- Dati utente (PII): dettagli profilo, identificatori, documenti, informazioni sanitarie o finanziarie
- ID dispositivo e analytics: advertising ID, device ID, install ID (ancora sensibili secondo molte policy)
Android Keystore: quando usarlo
Android Keystore è ideale quando devi proteggere segreti che non dovrebbero mai lasciare il dispositivo in forma leggibile. È una cassaforte per chiavi crittografiche, non un database per i tuoi dati reali.
Cosa fa bene: generare e conservare chiavi usate per crittografare, decrittografare, firmare o verificare. Tipicamente cifri un token o dati offline altrove, e una chiave nel Keystore è ciò che li sblocca.
Chiavi con supporto hardware: cosa significa davvero
Su molti dispositivi le chiavi del Keystore possono essere hardware-backed. Questo significa che le operazioni sulla chiave avvengono in un ambiente protetto e il materiale della chiave non può essere estratto. Riduce il rischio da malware che può leggere i file dell'app.
L'hardware-backed non è garantito su ogni dispositivo e il comportamento varia per modello e versione Android. Progetta come se le operazioni sulle chiavi potessero fallire.
Gate di autenticazione utente
Il Keystore può richiedere la presenza dell'utente prima che una chiave venga usata. Così puoi legare l'accesso alla biometria o alle credenziali del dispositivo. Per esempio, puoi cifrare un token di esportazione e decifrarlo solo dopo che l'utente conferma con l'impronta o il PIN.
Keystore è una scelta forte quando vuoi una chiave non esportabile, quando vuoi approvazione biometrica o credenziali del dispositivo per azioni sensibili, e quando vuoi segreti per dispositivo che non devono sincronizzarsi o viaggiare con i backup.
Pianifica i punti deboli: le chiavi possono essere invalidate dopo cambi del blocco schermo, cambi biometrici o eventi di sicurezza. Aspettati fallimenti e implementa un fallback pulito: rileva chiavi invalide, cancella i blob cifrati e chiedi all'utente di effettuare nuovamente l'accesso.
EncryptedSharedPreferences: quando è sufficiente
EncryptedSharedPreferences è un buon default per un piccolo insieme di segreti in forma key-value. È "SharedPreferences, ma cifrate", quindi qualcuno non può semplicemente aprire un file e leggere i valori.
Sotto il cofano usa una master key per cifrare e decifrare i valori. Questa master key è protetta da Android Keystore, quindi la tua app non memorizza la chiave di cifratura in chiaro.
Di solito è sufficiente per pochi elementi piccoli letti spesso, come access e refresh token, ID di sessione, ID dispositivo, flag d'ambiente o piccoli stati come l'ultima ora di sync. Va bene anche per pezzetti di dati utente se davvero devi conservarli, ma non dovrebbe diventare il tuo deposito per tutta la PII.
Non è adatto per niente di grande o strutturato. Se ti servono liste offline, ricerche o query per campi (clienti, ticket, ordini), EncryptedSharedPreferences diventa lento e scomodo. È il momento di passare a un database cifrato.
Una regola semplice: se riesci a elencare ogni chiave salvata in una schermata, probabilmente EncryptedSharedPreferences va bene. Se ti servono righe e query, vai oltre.
Crittografia del database: quando serve
La crittografia del database è importante quando salvi più di una piccola impostazione o un token. Se la tua app tiene dati business sul dispositivo, presumine l'estrazione da un telefono perso a meno che non lo protegga.
Un database ha senso quando ti serve accesso offline a record, caching locale per performance, cronologia/log o note e allegati lunghi.
Due approcci comuni alla crittografia
Crittografia completa del database (spesso in stile SQLCipher) cifra l'intero file a riposo. La tua app lo apre con una chiave. È facile da ragionare perché non devi ricordare quali colonne sono protette.
Crittografia a livello di app/campo cifra solo alcuni campi prima di scrivere, poi li decifra dopo la lettura. Può funzionare se la maggior parte dei record non è sensibile, o se vuoi mantenere una struttura del database senza cambiare il formato del file.
Tradeoff: riservatezza vs ricerca e ordinamento
La crittografia completa nasconde tutto su disco, ma una volta sbloccato il database la tua app può interrogare normalmente.
La crittografia a campo protegge colonne specifiche, ma perdi ricerca e ordinamento semplici sui valori cifrati. Ordinare per un cognome cifrato non funziona in modo affidabile, e la ricerca diventa o "cerca dopo decrittazione" (lenta) o "memorizza indici extra" (più complessità e potenziali fughe).
Fondamenti di gestione delle chiavi
La chiave del database non dovrebbe mai essere hardcodata o inclusa nell'app. Un pattern comune è generare una chiave casuale per il database, poi conservarla wrapped (cifrata) usando una chiave custodita in Android Keystore. Al logout puoi cancellare la chiave wrapped e trattare il database locale come usa-e-getta, oppure mantenerlo se l'app deve funzionare offline tra sessioni.
Come scegliere: un confronto pratico
Non stai scegliendo "l'opzione più sicura" in generale. Stai scegliendo l'opzione più sicura che si adatta a come la tua app usa i dati.
Domande che guidano davvero la scelta:
- Quanto spesso vengono letti i dati (ad ogni avvio o raramente)?
- Quanto sono grandi i dati (pochi byte o migliaia di record)?
- Cosa succede se trapelano (fastidio, costo, obbligo di notifica legale)?
- Ti serve accesso offline, ricerca o ordinamento?
- Hai requisiti di compliance (conservazione, audit, regole di cifratura)?
Una mappatura pratica:
- Token (OAuth access e refresh token) di solito vanno in
EncryptedSharedPreferencesperché sono piccoli e letti spesso. - Materiale chiave dovrebbe vivere in Android Keystore quando possibile per ridurre la possibilità che venga copiato dal dispositivo.
- PII e dati business offline solitamente richiedono la crittografia del database quando memorizzi più di poche colonne o hai bisogno di filtri offline.
Dati misti sono normali nelle app business. Un pattern pratico è generare una chiave casuale di cifratura dei dati (DEK) per il database o i file locali, conservare solo il DEK wrapped usando una chiave Keystore e ruotarla quando necessario.
Se non sei sicuro, scegli la strada più semplice e sicura: conserva meno. Evita PII offline a meno che non sia davvero necessario e tieni le chiavi nel Keystore.
Passo dopo passo: implementare lo storage sicuro in un'app Kotlin
Inizia scrivendo ogni valore che prevedi di salvare sul dispositivo e la ragione precisa per cui deve starci. È il modo più veloce per evitare storage "giusto in caso".
Prima di scrivere codice, decidi le regole: quanto deve vivere ogni elemento, quando va sostituito e cosa significa veramente "logout". Un access token può scadere in 15 minuti, un refresh token può durare più a lungo e la PII offline potrebbe avere una regola ferma di "cancellare dopo 30 giorni".
Implementazione mantenibile:
- Crea un singolo wrapper "SecureStorage" così il resto dell'app non tocca direttamente SharedPreferences, Keystore o il database.
- Metti ogni elemento al posto giusto: token in
EncryptedSharedPreferences, chiavi di cifratura protette da Android Keystore e dataset offline più grandi in un database cifrato. - Gestisci i fallimenti intenzionalmente. Se lo storage sicuro fallisce, fallisci in modo chiuso. Non ricadere silenziosamente su storage in chiaro.
- Aggiungi diagnostica senza esporre dati: logga tipi di evento e codici di errore, mai token, chiavi o dettagli utente.
- Collega le vie di cancellazione: logout, rimozione account e "cancella dati app" dovrebbero convogliare nella stessa routine di wipe.
Poi testa i casi noiosi che rompono lo storage sicuro in produzione: restore da backup, aggiornamento da una versione precedente dell'app, cambi delle impostazioni di blocco del dispositivo, migrazione a un nuovo telefono. Assicurati che gli utenti non rimangano bloccati in un loop dove i dati salvati non si possono decriptare ma l'app continua a riprovare.
Infine, annota le decisioni in una pagina che tutto il team possa seguire: cosa è salvato, dove, periodi di retention e cosa succede quando la decrittazione fallisce.
Errori comuni che rompono lo storage sicuro
La maggior parte dei fallimenti non riguarda la libreria scelta. Succede quando una piccola scorciatoia copia segreti in posti che non volevi.
Il più grande segnale d'allarme è un refresh token (o token a lunga durata) salvato in chiaro da qualche parte: SharedPreferences, un file, una cache "temporanea" o una colonna di un database locale. Se qualcuno prende un backup, un dump di un dispositivo rootato o un artefatto di debug, quel token può sopravvivere alla password.
I segreti trapelano anche tramite visibilità, non solo storage. Loggare header completi di richieste, stampare token durante il debug o aggiungere contesto "utile" a report di crash e eventi di analytics può esporre credenziali fuori dal dispositivo. Tratta i log come pubblici.
La gestione delle chiavi è un altro punto debole comune. Usare una sola chiave per tutto aumenta l'impatto di una compromissione. Non ruotare mai le chiavi significa che compromessi vecchi restano validi. Includi un piano per versioning delle chiavi, rotazione e cosa succede ai dati cifrati vecchi.
Non dimenticare i percorsi "fuori dalla cassaforte"
La crittografia non impedisce ai backup cloud di copiare i dati locali dell'app. Non impedisce screenshot o registrazioni schermo che catturano PII. Non impedisce build di debug con impostazioni rilassate, o funzionalità di esportazione (CSV/share sheets) che perdono campi sensibili. Anche l'uso degli appunti può esporre codici monouso o numeri di conto.
Inoltre, la crittografia non risolve l'autorizzazione. Se la tua app mostra PII dopo che un utente ha fatto logout, o mantiene cache accessibili senza ricontrollare le credenziali, è un bug di controllo accessi. Blocca l'interfaccia, cancella le cache sensibili al logout e ricontrolla i permessi prima di mostrare dati protetti.
Dettagli operativi: lifecycle, logout e casi ai margini
Lo storage sicuro non è solo dove metti i segreti. È come si comportano nel tempo: quando l'app va in sleep, quando l'utente fa logout e quando il dispositivo è bloccato.
Per i token, pianifica l'intero ciclo di vita. Gli access token dovrebbero essere a vita corta. I refresh token vanno trattati come password. Se un token è scaduto, rinnovalo silenziosamente. Se il refresh fallisce (revocato, password cambiata, dispositivo rimosso), interrompi i retry loop e forza un nuovo accesso pulito. Supporta anche la revoca lato server. Lo storage locale perfetto non serve a nulla se non invalidi credenziali rubate.
Usa la biometria per ri-autenticazioni, non per tutto. Chiedi la biometria quando l'azione ha rischio reale (visualizzare PII, esportare dati, cambiare dettagli di payout, mostrare una chiave monouso). Non richiederla a ogni apertura dell'app.
Al logout, sii severo e prevedibile:
- Pulisci prima le copie in memoria (token in singleton, interceptor o ViewModel).
- Cancella i token e lo stato di sessione memorizzati (inclusi i refresh token).
- Rimuovi o invalida le chiavi di cifratura locali se il tuo design lo supporta.
- Elimina PII offline e risposte API in cache.
- Disabilita job in background che potrebbero riscaricare dati.
I casi ai margini contano nelle app business: più account su un dispositivo, work profile, backup/restore, trasferimenti device-to-device e logout parziali (cambio workspace anziché sign-out completo). Testa force stop, upgrade OS e cambi d'orologio perché la deriva del tempo può rompere la logica di expiry.
La rilevazione di manomissioni è un compromesso. Controlli base (build debuggable, flag emulator, segnali semplici di root, verdict Play Integrity) possono ridurre abusi casuali, ma attaccanti determinati possono aggirarli. Tratta i segnali di manomissione come input di rischio: limita l'accesso offline, richiedi ri-auth e registra l'evento.
Checklist rapida prima del rilascio
Usa questa lista prima del rilascio. Si concentra sui punti in cui lo storage sicuro fallisce nelle app business reali.
- Assumi che il dispositivo possa essere ostile. Se un attaccante ha un dispositivo rootato o un'immagine completa del device, può leggere token, chiavi o PII da file dell'app, preferenze, log o screenshot? Se la risposta è "forse", sposta i segreti su protezione Keystore-backed e tieni il payload cifrato.
- Controlla backup e trasferimenti dispositivo. Tieni file sensibili fuori da Android Auto Backup, backup cloud e trasferimenti device-to-device. Se perdere una chiave al restore rompe la decrittazione, pianifica il recupero (ri-auth e riscarica invece di tentare di decriptare).
- Cerca plaintext accidentale su disco. Controlla temp file, cache HTTP, report di crash, eventi analytics e cache immagini che potrebbero contenere PII o token. Verifica log di debug e dump JSON.
- Scadenza e rotazione. Gli access token dovrebbero essere a vita corta, i refresh token protetti e le sessioni server revocabili. Definisci la rotazione delle chiavi e cosa fa l'app quando un token viene respinto (cancellare, ri-auth, ritentare una volta).
- Comportamento su reinstall e cambio dispositivo. Testa disinstall e reinstall, poi apri offline. Se le chiavi Keystore sono sparite, l'app dovrebbe fallire in modo sicuro (cancellare i dati cifrati, mostrare il login, evitare letture parziali che corromgono lo stato).
Una validazione rapida è il test del "giorno no": un utente fa logout, cambia la password, ripristina un backup su un nuovo telefono e apre l'app in volo. Il risultato dovrebbe essere prevedibile: o i dati si decriptano per l'utente giusto, o vengono cancellati e riscaricati dopo il login.
Scenario di esempio: un'app field sales che conserva PII offline
Immagina un'app per vendite sul campo usata in aree con segnale scarso. I venditori effettuano il login una volta al mattino, consultano clienti assegnati offline, aggiungono note agli incontri e poi fanno sync più tardi. Qui la checklist di storage smette di essere teoria e inizia a prevenire fughe reali.
Una separazione pratica:
- Access token: mantienilo a vita corta e salvalo in
EncryptedSharedPreferences. - Refresh token: proteggilo più strettamente e vincola l'accesso tramite Android Keystore.
- PII cliente (nomi, telefoni, indirizzi): salva in un database locale cifrato.
- Note offline e allegati: memorizza nel database cifrato, con attenzione extra per esportazioni e condivisioni.
Ora aggiungi due feature e il rischio cambia.
Se aggiungi "ricordami", il refresh token diventa la porta principale per entrare nell'account. Trattalo come una password. A seconda degli utenti, potresti richiedere lo sblocco del dispositivo (PIN/pattern/biometria) prima di decriptarlo.
Se aggiungi la modalità offline, non stai più proteggendo solo una sessione. Proteggi un'intera lista clienti che può avere valore di per sé. Questo solitamente spinge verso crittografia del database più regole di logout chiare: wipe della PII locale, mantenere solo il necessario per il prossimo login e cancellare la sincronizzazione in background.
Testa su dispositivi reali, non solo emulatori. Al minimo, verifica comportamento lock/unlock, reinstall, backup/restore e separazione multi-utente o work profile.
Prossimi passi: trasformalo in un'abitudine ripetibile del team
Lo storage sicuro funziona solo quando è un'abitudine. Scrivi una policy breve che il team possa seguire: cosa va dove (Keystore, EncryptedSharedPreferences, database cifrato), cosa non va mai salvato e cosa deve essere wipe al logout.
Rendilo parte della delivery quotidiana: definition of done, code review e controlli di rilascio.
Una checklist leggera per il reviewer:
- Ogni elemento salvato è etichettato (token, materiale chiave o PII).
- La scelta di storage è giustificata nei commenti di codice.
- Logout e cambio account rimuovono i dati corretti (e solo quelli).
- Errori e log non stampano segreti o PII completi.
- Qualcuno è responsabile della policy e la mantiene aggiornata.
Se il tuo team usa AppMaster (appmaster.io) per costruire app business ed esporta Kotlin per il client Android, mantieni lo stesso approccio SecureStorage così codice generato e custom seguono una policy coerente.
Inizia con un piccolo proof-of-concept
Costruisci un piccolo POC che salva un auth token e un record PII (per esempio, il numero di telefono di un cliente necessario offline). Poi testa installazione pulita, upgrade, logout, cambi del blocco schermo e "cancella dati app". Espandi solo dopo che il comportamento di wipe è corretto e ripetibile.
FAQ
Inizia elencando esattamente cosa salvi e perché. Metti i segreti di sessione piccoli come access e refresh token in EncryptedSharedPreferences, conserva le chiavi crittografiche in Android Keystore e usa un database crittografato per record business offline e PII quando hai più di poche colonne o hai bisogno di query.
Le SharedPreferences normali salvano valori in un file che può spesso essere letto da backup del dispositivo, da accesso su device rooted o da artefatti di debug. Se il valore è un token o qualsiasi PII, trattarlo come una normale impostazione rende molto più facile copiarlo e riutilizzarlo fuori dall'app.
Usa Android Keystore per generare e tenere chiavi crittografiche che non devono essere estraibili. Tipicamente usi quelle chiavi per cifrare altri dati (token, chiavi del database, file) e puoi richiedere autenticazione utente (biometria o credenziali del dispositivo) prima che la chiave sia utilizzabile.
Significa che le operazioni con la chiave possono avvenire in hardware protetto in modo che il materiale della chiave sia più difficile da estrarre, anche se un attaccante può leggere i file dell'app. Non darlo per garantito su tutti i dispositivi o versioni Android; progetta per gestire fallimenti e prevedi un flusso di recupero quando le chiavi non sono disponibili o vengono invalidate.
Di solito è sufficiente per un piccolo insieme di segreti key-value letti frequentemente come access/refresh token, session ID e piccoli pezzi di stato. Non è adatto per dati grandi, strutturati o per tutto ciò che devi interrogare e filtrare come clienti, ticket o ordini.
Scegli un database crittografato quando conservi dati business offline o PII su larga scala, hai bisogno di query/ricerche/sort o mantieni cronologie per l'uso offline. Riduce il rischio che un dispositivo perso esponga intere liste di clienti o note, permettendo comunque all'app di funzionare offline con una strategia chiara per le chiavi.
La crittografia completa del database protegge l'intero file a riposo ed è più semplice da gestire perché non devi tracciare quali colonne sono sensibili. La crittografia a livello di campo può funzionare per poche colonne ma rende difficile ricerca e ordinamento, e si rischia di perdere dati tramite indici o campi derivati.
Genera una chiave casuale per il database, poi memorizzala solo in forma wrapped (cifrata) usando una chiave protetta da Keystore. Mai hardcodare o spedire chiavi nell'app, e decidi cosa succede al logout o quando una chiave viene invalidata (spesso: cancellare la chiave wrapped e considerare i dati locali come usa e getta).
Le chiavi possono essere invalidate da cambiamenti delle impostazioni del blocco schermo, cambi biometrici, eventi di sicurezza o scenari di restore/migrazione. Gestiscilo esplicitamente: rileva i fallimenti di decrittazione, pulisci i blob cifrati o il database locale in modo sicuro e invita l'utente a effettuare il login piuttosto che continuare a riprovare o cadere su storage in chiaro.
La maggior parte delle fughe avviene “fuori dalla cassaforte”: log, crash report, eventi di analytics, stampe di debug, cache HTTP, screenshot, appunti e percorsi di backup/restore. Tratta i log come pubblici, non registrare mai token o intere PII, disabilita esportazioni accidentali e assicurati che il logout pulisca sia i dati memorizzati sia le copie in memoria.


