DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Beware of FlutterSecureStorage on iOS
Isuru
Isuru

Posted on

Beware of FlutterSecureStorage on iOS

This is not a guide on how to use the flutter_secure_storage package. If you need to learn about that, please check the documentation. The purpose of this post is to warn about a certain pitfall with FlutterSecureStorage.


SharedPreferences (shared_preferences) and FlutterSecureStorage (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 on Android and 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 on Android (EncryptedSharedPreferences in flutter_secure_storage v5.0.0 and above) and Keychain 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.

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');
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
    }
  }

}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
xerx profile image
xerx

Nice one. I had used the exact same solution as soon as I found out how keychain retains those entries.

Collapse
 
isurujn profile image
Isuru

Thank you! I first discovered this while the app was in production πŸ˜… Lesson learned.

Here is a post you might want to check out:

Regex for lazy developers

regex for lazy devs

Sorry for the callout πŸ˜†