Define a production-ready, offline-first encrypted document vault architecture in Flutter with concrete tech stack, data flows, and security boundaries.
Everything is encrypted before leaving memory boundaries.
No plaintext persistence outside RAM during active operations.
┌──────────────────────────────┐
│ Flutter UI │
└────────────┬─────────────────┘
│
┌────────────▼─────────────────┐
│ Application Layer │
│ (Vault Controller / State) │
└────────────┬─────────────────┘
│
┌────────────▼─────────────────┐
│ Domain Layer │
│ - Vault Service │
│ - Auth Service │
│ - Import/Export Service │
└────────────┬─────────────────┘
│
┌────────────▼─────────────────┐
│ Crypto Layer (CRITICAL) │
│ - Key Derivation (Argon2) │
│ - AES-256-GCM Encryption │
│ - Random Generator │
└────────────┬─────────────────┘
│
┌────────────▼─────────────────┐
│ Storage Layer │
│ - Encrypted files (.enc) │
│ - SQLCipher metadata DB │
│ - Secure storage (keys only) │
└──────────────────────────────┘
-
flutter(obvious) -
flutter_bloc(or Riverpod, but Riverpod is less boilerplate-heavy) -
freezed(immutable state models, will be used for release only) -
json_serializable(will be used for release only)
cryptography(Dart package)
Why:
-
Clean API
-
Modern primitives
-
Safer than raw PointyCastle usage
-
Android: Jetpack Security / Keystore
-
iOS: Keychain + CryptoKit (via platform channels if needed)
flutter_secure_storage
Backed by:
-
Android Keystore
-
iOS Keychain
Use ONLY for:
-
Vault master key (encrypted form)
-
salt metadata
-
config flags
Never store:
-
documents
-
decrypted keys in persistent storage
local_auth(Biometric authentication)
Backed by:
-
Android BiometricPrompt
-
iOS LocalAuthentication
Role:
- Unlock vault key, not replace password
sqflite_sqlcipherORsqlcipher_flutter_libs
This gives:
-
Encrypted SQLite DB
-
Searchable metadata (still encrypted at rest)
-
path_provider -
dart:io
Store:
/vault/
docs/
thumbs/
cache/ (temporary only)
All .enc
file_picker
-
flutter_windowmanager(Android FLAG_SECURE) -
iOS: handled via native config
-
uuid -
crypto(for hashes if needed, but not for encryption) -
path
Master Password
↓
Argon2id (salted)
↓
Master Key (KEK)
↓
Vault Key (DEK)
↓
AES-256-GCM encryption for files
-
Master key: derived from password
-
Vault key: rotates without changing password
- AES-256-GCM
Properties:
-
Confidentiality
-
Integrity (auth tag)
-
Tamper detection
Use:
- Argon2id (NOT SHA256)
Config:
Memory: 64–128 MB
Iterations: 2–3
Parallelism: 2
Document {
id: UUID,
title: String,
type: String,
encryptedFilePath: String,
thumbnailPath: String,
createdAt: Timestamp,
updatedAt: Timestamp
}Everything inside DB is encrypted at rest.
Each document:
file.enc
├── header (version, algorithm)
├── nonce
├── ciphertext
├── auth tag
User enters password
↓
Generate salt
↓
Argon2id(password, salt)
↓
Create Master Key
↓
Generate Vault Key
↓
Encrypt Vault Key with Master Key
↓
Store in secure storage
↓
Initialize encrypted DB
User enters password / biometrics
↓
Derive Master Key (Argon2id)
↓
Decrypt Vault Key
↓
Unlock session (in memory only)
Vault key never touches disk in plaintext.
Pick file
↓
Copy to temp directory
↓
Read bytes
↓
Encrypt (AES-GCM + Vault Key)
↓
Write .enc file
↓
Delete temp file
↓
Insert metadata into SQLCipher DB
Critical:
- temp cleanup must be guaranteed
Read .enc file
↓
Decrypt in memory
↓
Render (image/pdf viewer)
↓
Clear memory reference ASAP
No caching plaintext.
Collect metadata + encrypted files
↓
Package into archive
↓
Encrypt archive using:
backup password → Argon2id
↓
Output .vaultbackup
Backup is independent of vault password.
Select .vaultbackup
↓
Enter backup password
↓
Decrypt archive
↓
Rebuild DB + file structure
-
Background app → lock
-
Idle timeout → lock
Android:
FLAG_SECURE- advisory only (not blocking)
-
Avoid long-lived plaintext objects
-
Explicit nulling after use
Keep state simple:
VaultState
├── locked/unlocked
├── documents list
├── selected document
├── loading states
Do NOT mix crypto logic in UI.
-
Keystore-backed encryption key protection
-
FLAG_SECURE enforcement
-
Keychain storage
-
Secure enclave when available
Only use platform channels for:
-
hardened key storage
-
screen security
Not for app logic.
Solution:
- chunk-based encryption (optional later)
Solution:
- indexed metadata only
Solution:
- stream processing instead of full file load (future optimization)
-
Everything above except:
-
cloud sync
-
QR sharing
-
multi-device identity
-
-
End-to-end encryption sync
-
device pairing
-
conflict resolution
-
verifiable credentials
-
QR-based selective disclosure
Likelihood: High
Likelihood: High
Likelihood: Guaranteed
Likelihood: Medium
Likelihood: High
Build exactly this stack:
-
Flutter
-
Riverpod
-
cryptography
-
flutter_secure_storage
-
local_auth
-
sqflite_sqlcipher
-
file_picker
-
path_provider
-
AES-256-GCM
-
Argon2id
-
encrypted files on disk
-
SQLCipher metadata DB
-
secure storage only for wrapped keys
Implement an offline-first vault with:
-
Argon2id-derived master key
-
AES-256-GCM encrypted documents
-
SQLCipher encrypted metadata
-
biometric unlock via native APIs
-
encrypted import/export system
-
encryption/decryption time per 10MB file
-
vault unlock latency
-
import/export success rate
-
crash recovery after interrupted import
-
memory peak during encryption
-
user password weakness (cannot solve fully)
-
device compromise (root/jailbreak)
-
Flutter plugin vulnerabilities
-
accidental plaintext leaks in debug logs
You are trading:
-
complexity of cloud identity systems ❌
-
for deterministic local security model ✔️
This is the correct move for a solo developer.
If you execute this cleanly, you don’t get a “feature app.”
You get a legitimate security product that people can actually trust offline.