super_cache_secure 1.0.1
super_cache_secure: ^1.0.1 copied to clipboard
AES-256-GCM encrypted in-memory cache for Flutter. Keys stored in iOS Keychain and Android Keystore. Part of the super_cache family.
// ignore_for_file: avoid_print
import 'dart:convert';
import 'dart:typed_data';
import 'package:super_cache/super_cache.dart';
import 'package:super_cache_secure/super_cache_secure.dart';
/// super_cache_secure walkthrough
///
/// Simulates an authentication service that needs to keep tokens and
/// credentials in memory — encrypted so they can't be read from a memory
/// dump or by another process. Covers each SecureCache feature step-by-step:
///
/// 1. Quick-start: store an OAuth access token
/// 2. TTL — session tokens expire automatically
/// 3. Multiple vaults — separate AES-256 keys for different data categories
/// 4. Logout — removeWhere clears one user's credentials
/// 5. Structured data with a custom codec
/// 6. Using SecureCache as L2 in a CacheOrchestrator
///
/// In production replace `_InMemoryKeyStore` with `FlutterSecureKeyStore`
/// (from `package:super_cache_secure/super_cache_secure_flutter.dart`), which
/// stores the AES master key in the iOS Keychain or Android Keystore.
///
/// Run with:
/// dart run example/main.dart
void main() async {
await _section(
'1. Quick-start: store an OAuth access token', _demo1QuickStart);
await _section('2. TTL — session tokens expire automatically', _demo2TTL);
await _section('3. Multiple vaults', _demo3MultipleVaults);
await _section(
'4. Logout — removeWhere clears one user\'s data', _demo4Logout);
await _section(
'5. Structured data with a custom codec', _demo5StructuredData);
await _section(
'6. SecureCache as L2 in a CacheOrchestrator', _demo6Orchestrator);
print('\nDone!');
}
// ---------------------------------------------------------------------------
// 1. Quick-start
//
// The minimum setup: a KeyStore + a value codec. initialize() loads or
// generates the AES-256 key from the KeyStore. Every value stored after that
// is encrypted with a unique random nonce before it touches RAM.
// ---------------------------------------------------------------------------
Future<void> _demo1QuickStart() async {
final cache = SecureCache<String, String>(
keyStore:
_InMemoryKeyStore(), // replace with FlutterSecureKeyStore() in production
codec: const _Utf8Codec(),
defaultTTL: const Duration(hours: 1),
);
await cache.initialize(); // generates the AES-256 key on first run
// Store an OAuth access token. In RAM it is AES-256-GCM ciphertext.
await cache.put('access_token', 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...');
await cache.put('refresh_token', 'rt_9f4e1c3a7b2d6e8f0a1c3e5b7d9f2a4c...');
final accessToken = await cache.get('access_token');
final refreshToken = await cache.get('refresh_token');
// Print only the first 20 characters so the token isn't shown in full.
print(' access_token → ${accessToken?.substring(0, 20)}…');
print(' refresh_token → ${refreshToken?.substring(0, 20)}…');
// Observability works just like MemoryCache.
final m = cache.metrics;
print(' hits: ${m.hits} misses: ${m.misses} entries: ${m.currentEntries}');
await cache.dispose();
}
// ---------------------------------------------------------------------------
// 2. TTL — session tokens expire automatically
//
// Set defaultTTL on the constructor for a cache-wide default, or pass
// ttl: Duration(...) per entry to override it for that specific key.
// A background sweep removes expired entries on the sweepInterval.
// ---------------------------------------------------------------------------
Future<void> _demo2TTL() async {
final cache = SecureCache<String, String>(
keyStore: _InMemoryKeyStore(),
codec: const _Utf8Codec(),
defaultTTL: const Duration(milliseconds: 100), // short for demo
sweepInterval: const Duration(milliseconds: 50),
);
await cache.initialize();
await cache.put('session_id', 'sess_7f3a9c1e');
final hit = await cache.get('session_id');
print(' immediately → $hit'); // sess_7f3a9c1e
await Future<void>.delayed(const Duration(milliseconds: 150));
final expired = await cache.get('session_id');
print(' after 150 ms → $expired'); // null — expired
// Per-entry override: refresh token lasts 24 h even though defaultTTL is 100 ms.
await cache.put('refresh_token', 'rt_xyz', ttl: const Duration(hours: 24));
await Future<void>.delayed(const Duration(milliseconds: 150));
final rt = await cache.get('refresh_token');
print(
' refresh (24h TTL) after 150 ms → ${rt != null ? "still valid" : "expired"}');
await cache.dispose();
}
// ---------------------------------------------------------------------------
// 3. Multiple vaults
//
// Give each SecureCache instance a unique keyAlias so it uses an independent
// AES-256 key. Useful when you need separate rotation schedules or to wipe
// credentials without touching PII, or vice-versa.
// ---------------------------------------------------------------------------
Future<void> _demo3MultipleVaults() async {
// One shared KeyStore — both vaults store their AES keys side by side.
final keyStore = _InMemoryKeyStore();
final tokenVault = SecureCache<String, String>(
keyStore: keyStore,
codec: const _Utf8Codec(),
config: const CacheEncryptionConfig(keyAlias: 'tokens_aes_key'),
);
final piiVault = SecureCache<String, String>(
keyStore: keyStore,
codec: const _Utf8Codec(),
config: const CacheEncryptionConfig(keyAlias: 'pii_aes_key'),
);
await tokenVault.initialize();
await piiVault.initialize();
await tokenVault.put('access_token', 'tok_prod_abc123');
await piiVault.put('credit_card', '**** **** **** 4242');
print(' tokens vault → ${await tokenVault.get("access_token")}');
print(' pii vault → ${await piiVault.get("credit_card")}');
// Wiping the token vault does not touch the PII vault.
await tokenVault.clear();
print(' after tokenVault.clear():');
print(' token vault → ${await tokenVault.get("access_token")}'); // null
print(
' pii vault → ${await piiVault.get("credit_card")}'); // still there
await tokenVault.dispose();
await piiVault.dispose();
}
// ---------------------------------------------------------------------------
// 4. Logout — removeWhere clears one user's data
//
// On logout, invalidate everything belonging to the departing user without
// touching entries for other users (relevant on a shared-device app).
//
// Note: removeWhere must decrypt every entry to evaluate the predicate — it
// is O(n × decrypt). For large caches, prefer explicit key-based removes.
// ---------------------------------------------------------------------------
Future<void> _demo4Logout() async {
final cache = SecureCache<String, String>(
keyStore: _InMemoryKeyStore(),
codec: const _Utf8Codec(),
);
await cache.initialize();
// Two users' tokens stored in the same cache instance.
await cache.put('alice:access_token', 'tok_alice_abc');
await cache.put('alice:refresh_token', 'rt_alice_xyz');
await cache.put('bob:access_token', 'tok_bob_def');
await cache.put('bob:refresh_token', 'rt_bob_uvw');
print(' before logout:');
print(' alice:access_token → ${await cache.get("alice:access_token")}');
print(' bob:access_token → ${await cache.get("bob:access_token")}');
// Alice logs out — remove all her entries by key prefix.
await cache.removeWhere((key, _) => key.startsWith('alice:'));
print(' after alice logs out:');
print(
' alice:access_token → ${await cache.get("alice:access_token")}'); // null
print(
' alice:refresh_token → ${await cache.get("alice:refresh_token")}'); // null
print(
' bob:access_token → ${await cache.get("bob:access_token")}'); // still there
await cache.dispose();
}
// ---------------------------------------------------------------------------
// 5. Structured data with a custom codec
//
// SecureCache<K, V> is fully generic. Implement CacheCodec<V> to serialize
// your model type to Uint8List. JSON + UTF-8 is the simplest approach for
// any type that already provides toJson / fromJson.
// ---------------------------------------------------------------------------
Future<void> _demo5StructuredData() async {
final cache = SecureCache<String, AuthCredentials>(
keyStore: _InMemoryKeyStore(),
codec: const _CredentialsCodec(),
defaultTTL: const Duration(hours: 8), // typical work-day session
);
await cache.initialize();
const credentials = AuthCredentials(
accessToken: 'eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyOjEyMyJ9...',
refreshToken: 'rt_9f4e1c3a7b2d6e8f',
expiresInSeconds: 3600,
);
await cache.put('user:123:credentials', credentials);
final stored = await cache.get('user:123:credentials');
print(' access_token → ${stored?.accessToken.substring(0, 24)}…');
print(' refresh_token → ${stored?.refreshToken}');
print(' expires_in → ${stored?.expiresInSeconds}s');
await cache.dispose();
}
// ---------------------------------------------------------------------------
// 6. SecureCache as L2 in a CacheOrchestrator
//
// Pair a fast MemoryCache (L1, plaintext) with a SecureCache (L2, encrypted).
// L1 gives sub-microsecond reads. L2 provides a defence-in-depth layer: even
// if L1 evicts an entry, it can be retrieved (and decrypted) from L2 and
// promoted back to L1 automatically.
// ---------------------------------------------------------------------------
Future<void> _demo6Orchestrator() async {
// SecureCache must be initialized before being handed to the orchestrator.
final secureL2 = SecureCache<String, String>(
keyStore: _InMemoryKeyStore(),
codec: const _Utf8Codec(),
);
await secureL2.initialize();
// L1 holds only 2 entries so we can easily trigger L1 eviction.
final orchestrator = CacheOrchestrator<String, String>(
l1: MemoryCache<String, String>(maxEntries: 2),
l2: secureL2,
);
// put writes to both L1 and L2 simultaneously.
await orchestrator.put('token:a', 'tok_aaa');
await orchestrator.put('token:b', 'tok_bbb');
await orchestrator.put('token:c', 'tok_ccc'); // L1 evicts token:a (LRU)
// token:a is gone from L1 but still encrypted in L2.
final fromL2 = await orchestrator.get('token:a');
print(' token:a (L2 hit + promoted to L1) → $fromL2');
// Now token:a is back in L1 — next read is a sub-microsecond L1 hit.
final fromL1 = await orchestrator.get('token:a');
print(' token:a (L1 hit after promotion) → $fromL1');
await orchestrator.dispose();
}
// ---------------------------------------------------------------------------
// Test doubles and codecs
// ---------------------------------------------------------------------------
/// An in-memory [KeyStore] suitable for tests and CLI examples.
///
/// In a Flutter app, replace this with `FlutterSecureKeyStore` from
/// `package:super_cache_secure/super_cache_secure_flutter.dart` — it backs
/// the AES master key with the iOS Keychain or Android Keystore.
final class _InMemoryKeyStore implements KeyStore {
final _store = <String, String>{};
@override
Future<String?> read(String key) async => _store[key];
@override
Future<void> write(String key, String value) async => _store[key] = value;
@override
Future<void> delete(String key) async => _store.remove(key);
}
/// A minimal [CacheCodec] that converts Dart [String] values to UTF-8 bytes.
/// Suitable for plain strings — tokens, keys, small serialized blobs.
final class _Utf8Codec implements CacheCodec<String> {
const _Utf8Codec();
@override
Uint8List encode(String value) => Uint8List.fromList(utf8.encode(value));
@override
String decode(Uint8List bytes) => utf8.decode(bytes);
}
// ---------------------------------------------------------------------------
// AuthCredentials model + codec
// ---------------------------------------------------------------------------
/// Represents a complete OAuth credential set.
final class AuthCredentials {
const AuthCredentials({
required this.accessToken,
required this.refreshToken,
required this.expiresInSeconds,
});
final String accessToken;
final String refreshToken;
final int expiresInSeconds;
Map<String, dynamic> toJson() => {
'access_token': accessToken,
'refresh_token': refreshToken,
'expires_in': expiresInSeconds,
};
factory AuthCredentials.fromJson(Map<String, dynamic> json) =>
AuthCredentials(
accessToken: json['access_token'] as String,
refreshToken: json['refresh_token'] as String,
expiresInSeconds: json['expires_in'] as int,
);
}
/// A [CacheCodec] that serializes [AuthCredentials] to JSON bytes.
final class _CredentialsCodec implements CacheCodec<AuthCredentials> {
const _CredentialsCodec();
@override
Uint8List encode(AuthCredentials value) =>
Uint8List.fromList(utf8.encode(jsonEncode(value.toJson())));
@override
AuthCredentials decode(Uint8List bytes) => AuthCredentials.fromJson(
jsonDecode(utf8.decode(bytes)) as Map<String, dynamic>,
);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
Future<void> _section(String title, Future<void> Function() demo) async {
print('\n${'─' * 55}');
print(' $title');
print('─' * 55);
await demo();
}