Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Implement Data Encryption for Sensitive Information in FlutterFlow

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.

What you'll learn

  • How to identify which data fields in your app require encryption
  • How to implement AES-256-GCM field-level encryption using the encrypt package
  • How to manage encryption keys securely with flutter_secure_storage
  • How to add a searchable hash field so encrypted data remains queryable
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner11 min read30-45 minFlutterFlow Pro+ (code export required for encrypt and flutter_secure_storage packages)March 2026RapidDev Engineering Team
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.

Prerequisites

  • 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
1# pubspec.yaml
2dependencies:
3 encrypt: ^5.0.3
4 flutter_secure_storage: ^9.0.0
5 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
1// lib/services/encryption_service.dart
2import 'dart:math';
3import 'dart:convert';
4import 'dart:typed_data';
5import 'package:encrypt/encrypt.dart' as enc;
6import 'package:flutter_secure_storage/flutter_secure_storage.dart';
7
8class EncryptedField {
9 final String ciphertext;
10 final String iv;
11 EncryptedField({required this.ciphertext, required this.iv});
12 Map<String, String> toMap() => {'ciphertext': ciphertext, 'iv': iv};
13}
14
15class EncryptionService {
16 static final EncryptionService _instance = EncryptionService._internal();
17 factory EncryptionService() => _instance;
18 EncryptionService._internal();
19
20 static const _keyStorageKey = 'aes_encryption_key';
21 static const _storage = FlutterSecureStorage();
22 enc.Key? _key;
23
24 Future<void> initialize() async {
25 String? storedKey = await _storage.read(key: _keyStorageKey);
26 if (storedKey == null) {
27 // Generate new 256-bit key
28 final random = Random.secure();
29 final keyBytes = List<int>.generate(32, (_) => random.nextInt(256));
30 storedKey = base64.encode(keyBytes);
31 await _storage.write(key: _keyStorageKey, value: storedKey);
32 }
33 _key = enc.Key(base64.decode(storedKey));
34 }
35
36 EncryptedField encrypt(String plaintext) {
37 assert(_key != null, 'Call initialize() before encrypting');
38 final iv = enc.IV.fromSecureRandom(16);
39 final encrypter = enc.Encrypter(enc.AES(_key!, mode: enc.AESMode.gcm));
40 final encrypted = encrypter.encrypt(plaintext, iv: iv);
41 return EncryptedField(
42 ciphertext: encrypted.base64,
43 iv: base64.encode(iv.bytes),
44 );
45 }
46
47 String decrypt(String ciphertext, String ivBase64) {
48 assert(_key != null, 'Call initialize() before decrypting');
49 final iv = enc.IV(base64.decode(ivBase64));
50 final encrypter = enc.Encrypter(enc.AES(_key!, mode: enc.AESMode.gcm));
51 return encrypter.decrypt64(ciphertext, iv: iv);
52 }
53}

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
1// custom_actions/encrypt_field.dart
2import '../services/encryption_service.dart';
3
4Future<Map<String, String>> encryptField(String plaintext) async {
5 final service = EncryptionService();
6 if (!service.isInitialized) await service.initialize();
7 final result = service.encrypt(plaintext);
8 return result.toMap();
9}
10
11// custom_actions/decrypt_field.dart
12import '../services/encryption_service.dart';
13
14Future<String> decryptField(String ciphertext, String iv) async {
15 final service = EncryptionService();
16 if (!service.isInitialized) await service.initialize();
17 try {
18 return service.decrypt(ciphertext, iv);
19 } catch (e) {
20 return '[Decryption failed]';
21 }
22}

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
1// custom_actions/hash_for_search.dart
2import 'dart:convert';
3import 'package:crypto/crypto.dart';
4
5// Returns a consistent SHA-256 hash for use as a searchable index
6// Add a static salt per field to prevent cross-field hash collisions
7String hashForSearch(String value, {String fieldSalt = ''}) {
8 final appSalt = 'your-app-specific-salt-here'; // Store in app constants
9 final input = '$appSalt:$fieldSalt:${value.toLowerCase().trim()}';
10 final bytes = utf8.encode(input);
11 return sha256.convert(bytes).toString();
12}

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 working example

lib/services/encryption_service.dart
1// encryption_service.dart — AES-256-GCM field-level encryption
2// Manages key generation, storage, and encrypt/decrypt operations
3// Initialize once in main() before runApp()
4
5import 'dart:math';
6import 'dart:convert';
7import 'dart:typed_data';
8import 'package:encrypt/encrypt.dart' as enc;
9import 'package:flutter_secure_storage/flutter_secure_storage.dart';
10import 'package:crypto/crypto.dart';
11
12class EncryptedField {
13 final String ciphertext;
14 final String iv;
15 const EncryptedField({required this.ciphertext, required this.iv});
16 Map<String, String> toMap() => {'ciphertext': ciphertext, 'iv': iv};
17}
18
19class EncryptionService {
20 static final EncryptionService _instance = EncryptionService._internal();
21 factory EncryptionService() => _instance;
22 EncryptionService._internal();
23
24 static const _keyStorageKey = 'aes_256_gcm_key_v1';
25 static const _appSalt = 'REPLACE_WITH_YOUR_APP_SALT';
26 static const _storage = FlutterSecureStorage(
27 aOptions: AndroidOptions(encryptedSharedPreferences: true),
28 iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
29 );
30
31 enc.Key? _key;
32 bool get isInitialized => _key != null;
33
34 Future<void> initialize() async {
35 String? storedKey = await _storage.read(key: _keyStorageKey);
36 if (storedKey == null) {
37 final random = Random.secure();
38 final keyBytes =
39 Uint8List.fromList(List.generate(32, (_) => random.nextInt(256)));
40 storedKey = base64Url.encode(keyBytes);
41 await _storage.write(key: _keyStorageKey, value: storedKey);
42 }
43 _key = enc.Key(base64Url.decode(storedKey));
44 }
45
46 /// Encrypt a plaintext string. Returns ciphertext + IV pair.
47 /// Always generates a fresh random IV — never reuse IVs.
48 EncryptedField encrypt(String plaintext) {
49 _assertInitialized();
50 final iv = enc.IV.fromSecureRandom(16);
51 final encrypter = enc.Encrypter(enc.AES(_key!, mode: enc.AESMode.gcm));
52 final encrypted = encrypter.encrypt(plaintext, iv: iv);
53 return EncryptedField(
54 ciphertext: encrypted.base64,
55 iv: base64.encode(iv.bytes),
56 );
57 }
58
59 /// Decrypt a ciphertext using the stored AES key and provided IV.
60 String decrypt(String ciphertext, String ivBase64) {
61 _assertInitialized();
62 final iv = enc.IV(base64.decode(ivBase64));
63 final encrypter = enc.Encrypter(enc.AES(_key!, mode: enc.AESMode.gcm));
64 return encrypter.decrypt64(ciphertext, iv: iv);
65 }
66
67 /// Hash a value for searchable indexing.
68 /// Uses SHA-256 with app salt and optional field-level salt.
69 /// Normalize input before hashing (lowercase, trim).
70 String hashForSearch(String value, {String fieldSalt = ''}) {
71 final normalized = value.toLowerCase().trim();
72 final input = '$_appSalt:$fieldSalt:$normalized';
73 return sha256.convert(utf8.encode(input)).toString();
74 }
75
76 /// Encrypt a field and compute its search hash in one call.
77 Map<String, String> encryptWithHash(String plaintext,
78 {String fieldSalt = ''}) {
79 final encrypted = encrypt(plaintext);
80 final hash = hashForSearch(plaintext, fieldSalt: fieldSalt);
81 return {
82 'ciphertext': encrypted.ciphertext,
83 'iv': encrypted.iv,
84 'search_hash': hash,
85 };
86 }
87
88 void _assertInitialized() {
89 if (_key == null) {
90 throw StateError(
91 'EncryptionService not initialized. Call initialize() in main().');
92 }
93 }
94}

Common mistakes when implementing Data Encryption for Sensitive Information in FlutterFlow

Why it's a problem: Using the same IV (Initialization Vector) for every encryption operation

How to avoid: 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.

Why it's a problem: Storing the encryption key in SharedPreferences instead of flutter_secure_storage

How to avoid: 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.

Why it's a problem: Encrypting the same field with AES-ECB mode instead of AES-GCM

How to avoid: 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.

Why it's a problem: Trying to do full-text search on encrypted fields

How to avoid: 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.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a Flutter health app that stores sensitive medical records in Firestore. Write a Dart EncryptionService class that uses AES-256-GCM with the encrypt package. The service should: generate a 256-bit key on first launch and store it in flutter_secure_storage, generate a fresh random 16-byte IV for each encrypt call, store the IV as Base64 alongside the ciphertext, and provide a hashForSearch method using SHA-256 for creating queryable indexes on encrypted fields.

FlutterFlow Prompt

In my FlutterFlow project I have a patient intake form with fields for name, date of birth, and medical notes. I need to encrypt these before saving to Firestore. I have a Custom Action called 'encryptField' that takes a String and returns a Map with 'ciphertext' and 'iv'. Walk me through wiring the form's Submit button action flow to: call encryptField for each sensitive field, then create a Firestore document with the encrypted values and their IVs, and a plain-text search hash.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.