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');
}
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);
}
}
}
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 (3)
Nice one. I had used the exact same solution as soon as I found out how keychain retains those entries.
Thank you! I first discovered this while the app was in production 😅 Lesson learned.
SharedPreferences and FlutterSecureStorage both store data in a human-readable format.
Is this correct that FlutterSecureStorage store data in a human-readable format?