import 'dart:convert'; import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; /// Platform-hardened vault for the SQLCipher AES-256 master key. /// /// Every target stores the key in its native secret store with safe /// defaults: /// /// | Platform | Backend | Hardening | /// |-----------|----------------------------------|----------------------------------------| /// | Android | Jetpack Crypto `EncryptedSharedPreferences` | `encryptedSharedPreferences: true` | /// | iOS | Keychain | `KeychainAccessibility.first_unlock` | /// | macOS | Keychain | `KeychainAccessibility.first_unlock` | /// | Windows | DPAPI-encrypted JSON file (`CryptProtectData`) | plugin backend default | /// | Linux | libsecret / Secret Service | plugin backend default | /// | Web | Browser-scoped obfuscated store | never used for the SQLCipher key | /// /// `first_unlock` (rather than `when_unlocked`) lets background services /// (health sync, dose reminders) open the encrypted database after a reboot /// without requiring the device to be unlocked again. /// /// When the platform vault is unavailable — headless Linux without a Secret /// Service, locked-down CI runners, exotic embedders — the manager degrades /// to an ephemeral in-memory key instead of crashing: the database stays /// encrypted for the current process run and re-keys (re-syncs) on the next /// launch. class CryptoManager { static const _databaseKeyName = 'glp1_sqlcipher_master_key'; /// Platform-safe vault configuration (see the class documentation). static const FlutterSecureStorage _platformVault = FlutterSecureStorage( // Android: AES-256 EncryptedSharedPreferences (API 23+). aOptions: AndroidOptions(encryptedSharedPreferences: true), // iOS: Keychain readable after the first post-boot unlock. iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), // macOS: same Keychain accessibility contract as iOS. mOptions: MacOsOptions(accessibility: KeychainAccessibility.first_unlock), // Windows: DPAPI-sealed file managed by the plugin's FFI backend. wOptions: WindowsOptions.defaultOptions, // Linux: libsecret (Secret Service / GNOME Keyring) backend. lOptions: LinuxOptions.defaultOptions, ); CryptoManager({FlutterSecureStorage? secureStorage}) : _secureStorage = secureStorage ?? _platformVault; final FlutterSecureStorage _secureStorage; /// Session-scoped replacement key used only when the platform vault is /// unavailable (see the class documentation). String? _ephemeralKey; /// Returns the persistent SQLCipher master key, generating and vaulting it /// on first run. Never throws: a vault failure falls back to an ephemeral /// in-memory key (database usable until the process exits). Future getOrCreateDatabaseEncryptionKey() async { final existingKey = await _tryOrNull(() => _secureStorage.read(key: _databaseKeyName)); if (existingKey != null && existingKey.isNotEmpty) { return existingKey; } final newKey = _generateKey(); final stored = await _tryOrNull(() async { await _secureStorage.write(key: _databaseKeyName, value: newKey); return true; }); if (stored == true) return newKey; debugPrintStack( label: 'Secure vault unavailable - using an ephemeral in-memory ' 'SQLCipher key for this session (data re-keys on next launch).', ); return _ephemeralKey ??= newKey; } /// 256-bit cryptographically random key, base64url-encoded for safe /// embedding in `PRAGMA key`. static String _generateKey() { final random = Random.secure(); final values = List.generate(32, (i) => random.nextInt(256)); return base64UrlEncode(values); } /// Runs a vault operation, returning `null` (instead of throwing) on any /// platform failure so callers can degrade gracefully. Future _tryOrNull(Future Function() operation) async { try { return await operation(); } catch (error, stackTrace) { debugPrintStack( stackTrace: stackTrace, label: 'Secure vault access failed (non-fatal): $error', ); return null; } } }