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

How to Implement a Custom Encryption System for Data Security in FlutterFlow

Implement field-level encryption in FlutterFlow using the encrypt Dart package with AES-256-CBC, deriving encryption keys from user passwords with PBKDF2 rather than hardcoding them. Store encrypted values as Base64 strings in Firestore. For enterprise applications, use Google Cloud KMS to manage keys server-side. Never put an encryption key as a plain string in Custom Action code.

What you'll learn

  • How to use the encrypt Dart package for AES-256 field-level encryption in FlutterFlow Custom Actions
  • How to derive secure encryption keys from user passwords using PBKDF2 instead of hardcoded strings
  • How to store and retrieve encrypted Base64 data in Firestore without corrupting the ciphertext
  • How to use Google Cloud KMS for server-side key management in high-security applications
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate10 min read50-70 minFlutterFlow Pro+ (code export required for custom packages)March 2026RapidDev Engineering Team
TL;DR

Implement field-level encryption in FlutterFlow using the encrypt Dart package with AES-256-CBC, deriving encryption keys from user passwords with PBKDF2 rather than hardcoding them. Store encrypted values as Base64 strings in Firestore. For enterprise applications, use Google Cloud KMS to manage keys server-side. Never put an encryption key as a plain string in Custom Action code.

Field-Level Encryption for Sensitive Firestore Data in FlutterFlow

Firestore provides transport encryption (TLS) and at-rest encryption by default, but these protections do not prevent Google, Firebase administrators, or anyone with service account access from reading your users' data. For sensitive fields such as medical records, private messages, or financial information, you need field-level encryption — encrypting the data before it reaches Firestore so that only the user who holds the key can read it. In FlutterFlow, this is implemented through Custom Actions using the encrypt Dart package. The critical design decision is key management: where does the encryption key come from? This tutorial covers three key sources from least to most secure — all better than hardcoding a key string in your code.

Prerequisites

  • FlutterFlow Pro plan with code export enabled
  • The encrypt and pointycastle Dart packages added to pubspec.yaml
  • Firebase Authentication configured — user identity is central to key derivation
  • Understanding of what data in your app requires encryption and why

Step-by-step guide

1

Add the encrypt package and configure AES-256

In FlutterFlow, go to Settings → Pubspec Dependencies and add encrypt: ^5.0.3 and pointycastle: ^3.7.4. The encrypt package provides a high-level AES API. The pointycastle package provides the underlying cryptographic primitives including PBKDF2 for key derivation. Create a Custom Action called encryptField that accepts plaintext (String) and encryptionKey (String) as parameters and returns an encrypted Base64 string. Inside the action, convert the 32-character key string to a Key object using Key.fromUtf8(keyString.padRight(32).substring(0, 32)), generate a random IV using IV.fromSecureRandom(16), and encrypt using Encrypter(AES(key, mode: AESMode.cbc)).encrypt(plaintext, iv: iv). Return the IV appended to the ciphertext as a single Base64 string for storage.

encrypt_field.dart
1// Custom Action: encryptField
2// Parameters: plaintext (String), keyString (String)
3// Returns: String (base64 encoded IV + ciphertext)
4import 'package:encrypt/encrypt.dart' as enc;
5
6Future<String> encryptField(String plaintext, String keyString) async {
7 // Key must be exactly 32 bytes for AES-256
8 final keyBytes = keyString.padRight(32).substring(0, 32);
9 final key = enc.Key.fromUtf8(keyBytes);
10 final iv = enc.IV.fromSecureRandom(16);
11 final encrypter = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc));
12 final encrypted = encrypter.encrypt(plaintext, iv: iv);
13 // Combine IV + ciphertext for storage
14 final combined = iv.base64 + ':' + encrypted.base64;
15 return combined;
16}
17
18// Custom Action: decryptField
19// Parameters: ciphertext (String), keyString (String)
20// Returns: String (original plaintext)
21Future<String> decryptField(String ciphertext, String keyString) async {
22 final parts = ciphertext.split(':');
23 if (parts.length != 2) return '';
24 final keyBytes = keyString.padRight(32).substring(0, 32);
25 final key = enc.Key.fromUtf8(keyBytes);
26 final iv = enc.IV.fromBase64(parts[0]);
27 final encrypter = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc));
28 return encrypter.decrypt64(parts[1], iv: iv);
29}

Expected result: The encryptField Custom Action returns a Base64 string that looks like random data. The decryptField action reverses it to the original plaintext.

2

Derive encryption keys with PBKDF2 instead of using hardcoded strings

A hardcoded key string in your Custom Action code is the most dangerous encryption pattern. It means everyone who has your app's source code (or decompiles the binary) has your encryption key. Instead, derive the key from the user's password using PBKDF2 (Password-Based Key Derivation Function 2). PBKDF2 takes a password, a random salt, and an iteration count (minimum 100,000 for security), and produces a fixed-length key. Store the salt in the user's Firestore document (it does not need to be secret). Derive the key each session after the user logs in, store it only in memory as an App State variable (not persisted), and discard it when the user logs out. This means only someone who knows the password can derive the correct key.

derive_key.dart
1// Custom Action: deriveKey
2// Parameters: password (String), saltBase64 (String)
3// Returns: String (hex key for use in encrypt/decrypt actions)
4import 'package:pointycastle/export.dart';
5import 'dart:convert';
6import 'dart:typed_data';
7
8Future<String> deriveKey(String password, String saltBase64) async {
9 final salt = base64.decode(saltBase64);
10 final passwordBytes = utf8.encode(password);
11
12 final params = Pbkdf2Parameters(
13 Uint8List.fromList(salt),
14 100000, // iterations
15 32, // output key length in bytes (256 bits)
16 );
17
18 final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64));
19 pbkdf2.init(params);
20
21 final key = pbkdf2.process(Uint8List.fromList(passwordBytes));
22 return base64.encode(key);
23}

Expected result: Given the same password and salt, deriveKey always returns the same Base64 key. Given a different password with the same salt, it returns a completely different key.

3

Store and retrieve encrypted fields in Firestore

Encrypted fields are stored as Base64 strings in Firestore. The string looks like two Base64 values separated by a colon: the IV and the ciphertext. In FlutterFlow, when writing a sensitive field (such as a medical note or private message), call encryptField in your Action Flow before the Firestore Create Document action, passing the plaintext and the derived key from App State. Bind the Firestore field to the return value of encryptField. When reading the field, call decryptField with the stored ciphertext and the in-memory derived key, and bind the result to your UI widget instead of the raw Firestore value. Never display the raw Base64 ciphertext string directly in the UI.

Expected result: Sensitive fields in Firestore contain Base64 ciphertext strings. The FlutterFlow UI displays the decrypted plaintext after calling decryptField on read.

4

Use Google Cloud KMS for enterprise key management

For applications that require regulatory compliance (HIPAA, GDPR for medical data), the client-derived key approach has a limitation: if a user forgets their password, their data is unrecoverable. Google Cloud KMS (Key Management Service) stores encryption keys in a hardware security module (HSM) and provides an API to encrypt and decrypt data server-side. In this pattern, your Cloud Function calls KMS to encrypt sensitive data before writing it to Firestore, and calls KMS again to decrypt on read. The KMS key never leaves the HSM. Access to the KMS key is controlled by Google IAM roles, providing a full audit trail. Set up KMS in the Google Cloud console: create a Key Ring, create an AES-256 key, and give your Cloud Function's service account the Cloud KMS Crypto Key Encrypter/Decrypter role.

encrypt_sensitive_data.js
1// Cloud Function: encryptSensitiveData
2// Uses Google Cloud KMS for server-side encryption
3const { KeyManagementServiceClient } = require('@google-cloud/kms');
4const functions = require('firebase-functions');
5
6const kmsClient = new KeyManagementServiceClient();
7const keyName = 'projects/YOUR_PROJECT/locations/global/keyRings/YOUR_RING/cryptoKeys/YOUR_KEY';
8
9exports.encryptSensitiveData = functions.https.onCall(async (data, context) => {
10 if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Must be logged in');
11 const plaintext = Buffer.from(data.plaintext, 'utf8');
12 const [result] = await kmsClient.encrypt({ name: keyName, plaintext });
13 return { ciphertext: result.ciphertext.toString('base64') };
14});

Expected result: Sensitive data is encrypted by KMS before reaching Firestore. The Cloud Function's service account is the only identity that can call the KMS decrypt API.

Complete working example

encryption_actions.dart
1// Complete encryption Custom Actions for FlutterFlow
2// Add encrypt: ^5.0.3 and pointycastle: ^3.7.4 to pubspec.yaml
3
4import 'package:encrypt/encrypt.dart' as enc;
5import 'package:pointycastle/export.dart';
6import 'dart:convert';
7import 'dart:typed_data';
8
9// ─── Action 1: deriveKeyFromPassword ───────────────────────────
10// Parameters: password (String), saltBase64 (String)
11// Returns: String (base64 encoded 256-bit key)
12Future<String> deriveKeyFromPassword(
13 String password, String saltBase64) async {
14 final salt = base64.decode(saltBase64);
15 final passwordBytes = utf8.encode(password);
16 final params = Pbkdf2Parameters(
17 Uint8List.fromList(salt),
18 100000,
19 32,
20 );
21 final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64));
22 pbkdf2.init(params);
23 final keyBytes = pbkdf2.process(Uint8List.fromList(passwordBytes));
24 return base64.encode(keyBytes);
25}
26
27// ─── Action 2: encryptField ─────────────────────────────────────
28// Parameters: plaintext (String), keyBase64 (String)
29// Returns: String ("<ivBase64>:<ciphertextBase64>")
30Future<String> encryptField(String plaintext, String keyBase64) async {
31 final keyBytes = base64.decode(keyBase64);
32 final key = enc.Key(Uint8List.fromList(keyBytes));
33 final iv = enc.IV.fromSecureRandom(16);
34 final encrypter = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc));
35 final encrypted = encrypter.encrypt(plaintext, iv: iv);
36 return 'enc:${iv.base64}:${encrypted.base64}';
37}
38
39// ─── Action 3: decryptField ─────────────────────────────────────
40// Parameters: ciphertext (String), keyBase64 (String)
41// Returns: String (original plaintext, or '' on failure)
42Future<String> decryptField(String ciphertext, String keyBase64) async {
43 if (!ciphertext.startsWith('enc:')) return ciphertext;
44 final parts = ciphertext.substring(4).split(':');
45 if (parts.length != 2) return '';
46 try {
47 final keyBytes = base64.decode(keyBase64);
48 final key = enc.Key(Uint8List.fromList(keyBytes));
49 final iv = enc.IV.fromBase64(parts[0]);
50 final encrypter = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc));
51 return encrypter.decrypt64(parts[1], iv: iv);
52 } catch (_) {
53 return '';
54 }
55}

Common mistakes when implementing a Custom Encryption System for Data Security in FlutterFlow

Why it's a problem: Encrypting data with a hardcoded key string in Custom Action code

How to avoid: Derive the encryption key from the user's password using PBKDF2 with a unique per-user salt. The key never exists in your code — it only exists in memory after the user authenticates. If you need a server-managed key, use Google Cloud KMS.

Why it's a problem: Reusing the same IV for multiple encryptions with the same key

How to avoid: Generate a new random IV for every encryption operation using IV.fromSecureRandom(16). Store the IV alongside the ciphertext (they are not secret). The combined IV + ciphertext format ensures each encryption is unique.

Why it's a problem: Storing the derived encryption key in a persisted App State variable

How to avoid: Store the derived key in a non-persisted (in-memory only) App State variable. Set it after the user logs in and clear it when the user logs out. Users will re-derive the key from their password on each session.

Best practices

  • Never hardcode encryption keys — derive them from user passwords with PBKDF2 or use Cloud KMS
  • Always use a random IV for every encryption operation and store it alongside the ciphertext
  • Use AES-256-CBC with at least 100,000 PBKDF2 iterations for password-derived keys
  • Store derived keys in non-persisted in-memory App State only — never write them to disk or Firestore
  • Add the 'enc:' prefix to encrypted Firestore field values to prevent accidentally displaying ciphertext as UI text
  • For regulatory compliance (HIPAA, GDPR sensitive data), use Cloud KMS with IAM audit logging rather than client-side encryption
  • Test your encryption round-trip before writing to Firestore — decrypt immediately after encrypting and verify the plaintext matches

Still stuck?

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

ChatGPT Prompt

I need to encrypt sensitive user data (medical notes) before storing it in Firestore from a FlutterFlow app. I want to use the user's login password to derive the encryption key using PBKDF2, encrypt with AES-256, and store as Base64 in Firestore. How do I implement this in Dart using the encrypt and pointycastle packages? Include key derivation, encryption with random IV, and decryption.

FlutterFlow Prompt

Create three FlutterFlow Custom Actions: deriveKeyFromPassword (takes password String and saltBase64 String, returns keyBase64 String using PBKDF2 with 100000 iterations), encryptField (takes plaintext String and keyBase64 String, returns iv:ciphertext Base64 String using AES-256-CBC), and decryptField (takes the combined ciphertext String and keyBase64 String, returns original plaintext). Use the encrypt and pointycastle Dart packages.

Frequently asked questions

Does Firestore not already encrypt my data?

Firestore encrypts data in transit (TLS) and at rest using Google-managed keys. However, this encryption protects against hardware theft at Google's data centres. Google, Firebase team members, or anyone with your service account credentials can still read your data. Field-level encryption ensures that only someone with the encryption key can read the data, even if they have full Firestore access.

What happens to a user's encrypted data if they forget their password?

With client-side password-derived encryption, the data is unrecoverable without the original password. This is the privacy trade-off. If data recovery is required, use a key escrow approach: encrypt the user's derived key with a second key stored in Cloud KMS, giving administrators the ability to recover data with proper authorization. Document this clearly in your privacy policy.

Can I search encrypted Firestore fields?

No. Encrypted fields contain ciphertext — Firestore cannot index or search the underlying plaintext. If you need to search sensitive fields, consider using a deterministic encryption mode (like AES-ECB or AES-SIV) for low-cardinality fields, or move search to a server-side component that decrypts before comparing. For most use cases, accepting that encrypted fields are not searchable is the correct security decision.

Is the encrypt Dart package safe to use in production?

The encrypt package (version 5.x) wraps PointyCastle, which is the de-facto cryptography library for Dart/Flutter. It implements standard AES correctly. However, like all cryptographic implementations, it is only as safe as how you use it — key management and IV handling are your responsibility.

How do I encrypt data that multiple users need to read, like a shared document?

This is the multi-party encryption problem. One approach is to encrypt the document with a random document key, then encrypt that document key separately for each authorized user using their individual derived key. Store the encrypted document keys as a map in the document. Each user decrypts only their copy of the document key, then uses it to decrypt the document. This pattern is used by apps like Signal and WhatsApp.

Can RapidDev help set up encryption for a healthcare or fintech application?

Yes. For applications requiring HIPAA or PCI-DSS compliance, encryption architecture decisions have legal implications. RapidDev Engineering Team can review your data model, recommend the appropriate encryption strategy (client-side vs KMS), and help implement it correctly. Compliance requirements vary significantly by jurisdiction and data type.

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.