super_cache_secure 1.0.1 copy "super_cache_secure: ^1.0.1" to clipboard
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.

example/main.dart

// 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();
}
1
likes
150
points
101
downloads

Publisher

verified publisherjihedmrouki.com

Weekly Downloads

AES-256-GCM encrypted in-memory cache for Flutter. Keys stored in iOS Keychain and Android Keystore. Part of the super_cache family.

Repository (GitHub)
View/report issues

Topics

#cache #caching #encryption #security #storage

Documentation

API reference

License

MIT (license)

Dependencies

cryptography, flutter, flutter_secure_storage, super_cache

More

Packages that depend on super_cache_secure