Implementing Data Encryption for Local Storage in Flutter

Data encryption is a crucial aspect of mobile app security, especially when dealing with sensitive user information. In Flutter, securing locally stored data is essential for protecting user privacy and preventing unauthorized access. This blog post delves into how to implement data encryption for local storage in Flutter, covering various techniques, libraries, and best practices.

Why Encrypt Local Storage in Flutter?

  • Protect Sensitive Data: Safeguard personal user information, financial details, and other sensitive data.
  • Prevent Unauthorized Access: Ensure that even if the device is compromised, the data remains unreadable without the correct decryption key.
  • Comply with Regulations: Meet data protection standards and regulatory requirements such as GDPR and CCPA.
  • Enhance User Trust: Demonstrate a commitment to user privacy and data security, improving user confidence in your application.

Encryption Techniques for Local Storage

Several techniques can be employed to encrypt local storage in Flutter:

1. Symmetric Encryption

Symmetric encryption uses the same key to encrypt and decrypt data. This method is faster and more efficient for large amounts of data.

Using encrypt Package

The encrypt package in Flutter provides implementations of common symmetric encryption algorithms.

Step 1: Add Dependency

Include the encrypt package in your pubspec.yaml file:

dependencies:
  encrypt: ^5.0.1
Step 2: Implement Encryption and Decryption
import 'package:encrypt/encrypt.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class EncryptionService {
  static final _key = Key.fromLength(32); // 256-bit key
  static final _iv = IV.fromLength(16); // 128-bit initialization vector
  static final _encrypter = Encrypter(AES(_key));

  static Future encryptData(String plainText) async {
    final encrypted = _encrypter.encrypt(plainText, iv: _iv);
    return encrypted.base64;
  }

  static Future decryptData(String encryptedText) async {
    final encrypted = Encrypted.fromBase64(encryptedText);
    return _encrypter.decrypt(encrypted, iv: _iv);
  }
}

Future main() async {
  final plainText = "Sensitive data to be encrypted";
  final encryptedText = await EncryptionService.encryptData(plainText);
  final decryptedText = await EncryptionService.decryptData(encryptedText);

  print("Plain Text: $plainText");
  print("Encrypted Text: $encryptedText");
  print("Decrypted Text: $decryptedText");
}

In this example:

  • A 256-bit key and a 128-bit initialization vector (IV) are created.
  • The AES algorithm is used for encryption.
  • The encryptData function encrypts the plaintext using the AES algorithm.
  • The decryptData function decrypts the ciphertext back into plaintext.

2. Asymmetric Encryption

Asymmetric encryption uses a pair of keys—a public key for encryption and a private key for decryption. This method is more secure but computationally expensive and less suitable for large data.

Using RSA Algorithm

You can use the RSA algorithm for asymmetric encryption in Flutter. However, keep in mind the performance implications.

Step 1: Generate RSA Key Pair
import 'package:pointycastle/api.dart';
import 'package:pointycastle/asymmetric/api.dart';
import 'package:pointycastle/key_generators/rsa_key_generator.dart';
import 'package:pointycastle/random/fortuna.dart';

class RSAHelper {
  static AsymmetricKeyPair<PublicKey, PrivateKey> generateKeyPair() {
    final secureRandom = FortunaRandom();
    final random = SecureRandom("AES/CTR/AUTO-SEED-PRNG");
    final keyGen = RSAKeyGenerator()
      ..init(ParametersWithRandom(
          RSAKeyGeneratorParameters(BigInt.parse('65537'), 2048, 12),
          random));

    return keyGen.generateKeyPair();
  }
}

void main() {
  final keyPair = RSAHelper.generateKeyPair();
  final publicKey = keyPair.publicKey as RSAPublicKey;
  final privateKey = keyPair.privateKey as RSAPrivateKey;

  print("Public Key: $publicKey");
  print("Private Key: $privateKey");
}
Step 2: Implement RSA Encryption and Decryption
import 'dart:convert';
import 'package:pointycastle/api.dart';
import 'package:pointycastle/asymmetric/api.dart';
import 'package:pointycastle/encrypt/rsa.dart';
import 'package:pointycastle/params/rsa/pkcs1_encoding_parameters.dart';
import 'package:pointycastle/padding/pkcs7.dart';

class RSAEncryptionService {
  static String encrypt(String plainText, RSAPublicKey publicKey) {
    final cipher = PKCS1Encoding(RSAEngine())
      ..init(true, PublicKeyParameter<RSAPublicKey>(publicKey));
    
    final plainTextBytes = utf8.encode(plainText);
    final encryptedBytes = cipher.process(plainTextBytes);
    return base64.encode(encryptedBytes);
  }

  static String decrypt(String encryptedText, RSAPrivateKey privateKey) {
    final cipher = PKCS1Encoding(RSAEngine())
      ..init(false, PrivateKeyParameter<RSAPrivateKey>(privateKey));

    final encryptedBytes = base64.decode(encryptedText);
    final decryptedBytes = cipher.process(encryptedBytes);
    return utf8.decode(decryptedBytes);
  }
}

void main() {
  final keyPair = RSAHelper.generateKeyPair();
  final publicKey = keyPair.publicKey as RSAPublicKey;
  final privateKey = keyPair.privateKey as RSAPrivateKey;

  final plainText = "Sensitive data to be encrypted with RSA";
  final encryptedText = RSAEncryptionService.encrypt(plainText, publicKey);
  final decryptedText = RSAEncryptionService.decrypt(encryptedText, privateKey);

  print("Plain Text: $plainText");
  print("Encrypted Text: $encryptedText");
  print("Decrypted Text: $decryptedText");
}

Using RSA in Flutter requires handling keys securely and understanding the performance trade-offs.

3. Hybrid Encryption

Hybrid encryption combines the benefits of both symmetric and asymmetric encryption. It involves encrypting data with a symmetric key, which is then encrypted with the recipient’s public key. The recipient decrypts the symmetric key with their private key and uses it to decrypt the data.

Securely Storing Encryption Keys

Storing encryption keys securely is as critical as the encryption itself. Compromised keys render encryption useless.

Using flutter_secure_storage

The flutter_secure_storage plugin provides secure storage for sensitive data like encryption keys. It uses platform-specific secure storage mechanisms (Keychain on iOS, Keystore on Android).

Step 1: Add Dependency

Include flutter_secure_storage in your pubspec.yaml file:

dependencies:
  flutter_secure_storage: ^9.0.0
Step 2: Store and Retrieve Keys
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureKeyStorage {
  static final _storage = FlutterSecureStorage();

  static Future saveKey(String keyName, String keyValue) async {
    await _storage.write(key: keyName, value: keyValue);
  }

  static Future readKey(String keyName) async {
    return await _storage.read(key: keyName);
  }

  static Future deleteKey(String keyName) async {
    await _storage.delete(key: keyName);
  }
}

Future main() async {
  const keyName = 'myEncryptionKey';
  const keyValue = 'yourSecretEncryptionKey';

  // Store the encryption key securely
  await SecureKeyStorage.saveKey(keyName, keyValue);

  // Retrieve the encryption key
  final retrievedKey = await SecureKeyStorage.readKey(keyName);
  print("Retrieved Key: $retrievedKey");

  // Delete the encryption key when no longer needed
  // await SecureKeyStorage.deleteKey(keyName);
}

Using flutter_secure_storage ensures that keys are stored securely in platform-specific secure storage.

Practical Implementation: Encrypting SharedPreferences

SharedPreferences is commonly used for storing simple data in Flutter. Encrypting the data stored in SharedPreferences ensures sensitive information remains protected.

Step 1: Encrypt Data Before Saving

import 'package:shared_preferences/shared_preferences.dart';

Future saveEncryptedData(String key, String value) async {
  final encryptedValue = await EncryptionService.encryptData(value);
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString(key, encryptedValue);
}

Step 2: Decrypt Data After Retrieving

Future getDecryptedData(String key) async {
  final prefs = await SharedPreferences.getInstance();
  final encryptedValue = prefs.getString(key);
  if (encryptedValue == null) {
    return null;
  }
  return await EncryptionService.decryptData(encryptedValue);
}

Complete Example

import 'package:encrypt/encrypt.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';

class EncryptionService {
  static final _key = Key.fromLength(32);
  static final _iv = IV.fromLength(16);
  static final _encrypter = Encrypter(AES(_key));

  static Future encryptData(String plainText) async {
    final encrypted = _encrypter.encrypt(plainText, iv: _iv);
    return encrypted.base64;
  }

  static Future decryptData(String encryptedText) async {
    final encrypted = Encrypted.fromBase64(encryptedText);
    return _encrypter.decrypt(encrypted, iv: _iv);
  }
}

class SecureKeyStorage {
  static final _storage = FlutterSecureStorage();

  static Future saveKey(String keyName, String keyValue) async {
    await _storage.write(key: keyName, value: keyValue);
  }

  static Future readKey(String keyName) async {
    return await _storage.read(key: keyName);
  }

  static Future deleteKey(String keyName) async {
    await _storage.delete(key: keyName);
  }
}

Future saveEncryptedData(String key, String value) async {
  final encryptedValue = await EncryptionService.encryptData(value);
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString(key, encryptedValue);
}

Future getDecryptedData(String key) async {
  final prefs = await SharedPreferences.getInstance();
  final encryptedValue = prefs.getString(key);
  if (encryptedValue == null) {
    return null;
  }
  return await EncryptionService.decryptData(encryptedValue);
}

Future main() async {
  const dataKey = 'sensitiveData';
  const plainText = 'My secret data to be stored securely.';

  // Save encrypted data
  await saveEncryptedData(dataKey, plainText);
  print('Encrypted data saved successfully.');

  // Retrieve and decrypt data
  final decryptedData = await getDecryptedData(dataKey);
  print('Decrypted data: $decryptedData');
}

Best Practices for Data Encryption

  • Use Strong Encryption Algorithms: Choose well-established encryption algorithms like AES or RSA with appropriate key lengths.
  • Securely Manage Keys: Store encryption keys using platform-specific secure storage mechanisms (flutter_secure_storage).
  • Regularly Update Encryption Libraries: Keep encryption libraries updated to patch vulnerabilities.
  • Implement Defense in Depth: Combine encryption with other security measures, such as data masking and access controls.
  • Handle Exceptions and Errors Gracefully: Ensure proper error handling to prevent data leakage in case of encryption or decryption failures.
  • Conduct Security Audits: Regularly audit your code and security measures to identify and address potential vulnerabilities.

Conclusion

Implementing data encryption for local storage in Flutter is essential for protecting user privacy and ensuring data security. By using robust encryption techniques, securely managing keys, and adhering to best practices, you can build Flutter applications that protect sensitive user information. Whether using symmetric, asymmetric, or hybrid encryption, the key is to prioritize security and maintain vigilance against potential threats.