Memorizza i token di autenticazione in uno storage criptato gestito dalla piattaforma, non nel localStorage della WebView o in semplici SharedPreferences. Su iOS, usa Keychain. Su Android, proteggi le chiavi con Android Keystore (software, TEE o StrongBox a seconda del dispositivo) e persiste il ciphertext con Jetpack DataStore + Tink. EncryptedSharedPreferences è deprecato in androidx.security.crypto; pianifica le migrazioni ora. Per Capacitor le app, combina la sicurezza del trasporto (HTTPS, pinning) con plugin di storage nativi invece di duplicare la crittografia in JavaScript.
Come abbiamo scelto questi pattern di storage
Ogni raccomandazione qui passa attraverso quattro filtri. Primo, hardware backing: il materiale segreto deve risiedere in Keychain/Keystore o essere criptato con chiavi che lo fanno. Secondo, un percorso di migrazione ufficiale — se Google o Apple hanno deprecato l'approccio, non lo raccomandiamo per i nuovi code. Terzo, compatibilità con Capacitor: il pattern deve funzionare oltre il confine del plugin senza esporre chiavi raw a JavaScript. Quarto, revocabilità: una credenziale compromessa deve poter essere invalidata lato server.
L'ultimo filtro è il motivo per cui la rotazione dei refresh token compare in tutto questo articolo. Nella nostra analisi dei dati sugli incidenti del 2024 raccolti da Chainalysis, i compromessi che coinvolgono chiavi private e credenziali hanno causato la maggior parte delle criptovalute rubate — inclusi oltre 305 milioni di dollari sottratti a DMM Bitcoin in Giappone e 235 milioni a WazirX in India — e il fallimento comune è stato un'eccessiva fiducia in un'unica credenziale valida indefinitamente (rapporto criminalità Chainalysis 2024). I token di auth mobile falliscono allo stesso modo: un bearer token a lunga durata in uno storage in chiaro è un invito aperto. Smetteremmo di raccomandare qualsiasi architettura che persiste una credenziale non rotante indefinitamente.
Principi applicabili a ogni piattaforma
La tabella seguente riassume il concetto: la logica di base è la separazione tra access e refresh token. Un token di accesso ha una durata breve — minuti, non giorni — ed è quello che il tuo API definisce 'presente'. Poiché il suo raggio d'azione è limitato e scade rapidamente, va conservato solo in memoria, ricostruito ad ogni avvio dell'app. Il refresh token è il segreto persistente che ti permette di generare nuovi access token, quindi è l'unico che merita un posto in uno storage criptato e protetto dall'hardware. Cicli di vita diversi richiedono modalità di archiviazione diverse; salvarli entrambi allo stesso modo è l'origine della maggior parte delle violazioni.
La sequenza completa a runtime è questa: l'app si avvia senza access token, chiama il tuo server di autenticazione con il refresh token salvato, riceve un nuovo access token in memoria e distrugge il vecchio refresh token perché il server lo ruota a ogni utilizzo. Se il server rileva un refresh token già utilizzato, lo considera prova di furto e revoca l'intera famiglia di token. Al logout, il client svuota lo storage locale e il server revoca l'autorizzazione — eliminare i dati localmente senza la revoca del server è speranza, non sicurezza.
| Best practice | Perché è importante |
|---|---|
| Access token a breve durata | Limita i danni in caso di leak di un token |
| Rotazione del refresh token | Invalida i refresh token rubati dopo l'uso legittimo |
| Solo HTTPS | Blocca l'intercettazione di rete casuale |
| Certificate pinning (se necessario) | Riduce il rischio di root errati o custom; vedi la guida OWASP sul certificate pinning |
| Nessun token in log, URL o analytics | I token negli strumenti di observability diventano data breach |
| Revoca al logout | L'invalidazione lato server batte la speranza che il client cancelli i dati |
@capgo/capacitor-ssl-pinning può fare il pinning TLS per Capacitor HTTP su iOS e Android quando servono controlli di trasporto più rigidi.
Android: Keystore e DataStore (non EncryptedSharedPreferences)
Google ha deprecato androidx.security.crypto helper, tra cui EncryptedSharedPreferences e MasterKey. Il riepilogo del pacchetto consiglia di usare KeyGenerator con il provider AndroidKeyStore per il nuovo materiale delle chiavi.
Pattern consigliato
Usa una pipeline basata su Tink end-to-end:
- Crea un keyset Tink con
AndroidKeysetManager, salvando la master key in Android Keystore tramite unandroid-keystore://URI (vedi ledatastore-tinknote di rilascio). - Ottieni un
Aeadprimitivo da quel keyset. - Aggiungi
androidx.datastore:datastore-tink(disponibile da DataStore 1.3.0-rc01; la versione stabile 1.2.x non include questo artifact), avvolgi il tuoSerializerconAeadSerializer, e passalo aDataStore. Quando più store condividono lo stessoAeadkey, assegna a ogni store unaassociatedDataunica, come un valore stabile derivato dal nome del file.
Google lo documenta come il percorso di sostituzione supportato per le androidx.security.crypto helpers deprecate. Non mischiare un JCA standalone SecretKey con AeadSerializer; il serializzatore si aspetta un'istanza Aead Tink.
Nota sul versionamento: poiché l' datastore-tink artifact esiste solo dalla versione 1.3.0-rc01 in poi, i team che usano versioni stable 1.2.x non troveranno il sostituto documentato nel proprio dependency tree. Questo gap risale ormai a diverse release e, secondo la nostra opinione, è il motivo principale per cui i piani di migrazione si bloccano: decidi consapevolmente se usare una release candidate o aspettare, invece di lasciarlo accadere per caso.
Opzioni Keystore da conoscere
setIsStrongBoxBacked(true)quandoFEATURE_STRONGBOX_KEYSTOREè disponibile per un isolamento delle chiavi più forte.- Autenticazione utente binding (
setUserAuthenticationRequired) quando i token devono essere accessibili solo dopo biometria o credenziali del dispositivo. - Evita l'Auto Backup per i file che contengono keyset o ciphertext, a meno che non si conosca il comportamento del restore; la documentazione di Google su EncryptedSharedPreferences avverte che il backup può ripristinare il ciphertext senza le chiavi.
I moduli nativi Kotlin o Java in Capacitor dovrebbero implementare questo pattern all'interno del plugin; JavaScript dovrebbe ricevere solo handle opachi o lo stato della sessione, mai le chiavi in chiaro.
iOS: Keychain e Secure Enclave
L'oggetto Keychain rimane il posto corretto per i token su iOS. Gli elementi sono criptati e protetti dal sistema. Usa una costante di accessibilità con scope limitato e Controllo degli accessi quando è necessario l'accesso tramite Face ID, Touch ID o codice.
La scelta dell'accessibilità è una decisione reale, non un default da copiare. Scegli kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly quando i token devono essere usati da processi in background — refresh via push, background fetch — ma non devono mai migrare su un nuovo dispositivo tramite backup. Scegli kSecAttrAccessibleWhenUnlockedThisDeviceOnly per l'uso dei token solo in foreground, dove il vincolo maggiore vale la restrizione. In entrambi i casi, abilita kSecUseDataProtectionKeychain su target moderni per utilizzare la Keychain con data-protection invece di quella legacy basata su file.
CryptoKit aiuta quando cripti i payload manualmente, ma per i classici bearer token in stile OAuth, salvare la stringa del token direttamente in Keychain con i corretti permessi di accessibilità è solitamente sufficiente. Le chiavi basate su Secure Enclave (una chiave P-256 generata con SecureEnclave in CryptoKit) sono vantaggiose solo quando la chiave stessa non deve mai lasciare l'hardware sicuro — come per le operazioni di firma o la device attestation. Non possono contenere una stringa di token; proteggono la tua chiave di cifratura. Se non ruoti le chiavi e non ne gestisci il ciclo di vita, il semplice storage in Keychain è più semplice e altrettanto sicuro. Un'ulteriore cifratura a livello app serve solo se gestisci con cura la rotazione delle chiavi e degli envelope.
Capacitor @capacitor/preferences scrive su UserDefaults, che è non uno storage non sicuro. Non salvare i token lì.
Capacitor: opzioni di storage sicuro nell'org Capgo
Capgo effettua non rilascia un pacchetto chiamato “secure storage”, ma due plugin coprono i comuni Capacitor:
| Plugin | Ideale per | Supporto nativo |
|---|---|---|
@capgo/capacitor-native-biometric setData / getSecureData |
Secret piccoli (token di sessione, chiavi API) | iOS Keychain; crittografia su Android Keystore in SharedPreferences |
| @capgo/capacitor-data-storage-sqlite con opzioni di crittografia | Dati locali grandi o strutturati | SQLite con SQLCipher opzionale |
L'oggetto guida allo storage key-value confronta Preferences, SQLite e NativeBiometric.setData per i segreti.
Richiede @capgo/capacitor-native-biometric 8.6.0 o versioni successive (allinea la major version del plugin alla major version di Capacitor). Esempio con letture protette da biometria:
import {
AccessControl,
NativeBiometric,
} from '@capgo/capacitor-native-biometric';
const TOKEN_KEY = 'auth.refresh_token';
export async function saveRefreshToken(token: string) {
await NativeBiometric.setData({
key: TOKEN_KEY,
value: token,
accessControl: AccessControl.BIOMETRY_CURRENT_SET,
});
}
export async function loadRefreshToken(): Promise<string | null> {
try {
const { value } = await NativeBiometric.getSecureData({
key: TOKEN_KEY,
reason: 'Unlock your session',
title: 'Authenticate',
});
return value ?? null;
} catch {
return null;
}
}
getData restituisce solo i valori salvati senza accessControl. Per la protezione biometrica, usa sempre BIOMETRY_CURRENT_SET o BIOMETRY_ANY con getSecureData.
Per i build web, non salvare mai i token di produzione in localStorage. Usa invece il modello di sessione web del tuo backend.
Ciclo di vita del token tra server e client
Sì, le app mobile dovrebbero memorizzare i refresh token: l'alternativa è costringere gli utenti alla riautenticazione a ogni avvio, il che è più una farsa della sicurezza che vera protezione. La differenza sta in dove e come: persiste solo il refresh token, esclusivamente in uno storage protetto da Keychain o Keystore, e solo con rotazione e revoca lato server.
La rotazione è ciò che rende sicura la persistenza. Emetti i refresh token come one-time-use : ogni utilizzo restituisce un nuovo refresh token e invalida il precedente. Il server mantiene una breve finestra di rilevamento del riutilizzo — se un token presumibilmente già usato viene presentato di nuovo, l'intero gruppo viene considerato compromesso e l'intera catena viene revocata, forzando un nuovo login su ogni dispositivo che lo condivideva. Prevedi un breve periodo di tolleranza (da pochi secondi a un minuto) per gestire richieste simultanee o riconnessioni offline, in cui un client legittimo potrebbe presentare l'ultimo token prima di ricevere quello nuovo. Senza tolleranza, gli utenti con connessioni instabili rimarrebbero bloccati; una finestra troppo lunga, invece, espande la superficie di attacco.
La posizione di ogni token dipende direttamente dalla sua durata:
- Problema access token con TTL brevi (minuti, non giorni), e conservali solo in memoria durante la sessione.
- Persisti il refresh token in Keychain o in uno storage cifrato tramite Keystore, e ruotalo come sopra.
- On password change, logout, or device loss, revoca refresh token lato server — l'eliminazione dei dati locali da parte del client non equivale a una revoca.
- Valuta una strategia di token binding leggera (device key attestation o app instance ID) se il tuo modello di minaccia lo richiede.
Quando rilasci fix di sicurezza per la gestione dei token nelle app Capacitor, Live Updates di Capgo puoi distribuire rapidamente i cambiamenti al web layer, rispettando le policy degli store. I cambiamenti allo storage nativo richiedono comunque un nuovo build.
Confronto opzioni di storage
| Metodo | Sicurezza | adattabilità Capacitor |
|---|---|---|
| Variabile JS in memoria | Alta durante l'esecuzione, persa al kill | OK solo per access token di sessione |
localStorage / Preferenze |
Basso | Solo impostazioni, mai segreti |
| iOS Keychain / Android Keystore | Alto | Tramite code nativo o plugin Capgo |
| DataStore + Tink (nativo Android) | Alto | Plugin Capacitor custom o SDK nativo |
| Crittografia custom in JS | Variabile | Evita: le chiavi in JS sono estraibili |
Nota bene sull'ultima riga: il punto debole della crypto in JavaScript non sono gli algoritmi deboli, ma la posizione. Qualsiasi chiave leggibile da un runtime JS può essere esfiltrata da qualsiasi altro elemento presente nel runtime; quindi, da quel livello, un'alta sicurezza è strutturalmente irraggiungibile, a prescindere dalla libreria scelta.
Checklist di migrazione da EncryptedSharedPreferences
- Inventory which keys hold secrets versus public config.
- Implementa DataStore +
AeadSerializercon un keyset Tink supportato da Keystore. - Migra con
SharedPreferencesMigrationusando un producer che apre la vecchia EncryptedSharedPreferences instance (non in chiaroSharedPreferences), quindi elimina i file legacy dopo una lettura riuscita. - Rilascia la modifica nativa in una release dello store; testa backup e restore su un dispositivo fisico.
- Rimuovi
androidx.security.cryptole dipendenze quando non sono più referenziate.
FAQ
Dovrei crittografare i token prima di salvarli nel Keychain?
Solitamente non serve un ulteriore livello per i classici bearer token, se l'accessibilità del Keychain è coerente con il tuo threat model. Usa l'envelope encryption se richiesto da normative o policy di key rotation.
SQLCipher in un plugin Capacitor sostituisce Keystore?
SQLCipher protegge il file del database, ma avrai comunque bisogno di una chiave forte in Keystore o Keychain per decriptarlo. Gestisci la passphrase di crittografia come parte integrante del tuo design di secure storage.
Posso salvare i token in Capacitor Preferences usando l'obfuscation?
L'obfuscation non è crittografia. Preferences mappa su UserDefaults e SharedPreferences senza protezione hardware-backed.
Le app mobile dovrebbero salvare i refresh token?
Sì — ma solo in uno storage crittografato supportato da Keychain o Keystore, abbinato a rotazione e revoca lato server. Non salvarli significa richiedere l'autenticazione a ogni avvio, cosa che gli utenti evitano pretendendo login meno sicuri. Ciò che non è mai accettabile è salvare il refresh token in Preferences, localStorage o qualsiasi file in plaintext.
Qual è il modo più sicuro per salvare gli access token in un'app moderna?
Non salvarli affatto. Mantieni l'access token a breve scadenza in memoria durante la sessione e rigeneralo dal refresh token a ogni avvio. Salva solo il refresh token — in iOS Keychain o in uno storage criptato basato su Android Keystore — e lascia che la rotazione e la revoca limitino i danni in caso di compromissione del dispositivo.
Torna a Secure Token Storage: Best Practices for Mobile Developers
Se stai usando Storage sicuro dei token: best practice per sviluppatori mobile per pianificare sicurezza e compliance, collegalo a Crittografia per i dettagli di implementazione in Encryption, Conformità per i dettagli di implementazione in Compliance, Scanner di sicurezza Capgo per il workflow di Capgo Security Scanner, Sicurezza Capgo per il workflow di Capgo Security, e Trust Center di Capgo per il workflow di Capgo Trust Center.