Implementing Secure Data Storage on Devices in Flutter

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.