Skip to content
Blog

Secure Storage: Keystore, Keychain, and Flutter Secure Storage

Learn how to store API keys, tokens, and secrets securely on Android and iOS using platform keystores and the Flutter secure storage plugin.

Published on August 17, 2026

AI Assistant

Storing a user’s API token in SharedPreferences or NSUserDefaults is the mobile equivalent of writing your password on a sticky note. Anyone with a rooted device — or a copy of your app’s data backup — can read it. The platform answer is the Android Keystore and the iOS Keychain: hardware-backed vaults that encrypt data with keys that never leave the secure enclave. In Flutter, the flutter_secure_storage plugin wraps both, plus the equivalent mechanisms on desktop and web.

In this tutorial, you will learn how the platform keystores work, how to store and read secrets with flutter_secure_storage, and how to configure it for production — including biometric authentication and Android backup handling. Key technologies: Android Keystore, iOS Keychain, flutter_secure_storage.

Prerequisites

  • Flutter 3.x installed
  • An Android or iOS device/emulator
  • A secret to store (an API key or token will do)

Core Content

How the platform storage works

  • Android Keystore: keys live in a hardware-backed (or software-backed on older devices) keystore and never leave it. Since version 10.0.0 of flutter_secure_storage, the default scheme is RSA-OAEP key wrapping plus AES-GCM data encryption, stored in encrypted SharedPreferences. The old encryptedSharedPreferences from the Jetpack Security library is deprecated.
  • iOS Keychain: encrypted storage managed by the OS; data is accessible to your app (and extensions sharing your keychain access group) and is included in encrypted device backups.

The point of both is the same: the encryption key is separated from the ciphertext, and the key never appears in plaintext on disk.

Install and initialize

Add the dependency:

dependencies:
  flutter_secure_storage: ^11.0.0

Call WidgetsFlutterBinding.ensureInitialized() before any storage interaction, then create the storage instance:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

Store, read, and delete secrets

The API is a simple key-value store, but the values are encrypted at rest:

final storage = const FlutterSecureStorage();

// Write
await storage.write(key: 'auth_token', value: 'sk_live_1234');

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

// Check existence
final hasToken = await storage.containsKey(key: 'auth_token');

// Delete one or all
await storage.delete(key: 'auth_token');
await storage.deleteAll();

Configure iOS accessibility

On iOS, IOSOptions controls when the Keychain will hand the value back. The default is unlocked — the app can only read the secret while the device is unlocked:

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

first_unlock allows access after the first unlock following a reboot — useful for background refresh before the user ever unlocks the phone. Choose the most restrictive option that keeps your feature working.

Optional biometric protection on Android

You can require biometric authentication before a secret is returned (Android 6.0+, enforced biometrics require API 28+). enforceBiometrics: false degrades gracefully — the data still works without biometrics:

final storage = FlutterSecureStorage(
  aOptions: AndroidOptions.biometric(
    enforceBiometrics: false,
    biometricPromptTitle: 'Authenticate to view your token',
  ),
);

With enforceBiometrics: true, reading throws if the device has no PIN, pattern, or biometric enrolled. This mode suits high-value secrets like wallet seeds.

Handle Android backup correctly

By default Android backs up app data to Google Drive, which can break decryption on a restored device (the keystore key doesn’t migrate, producing an InvalidKeyException). Either disable backup or exclude the secure storage prefs:

<application
    android:allowBackup="false"
    ...>
</application>

Or use a backup rules XML to exclude the SharedPreferences file that flutter_secure_storage writes.

Putting It All Together

A production setup stores secrets only via flutter_secure_storage — never in SharedPreferences, NSUserDefaults, or the source code. The platform default ciphers are strong: RSA-OAEP + AES-GCM on Android, the Keychain’s own encryption on iOS. Add biometrics for high-value data, configure iOS accessibility to match your background use case, and disable Android backup to avoid key/schema mismatches after restore.

Conclusion & Next Steps

You’ve stored secrets the right way: encrypted at rest with platform keystores, with optional biometric gating and correct backup handling. Your API keys are no longer a plaintext liability.

Next Steps: enable migrateWithBackup for crash-resistant migrations between cipher versions, test on a rooted device to verify ciphertext extraction yields nothing useful, and consider flutter_secure_storage’s WebCrypto-backed web support (HTTPS only) if you ship Flutter web.

References: