Local storage is a crucial part of mobile app development for maintaining and preserving users’ data until the app is removed. Today, an app’s trustworthiness hinges upon how its data is being saved.
In this tutorial, you’ll learn how to securely save data locally from your Flutter app.
You might be wondering why we need local storage when we can save the data on a server. What if you need the data in your app in offline mode? You don’t want to show the “No Connectivity” warning to your end users, which is why local storage is important.
So, how do you save the data locally? There are several ways to do that in Flutter, but you’ll use the flutter_secure_storage package for this tutorial.
N.B., if you are new to Flutter, please go through the official documentation to learn about it.
Why use flutter_secure_storage?
flutter_secure_storage is a Flutter plugin used to store data in secure storage. So what is this secure storage, and how secure is it? Well, the description of secure storage changes with consideration for the platform.
If the platform is Android, then flutter_secure_storage stores data in encryptedSharedPreference
, which are shared preferences that encrypt keys and values. It handles AES encryption to generate a secret key encrypted with RSA and stored in KeyStore.
For the iOS platform, flutter_secure_storage uses the KeyChain which is an iOS-specific secure storage used to store and access cryptographic keys only in your app.
In the case of the web, flutter_secure_storage uses the Web Cryptography (Web Crypto) API.
Getting started with the local storage demo
Download the starter project containing the prebuilt UI and minimal configuration from here.
Open it in your editor, then build and run the app:
The file structure of the starter project looks like this:
main.dart
— the entry point for the whole apphome_view.dart
— contains the Home view where the secured data list will be displayedstorage_service.dart
— contains the methods responsible for reading, writing, and deleting data from secured storagestorage_item.dart
— the model or state to store data in a key-value pair:
class StorageItem { StorageItem(this.key, this.value); final String key; final String value; }
- widgets — directory containing additional custom widgets to help build the UI:
add_data_dialog.dart
— a dialog widget used to add new data to the secure storageadd_data_dialog.dart
— a dialog widget that edits aStorageItem
objectsearch_key_value_dialog.dart
— a dialog widget to search for a key valuetextfield_decoration.dart
— a widget to add shadow over the input text field in the Dialog widgetsvault_card.dart
— a custom widget to display a list tile in the home screen and provide features like editing the value, and long-press value visibility
Setting up secure storage
Add the flutter_secure_storage in the pubspec dependencies:
#yaml flutter_secure_storage: ^5.0.2
Android Configuration
In your project-level build.gradle
file, update the minSdkVersion
:
//gradle android { ... defaultConfig { ... minSdkVersion 18 ... } }
Apps targeting API level 23+ automatically back up almost all the data to Google Drive. You can disable it in your app manifest
file:
<!--xml--> <manifest ... > ... <application android:allowBackup="true" ... > ... </application> </manifest>
If you want to control what gets backed up, you need to define a custom XML rule, as mentioned here.
Web Configuration
Be sure to enable HTTP Strict-Transport-Security (HSTS) and other important HTTP security headers because Web Crypto only works when the website is running with HTTPS or localhost.
Linux Configuration
Use Flutter’s manual installation instead of Flutter Snap. Also, you need libsecret-1-dev
and libjsoncpp-dev
on your machine to build the project, and libsecret-1-0
and libjsoncpp1
to run the application:
sudo apt reinstall libjsoncpp1 libsecret-1-0 libjsoncpp-dev libsecret-1-dev -y #OR sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev
Secure storage methods
You are now ready to create a class of all methods required to read, write, and delete data from secure storage.
In your lib
directory, create a new directory named services
and create a secure_storeage.dart
file inside it:
Now, create a StorageService
class inside the file as below:
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class StorageService { final _secureStorage = const FlutterSecureStorage(); }
Here you initialized _secureStorage
, an instance of FlutterSecureStorage()
.
Write
Now create the method responsible for writing data into secure storage:
Future<void> writeSecureData(StorageItem newItem) async { await _secureStorage.write( key: newItem.key, value: newItem.value, aOptions: _getAndroidOptions()); }
In the above code, _getAndroidOptions()
is also a method of the StorageService
class used to set the encryptedSharedPreference
property true:
AndroidOptions _getAndroidOptions() => const AndroidOptions( encryptedSharedPreferences: true, );
You can also create more options methods concerning your device.
N.B., When upgrading the flutter_secure_storage to 5.0.0 in Android, you can migrate to EncryptedSharedPreferences
using the above method. This will auto-migrate all the preferences. Once migrated, this cannot be undone. If you try to disable the encryptedSharedPreference
, you’ll be unable to read the value.
Read
Next, create the readSecureData
method to read the secured data concerning the key:
Future<String?> readSecureData(String key) async { var readData = await _secureStorage.read(key: key, aOptions: _getAndroidOptions()); return readData; }
Delete
Now, to delete a key-value pair, create the deleteSecureData
method as below:
Future<void> deleteSecureData(StorageItem item) async { await _secureStorage.delete(key: item.key, aOptions: _getAndroidOptions()); }
containsKey
Create a containsKeyInSecureData
method responsible for checking whether the storage contains the provided key or not:
Future<bool> containsKeyInSecureData(String key) async { var containsKey = await _secureStorage.containsKey(key: key, aOptions: _getAndroidOptions()); return containsKey; }
readAll
To read all the secured data, create the readAllSecureData
method as below:
Future<List<StorageItem>> readAllSecureData() async { var allData = await _secureStorage.readAll(aOptions: _getAndroidOptions()); List<StorageItem> list = allData.entries.map((e) => StorageItem(e.key, e.value)).toList(); return list; }
In the above code, you return a list of StorageItem
s after reading all the data.
deleteAll
Next, to delete all the secured data, create the deleteAllSecureData
method:
Future<void> deleteAllSecureData() async { await _secureStorage.deleteAll(aOptions: _getAndroidOptions()); }
Using these methods in your Flutter app
Now, you’ll update the starter project so that you can utilize the above methods.
Reading all the data
Let’s start with reading all the data present in the storage and displaying it in the UI. So, in the home_view.dart
, first initialize the StorageService
instance:
final StorageService _storageService = StorageService();
Next, update the initList()
method:
void initList() async { _items = await _storageService.readAllSecureData(); _loading = false; setState(() {}); }
In the above code, you are using the readAllSecureData
method to update and set the list in the initState
.
This will auto-update the home screen if the data exits as we already have a ListView.builder
widget rendering each list item using the VaultCard
widget.
Writing new data
To write new data, first update the AddDataDialog
widget:
ElevatedButton( onPressed: () { final StorageItem storageItem = StorageItem( _keyController.text, _valueController.text); Navigator.of(context).pop(storageItem); }, child: const Text('Secure'))
In the above code, you used the _keyController
and _valueController
controllers to create a new StorageItem
object and return it to the home screen.
Next, use the onPressed
property of the Add Data button from your home_view.dart
:
ElevatedButton( onPressed: () async { // 1 final StorageItem? newItem = await showDialog<StorageItem>( context: context, builder: (_) => AddDataDialog()); if (newItem != null) { // 2 _storageService.writeSecureData(newItem).then((value) { setState(() { _loading = true; }); // 3 initList(); }); } }, child: const Text("Add Data"), )
In the above code, you did the following:
newItem
: added a nullableStorageItem
object coming from theAddDataDialog
widget- Passed the
newItem
object into thewriteSecureData
method to write new data - Used the
initList
method to fetch all the existing data from secure storage
Build and restart your app, and you’ll be able to write new data in secure storage.
Deleting data
Now, to delete any particular data from the list, update the ListView.builder
widget in the home_view.dart
:
ListView.builder( itemCount: _items.length, padding: const EdgeInsets.symmetric(horizontal: 8), itemBuilder: (_, index) { // 1 return Dismissible( // 2 key: Key(_items[index].toString()), child: VaultCard(item: _items[index]), // 3 onDismissed: (direction) async { // 4 await _storageService .deleteSecureData(_items[index]) .then((value) => _items.removeAt(index)); // 5 initList(); }, ); })
In the above code, you did the following:
- Wrapped the
VaultCard
widget with theDismissible
widget to dismiss the respective card horizontally - Used the
Key
widget with the help of the current list item to create a unique key for the mandatorykey
property of theDismissible
widget. You can have random numeric or alphanumeric values as well - Used the
onDismissed
property of theDismissible
widget to remove the particular card/data from the list - With the help of the
deleteSecureData
, you passed the respective element and deleted it. However, make sure to remove the item from the list as well, else the Dismissible widget will throw an error - Used the
initList()
method to update the list after deleting the item
Build and restart your app, and you’ll be able to delete data from the secure storage.
Searching for data
To search existing data, you need the key to use it in the SearchKeyValueDialog widget. So update the AppBar
in your home_view.dart
to show the SearchKeyValueDialog
as below:
IconButton( icon: const Icon(Icons.search, color: Colors.black), onPressed: () => showDialog( context: context, builder: (_) => const SearchKeyValueDialog()), )
Next, initialize the StorageService
instance in the SearchKeyValueDialog
widget and update the onPressed
property of the Search button:
ElevatedButton( onPressed: () async { _value = await _storageService .readSecureData(_keyController.text); setState(() {}); }, child: const Text('Search'))
Here you have updated the _value
by using the readSecureData
method and passing the key in it. This will auto-update the dialog and display the value below the button.
Build and restart your app.
Deleting all the data
To delete all the data, go back to the home_view.dart
and update the Delete All Data button:
ElevatedButton( style: ElevatedButton.styleFrom(primary: Colors.red), onPressed: () async { _storageService .deleteAllSecureData() .then((value) => initList()); }, child: const Text("Delete All Data"), )
Here, you utilized the deleteAllSecureData
method and updated the list using the initList
method.
Edit storage items
Editing existing data is performed in the VaultCard
widget, so update it by initializing an instance of the StorageService
:
final StorageService _storageService = StorageService();
Next, use the EditDataDialog
widget to get the updated value back in the VaultCard
widget using the StorageItem
. Update the EditDataDialog
:
final StorageItem item; const EditDataDialog({Key? key, required this.item}) : super(key: key);
You update the constructor in the above code to receive the StorageItem
object.
Next, update the Update button:
ElevatedButton( onPressed: () { Navigator.of(context).pop(_valueController.text); }, child: const Text('Update'))
Here you return the updated value to the VaultCard
widget.
Now, update the onPressed
property of the edit IconButton
in the VaultCard
:
IconButton( icon: const Icon(Icons.edit), onPressed: () async { // 1 final String updatedValue = await showDialog( context: context, builder: (_) => EditDataDialog(item: widget.item)); if (updatedValue.isNotEmpty) { // 2 _storageService .writeSecureData( StorageItem(widget.item.key, updatedValue)) .then((value) { // 3 widget.item = StorageItem(widget.item.key, updatedValue); setState(() {}); }); } }, )
In the above code, you did the following:
- Received the updated value by passing the
StorageItem
object as an argument to theEditDataDialog
widget - Updated the data using the
writeSecureData
method if the received value is not empty - After the update, you updated the
StorageItem
object itself
Build and restart your app.
So this is how you can use the flutter_secure_storage. You can also use it at the time of authentication to store user data.
Conclusion
You can find the final project here.
In this tutorial, you learned about flutter_secure_storage and how you can use it in your Flutter app. For the next step, you can use any state management solution with secure storage (such as GetX) for more efficiency, or learn about using the sqlcipher library on both iOS and Android devices, which also provides the ability to store data securely.
Bonus: Alternatives to flutter_secure_storage
There are more databases providing encryption and decryption of data to your Flutter app; it all depends upon how they are supporting it. Is the encrypt/decrypt done on the application layer? Does it depend upon third-party packages?
After understanding how to secure your data locally using the flutter_secure_storage, check out these alternatives, which may make your life easier:
- SQFlite with SQLCipher support
- Sembast
- Hive
- Drift
SQFlite with SQLCipher support
SQFlite with SQLCipher support, or sqflite_sqlcipher, is a plugin which uses the sqflite_common Dart package/wrapper with the SQLCipher library to handle the platform channel calls from SQLite on both iOS and Android.
What is the SQLCipher library? It is a fork of the SQLite database with some features listed below:
- An addition of 100% 256-bit AES encryption of database files
- No configuration or application-level cryptography
- Good security practices like key encryption are through PBKDF2 key derivation
You can learn more about the library here.
It has a similar API to the SQLite plugin with the only difference being an optional parameter password in the openDatabase
method:
import 'package:sqflite_sqlcipher/sqflite.dart'; Database db = await openDatabase( path, password: "my password", ... );
On desktop, the encryption is supported by using the SQLCipher with sqflite_common_ffi, which is a SQLite-based FFI implementation(based on SQLite 3) on Linux, macOS, and Windows on both Flutter and Dart VM.
However, there are a few limitations, like the database calls are done in a separate isolate and it is better to use path_provider to get the database path instead of the getDatabasesPath
method.
Sembast
Sembast is a NoSQL persistent database where the whole DB is based on a single text file (JSON format) loaded in memory when opened. The changes to it are appended right away to the file and are autocompacted when required.
It works on Dart VM and Flutter, so it supports all the platforms. Sembast provides a way to support encryption/decryption of data using a user-defined codec (like SHA256) or application layer cryptography using external packages like encrypt, and crypto:
// Initialize the encryption codec with a user password var codec = getEncryptSembastCodec(password: '[your_user_password]'); // Open the database with the codec Database db = await factory.openDatabase(dbPath, codec: codec);
The database header will contain a codec encoded signature so that a database cannot be opened if the password is wrong.
Any custom encryption/codec can be used as long as there is a way to encode/decode a JSON encodable object (Map
, List
, num
, String
, and Bool
) to/from a single line String
.
You can learn more about the codec and encryption from here.
Hive
Hive supports AES-256 encryption with the only need for a 256-bit encryption key. You can generate the key by yourselves or use the helper function provided by Hive using the Fortuna random number generator.
After generating the key, you can use it to open the box using the encryptionCipher
property:
import 'dart:convert'; import 'package:hive/hive.dart'; void main() async { final key = Hive.generateSecureKey(); final encryptedBox= await Hive.openBox('vaultBox', encryptionCipher: HiveAesCipher(key)); encryptedBox.put('secret', 'Hive is cool'); print(encryptedBox.get('secret')); }
Additionally, you should encrypt your key at the application layer as well (using RSA encryption or other) and store it using the flutter_secure_storage plugin mentioned above. This secures your key as well, however, this is done using an external package.
Drift
Drift is a persistence library for Flutter and Dart, built on top of SQLite, which supports queries in both SQL and Dart available on mobile. It provides experimental support for the web at the time of writing this update.
Drift on both mobile and desktop is using the sqlite3 to send queries.
To use Drift for encrypted databases, you can use the encrypted_moor package similar to moor_flutter, using the sqflite_sqlcipher library mentioned above as one of the alternatives.
Alternatively, you can use the sqlcipher_flutter_libs package to include native SQLCipher libraries in your app.
You can learn more about the native encryption with Drift from here.
You might have observed by now that majority of the alternatives mentioned above are dependent upon an external package or another alternative mentioned. So make sure to use a database which suits your requirements the most. For me, Sqflite with SqlCipher support looks like a good alternative as I don’t have to create a codec, or depend upon a package to save my keys. Let us know what alternative are you going ahead with and why if possible.
Get setup with LogRocket's modern error tracking in minutes:
- Visit https://logrocket.com/signup/ to get an app ID.
- Install LogRocket via NPM or script tag.
LogRocket.init()
must be called client-side, not server-side. - (Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- ngrx middleware
- Vuex plugin
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>