import 'dart:convert'; import 'dart:io'; import 'file_store_support.dart'; import 'runtime_models.dart'; abstract class SecureStorageClient { Future read({required String key}); Future write({required String key, required String value}); Future delete({required String key}); } class FileSecureStorageClient implements SecureStorageClient { FileSecureStorageClient(this._directoryResolver); final Future Function() _directoryResolver; @override Future delete({required String key}) async { final file = await _fileForKey(key); if (file != null && await file.exists()) { await file.delete(); } } @override Future read({required String key}) async { final file = await _fileForKey(key); if (file == null || !await file.exists()) { return null; } final value = (await file.readAsString()).trim(); return value.isEmpty ? null : value; } @override Future write({required String key, required String value}) async { final file = await _fileForKey(key); if (file == null) { throw StateError('Secret directory unavailable for $key'); } await atomicWriteString(file, '$value\n', ownerOnly: true); } Future _fileForKey(String key) async { final directory = await _directoryResolver(); if (directory == null) { return null; } if (!await directory.exists()) { await directory.create(recursive: true); } await ensureOwnerOnlyDirectory(directory); return File('${directory.path}/${encodeStableFileKey(key)}.secret'); } } class SecretStore { SecretStore({ Future Function()? fallbackDirectoryPathResolver, Future Function()? databasePathResolver, Future Function()? defaultSupportDirectoryPathResolver, SecureStorageClient? secureStorage, bool enableSecureStorage = true, StoreLayoutResolver? layoutResolver, }) : _layoutResolver = layoutResolver ?? StoreLayoutResolver( localRootPathResolver: databasePathResolver, secretRootPathResolver: fallbackDirectoryPathResolver, supportRootPathResolver: defaultSupportDirectoryPathResolver, ), _secureStorageOverride = secureStorage; static const String legacyLocalStateKey = 'xworkmate.local_state.key'; static const String _legacyGatewayTokenKey = 'xworkmate.gateway.token'; static const String _legacyGatewayPasswordKey = 'xworkmate.gateway.password'; static const String _gatewayDeviceIdKey = 'xworkmate.gateway.device.id'; static const String _gatewayDevicePublicKeyKey = 'xworkmate.gateway.device.public_key'; static const String _gatewayDevicePrivateKeyKey = 'xworkmate.gateway.device.private_key'; static const String _gatewayDeviceCreatedAtKey = 'xworkmate.gateway.device.created_at_ms'; static const String _ollamaCloudApiKeyKey = 'xworkmate.ollama.cloud.api_key'; static const String _vaultTokenKey = 'xworkmate.vault.token'; static const String _aiGatewayApiKeyKey = 'xworkmate.ai_gateway.api_key'; final StoreLayoutResolver _layoutResolver; final SecureStorageClient? _secureStorageOverride; final Map _memorySecure = {}; StoreLayout? _layout; SecureStorageClient? _secureStorage; bool _initialized = false; PersistentWriteFailure? _secretsWriteFailure; PersistentWriteFailure? get secretsWriteFailure => _secretsWriteFailure; Future initialize() async { if (_initialized) { return; } _initialized = true; if (_secureStorageOverride != null) { _secureStorage = _secureStorageOverride; return; } try { _layout = await _layoutResolver.resolve(); _secureStorage = FileSecureStorageClient( () async => _layout?.secretDirectory, ); } catch (_) { _layout = null; _secureStorage = null; } } Future loadGatewayToken({int? profileIndex}) async { if (profileIndex != null) { final scopedValue = await _readSecure( _gatewayTokenKeyForProfile(profileIndex), ); if ((scopedValue ?? '').trim().isNotEmpty) { return scopedValue; } return _readSecure(_legacyGatewayTokenKey); } final legacyValue = await _readSecure(_legacyGatewayTokenKey); if ((legacyValue ?? '').trim().isNotEmpty) { return legacyValue; } for (final index in _gatewayProfileFallbackOrder) { final scopedValue = await _readSecure(_gatewayTokenKeyForProfile(index)); if ((scopedValue ?? '').trim().isNotEmpty) { return scopedValue; } } return null; } Future saveGatewayToken(String value, {int? profileIndex}) => _writeSecure( profileIndex == null ? _legacyGatewayTokenKey : _gatewayTokenKeyForProfile(profileIndex), value, ); Future clearGatewayToken({int? profileIndex}) => _deleteSecure( profileIndex == null ? _legacyGatewayTokenKey : _gatewayTokenKeyForProfile(profileIndex), ); Future loadGatewayPassword({int? profileIndex}) async { if (profileIndex != null) { final scopedValue = await _readSecure( _gatewayPasswordKeyForProfile(profileIndex), ); if ((scopedValue ?? '').trim().isNotEmpty) { return scopedValue; } return _readSecure(_legacyGatewayPasswordKey); } final legacyValue = await _readSecure(_legacyGatewayPasswordKey); if ((legacyValue ?? '').trim().isNotEmpty) { return legacyValue; } for (final index in _gatewayProfileFallbackOrder) { final scopedValue = await _readSecure( _gatewayPasswordKeyForProfile(index), ); if ((scopedValue ?? '').trim().isNotEmpty) { return scopedValue; } } return null; } Future saveGatewayPassword(String value, {int? profileIndex}) => _writeSecure( profileIndex == null ? _legacyGatewayPasswordKey : _gatewayPasswordKeyForProfile(profileIndex), value, ); Future clearGatewayPassword({int? profileIndex}) => _deleteSecure( profileIndex == null ? _legacyGatewayPasswordKey : _gatewayPasswordKeyForProfile(profileIndex), ); Future loadOllamaCloudApiKey() => _readSecure(_ollamaCloudApiKeyKey); Future saveOllamaCloudApiKey(String value) => _writeSecure(_ollamaCloudApiKeyKey, value); Future loadVaultToken() => _readSecure(_vaultTokenKey); Future saveVaultToken(String value) => _writeSecure(_vaultTokenKey, value); Future loadAiGatewayApiKey() => _readSecure(_aiGatewayApiKeyKey); Future saveAiGatewayApiKey(String value) => _writeSecure(_aiGatewayApiKeyKey, value); Future clearAiGatewayApiKey() => _deleteSecure(_aiGatewayApiKeyKey); Future> loadSecureRefs() async { await initialize(); final secureRefs = {}; final legacyGatewayToken = await _readSecure(_legacyGatewayTokenKey); final legacyGatewayPassword = await _readSecure(_legacyGatewayPasswordKey); if (legacyGatewayToken case final value?) { secureRefs['gateway_token'] = value; } if (legacyGatewayPassword case final value?) { secureRefs['gateway_password'] = value; } for (var index = 0; index < kGatewayProfileListLength; index += 1) { final scopedToken = await _readSecure(_gatewayTokenKeyForProfile(index)); final scopedPassword = await _readSecure( _gatewayPasswordKeyForProfile(index), ); if (scopedToken case final value?) { secureRefs[_gatewayTokenRefKey(index)] = value; } if (scopedPassword case final value?) { secureRefs[_gatewayPasswordRefKey(index)] = value; } } final deviceIdentity = await loadDeviceIdentity(); if (deviceIdentity != null) { final deviceToken = await loadDeviceToken( deviceId: deviceIdentity.deviceId, role: 'operator', ); if (deviceToken case final value?) { secureRefs['gateway_device_token_operator'] = value; } } final ollamaKey = await loadOllamaCloudApiKey(); final vaultToken = await loadVaultToken(); final aiGatewayApiKey = await loadAiGatewayApiKey(); if (ollamaKey case final value?) { secureRefs['ollama_cloud_api_key'] = value; } if (vaultToken case final value?) { secureRefs['vault_token'] = value; } if (aiGatewayApiKey case final value?) { secureRefs['ai_gateway_api_key'] = value; } return secureRefs; } static String gatewayTokenRefKey(int profileIndex) => _gatewayTokenRefKey(profileIndex); static String gatewayPasswordRefKey(int profileIndex) => _gatewayPasswordRefKey(profileIndex); Future loadDeviceIdentity() async { await initialize(); final deviceId = await _readSecure(_gatewayDeviceIdKey); final publicKey = await _readSecure(_gatewayDevicePublicKeyKey); final privateKey = await _readSecure(_gatewayDevicePrivateKeyKey); if (deviceId == null || publicKey == null || privateKey == null) { return null; } final createdAtMs = int.tryParse(await _readSecure(_gatewayDeviceCreatedAtKey) ?? '') ?? 0; return LocalDeviceIdentity( deviceId: deviceId, publicKeyBase64Url: publicKey, privateKeyBase64Url: privateKey, createdAtMs: createdAtMs, ); } Future saveDeviceIdentity(LocalDeviceIdentity identity) async { await initialize(); await _writeSecure(_gatewayDeviceIdKey, identity.deviceId); await _writeSecure(_gatewayDevicePublicKeyKey, identity.publicKeyBase64Url); await _writeSecure( _gatewayDevicePrivateKeyKey, identity.privateKeyBase64Url, ); await _writeSecure( _gatewayDeviceCreatedAtKey, identity.createdAtMs.toString(), ); } Future loadDeviceToken({ required String deviceId, required String role, }) => _readSecure(_deviceTokenKey(deviceId, role)); Future saveDeviceToken({ required String deviceId, required String role, required String token, }) => _writeSecure(_deviceTokenKey(deviceId, role), token); Future clearDeviceToken({ required String deviceId, required String role, }) => _deleteSecure(_deviceTokenKey(deviceId, role)); Future?> loadLegacyLocalStateKeyBytes() async { final encoded = await _readSecure(legacyLocalStateKey); final trimmed = encoded?.trim() ?? ''; if (trimmed.isEmpty) { return null; } return _base64UrlDecode(trimmed); } Future dispose() async { _memorySecure.clear(); _secureStorage = null; _layout = null; _initialized = false; } static String maskValue(String value) { final trimmed = value.trim(); if (trimmed.isEmpty) { return 'Not set'; } if (trimmed.length <= 6) { return '••••••'; } return '${trimmed.substring(0, 3)}••••${trimmed.substring(trimmed.length - 3)}'; } static String _gatewayTokenRefKey(int profileIndex) => 'gateway_token_$profileIndex'; static String _gatewayPasswordRefKey(int profileIndex) => 'gateway_password_$profileIndex'; static const List _gatewayProfileFallbackOrder = [ kGatewayRemoteProfileIndex, kGatewayLocalProfileIndex, 2, 3, 4, ]; Future _readSecure(String key) async { await initialize(); final client = _secureStorage; if (client != null) { try { final value = (await client.read(key: key))?.trim(); if (value != null && value.isNotEmpty) { _memorySecure[key] = value; return value; } } catch (_) { // Fall back to memory only when the secret path is unavailable. } } final memoryValue = _memorySecure[key]?.trim() ?? ''; return memoryValue.isEmpty ? null : memoryValue; } Future _writeSecure(String key, String value) async { await initialize(); final trimmed = value.trim(); if (trimmed.isEmpty) { return; } _memorySecure[key] = trimmed; final client = _secureStorage; if (client == null) { _secretsWriteFailure = _buildWriteFailure( 'writeSecret', StateError('Persistent secret path unavailable; using memory only.'), ); return; } try { await client.write(key: key, value: trimmed); _secretsWriteFailure = null; } catch (error) { _secretsWriteFailure = _buildWriteFailure('writeSecret', error); } } Future _deleteSecure(String key) async { await initialize(); _memorySecure.remove(key); final client = _secureStorage; if (client == null) { _secretsWriteFailure = _buildWriteFailure( 'deleteSecret', StateError( 'Persistent secret path unavailable; clear applied in memory only.', ), ); return; } try { await client.delete(key: key); _secretsWriteFailure = null; } catch (error) { _secretsWriteFailure = _buildWriteFailure('deleteSecret', error); } } static String _deviceTokenKey(String deviceId, String role) { final safeRole = role.trim().isEmpty ? 'operator' : role.trim(); return 'xworkmate.gateway.device_token.$deviceId.$safeRole'; } static String _gatewayTokenKeyForProfile(int profileIndex) => 'xworkmate.gateway.profile.$profileIndex.token'; static String _gatewayPasswordKeyForProfile(int profileIndex) => 'xworkmate.gateway.profile.$profileIndex.password'; static List _base64UrlDecode(String value) { final normalized = value.replaceAll('-', '+').replaceAll('_', '/'); final padded = normalized + '=' * ((4 - normalized.length % 4) % 4); return base64.decode(padded); } PersistentWriteFailure _buildWriteFailure(String operation, Object error) { return PersistentWriteFailure( scope: PersistentStoreScope.secrets, operation: operation, message: error.toString(), timestampMs: DateTime.now().millisecondsSinceEpoch, ); } }