Beware of FlutterSecureStorage on iOS
Tags: flutter, dart
Published: 2022-11-20 09:05:46

This is not a guide on how to use the [`flutter_secure_storage`](https://pub.dev/packages/flutter_secure_storage) package. If you need to learn about that, please check the [documentation](https://pub.dev/packages/flutter_secure_storage/example). The purpose of this post is to warn about a certain pitfall with `FlutterSecureStorage`.

---

`SharedPreferences` ([shared_preferences](https://pub.dev/packages/shared_preferences)) and `FlutterSecureStorage` ([flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage)) are both lightweight local storage solutions available for Flutter with some notable differences. `SharedPreferences` support storing many data types such as string, integer, double, boolean and even string arrays. But in `FlutterSecureStorage`, you can only store string values.

In addition to that, the biggest difference between `SharedPreferences` and `FlutterSecureStorage` is security. `SharedPreferences` is backed by similarly named [`SharedPreferences`](https://developer.android.com/reference/android/content/SharedPreferences) on Android and [`NSUserDefaults`](https://developer.apple.com/documentation/foundation/nsuserdefaults) on iOS. While neither of these local data stores can be easily accessed by a regular app user, a curious person given a little bit of time can dig them up by decompiling the app executable using commonly available tools/methods. `SharedPreferences` and `FlutterSecureStorage` both store data in a human-readable format.

Whereas, `FlutterSecureStorage` saves data in a much more secure way by using the [Keystore](https://developer.android.com/training/articles/keystore.html) on Android ([`EncryptedSharedPreferences`](https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences) in flutter_secure_storage v5.0.0 and above) and [Keychain](https://developer.apple.com/documentation/security/keychain_services#//apple_ref/doc/uid/TP30000897-CH203-TP1) on iOS. Data saved in these locations are encrypted so they are not human-readable.

Due to this reason, many developers use `FlutterSecureStorage` to store sensitive data. However, there is a caveat when working with `FlutterSecureStorage` on iOS.

As mentioned above, on iOS, FlutterSecureStorage stores data in the Keychain. The Keychain is a system level service. Therefore even if an app is deleted from the phone, the values that app saved in the Keychain do not get removed. To demonstrate why this can be problematic, let us see a possible real-world example.

> _This problem does not exist on Android. When the app is deleted on Android, it removes the values saved in the Keystore/`EncryptedSharedPreferences` as well._

Let’s imagine you have an app that has a user account feature. You use `FlutterSecureStorage` to save a token (tokens are commonly used to authenticate API calls). At app launch, you check whether a token exists in storage, if it doesn’t, you prompt the user to login. Upon successful login, you save the received token in `FlutterSecureStorage`.

```dart
void authorizeUser() async {
  String? token = await getToken();

  if (token == null) {
    // Navigate to login screen
  }
}

Future<String?> getToken() async {
  FlutterSecureStorage storage = const FlutterSecureStorage();
  return await storage.read(key: 'token');
}

Future<void> login() async {
  // Parse login API response and retrieve token

  FlutterSecureStorage storage = const FlutterSecureStorage();
  await storage.write(key: 'token', value: 'AYR87uYy7jD6jN6RzJeg*G');
}
```

Now if the user deletes the app from their phone and re-install it, ideally the user should be taken to the login screen to log back in. But because on iOS, Keychain values do not get removed, the `getToken()` method will still return a token saved in a previous login session! If there has been a long time gap between the removal of the app and the re-installation, that token could be no longer valid.

As mentioned earlier, unlike `FlutterSecureStorage`, `SharedPreferences` do get removed when the app is deleted. This behavior can help with working around the Keychain issue.

---

Create a boolean flag called `is_first_app_launch` to be saved in `SharedPreferences`. This flag determines if the app is launched for the first time (whether it be the very first install or any re-installs). Initially `is_first_app_launch` will be `null`.

At the app launch, check if `is_first_app_launch` is `true` or `null`, and if it is, delete the `FlutterSecureStorage` completely. If the app previously existed and had any values saved in the Keychain, they are removed and the app now has a clean slate.

Make sure to set the `is_first_app_launch` value to `false`, so that subsequent launches skip this step. If the app is deleted, it will delete the `SharedPreferences` values automatically, thus making `is_first_app_launch` value `null`.

```dart
class _HomeScreenState extends State<HomeScreen> {

  @override
  initState() {
    super.initState();

    clearKeychainValues();
    authorizeUser();
  }

  Future<void> clearKeychainValues() async {
    final prefs = await SharedPreferences.getInstance();

    if (prefs.getBool('is_first_app_launch') ?? true) {
      FlutterSecureStorage storage = const FlutterSecureStorage();
      await storage.deleteAll();

      await prefs.setBool('is_first_app_launch', false);
    }
  }

}
```

---

That is it! It is a little cumbersome solution to accomplish a very simple task. But in Flutter, you have no choice but to work with or around platform limitations/rules.