Secure storage adalah komponen kritis dalam pengembangan aplikasi mobile yang sering diabaikan. Di Flutter, implementasi penyimpanan yang aman melibatkan berbagai teknik dan library untuk melindungi data sensitif seperti token autentikasi, kredensial pengguna, dan informasi pribadi lainnya.
Mengapa Secure Storage Penting?
Data yang tersimpan di local storage biasa dapat dengan mudah diakses oleh: - Aplikasi lain (jika device di-root) - Malware - Physical access ke device - Backup yang tidak terenkripsi
Library Utama untuk Secure Storage
1. flutter_secure_storage
Library paling populer untuk secure storage di Flutter.
dependencies:
flutter_secure_storage: ^9.0.0
Setup dasar:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorageService {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
keyCipherAlgorithm: KeyCipherAlgorithm.RSA_ECB_PKCS1Padding,
storageCipherAlgorithm: StorageCipherAlgorithm.AES_GCM_NoPadding,
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
// Menyimpan data
static Future<void> storeData(String key, String value) async {
await _storage.write(key: key, value: value);
}
// Mengambil data
static Future<String?> getData(String key) async {
return await _storage.read(key: key);
}
// Menghapus data
static Future<void> deleteData(String key) async {
await _storage.delete(key: key);
}
// Menghapus semua data
static Future<void> deleteAll() async {
await _storage.deleteAll();
}
}
2. Hive dengan Encryption
Alternatif yang powerful untuk data yang lebih kompleks.
dependencies:
hive: ^2.2.3
hive_flutter: ^1.1.0
crypto: ^3.0.3
import 'package:hive_flutter/hive_flutter.dart';
import 'package:crypto/crypto.dart';
class HiveSecureStorage {
static late Box _secureBox;
static Future<void> init() async {
await Hive.initFlutter();
// Generate encryption key
final key = Hive.generateSecureKey();
_secureBox = await Hive.openBox(
'secureBox',
encryptionCipher: HiveAesCipher(key),
);
}
static Future<void> store<T>(String key, T value) async {
await _secureBox.put(key, value);
}
static T? get<T>(String key) {
return _secureBox.get(key);
}
}
Advanced Security Patterns
1. Token Management dengan Auto-Refresh
class TokenManager {
static const _accessTokenKey = 'access_token';
static const _refreshTokenKey = 'refresh_token';
static const _expiryKey = 'token_expiry';
static Future<void> saveTokens({
required String accessToken,
required String refreshToken,
required DateTime expiry,
}) async {
await Future.wait([
SecureStorageService.storeData(_accessTokenKey, accessToken),
SecureStorageService.storeData(_refreshTokenKey, refreshToken),
SecureStorageService.storeData(_expiryKey, expiry.toIso8601String()),
]);
}
static Future<String?> getValidAccessToken() async {
final token = await SecureStorageService.getData(_accessTokenKey);
final expiryStr = await SecureStorageService.getData(_expiryKey);
if (token == null || expiryStr == null) return null;
final expiry = DateTime.parse(expiryStr);
if (DateTime.now().isAfter(expiry)) {
// Token expired, try refresh
return await _refreshToken();
}
return token;
}
static Future<String?> _refreshToken() async {
final refreshToken = await SecureStorageService.getData(_refreshTokenKey);
if (refreshToken == null) return null;
// Call API to refresh token
// Implementation depends on your API
return null;
}
}
2. Biometric Authentication Integration
import 'package:local_auth/local_auth.dart';
class BiometricSecureStorage {
static const _auth = LocalAuthentication();
static Future<bool> _authenticateUser() async {
try {
final isAvailable = await _auth.isDeviceSupported();
if (!isAvailable) return false;
final isAuthenticated = await _auth.authenticate(
localizedReason: 'Authenticate to access secure data',
options: const AuthenticationOptions(
biometricOnly: true,
stickyAuth: true,
),
);
return isAuthenticated;
} catch (e) {
return false;
}
}
static Future<void> storeWithBiometric(String key, String value) async {
final authenticated = await _authenticateUser();
if (!authenticated) throw Exception('Authentication failed');
await SecureStorageService.storeData(key, value);
}
static Future<String?> getWithBiometric(String key) async {
final authenticated = await _authenticateUser();
if (!authenticated) return null;
return await SecureStorageService.getData(key);
}
}
3. Data Encryption Layer
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:encrypt/encrypt.dart';
class EncryptionService {
static final _key = Key.fromSecureRandom(32);
static final _iv = IV.fromSecureRandom(16);
static final _encrypter = Encrypter(AES(_key));
static String encryptData(String data) {
final encrypted = _encrypter.encrypt(data, iv: _iv);
return encrypted.base64;
}
static String decryptData(String encryptedData) {
final encrypted = Encrypted.fromBase64(encryptedData);
return _encrypter.decrypt(encrypted, iv: _iv);
}
// Hash sensitive data
static String hashData(String data) {
final bytes = utf8.encode(data);
final digest = sha256.convert(bytes);
return digest.toString();
}
}
class SecureDataManager {
static Future<void> storeEncrypted(String key, String data) async {
final encrypted = EncryptionService.encryptData(data);
await SecureStorageService.storeData(key, encrypted);
}
static Future<String?> getDecrypted(String key) async {
final encrypted = await SecureStorageService.getData(key);
if (encrypted == null) return null;
try {
return EncryptionService.decryptData(encrypted);
} catch (e) {
// Handle decryption error
return null;
}
}
}
Best Practices
1. Key Management
class KeyManager {
// Jangan hardcode keys dalam kode
static const _keyAlias = 'app_master_key';
static Future<String> getMasterKey() async {
String? key = await SecureStorageService.getData(_keyAlias);
if (key == null) {
// Generate new key
key = _generateSecureKey();
await SecureStorageService.storeData(_keyAlias, key);
}
return key;
}
static String _generateSecureKey() {
// Implementation untuk generate secure key
return base64.encode(List<int>.generate(32, (i) => Random().nextInt(256)));
}
}
2. Error Handling dan Logging
class SecureStorageManager {
static Future<Result<String>> safeGet(String key) async {
try {
final value = await SecureStorageService.getData(key);
return Result.success(value);
} catch (e) {
// Log error tanpa expose sensitive data
logger.error('Failed to retrieve secure data for key: ${key.hashCode}');
return Result.failure('Failed to retrieve data');
}
}
static Future<Result<void>> safeStore(String key, String value) async {
try {
await SecureStorageService.storeData(key, value);
return Result.success(null);
} catch (e) {
logger.error('Failed to store secure data for key: ${key.hashCode}');
return Result.failure('Failed to store data');
}
}
}
class Result<T> {
final T? data;
final String? error;
final bool isSuccess;
Result.success(this.data) : error = null, isSuccess = true;
Result.failure(this.error) : data = null, isSuccess = false;
}
3. Data Migration dan Versioning
class SecureStorageVersion {
static const _versionKey = 'storage_version';
static const currentVersion = 2;
static Future<void> migrateIfNeeded() async {
final version = await _getCurrentVersion();
if (version < currentVersion) {
await _performMigration(version);
await _setVersion(currentVersion);
}
}
static Future<int> _getCurrentVersion() async {
final versionStr = await SecureStorageService.getData(_versionKey);
return int.tryParse(versionStr ?? '1') ?? 1;
}
static Future<void> _performMigration(int fromVersion) async {
switch (fromVersion) {
case 1:
await _migrateFromV1ToV2();
break;
}
}
static Future<void> _migrateFromV1ToV2() async {
// Implementasi migrasi data
}
static Future<void> _setVersion(int version) async {
await SecureStorageService.storeData(_versionKey, version.toString());
}
}
Testing Secure Storage
// test/secure_storage_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
class MockSecureStorage extends Mock implements FlutterSecureStorage {}
void main() {
group('SecureStorageService Tests', () {
late MockSecureStorage mockStorage;
setUp(() {
mockStorage = MockSecureStorage();
});
testWidgets('should store and retrieve data correctly', (tester) async {
const key = 'test_key';
const value = 'test_value';
when(mockStorage.write(key: key, value: value))
.thenAnswer((_) async => {});
when(mockStorage.read(key: key))
.thenAnswer((_) async => value);
// Test implementation
verify(mockStorage.write(key: key, value: value)).called(1);
verify(mockStorage.read(key: key)).called(1);
});
});
}
Platform-Specific Considerations
Android
- Data tersimpan di Android Keystore
- Mendukung hardware-backed security
- Perlu permission khusus untuk biometric
iOS
- Data tersimpan di iOS Keychain
- Automatic backup exclusion
- Touch ID/Face ID integration
Monitoring dan Analytics
class SecureStorageAnalytics {
static void trackStorageEvent(String event, Map<String, dynamic> params) {
// Jangan log sensitive data
final sanitizedParams = Map<String, dynamic>.from(params);
sanitizedParams.removeWhere((key, value) =>
key.toLowerCase().contains('token') ||
key.toLowerCase().contains('password'));
// Send to analytics service
}
}
Kesimpulan
Implementasi secure storage yang baik memerlukan:
- Layer Security Berlapis: Encryption, secure storage, dan biometric
- Proper Key Management: Jangan hardcode, gunakan secure key generation
- Error Handling: Graceful handling tanpa expose sensitive data
- Testing: Unit test dan integration test
- Migration Strategy: Untuk update aplikasi yang seamless
- Monitoring: Track usage tanpa compromise security
Secure storage bukan hanya tentang menggunakan library, tetapi membangun arsitektur yang comprehensive untuk melindungi data pengguna.