Back to list
gsmlg-app

project-secure-storage

by gsmlg-app

Flutter Application Template for AI coder

1🍴 0📅 Jan 19, 2026

SKILL.md


name: project-secure-storage description: Guide for storing secrets securely using app_secure_storage package with platform-native storage (project)

Flutter Secure Storage Skill

This skill guides the implementation of secure storage for sensitive user data using the app_secure_storage package.

When to Use

Trigger this skill when:

  • Storing API tokens, passwords, or encryption keys
  • Managing user authentication credentials
  • Saving sensitive configuration data
  • Storing third-party API keys (OpenAI, Stripe, Firebase, etc.) - MUST use secure storage
  • User asks to "store secret", "save token", "secure storage", "keychain", "save credentials"

IMPORTANT: Third-party API keys and secrets MUST be stored using app_secure_storage. Never store API keys in:

  • SharedPreferences (not encrypted)
  • Database (not designed for secrets)
  • Environment variables bundled in app (can be extracted)
  • Hardcoded strings (visible in binary)

Package Import

import 'package:app_secure_storage/app_secure_storage.dart';

Platform Storage Mechanisms

PlatformStorage Backend
iOSKeychain Services
macOSKeychain Services
AndroidEncryptedSharedPreferences (AES-GCM)
Linuxlibsecret (GNOME Keyring / KWallet)
WindowsCredential Manager

Accessing the Vault

The VaultRepository is injected via MainProvider and available throughout the app:

import 'package:app_secure_storage/app_secure_storage.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// In any widget with access to BuildContext
final vault = context.read<VaultRepository>();

Basic Operations

Store a Secret

await vault.write(key: 'api_token', value: 'your-secret-token');
await vault.write(key: 'refresh_token', value: 'refresh-token-value');

Read a Secret

final token = await vault.read(key: 'api_token');
if (token != null) {
  // Use the token for API authentication
  headers['Authorization'] = 'Bearer $token';
}

Check if Key Exists

final hasToken = await vault.containsKey(key: 'api_token');
if (!hasToken) {
  // Redirect to login
  context.goNamed('login');
}

Delete a Secret

await vault.delete(key: 'api_token');

Delete All Secrets

await vault.deleteAll();

Read All Secrets

final allSecrets = await vault.readAll();
// Returns Map<String, String>
for (final entry in allSecrets.entries) {
  print('Key: ${entry.key}');
}

Using Namespaces

Namespaces prevent key collisions between features and allow scoped deletion:

// In main.dart - create namespaced vaults
final authVault = SecureStorageVaultRepository(namespace: 'auth');
final apiVault = SecureStorageVaultRepository(namespace: 'api');

// Keys are automatically prefixed internally
await authVault.write(key: 'access_token', value: 'jwt-token');
// Actually stored as 'auth_access_token'

await apiVault.write(key: 'key', value: 'api-key-123');
// Actually stored as 'api_key'

// Scoped deletion - only deletes keys with 'auth_' prefix
await authVault.deleteAll();

Complete Example: Third-Party API Keys

When integrating third-party services (OpenAI, Stripe, Google Maps, etc.), always store API keys securely:

class ApiKeyService {
  final VaultRepository _vault;

  ApiKeyService(this._vault);

  // Key constants for third-party services
  static const _openAiKeyKey = 'openai_api_key';
  static const _stripeKeyKey = 'stripe_publishable_key';
  static const _googleMapsKeyKey = 'google_maps_api_key';

  // OpenAI
  Future<void> saveOpenAiKey(String apiKey) async {
    await _vault.write(key: _openAiKeyKey, value: apiKey);
  }

  Future<String?> getOpenAiKey() async {
    return _vault.read(key: _openAiKeyKey);
  }

  Future<bool> hasOpenAiKey() async {
    return _vault.containsKey(key: _openAiKeyKey);
  }

  Future<void> deleteOpenAiKey() async {
    await _vault.delete(key: _openAiKeyKey);
  }

  // Generic method for any third-party service
  Future<void> saveApiKey({
    required String service,
    required String apiKey,
  }) async {
    await _vault.write(key: '${service}_api_key', value: apiKey);
  }

  Future<String?> getApiKey({required String service}) async {
    return _vault.read(key: '${service}_api_key');
  }
}

// Usage in widget
class SettingsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final vault = context.read<VaultRepository>();
    final apiKeyService = ApiKeyService(vault);

    return ListTile(
      title: const Text('OpenAI API Key'),
      subtitle: FutureBuilder<bool>(
        future: apiKeyService.hasOpenAiKey(),
        builder: (context, snapshot) {
          final hasKey = snapshot.data ?? false;
          return Text(hasKey ? 'Configured' : 'Not configured');
        },
      ),
      onTap: () => _showApiKeyDialog(context, apiKeyService),
    );
  }

  void _showApiKeyDialog(BuildContext context, ApiKeyService service) {
    final controller = TextEditingController();
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Enter OpenAI API Key'),
        content: TextField(
          controller: controller,
          obscureText: true,
          decoration: const InputDecoration(
            hintText: 'sk-...',
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () async {
              await service.saveOpenAiKey(controller.text);
              if (context.mounted) {
                Navigator.pop(context);
              }
            },
            child: const Text('Save'),
          ),
        ],
      ),
    );
  }
}

Complete Example: Authentication Service

class AuthService {
  final VaultRepository _vault;

  AuthService(this._vault);

  static const _accessTokenKey = 'access_token';
  static const _refreshTokenKey = 'refresh_token';
  static const _userIdKey = 'user_id';

  Future<void> saveTokens({
    required String accessToken,
    required String refreshToken,
    required String userId,
  }) async {
    await Future.wait([
      _vault.write(key: _accessTokenKey, value: accessToken),
      _vault.write(key: _refreshTokenKey, value: refreshToken),
      _vault.write(key: _userIdKey, value: userId),
    ]);
  }

  Future<String?> getAccessToken() async {
    return _vault.read(key: _accessTokenKey);
  }

  Future<String?> getRefreshToken() async {
    return _vault.read(key: _refreshTokenKey);
  }

  Future<bool> isLoggedIn() async {
    return _vault.containsKey(key: _accessTokenKey);
  }

  Future<void> logout() async {
    await Future.wait([
      _vault.delete(key: _accessTokenKey),
      _vault.delete(key: _refreshTokenKey),
      _vault.delete(key: _userIdKey),
    ]);
  }
}

Complete Example: Login Screen

class LoginScreen extends StatefulWidget {
  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  bool _isLoading = false;

  Future<void> _handleLogin(String email, String password) async {
    setState(() => _isLoading = true);

    try {
      // Call your API
      final response = await api.login(email: email, password: password);

      // Store tokens securely
      final vault = context.read<VaultRepository>();
      await vault.write(key: 'access_token', value: response.accessToken);
      await vault.write(key: 'refresh_token', value: response.refreshToken);

      if (mounted) {
        context.goNamed('home');
      }
    } catch (e) {
      if (mounted) {
        showErrorToast(
          context: context,
          message: 'Login failed: ${e.toString()}',
        );
      }
    } finally {
      if (mounted) {
        setState(() => _isLoading = false);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    // ... login form UI
  }
}

Complete Example: Auto-Login Check

class SplashScreen extends StatefulWidget {
  @override
  State<SplashScreen> createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> {
  @override
  void initState() {
    super.initState();
    _checkAuth();
  }

  Future<void> _checkAuth() async {
    final vault = context.read<VaultRepository>();
    final hasToken = await vault.containsKey(key: 'access_token');

    if (!mounted) return;

    if (hasToken) {
      // Validate token is still valid
      final token = await vault.read(key: 'access_token');
      if (token != null && _isTokenValid(token)) {
        context.goNamed('home');
      } else {
        // Token expired, clear and go to login
        await vault.deleteAll();
        context.goNamed('login');
      }
    } else {
      context.goNamed('login');
    }
  }

  bool _isTokenValid(String token) {
    // Check JWT expiration or validate with server
    return true;
  }

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(child: CircularProgressIndicator()),
    );
  }
}

Testing with Mock Repository

class MockVaultRepository implements VaultRepository {
  final Map<String, String> _storage = {};

  @override
  Future<void> write({required String key, required String value}) async {
    _storage[key] = value;
  }

  @override
  Future<String?> read({required String key}) async {
    return _storage[key];
  }

  @override
  Future<void> delete({required String key}) async {
    _storage.remove(key);
  }

  @override
  Future<bool> containsKey({required String key}) async {
    return _storage.containsKey(key);
  }

  @override
  Future<void> deleteAll() async {
    _storage.clear();
  }

  @override
  Future<Map<String, String>> readAll() async {
    return Map.from(_storage);
  }
}

// In tests
void main() {
  testWidgets('stores token on login', (tester) async {
    final mockVault = MockVaultRepository();

    await tester.pumpWidget(
      RepositoryProvider<VaultRepository>.value(
        value: mockVault,
        child: const MaterialApp(home: LoginScreen()),
      ),
    );

    // Perform login actions...

    // Verify token was stored
    expect(await mockVault.containsKey(key: 'access_token'), isTrue);
  });
}

Error Handling

Always handle potential storage errors:

Future<void> saveToken(String token) async {
  try {
    await vault.write(key: 'api_token', value: token);
  } on PlatformException catch (e) {
    // Handle platform-specific errors
    // e.g., Keychain access denied on iOS
    logger.e('Failed to save token: ${e.message}');
    rethrow;
  } catch (e) {
    logger.e('Unexpected error saving token: $e');
    rethrow;
  }
}

Platform Setup Requirements

iOS

Add to ios/Runner/DebugProfile.entitlements and ios/Runner/Release.entitlements:

<key>keychain-access-groups</key>
<array/>

macOS

Add to macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements:

<key>keychain-access-groups</key>
<array/>

Android

Disable auto backup in AndroidManifest.xml to prevent key errors:

<application android:allowBackup="false" ...>

Linux

Install dependencies:

sudo apt-get install libsecret-1-dev libjsoncpp-dev

See app_lib/secure_storage/README.md for complete platform setup instructions.

Best Practices

  1. Use meaningful key names - e.g., auth_access_token, api_refresh_token
  2. Use namespaces for logical grouping and scoped deletion
  3. Delete secrets on logout - don't leave credentials after user logs out
  4. Handle errors gracefully - storage can fail (permissions, keyring unavailable)
  5. Check mounted before updating UI after async operations
  6. Validate tokens - check expiration before using stored tokens
  7. Don't log secrets - never print token values to console
  8. Use parallel writes with Future.wait for multiple secrets
  9. Test with mocks - use MockVaultRepository for unit tests

API Reference

VaultRepository Interface

MethodReturnsDescription
write({key, value})Future<void>Store a secret
read({key})Future<String?>Read a secret (null if not found)
delete({key})Future<void>Delete a secret
containsKey({key})Future<bool>Check if key exists
deleteAll()Future<void>Delete all secrets
readAll()Future<Map<String, String>>Get all secrets

SecureStorageVaultRepository Constructor

ParameterTypeDescription
storageFlutterSecureStorage?Custom storage instance (optional)
namespaceString?Key prefix for scoped storage (optional)

Score

Total Score

55/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon