In Flutter app development, securely storing sensitive data on devices is critical for protecting user privacy and preventing unauthorized access. This article provides a comprehensive guide on implementing secure data storage using various methods available in Flutter.
Understanding the Need for Secure Data Storage
Secure data storage involves protecting sensitive information—such as user credentials, API keys, and personal data—from being accessed by unauthorized parties. Data at rest on a device can be vulnerable to various threats, including malware, device theft, and unauthorized access via rooted or jailbroken devices. Implementing robust security measures is essential to mitigate these risks.
Methods for Secure Data Storage in Flutter
1. Flutter Secure Storage
The flutter_secure_storage package is a popular choice for securely storing small amounts of data. It leverages platform-specific secure storage mechanisms:
- iOS: Keychain
- Android: Keystore
Step 1: Add the Dependency
Include flutter_secure_storage in your pubspec.yaml file:
dependencies:
flutter_secure_storage: ^9.0.0
Step 2: Store Data Securely
Use the FlutterSecureStorage class to store data:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final _storage = FlutterSecureStorage();
Future<void> storeData(String key, String value) async {
await _storage.write(key: key, value: value);
}
Step 3: Retrieve Data Securely
Retrieve stored data using the read method:
Future<String?> readData(String key) async {
return await _storage.read(key: key);
}
Step 4: Delete Data Securely
Delete data when it’s no longer needed to maintain security:
Future<void> deleteData(String key) async {
await _storage.delete(key: key);
}
Complete Example
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Secure Storage Demo',
home: SecureStorageDemo(),
);
}
}
class SecureStorageDemo extends StatefulWidget {
@override
_SecureStorageDemoState createState() => _SecureStorageDemoState();
}
class _SecureStorageDemoState extends State<SecureStorageDemo> {
final _storage = FlutterSecureStorage();
final _keyController = TextEditingController();
final _valueController = TextEditingController();
String _storedValue = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Secure Storage Demo'),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
controller: _keyController,
decoration: InputDecoration(labelText: 'Key'),
),
TextField(
controller: _valueController,
decoration: InputDecoration(labelText: 'Value'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
await _storage.write(key: _keyController.text, value: _valueController.text);
setState(() {
_storedValue = 'Stored';
});
},
child: Text('Store Data'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () async {
final value = await _storage.read(key: _keyController.text);
setState(() {
_storedValue = value ?? 'No data found';
});
},
child: Text('Read Data'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () async {
await _storage.delete(key: _keyController.text);
setState(() {
_storedValue = 'Deleted';
});
},
child: Text('Delete Data'),
),
SizedBox(height: 20),
Text('Stored Value: $_storedValue'),
],
),
),
);
}
}
2. Using SQLite with Encryption
For storing structured data, consider using SQLite with encryption. Several packages support encrypted SQLite databases in Flutter:
sqflite: Provides SQLite database access.sqlcipher_flutter_libs: Integrates SQLCipher for database encryption.
Step 1: Add Dependencies
Include the necessary dependencies in your pubspec.yaml file:
dependencies:
sqflite: ^2.3.0
sqlcipher_flutter_libs: ^0.1.3
Step 2: Initialize and Open the Database
Open an encrypted database using a passphrase:
import 'package:sqflite/sqflite.dart';
import 'package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart';
import 'package:path/path.dart';
Future<Database> openEncryptedDatabase() async {
var databasesPath = await getDatabasesPath();
var path = join(databasesPath, 'my_encrypted_db.db');
return await openDatabase(
path,
password: 'your_secret_passphrase', // Replace with a secure passphrase
version: 1,
onCreate: (Database db, int version) async {
await db.execute('CREATE TABLE my_table (id INTEGER PRIMARY KEY, value TEXT)');
},
);
}
Step 3: Perform Database Operations
Execute SQL commands to store and retrieve data:
Future<void> insertData(String value) async {
final db = await openEncryptedDatabase();
await db.transaction((txn) async {
await txn.rawInsert('INSERT INTO my_table(value) VALUES(?)', [value]);
});
await db.close();
}
Future<List<Map<String, dynamic>>> readData() async {
final db = await openEncryptedDatabase();
List<Map<String, dynamic>> list = await db.rawQuery('SELECT * FROM my_table');
await db.close();
return list;
}
Complete Example
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart';
import 'package:path/path.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Encrypted SQLite Demo',
home: EncryptedSQLiteDemo(),
);
}
}
class EncryptedSQLiteDemo extends StatefulWidget {
@override
_EncryptedSQLiteDemoState createState() => _EncryptedSQLiteDemoState();
}
class _EncryptedSQLiteDemoState extends State<EncryptedSQLiteDemo> {
Database? _db;
final _textController = TextEditingController();
List<Map<String, dynamic>> _dataList = [];
@override
void initState() {
super.initState();
_initDatabase();
}
Future<void> _initDatabase() async {
var databasesPath = await getDatabasesPath();
var path = join(databasesPath, 'my_encrypted_db.db');
_db = await openDatabase(
path,
password: 'your_secret_passphrase',
version: 1,
onCreate: (Database db, int version) async {
await db.execute('CREATE TABLE my_table (id INTEGER PRIMARY KEY, value TEXT)');
},
);
await _loadData();
}
Future<void> _loadData() async {
if (_db != null) {
List<Map<String, dynamic>> list = await _db!.rawQuery('SELECT * FROM my_table');
setState(() {
_dataList = list;
});
}
}
Future<void> _insertData(String value) async {
if (_db != null) {
await _db!.transaction((txn) async {
await txn.rawInsert('INSERT INTO my_table(value) VALUES(?)', [value]);
});
await _loadData();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Encrypted SQLite Demo'),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
controller: _textController,
decoration: InputDecoration(labelText: 'Enter Value'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
await _insertData(_textController.text);
_textController.clear();
},
child: Text('Insert Data'),
),
SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: _dataList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('ID: ${_dataList[index]['id']}, Value: ${_dataList[index]['value']}'),
);
},
),
),
],
),
),
);
}
@override
void dispose() {
_db?.close();
super.dispose();
}
}
3. Secure Preferences with Encryption
To store preferences with encryption, you can use the shared_preferences package combined with encryption. This approach ensures that preferences data remains secure on the device.
Step 1: Add Dependencies
Include the necessary dependencies in your pubspec.yaml file:
dependencies:
shared_preferences: ^2.2.2
encrypt: ^5.0.1
Step 2: Implement Encrypted Shared Preferences
Create utility functions to encrypt and decrypt the preferences data:
import 'package:shared_preferences/shared_preferences.dart';
import 'package:encrypt/encrypt.dart';
import 'package:pointycastle/asymmetric/api.dart';
class SecurePreferences {
static final _key = Key.fromLength(32); // Generate a 256-bit key
static final _iv = IV.fromLength(16); // Generate a 128-bit IV
static final _encrypter = Encrypter(AES(_key, mode: AESMode.cbc));
static Future<void> setEncryptedString(String key, String value) async {
final prefs = await SharedPreferences.getInstance();
final encrypted = _encrypter.encrypt(value, iv: _iv);
await prefs.setString(key, encrypted.base64);
}
static Future<String?> getEncryptedString(String key) async {
final prefs = await SharedPreferences.getInstance();
final encryptedString = prefs.getString(key);
if (encryptedString == null) {
return null;
}
try {
final encrypted = Encrypted.fromBase64(encryptedString);
return _encrypter.decrypt(encrypted, iv: _iv);
} catch (e) {
print('Decryption error: $e');
return null;
}
}
static Future<void> deleteEncryptedString(String key) async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(key);
}
}
Step 3: Use Secure Preferences
Store and retrieve data using the secure preferences functions:
Future<void> storePreference(String key, String value) async {
await SecurePreferences.setEncryptedString(key, value);
}
Future<String?> readPreference(String key) async {
return await SecurePreferences.getEncryptedString(key);
}
Future<void> deletePreference(String key) async {
await SecurePreferences.deleteEncryptedString(key);
}
Complete Example
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Secure Preferences Demo',
home: SecurePreferencesDemo(),
);
}
}
class SecurePreferencesDemo extends StatefulWidget {
@override
_SecurePreferencesDemoState createState() => _SecurePreferencesDemoState();
}
class _SecurePreferencesDemoState extends State<SecurePreferencesDemo> {
final _keyController = TextEditingController();
final _valueController = TextEditingController();
String _storedValue = '';
// Encryption setup
final key = encrypt.Key.fromLength(32);
final iv = encrypt.IV.fromLength(16);
final encrypter = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
Future<void> _storePreference(String key, String value) async {
final prefs = await SharedPreferences.getInstance();
final encrypted = encrypter.encrypt(value, iv: iv);
await prefs.setString(key, encrypted.base64);
setState(() {
_storedValue = 'Stored';
});
}
Future<String?> _readPreference(String key) async {
final prefs = await SharedPreferences.getInstance();
final encryptedString = prefs.getString(key);
if (encryptedString == null) {
return null;
}
try {
final encrypted = encrypt.Encrypted.fromBase64(encryptedString);
return encrypter.decrypt(encrypted, iv: iv);
} catch (e) {
print('Decryption error: $e');
return null;
}
}
Future<void> _deletePreference(String key) async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(key);
setState(() {
_storedValue = 'Deleted';
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Secure Preferences Demo'),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
controller: _keyController,
decoration: InputDecoration(labelText: 'Key'),
),
TextField(
controller: _valueController,
decoration: InputDecoration(labelText: 'Value'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
await _storePreference(_keyController.text, _valueController.text);
},
child: Text('Store Data'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () async {
final value = await _readPreference(_keyController.text);
setState(() {
_storedValue = value ?? 'No data found';
});
},
child: Text('Read Data'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () async {
await _deletePreference(_keyController.text);
},
child: Text('Delete Data'),
),
SizedBox(height: 20),
Text('Stored Value: $_storedValue'),
],
),
),
);
}
}
4. Key Considerations for Secure Data Storage
- Encryption Keys: Always manage encryption keys securely. Avoid hardcoding keys in your application; instead, use secure key management practices.
- User Authentication: Implement robust user authentication methods to ensure only authorized users can access sensitive data.
- Data Sanitization: Sanitize and validate all data inputs to prevent injection attacks and other security vulnerabilities.
- Regular Updates: Keep all dependencies and packages up to date to benefit from the latest security patches and improvements.
- Data Minimization: Only store data that is absolutely necessary to reduce the risk of data exposure.
Conclusion
Secure data storage is a critical aspect of Flutter app development. By leveraging techniques like flutter_secure_storage, encrypted SQLite databases, and secure preferences, developers can protect sensitive information and maintain user trust. Remember to adhere to best practices for key management, data sanitization, and regular updates to ensure the ongoing security of your applications.