Perché non basta SharedPreferences

Quando si sviluppa un'app reale, prima o poi bisogna salvare informazioni sensibili: token di autenticazione, refresh token, chiavi API o credenziali utente. Una tentazione comune è usare SharedPreferences (o shared_preferences in Flutter), ma è un errore: i dati vengono salvati in chiaro in un file XML su Android e nelle preferenze su iOS, facilmente leggibili su dispositivi compromessi o con root/jailbreak.

La soluzione corretta è il pacchetto flutter_secure_storage, che sfrutta i sistemi di sicurezza nativi delle piattaforme:

  • iOS/macOS: Keychain
  • Android: Keystore con cifratura AES (tramite EncryptedSharedPreferences)
  • Linux/Windows/Web: implementazioni dedicate (con alcune limitazioni)

Installazione

Aggiungi il pacchetto al pubspec.yaml:

dependencies:
  flutter_secure_storage: ^9.2.2

Su Android assicurati che il minSdkVersion sia almeno 18 nel file android/app/build.gradle:

defaultConfig {
    minSdkVersion 18
}

Operazioni di base

L'API è semplice e completamente asincrona. Ecco le operazioni fondamentali:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final storage = const FlutterSecureStorage();

// Scrittura
await storage.write(key: 'auth_token', value: 'eyJhbGciOiJI...');

// Lettura
final token = await storage.read(key: 'auth_token');

// Cancellazione di una singola chiave
await storage.delete(key: 'auth_token');

// Cancellazione di tutto
await storage.deleteAll();

// Verifica esistenza
final exists = await storage.containsKey(key: 'auth_token');

Configurare le opzioni di sicurezza

È buona pratica configurare esplicitamente le opzioni per ciascuna piattaforma. Su Android puoi forzare l'uso di EncryptedSharedPreferences, su iOS puoi definire l'accessibilità del Keychain:

final storage = const FlutterSecureStorage(
  aOptions: AndroidOptions(
    encryptedSharedPreferences: true,
  ),
  iOptions: IOSOptions(
    accessibility: KeychainAccessibility.first_unlock,
  ),
);

Il valore KeychainAccessibility.first_unlock indica che il dato è accessibile solo dopo il primo sblocco del dispositivo dall'ultimo riavvio: un buon compromesso tra sicurezza e usabilità (ad esempio per i task in background).

Un repository per i token

Nelle app reali conviene incapsulare l'accesso al secure storage in una classe dedicata, così da isolare la dipendenza e facilitare i test:

class TokenStorage {
  TokenStorage(this._storage);

  final FlutterSecureStorage _storage;

  static const _accessKey = 'access_token';
  static const _refreshKey = 'refresh_token';

  Future<void> saveTokens({
    required String accessToken,
    required String refreshToken,
  }) async {
    await Future.wait([
      _storage.write(key: _accessKey, value: accessToken),
      _storage.write(key: _refreshKey, value: refreshToken),
    ]);
  }

  Future<String?> get accessToken => _storage.read(key: _accessKey);
  Future<String?> get refreshToken => _storage.read(key: _refreshKey);

  Future<void> clear() async {
    await Future.wait([
      _storage.delete(key: _accessKey),
      _storage.delete(key: _refreshKey),
    ]);
  }
}

Questo pattern si integra perfettamente con la dependency injection: puoi registrare TokenStorage e iniettarlo dove serve, ad esempio in un interceptor di Dio per aggiungere automaticamente l'header Authorization.

Gestire le eccezioni

Le letture possono fallire, ad esempio quando si aggiorna una libreria di cifratura o si cambia configurazione su Android. È prudente gestire l'errore e ripulire lo storage in caso di dato corrotto:

Future<String?> readSafely(String key) async {
  try {
    return await storage.read(key: key);
  } on PlatformException catch (_) {
    // Dato non decifrabile: meglio rimuoverlo
    await storage.delete(key: key);
    return null;
  }
}

Buone pratiche

  • Non memorizzare dati enormi: il secure storage è pensato per stringhe brevi (token, chiavi), non per file o grandi quantità di dati.
  • Cancella i token al logout: chiama sempre clear() per evitare che restino accessibili.
  • Attenzione al Web: l'implementazione web usa cifratura ma è meno robusta dei keystore nativi; valuta strategie alternative se il target è critico.
  • Backup Android: disabilita il backup automatico delle chiavi nel AndroidManifest.xml per evitare che dati cifrati finiscano nei backup cloud.
<application
    android:allowBackup="false"
    android:fullBackupContent="false">

Conclusione

Proteggere i dati sensibili non è opzionale: flutter_secure_storage offre un'API semplice che si appoggia ai meccanismi di sicurezza nativi di ogni piattaforma. Incapsulando l'accesso in un repository dedicato e configurando correttamente le opzioni, otterrai una gestione delle credenziali robusta, testabile e conforme alle aspettative di sicurezza degli utenti.