TL;DR
We built PasswordSafeVault because cloud password managers claim "zero-knowledge" but still hold your encrypted data. Our solution: local-only storage with Flutter, using platform-specific security (iOS Keychain/Android Keystore), no cloud sync, no accounts, just your device. Cross-platform from a single codebase. App Store | Download
The Problem with Cloud Password Managers
As developers, we understand the appeal of cloud services. But when it comes to password managers, the cloud model has fundamental flaws:
// The cloud password manager flow
user -> encrypt -> internet -> cloud -> storage
↑ ↑ ↑ ↑
risk: risk: risk: risk:
master pwd local env network server
Recent incidents highlight the risks:
- LastPass: Multiple breaches, source code stolen
- 1Password: Government data requests acknowledged
- Bitwarden: Better (open source) but still cloud-based
The uncomfortable truth: "Zero-knowledge" ≠ Your data. They still hold your encrypted vault.
Our Solution: PasswordSafeVault (Built with Flutter)
We built a cross-platform password manager that's actually local-first, using Flutter for consistent security across iOS and Android:
// Core Flutter architecture
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:local_auth/local_auth.dart';
import 'package:encrypt/encrypt.dart';
void main() {
runApp(const PasswordSafeVaultApp());
}
class PasswordSafeVaultApp extends StatelessWidget {
const PasswordSafeVaultApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "'PasswordSafeVault',"
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const HomeScreen(),
// No network dependencies
// No cloud APIs
// No third-party authentication
);
}
}
Key Technical Decisions
1. Local-Only Storage with Flutter
class LocalStorageManager {
final FlutterSecureStorage _secureStorage = FlutterSecureStorage();
final Box<Map<String, dynamic>> _encryptedBox;
LocalStorageManager(this._encryptedBox);
Future<void> saveEncryptedData(Map<String, dynamic> data, String id) async {
await _encryptedBox.put(id, data);
}
Future<Map<String, dynamic>?> getEncryptedData(String id) async {
return _encryptedBox.get(id);
}
}
Why this matters:
- No server breaches possible
- No API vulnerabilities to exploit
- No network latency or connectivity issues
- Complete control over data location
- Same security model on iOS and Android
2. Platform-Specific Security Integration
class PlatformSecurityManager {
final FlutterSecureStorage _storage = FlutterSecureStorage();
// iOS: Uses Keychain Services (backed by Secure Enclave)
// Android: Uses Android Keystore system
Future<void> storeEncryptionKey(String key) async {
await _storage.write(
key: 'master_encryption_key',
value: key,
iOptions: const IOSOptions(
accessibility: KeychainAccessibility.unlockedThisDeviceOnly,
),
aOptions: const AndroidOptions(
encryptedSharedPreferences: true,
),
);
}
Future<String?> getEncryptionKey() async {
return await _storage.read(key: 'master_encryption_key');
}
}
Platform security benefits:
- iOS: Hardware-isolated key storage via Keychain/Secure Enclave
- Android: Hardware-backed encryption via Keystore
- Keys never leave platform secure storage
- Even with OS compromise, keys remain protected
- Rate limiting and anti-brute force built-in
3. Modern Flutter Architecture with Riverpod
// Using Riverpod for state management
final passwordListProvider = StateNotifierProvider<PasswordListNotifier, List<PasswordEntry>>(
(ref) => PasswordListNotifier(ref.watch(storageProvider)),
);
class PasswordListNotifier extends StateNotifier<List<PasswordEntry>> {
final LocalStorageManager _storage;
PasswordListNotifier(this._storage) : super([]) {
_loadPasswords();
}
Future<void> _loadPasswords() async {
final passwords = await _storage.getAllPasswords();
state = passwords;
}
Future<void> addPassword(PasswordEntry password) async {
await _storage.savePassword(password);
state = [...state, password];
}
Future<void> deletePassword(String id) async {
await _storage.deletePassword(id);
state = state.where((p) => p.id != id).toList();
}
}
Security Implementation Details
Cross-Platform Encryption Stack
┌─────────────────────────────────────┐
│ PasswordSafeVault Security Stack │
├─────────────────────────────────────┤
│ Platform Biometrics (local_auth) │
│ Master Password (PBKDF2) │
│ AES-256-GCM (encrypt package) │
│ Platform Secure Storage │
└─────────────────────────────────────┘
1. AES-256-GCM with Dart Encryption
import 'package:encrypt/encrypt.dart';
class PasswordEncryptor {
late Encrypter _encrypter;
final FlutterSecureStorage _secureStorage = FlutterSecureStorage();
Future<void> initialize(String masterPassword) async {
// Derive key from master password using PBKDF2
final salt = IV.fromSecureRandom(16);
final key = await _deriveKey(masterPassword, salt);
_encrypter = Encrypter(AES(key, mode: AESMode.gcm));
// Store salt in platform secure storage
await _secureStorage.write(
key: 'encryption_salt',
value: salt.base64,
);
}
Future<Key> _deriveKey(String password, IV salt) async {
// PBKDF2 key derivation
final pbkdf2 = PBKDF2(
keyLength: 32,
iterations: 100000,
);
final keyBytes = pbkdf2.process(
password.codeUnits,
salt: salt.bytes,
);
return Key(keyBytes);
}
Future<String> encrypt(String plaintext) async {
final iv = IV.fromSecureRandom(12);
final encrypted = _encrypter.encrypt(plaintext, iv: iv);
// Store IV with encrypted data for cross-platform compatibility
return '${iv.base64}:${encrypted.base64}';
}
Future<String> decrypt(String ciphertext) async {
final parts = ciphertext.split(':');
final iv = IV.fromBase64(parts[0]);
final encrypted = Encrypted.fromBase64(parts[1]);
return _encrypter.decrypt(encrypted, iv: iv);
}
}
2. Hive Database with Field-Level Encryption
import 'package:hive/hive.dart';
@HiveType(typeId: 0)
class PasswordEntry extends HiveObject {
@HiveField(0)
final String id;
@HiveField(1)
final String title;
@HiveField(2)
final String username;
@HiveField(3)
String _encryptedPassword;
@HiveField(4)
String? _encryptedNotes;
PasswordEntry({
required this.id,
required this.title,
required this.username,
required String password,
String? notes,
}) : _encryptedPassword = password,
_encryptedNotes = notes;
String get password => _encryptedPassword; // Already encrypted
String? get notes => _encryptedNotes;
set password(String value) {
_encryptedPassword = value; // Store already encrypted value
}
set notes(String? value) {
_encryptedNotes = value;
}
}
// Initialize encrypted Hive box
Future<Box<PasswordEntry>> initEncryptedBox() async {
final encryptionKey = await _getEncryptionKey();
return await Hive.openBox<PasswordEntry>(
'passwords',
encryptionCipher: HiveAesCipher(encryptionKey),
);
}
3. Cross-Platform Biometric Authentication
import 'package:local_auth/local_auth.dart';
class BiometricAuthenticator {
final LocalAuthentication _auth = LocalAuthentication();
Future<bool> authenticate({required String reason}) async {
try {
final canCheckBiometrics = await _auth.canCheckBiometrics;
final isDeviceSupported = await _auth.isDeviceSupported();
if (!canCheckBiometrics || !isDeviceSupported) {
return false;
}
final authenticated = await _auth.authenticate(
localizedReason: reason,
options: const AuthenticationOptions(
biometricOnly: true,
stickyAuth: true,
),
);
return authenticated;
} catch (e) {
// Platform-specific error handling
print('Biometric authentication failed: $e');
return false;
}
}
Future<List<BiometricType>> getAvailableBiometrics() async {
return await _auth.getAvailableBiometrics();
}
}
Performance Considerations
Benchmark Results (Flutter/Dart)
Future<void> runBenchmarks() async {
// Local password lookup
final localStart = DateTime.now();
final password = await vault.getPassword(for: 'github.com');
final localTime = DateTime.now().difference(localStart);
print('Local password lookup: ${localTime.inMilliseconds}ms');
// Result: ~40ms on both platforms
// Export/Import
final exportStart = DateTime.now();
await vault.exportToJSON();
final exportTime = DateTime.now().difference(exportStart);
print('Export 100 passwords: ${exportTime.inMilliseconds}ms');
// Result: ~1200ms
// Encryption/Decryption
final cryptoStart = DateTime.now();
final encrypted = await encryptor.encrypt('test-password');
final decrypted = await encryptor.decrypt(encrypted);
final cryptoTime = DateTime.now().difference(cryptoStart);
print('Encrypt/decrypt cycle: ${cryptoTime.inMilliseconds}ms');
// Result: ~5ms
}
Memory Management in Flutter
class PasswordCache {
final Map<String, String> _cache = {};
final int _maxSize = 50;
Future<String?> getPassword(String id) async {
if (_cache.containsKey(id)) {
return _cache[id];
}
// Decrypt from storage
final password = await _storage.decryptPassword(id);
// Cache if not at capacity
if (_cache.length < _maxSize) {
_cache[id] = password;
}
return password;
}
void clearCache() {
_cache.clear();
}
void dispose() {
clearCache();
}
}
Development Challenges & Solutions
Challenge 1: Platform-Specific Security Without Platform-Specific Code
Problem: Need iOS Keychain and Android Keystore access from single Flutter codebase.
Solution: Use flutter_secure_storage with platform channels:
// flutter_secure_storage handles platform differences internally
final storage = FlutterSecureStorage();
// iOS: Uses Keychain Services
// Android: Uses EncryptedSharedPreferences with Keystore
await storage.write(
key: 'sensitive_data',
value: 'encrypted_value',
iOptions: const IOSOptions(
accessibility: KeychainAccessibility.unlockedThisDeviceOnly,
),
aOptions: const AndroidOptions(
encryptedSharedPreferences: true,
),
);
Challenge 2: Cross-Platform Biometric Authentication
Problem: Different biometric APIs on iOS and Android.
Solution: Use local_auth package with platform-specific configuration:
class CrossPlatformBiometrics {
final LocalAuthentication _auth = LocalAuthentication();
Future<bool> authenticate() async {
try {
return await _auth.authenticate(
localizedReason: 'Authenticate to access passwords',
options: const AuthenticationOptions(
biometricOnly: true,
// Platform-specific options handled internally
),
);
} on PlatformException catch (e) {
// Handle platform-specific errors
if (e.code == 'NotAvailable') {
// Biometrics not available on this device
return false;
}
rethrow;
}
}
}
Challenge 3: Backup Strategy Without Cloud (Cross-Platform)
Problem: Users need backups that work across iOS and Android.
Solution: Encrypted exports with platform-agnostic format:
enum BackupDestination {
localFile,
platformShare,
encryptedCloud,
externalStorage,
Future<void> saveEncryptedData(Uint8List data, BuildContext context) async {
switch (this) {
case BackupDestination.localFile:
final path = await _getSavePath();
await File(path).writeAsBytes(data);
case BackupDestination.platformShare:
await Share.shareXFiles(
[XFile.fromData(data, mimeType: 'application/json')],
);
case BackupDestination.encryptedCloud:
// User chooses their own cloud provider
await _uploadToUserCloud(data);
case BackupDestination.externalStorage:
await _saveToExternalStorage(data);
}
}
}
Challenge 4: Cross-Device Sync Without Cloud
Problem: Users want passwords on multiple devices (iOS and Android).
Solution: Manual sync with platform-agnostic encrypted bundles:
class CrossPlatformSyncBundle {
final Uint8List encryptedData;
final SyncMetadata metadata;
final Uint8List signature;
Uint8List export() {
// Create encrypted bundle with metadata
final bundle = jsonEncode({
'data': base64Encode(encryptedData),
'metadata': metadata.toJson(),
'signature': base64Encode(signature),
});
return utf8.encode(bundle);
}
static CrossPlatformSyncBundle import(Uint8List data) {
final json = jsonDecode(utf8.decode(data));
return CrossPlatformSyncBundle(
encryptedData: base64Decode(json['data']),
metadata: SyncMetadata.fromJson(json['metadata']),
signature: base64Decode(json['signature']),
);
}
}
Testing Strategy for Flutter
Unit Tests with Mocking
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@GenerateMocks([FlutterSecureStorage, LocalAuthentication])
void main() {
late PasswordEncryptor encryptor;
late MockFlutterSecureStorage mockStorage;
late MockLocalAuthentication mockAuth;
setUp(() {
mockStorage = MockFlutterSecureStorage();
mockAuth = MockLocalAuthentication();
encryptor = PasswordEncryptor(storage: mockStorage);
});
test('Encryption and decryption work correctly', () async {
// Arrange
const plaintext = 'super-secret-password';
// Act
final encrypted = await encryptor.encrypt(plaintext);
final decrypted = await encryptor.decrypt(encrypted);
// Assert
expect(decrypted, equals(plaintext));
});
test('Different inputs produce different encrypted outputs', () async {
// Arrange
const input1 = 'password1';
const input2 = 'password2';
// Act
final encrypted1 = await encryptor.encrypt(input1);
final encrypted2 = await encryptor.encrypt(input2);
// Assert
expect(encrypted1, isNot(equals(encrypted2)));
});
}
Integration Tests
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
void main() {
group('PasswordSafeVault Integration Tests', () {
late FlutterDriver driver;
setUpAll(() async {
driver = await FlutterDriver.connect();
});
tearDownAll(() async {
driver.close();
});
test('Add and retrieve password', () async {
// Navigate to add password screen
await driver.tap(find.byValueKey('add_password_button'));
// Fill in password details
await driver.enterText(find.byValueKey('title_field'), 'GitHub');
await driver.enterText(find.byValueKey('username_field'), 'user@example.com');
await driver.enterText(find.byValueKey('password_field'), 'secure123');
// Save password
await driver.tap(find.byValueKey('save_button'));
// Verify password appears in list
expect(
await driver.getText(find.text('GitHub')),
equals('GitHub'),
);
});
});
}
Platform-Specific Testing
// Test platform-specific behavior
testWidgets('iOS Keychain integration', (WidgetTester tester) async {
// Mock iOS-specific behavior
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (methodCall) async {
if (methodCall.method == 'getKeychainAccessibility') {
return 'kSecAttrAccessibleWhenUnlockedThisDeviceOnly';
}
return null;
});
// Test that iOS-specific options are used
final storage = FlutterSecureStorage();
await storage.write(
key: 'test_key',
value: 'test_value',
iOptions: const IOSOptions(
accessibility: KeychainAccessibility.unlockedThisDeviceOnly,
),
);
// Verify the write succeeded
final value = await storage.read(key: 'test_key');
expect(value, equals('test_value'));
});
testWidgets('Android Keystore integration', (WidgetTester tester) async {
// Mock Android-specific behavior
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (methodCall) async {
if (methodCall.method == 'getAndroidEncryptionLevel') {
return 'strongBoxBacked';
}
return null;
});
// Test that Android-specific options are used
final storage = FlutterSecureStorage();
await storage.write(
key: 'test_key',
value: 'test_value',
aOptions: const AndroidOptions(
encryptedSharedPreferences: true,
),
);
// Verify the write succeeded
final value = await storage.read(key: 'test_key');
expect(value, equals('test_value'));
});
Why Flutter for a Security-Focused App?
1. Single Codebase, Multiple Platforms
- Write once, deploy to iOS and Android
- Consistent security model across platforms
- Faster feature development and updates
2. Rich Plugin Ecosystem
-
flutter_secure_storage: Platform-specific secure storage -
local_auth: Cross-platform biometric authentication -
encrypt: Pure Dart encryption library -
hive: Fast, encrypted local database
3. Performance & Native Integration
- Compiles to native ARM code (AOT compilation)
- Direct platform channel access for native APIs
- Hardware-accelerated graphics and animations
- Access to platform-specific security features
4. Security Benefits
- No JavaScript runtime (reduces attack surface)
- Type-safe language (Dart) with null safety
- Compile-time optimizations and tree shaking
- Easy to audit single codebase
Lessons Learned
1. Platform Security is Mature
- iOS Keychain and Android Keystore provide excellent security
- Flutter plugins abstract complexity while maintaining security
- Users trust platform-native security features
2. Local-First is Feasible
- Modern devices have ample storage for password databases
- Users appreciate control over their data
- Backup solutions exist without cloud dependency
3. Cross-Platform Doesn't Mean Compromise
- Security can be consistent across platforms
- Performance is excellent with proper optimization
- User experience can be better than native (single design system)
4. Community Matters
- Flutter's security plugin ecosystem is robust
- Open source allows for security audits
- Community contributions improve security over time
Conclusion
Building PasswordSafeVault with Flutter has shown us that:
- Local-first password management is viable and addresses real security concerns
- Cross-platform development doesn't compromise security when using platform-native security features
- Users want control over their sensitive data
- Flutter is production-ready for security-critical applications
The app proves that you can have:
- ✅ True local-only storage
- ✅ Cross-platform compatibility
- ✅ Platform-grade security
- ✅ Excellent user experience
- ✅ No cloud dependency
Try PasswordSafeVault today and experience password management that truly puts you in control.
Your passwords. Your device. Your code.
Top comments (0)