# How to Implement Data Encryption for Sensitive Information in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 30-45 min
- Compatibility: FlutterFlow Pro+ (code export required for encrypt and flutter_secure_storage packages)
- Last updated: March 2026

## TL;DR

Protect sensitive data in FlutterFlow by identifying which fields need encryption (PII, health data, financial records), encrypting them client-side with AES-256-GCM using the encrypt package, and storing the encryption key in flutter_secure_storage. Always generate a unique random IV per encryption operation. Add a SHA-256 hash of searchable values alongside the encrypted field so you can query without decrypting.

## Field-Level Encryption for HIPAA, GDPR, and PCI Compliance

Storing sensitive data in Firestore without encryption means anyone with database access — your team, Firebase support, or an attacker who compromises your service account — can read your users' personal information in plain text. Field-level encryption solves this: encrypt the sensitive value on the client device before it is written to Firestore, so the database only ever stores ciphertext. Only the app (holding the encryption key) can decrypt and display the real value. This tutorial covers practical encryption for three categories: personally identifiable information (PII), health data, and financial information.

## Before you start

- A FlutterFlow Pro account with code export enabled
- A Firebase project with Firestore and Authentication configured
- A Flutter project exported locally to add the encrypt and flutter_secure_storage packages
- Awareness of which fields in your app contain sensitive data

## Step-by-step guide

### 1. Audit your Firestore schema to identify fields requiring encryption

Before writing any code, make a list of every Firestore field that contains sensitive information. PII requiring encryption includes: full name, date of birth, government ID numbers, passport numbers, phone numbers, and home address. Health data includes: diagnosis codes, medication names, lab results, and mental health notes. Financial data includes: full credit card numbers, bank account numbers, and tax ID numbers. Fields that do NOT need encryption (but still need access controls): email address (used for auth lookup), user UID, timestamps, and non-sensitive metadata. Write this list into a comment at the top of your encryption utility file — it serves as documentation for your team and compliance audits.

**Expected result:** A documented list of 5-15 sensitive fields that will be encrypted, and a clear separation from non-sensitive fields that can remain as plaintext.

### 2. Add encryption packages after exporting the project

Export your FlutterFlow project and open pubspec.yaml. Add encrypt (version ^5.0.3) for AES-256-GCM encryption and flutter_secure_storage (version ^9.0.0) for secure key storage. Run flutter pub get. The encrypt package provides a clean API around the AES algorithm with support for GCM mode, which provides both encryption and authentication (detects tampering). Do not use older AES-CBC or AES-ECB modes — they lack authentication tags and are vulnerable to padding oracle attacks.

```
# pubspec.yaml
dependencies:
  encrypt: ^5.0.3
  flutter_secure_storage: ^9.0.0
  crypto: ^3.0.3  # For SHA-256 hash of searchable fields
```

**Expected result:** flutter pub get completes with no errors. All three packages appear in pubspec.lock.

### 3. Create an EncryptionService with key generation and AES-256-GCM

Create a file called lib/services/encryption_service.dart. This singleton service generates a 256-bit (32-byte) AES key on first app launch, stores it in flutter_secure_storage under the key 'aes_encryption_key', and loads it on subsequent launches. It exposes two methods: encrypt(String plaintext) returns an EncryptedField object with ciphertext (Base64 string) and iv (Base64 string stored separately), and decrypt(String ciphertext, String iv) returns the original plaintext. The IV is stored alongside the ciphertext in Firestore as a separate field — this is safe, the IV does not need to be secret, only unique.

```
// lib/services/encryption_service.dart
import 'dart:math';
import 'dart:convert';
import 'dart:typed_data';
import 'package:encrypt/encrypt.dart' as enc;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class EncryptedField {
  final String ciphertext;
  final String iv;
  EncryptedField({required this.ciphertext, required this.iv});
  Map<String, String> toMap() => {'ciphertext': ciphertext, 'iv': iv};
}

class EncryptionService {
  static final EncryptionService _instance = EncryptionService._internal();
  factory EncryptionService() => _instance;
  EncryptionService._internal();

  static const _keyStorageKey = 'aes_encryption_key';
  static const _storage = FlutterSecureStorage();
  enc.Key? _key;

  Future<void> initialize() async {
    String? storedKey = await _storage.read(key: _keyStorageKey);
    if (storedKey == null) {
      // Generate new 256-bit key
      final random = Random.secure();
      final keyBytes = List<int>.generate(32, (_) => random.nextInt(256));
      storedKey = base64.encode(keyBytes);
      await _storage.write(key: _keyStorageKey, value: storedKey);
    }
    _key = enc.Key(base64.decode(storedKey));
  }

  EncryptedField encrypt(String plaintext) {
    assert(_key != null, 'Call initialize() before encrypting');
    final iv = enc.IV.fromSecureRandom(16);
    final encrypter = enc.Encrypter(enc.AES(_key!, mode: enc.AESMode.gcm));
    final encrypted = encrypter.encrypt(plaintext, iv: iv);
    return EncryptedField(
      ciphertext: encrypted.base64,
      iv: base64.encode(iv.bytes),
    );
  }

  String decrypt(String ciphertext, String ivBase64) {
    assert(_key != null, 'Call initialize() before decrypting');
    final iv = enc.IV(base64.decode(ivBase64));
    final encrypter = enc.Encrypter(enc.AES(_key!, mode: enc.AESMode.gcm));
    return encrypter.decrypt64(ciphertext, iv: iv);
  }
}
```

**Expected result:** The EncryptionService generates and stores an AES key on first launch. Subsequent launches load the same key. Test by encrypting a string, restarting the app, and confirming it decrypts back to the original.

### 4. Create Custom Actions for encrypting and decrypting fields

In your FlutterFlow project, add two Custom Actions that wrap the EncryptionService. The first, 'encryptField', takes a String parameter 'plaintext' and returns a Map (ciphertext and iv). Call this action in any form submission flow before the Firestore create/update action. The second, 'decryptField', takes two String parameters (ciphertext and iv) and returns the decrypted String. Call this action in the page's initState or in a widget's onLoad trigger, storing the result in a Page State variable that the Text widget displays. Never display raw ciphertext in the UI — always decrypt first.

```
// custom_actions/encrypt_field.dart
import '../services/encryption_service.dart';

Future<Map<String, String>> encryptField(String plaintext) async {
  final service = EncryptionService();
  if (!service.isInitialized) await service.initialize();
  final result = service.encrypt(plaintext);
  return result.toMap();
}

// custom_actions/decrypt_field.dart
import '../services/encryption_service.dart';

Future<String> decryptField(String ciphertext, String iv) async {
  final service = EncryptionService();
  if (!service.isInitialized) await service.initialize();
  try {
    return service.decrypt(ciphertext, iv);
  } catch (e) {
    return '[Decryption failed]';
  }
}
```

**Expected result:** Test by filling a form with a sensitive value (e.g., a phone number), submitting it, and checking Firestore — the stored value should be unreadable Base64 strings. The form detail view should show the original number after decryption.

### 5. Add a searchable hash field for encrypted data

Encrypted fields cannot be queried directly — you cannot search Firestore for 'all users named John' when names are encrypted. The solution is a hash companion field. When you encrypt a value, also compute its SHA-256 hash (a deterministic one-way function) and store it as a separate plain-text field, e.g., phone_number_hash alongside phone_number_encrypted and phone_number_iv. When searching for a specific value, hash the search term with the same function and query Firestore for documents where phone_number_hash equals the hashed search term. The hash reveals nothing about the original value but enables exact-match lookups.

```
// custom_actions/hash_for_search.dart
import 'dart:convert';
import 'package:crypto/crypto.dart';

// Returns a consistent SHA-256 hash for use as a searchable index
// Add a static salt per field to prevent cross-field hash collisions
String hashForSearch(String value, {String fieldSalt = ''}) {
  final appSalt = 'your-app-specific-salt-here'; // Store in app constants
  final input = '$appSalt:$fieldSalt:${value.toLowerCase().trim()}';
  final bytes = utf8.encode(input);
  return sha256.convert(bytes).toString();
}
```

**Expected result:** Submitting a form stores three fields per sensitive value: the ciphertext, the IV, and the hash. A Firestore query filtering by the hash field returns the correct document instantly.

## Complete code example

File: `lib/services/encryption_service.dart`

```dart
// encryption_service.dart — AES-256-GCM field-level encryption
// Manages key generation, storage, and encrypt/decrypt operations
// Initialize once in main() before runApp()

import 'dart:math';
import 'dart:convert';
import 'dart:typed_data';
import 'package:encrypt/encrypt.dart' as enc;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:crypto/crypto.dart';

class EncryptedField {
  final String ciphertext;
  final String iv;
  const EncryptedField({required this.ciphertext, required this.iv});
  Map<String, String> toMap() => {'ciphertext': ciphertext, 'iv': iv};
}

class EncryptionService {
  static final EncryptionService _instance = EncryptionService._internal();
  factory EncryptionService() => _instance;
  EncryptionService._internal();

  static const _keyStorageKey = 'aes_256_gcm_key_v1';
  static const _appSalt = 'REPLACE_WITH_YOUR_APP_SALT';
  static const _storage = FlutterSecureStorage(
    aOptions: AndroidOptions(encryptedSharedPreferences: true),
    iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
  );

  enc.Key? _key;
  bool get isInitialized => _key != null;

  Future<void> initialize() async {
    String? storedKey = await _storage.read(key: _keyStorageKey);
    if (storedKey == null) {
      final random = Random.secure();
      final keyBytes =
          Uint8List.fromList(List.generate(32, (_) => random.nextInt(256)));
      storedKey = base64Url.encode(keyBytes);
      await _storage.write(key: _keyStorageKey, value: storedKey);
    }
    _key = enc.Key(base64Url.decode(storedKey));
  }

  /// Encrypt a plaintext string. Returns ciphertext + IV pair.
  /// Always generates a fresh random IV — never reuse IVs.
  EncryptedField encrypt(String plaintext) {
    _assertInitialized();
    final iv = enc.IV.fromSecureRandom(16);
    final encrypter = enc.Encrypter(enc.AES(_key!, mode: enc.AESMode.gcm));
    final encrypted = encrypter.encrypt(plaintext, iv: iv);
    return EncryptedField(
      ciphertext: encrypted.base64,
      iv: base64.encode(iv.bytes),
    );
  }

  /// Decrypt a ciphertext using the stored AES key and provided IV.
  String decrypt(String ciphertext, String ivBase64) {
    _assertInitialized();
    final iv = enc.IV(base64.decode(ivBase64));
    final encrypter = enc.Encrypter(enc.AES(_key!, mode: enc.AESMode.gcm));
    return encrypter.decrypt64(ciphertext, iv: iv);
  }

  /// Hash a value for searchable indexing.
  /// Uses SHA-256 with app salt and optional field-level salt.
  /// Normalize input before hashing (lowercase, trim).
  String hashForSearch(String value, {String fieldSalt = ''}) {
    final normalized = value.toLowerCase().trim();
    final input = '$_appSalt:$fieldSalt:$normalized';
    return sha256.convert(utf8.encode(input)).toString();
  }

  /// Encrypt a field and compute its search hash in one call.
  Map<String, String> encryptWithHash(String plaintext,
      {String fieldSalt = ''}) {
    final encrypted = encrypt(plaintext);
    final hash = hashForSearch(plaintext, fieldSalt: fieldSalt);
    return {
      'ciphertext': encrypted.ciphertext,
      'iv': encrypted.iv,
      'search_hash': hash,
    };
  }

  void _assertInitialized() {
    if (_key == null) {
      throw StateError(
          'EncryptionService not initialized. Call initialize() in main().');
    }
  }
}
```

## Common mistakes

- **Using the same IV (Initialization Vector) for every encryption operation** — In AES-GCM, reusing an IV with the same key is catastrophic — an attacker who observes two ciphertexts with the same IV can recover the encryption key. Even reusing an IV once breaks the security of all data encrypted with that key. Fix: Generate a new random 16-byte IV for every single encryption call using IV.fromSecureRandom(16). Store the IV alongside the ciphertext in Firestore — it is not secret, only unique.
- **Storing the encryption key in SharedPreferences instead of flutter_secure_storage** — SharedPreferences stores data in plain text in an XML file on the device's internal storage. On rooted Android devices or in some backup configurations, this file is readable without special access. Fix: Always use flutter_secure_storage, which uses Android Keystore on Android and the iOS Keychain on iOS — both hardware-backed secure enclaves designed specifically for key storage.
- **Encrypting the same field with AES-ECB mode instead of AES-GCM** — AES-ECB (Electronic Codebook) mode produces the same ciphertext for the same input every time. This makes encrypted data vulnerable to frequency analysis and pattern detection — even without decryption, an attacker can see which records share the same value. Fix: Always use AES-GCM mode. GCM mode includes a random IV that ensures identical plaintexts produce different ciphertexts, and it provides an authentication tag that detects any tampering.
- **Trying to do full-text search on encrypted fields** — Because the ciphertext of 'John Smith' shares nothing with the ciphertext of 'John' — full-text search (LIKE queries, starts-with) is impossible on encrypted data without decrypting everything first. Fix: Use the searchable hash approach for exact-match lookups. For full-text search, maintain a separate search index on the server using Firebase's Extension for Algolia or Typesense, populated by a Cloud Function that decrypts values server-side in a secure environment.

## Best practices

- Encrypt data on the client before it reaches Firestore — never rely on Firestore security rules alone to protect sensitive data.
- Generate a fresh random IV for every encryption operation — never reuse IVs, even across different fields.
- Use AES-256-GCM exclusively for field encryption. It provides both confidentiality and integrity verification in a single operation.
- Store encryption keys only in flutter_secure_storage, never in SharedPreferences, Firestore, or hardcoded in source code.
- Document every encrypted field in a data classification registry — compliance auditors and new team members need to know what is protected and why.
- Implement key rotation capability: when rotating to a new key, re-encrypt all sensitive documents in a background Cloud Function and delete the old key after successful rotation.
- Test decryption after app reinstall — users who reinstall the app lose their flutter_secure_storage keys on Android (unless backed up). Implement a key recovery flow or warn users that data will be inaccessible.

## Frequently asked questions

### Does this encryption satisfy HIPAA requirements?

AES-256 encryption satisfies HIPAA's encryption specification for data at rest when implemented correctly. However, HIPAA compliance also requires audit logging, access controls, breach notification procedures, and a Business Associate Agreement with Firebase. Consult a compliance professional before handling PHI in production.

### What happens if a user's encryption key is lost?

If the flutter_secure_storage key is deleted (app uninstall on Android without backup, factory reset), all encrypted data becomes permanently unreadable — this is an inherent property of strong encryption. Implement an optional server-side key escrow (the key encrypted with a user's password) for recovery, or clearly communicate this limitation to users.

### Can I encrypt data with one key per user instead of one shared key?

Yes, and this is better practice for multi-user apps. Generate a separate AES key for each user during registration, store it in flutter_secure_storage on their device, and optionally back it up encrypted with their account password. This means one compromised device does not expose all users' data.

### Why not just encrypt the entire Firestore document instead of individual fields?

Encrypting individual fields lets you keep non-sensitive metadata (timestamps, status fields, category) queryable in Firestore while protecting only the truly sensitive values. If you encrypt the entire document, all queries are impossible and you lose Firestore's indexing benefits entirely.

### Can server-side Cloud Functions read encrypted data?

No, unless the Cloud Function has access to the encryption key. For server-side processing of sensitive data, you have two options: use server-side encryption with a key stored in Firebase Secret Manager (not client-side), or send the decrypted value to the Cloud Function over a secure connection for processing and re-encrypt before storing.

### How do I handle encryption in a web version of my FlutterFlow app?

flutter_secure_storage falls back to localStorage on web, which is not hardware-backed. For web builds handling truly sensitive data, use the SubtleCrypto API via a JavaScript interop package, and store keys in sessionStorage (cleared on tab close) rather than localStorage.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-data-encryption-for-sensitive-information-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-data-encryption-for-sensitive-information-in-flutterflow
