649 lines
21 KiB
Dart
649 lines
21 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'file_store_support.dart';
|
|
import 'runtime_models.dart';
|
|
|
|
abstract class SecureStorageClient {
|
|
Future<String?> read({required String key});
|
|
|
|
Future<void> write({required String key, required String value});
|
|
|
|
Future<void> delete({required String key});
|
|
}
|
|
|
|
class FileSecureStorageClient implements SecureStorageClient {
|
|
FileSecureStorageClient(this._directoryResolver);
|
|
|
|
final Future<Directory?> Function() _directoryResolver;
|
|
|
|
@override
|
|
Future<void> delete({required String key}) async {
|
|
final file = await _fileForKey(key);
|
|
if (file != null && await file.exists()) {
|
|
await file.delete();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<String?> 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<void> 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<File?> _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<String?> Function()? secretRootPathResolver,
|
|
Future<String?> Function()? appDataRootPathResolver,
|
|
Future<String?> Function()? supportRootPathResolver,
|
|
SecureStorageClient? secureStorage,
|
|
bool enableSecureStorage = true,
|
|
StoreLayoutResolver? layoutResolver,
|
|
}) : _layoutResolver =
|
|
layoutResolver ??
|
|
StoreLayoutResolver(
|
|
appDataRootPathResolver: appDataRootPathResolver,
|
|
secretRootPathResolver: secretRootPathResolver,
|
|
supportRootPathResolver: supportRootPathResolver,
|
|
),
|
|
_secureStorageOverride = secureStorage;
|
|
|
|
static const String legacyLocalStateKey = 'xworkmate.local_state.key';
|
|
|
|
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';
|
|
static const String _accountSessionTokenKey =
|
|
'xworkmate.account.session.token';
|
|
static const String _accountSessionExpiresAtKey =
|
|
'xworkmate.account.session.expires_at';
|
|
static const String _accountSessionUserIdKey =
|
|
'xworkmate.account.session.user_id';
|
|
static const String _accountSessionIdentifierKey =
|
|
'xworkmate.account.session.identifier';
|
|
static const String _accountSessionSummaryKey =
|
|
'xworkmate.account.session.summary';
|
|
static const String _accountSyncStateKey = 'xworkmate.account.sync_state';
|
|
static const String _customSecretRefRegistryKey =
|
|
'xworkmate.secret.ref_registry';
|
|
|
|
final StoreLayoutResolver _layoutResolver;
|
|
final SecureStorageClient? _secureStorageOverride;
|
|
final Map<String, String> _memorySecure = <String, String>{};
|
|
StoreLayout? _layout;
|
|
SecureStorageClient? _secureStorage;
|
|
bool _initialized = false;
|
|
PersistentWriteFailure? _secretsWriteFailure;
|
|
|
|
Map<String, String> get secureRefs =>
|
|
Map<String, String>.unmodifiable(_memorySecure);
|
|
|
|
PersistentWriteFailure? get secretsWriteFailure => _secretsWriteFailure;
|
|
|
|
Future<void> initialize() async {
|
|
if (_initialized) {
|
|
return;
|
|
}
|
|
_initialized = true;
|
|
if (_secureStorageOverride != null) {
|
|
_secureStorage = _secureStorageOverride;
|
|
return;
|
|
}
|
|
try {
|
|
_layout = await _layoutResolver.resolve();
|
|
_secureStorage = FileSecureStorageClient(
|
|
() async => _layout?.secretDirectory,
|
|
);
|
|
} catch (error) {
|
|
debugPrint('Secret store initialization failed: $error');
|
|
_layout = null;
|
|
_secureStorage = null;
|
|
}
|
|
}
|
|
|
|
Future<String?> loadGatewayToken({int? profileIndex}) async {
|
|
return _readSecure(
|
|
_gatewayTokenKeyForProfile(profileIndex ?? kGatewayRemoteProfileIndex),
|
|
);
|
|
}
|
|
|
|
Future<void> saveGatewayToken(String value, {int? profileIndex}) =>
|
|
_writeSecure(
|
|
_gatewayTokenKeyForProfile(profileIndex ?? kGatewayRemoteProfileIndex),
|
|
value,
|
|
);
|
|
|
|
Future<void> clearGatewayToken({int? profileIndex}) => _deleteSecure(
|
|
_gatewayTokenKeyForProfile(profileIndex ?? kGatewayRemoteProfileIndex),
|
|
);
|
|
|
|
Future<String?> loadGatewayPassword({int? profileIndex}) async {
|
|
return _readSecure(
|
|
_gatewayPasswordKeyForProfile(profileIndex ?? kGatewayRemoteProfileIndex),
|
|
);
|
|
}
|
|
|
|
Future<void> saveGatewayPassword(String value, {int? profileIndex}) =>
|
|
_writeSecure(
|
|
_gatewayPasswordKeyForProfile(
|
|
profileIndex ?? kGatewayRemoteProfileIndex,
|
|
),
|
|
value,
|
|
);
|
|
|
|
Future<void> clearGatewayPassword({int? profileIndex}) => _deleteSecure(
|
|
_gatewayPasswordKeyForProfile(profileIndex ?? kGatewayRemoteProfileIndex),
|
|
);
|
|
|
|
Future<String?> loadOllamaCloudApiKey() => _readSecure(_ollamaCloudApiKeyKey);
|
|
|
|
Future<void> saveOllamaCloudApiKey(String value) =>
|
|
_writeSecure(_ollamaCloudApiKeyKey, value);
|
|
|
|
Future<String?> loadVaultToken() => _readSecure(_vaultTokenKey);
|
|
|
|
Future<void> saveVaultToken(String value) =>
|
|
_writeSecure(_vaultTokenKey, value);
|
|
|
|
Future<String?> loadAiGatewayApiKey() => _readSecure(_aiGatewayApiKeyKey);
|
|
|
|
Future<void> saveAiGatewayApiKey(String value) =>
|
|
_writeSecure(_aiGatewayApiKeyKey, value);
|
|
|
|
Future<void> clearAiGatewayApiKey() => _deleteSecure(_aiGatewayApiKeyKey);
|
|
|
|
Future<String?> loadAccountSessionToken() =>
|
|
_readSecure(_accountSessionTokenKey);
|
|
|
|
Future<void> saveAccountSessionToken(String value) =>
|
|
_writeSecure(_accountSessionTokenKey, value);
|
|
|
|
Future<void> clearAccountSessionToken() =>
|
|
_deleteSecure(_accountSessionTokenKey);
|
|
|
|
Future<int> loadAccountSessionExpiresAtMs() async {
|
|
final raw = await _readSecure(_accountSessionExpiresAtKey);
|
|
return int.tryParse((raw ?? '').trim()) ?? 0;
|
|
}
|
|
|
|
Future<void> saveAccountSessionExpiresAtMs(int value) =>
|
|
_writeSecure(_accountSessionExpiresAtKey, value.toString());
|
|
|
|
Future<void> clearAccountSessionExpiresAtMs() =>
|
|
_deleteSecure(_accountSessionExpiresAtKey);
|
|
|
|
Future<String?> loadAccountSessionUserId() =>
|
|
_readSecure(_accountSessionUserIdKey);
|
|
|
|
Future<void> saveAccountSessionUserId(String value) =>
|
|
_writeSecure(_accountSessionUserIdKey, value);
|
|
|
|
Future<void> clearAccountSessionUserId() =>
|
|
_deleteSecure(_accountSessionUserIdKey);
|
|
|
|
Future<String?> loadAccountSessionIdentifier() =>
|
|
_readSecure(_accountSessionIdentifierKey);
|
|
|
|
Future<void> saveAccountSessionIdentifier(String value) =>
|
|
_writeSecure(_accountSessionIdentifierKey, value);
|
|
|
|
Future<void> clearAccountSessionIdentifier() =>
|
|
_deleteSecure(_accountSessionIdentifierKey);
|
|
|
|
Future<AccountSessionSummary?> loadAccountSessionSummary() async {
|
|
final raw = await _readSecure(_accountSessionSummaryKey);
|
|
if ((raw ?? '').trim().isEmpty) {
|
|
return null;
|
|
}
|
|
try {
|
|
return AccountSessionSummary.fromJson(
|
|
(jsonDecode(raw!) as Map).cast<String, dynamic>(),
|
|
);
|
|
} catch (error) {
|
|
debugPrint('Account session summary decode failed: $error');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<void> saveAccountSessionSummary(AccountSessionSummary value) =>
|
|
_writeSecure(_accountSessionSummaryKey, jsonEncode(value.toJson()));
|
|
|
|
Future<void> clearAccountSessionSummary() =>
|
|
_deleteSecure(_accountSessionSummaryKey);
|
|
|
|
Future<AccountSyncState?> loadAccountSyncState() async {
|
|
final raw = await _readSecure(_accountSyncStateKey);
|
|
if ((raw ?? '').trim().isEmpty) {
|
|
return null;
|
|
}
|
|
try {
|
|
return AccountSyncState.fromJson(
|
|
(jsonDecode(raw!) as Map).cast<String, dynamic>(),
|
|
);
|
|
} catch (error) {
|
|
debugPrint('Account sync state decode failed: $error');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<void> saveAccountSyncState(AccountSyncState value) =>
|
|
_writeSecure(_accountSyncStateKey, jsonEncode(value.toJson()));
|
|
|
|
Future<void> clearAccountSyncState() => _deleteSecure(_accountSyncStateKey);
|
|
|
|
Future<Map<String, String>> loadAccountManagedSecrets() => loadSecureRefs();
|
|
|
|
Future<String?> loadAccountManagedSecret({required String target}) =>
|
|
_readSecure(_accountManagedSecretKey(target));
|
|
|
|
Future<void> saveAccountManagedSecret({
|
|
required String target,
|
|
required String value,
|
|
}) => _writeSecure(_accountManagedSecretKey(target), value);
|
|
|
|
Future<void> clearAccountManagedSecret({required String target}) =>
|
|
_deleteSecure(_accountManagedSecretKey(target));
|
|
|
|
Future<void> clearAccountManagedSecrets() async {
|
|
for (final target in kAccountManagedSecretTargets) {
|
|
await clearAccountManagedSecret(target: target);
|
|
}
|
|
}
|
|
|
|
Future<String?> loadSecretValueByRef(String refName) async {
|
|
final normalizedRef = refName.trim();
|
|
if (normalizedRef.isEmpty) {
|
|
return null;
|
|
}
|
|
return _readSecure(_secureStorageKeyForRef(normalizedRef));
|
|
}
|
|
|
|
Future<void> saveSecretValueByRef(String refName, String value) async {
|
|
final normalizedRef = refName.trim();
|
|
final trimmedValue = value.trim();
|
|
if (normalizedRef.isEmpty || trimmedValue.isEmpty) {
|
|
return;
|
|
}
|
|
final key = _secureStorageKeyForRef(normalizedRef);
|
|
await _writeSecure(key, trimmedValue);
|
|
if (_isCustomSecretRef(normalizedRef)) {
|
|
await _saveCustomSecretRefRegistryInternal(<String>{
|
|
...await _loadCustomSecretRefRegistryInternal(),
|
|
normalizedRef,
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> clearSecretValueByRef(String refName) async {
|
|
final normalizedRef = refName.trim();
|
|
if (normalizedRef.isEmpty) {
|
|
return;
|
|
}
|
|
await _deleteSecure(_secureStorageKeyForRef(normalizedRef));
|
|
if (_isCustomSecretRef(normalizedRef)) {
|
|
final refs = await _loadCustomSecretRefRegistryInternal();
|
|
refs.remove(normalizedRef);
|
|
await _saveCustomSecretRefRegistryInternal(refs);
|
|
}
|
|
}
|
|
|
|
Future<Map<String, String>> loadSecureRefs() async {
|
|
await initialize();
|
|
final secureRefs = <String, String>{};
|
|
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;
|
|
}
|
|
for (final target in kAccountManagedSecretTargets) {
|
|
final managedValue = await loadAccountManagedSecret(target: target);
|
|
if (managedValue case final value?) {
|
|
secureRefs[target] = value;
|
|
}
|
|
}
|
|
for (final refName in await _loadCustomSecretRefRegistryInternal()) {
|
|
final customValue = await loadSecretValueByRef(refName);
|
|
if (customValue case final value?) {
|
|
secureRefs[refName] = value;
|
|
}
|
|
}
|
|
return secureRefs;
|
|
}
|
|
|
|
static String gatewayTokenRefKey(int profileIndex) =>
|
|
_gatewayTokenRefKey(profileIndex);
|
|
|
|
static String gatewayPasswordRefKey(int profileIndex) =>
|
|
_gatewayPasswordRefKey(profileIndex);
|
|
|
|
Future<LocalDeviceIdentity?> 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<void> 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<String?> loadDeviceToken({
|
|
required String deviceId,
|
|
required String role,
|
|
}) => _readSecure(_deviceTokenKey(deviceId, role));
|
|
|
|
Future<void> saveDeviceToken({
|
|
required String deviceId,
|
|
required String role,
|
|
required String token,
|
|
}) => _writeSecure(_deviceTokenKey(deviceId, role), token);
|
|
|
|
Future<void> clearDeviceToken({
|
|
required String deviceId,
|
|
required String role,
|
|
}) => _deleteSecure(_deviceTokenKey(deviceId, role));
|
|
|
|
Future<List<int>?> loadLegacyLocalStateKeyBytes() async {
|
|
final encoded = await _readSecure(legacyLocalStateKey);
|
|
final trimmed = encoded?.trim() ?? '';
|
|
if (trimmed.isEmpty) {
|
|
return null;
|
|
}
|
|
return _base64UrlDecode(trimmed);
|
|
}
|
|
|
|
Future<void> 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 String _accountManagedSecretKey(String target) =>
|
|
'xworkmate.account.managed.${target.trim()}';
|
|
|
|
static String _customSecretRefKey(String refName) =>
|
|
'xworkmate.secret.ref.${refName.trim()}';
|
|
|
|
static bool _looksLikeGatewayProfileRef(String refName, String prefix) {
|
|
final normalized = refName.trim();
|
|
if (!normalized.startsWith(prefix)) {
|
|
return false;
|
|
}
|
|
final suffix = normalized.substring(prefix.length);
|
|
return int.tryParse(suffix) != null;
|
|
}
|
|
|
|
static bool _isCustomSecretRef(String refName) {
|
|
final normalized = refName.trim();
|
|
if (normalized.isEmpty ||
|
|
normalized == 'gateway_token' ||
|
|
normalized == 'gateway_password' ||
|
|
normalized == 'vault_token' ||
|
|
normalized == 'ai_gateway_api_key' ||
|
|
normalized == 'ollama_cloud_api_key' ||
|
|
isSupportedAccountManagedSecretTarget(normalized) ||
|
|
_looksLikeGatewayProfileRef(normalized, 'gateway_token_') ||
|
|
_looksLikeGatewayProfileRef(normalized, 'gateway_password_')) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static String _secureStorageKeyForRef(String refName) {
|
|
final normalized = refName.trim();
|
|
if (normalized == 'gateway_token') {
|
|
return _gatewayTokenKeyForProfile(kGatewayRemoteProfileIndex);
|
|
}
|
|
if (normalized == 'gateway_password') {
|
|
return _gatewayPasswordKeyForProfile(kGatewayRemoteProfileIndex);
|
|
}
|
|
if (_looksLikeGatewayProfileRef(normalized, 'gateway_token_')) {
|
|
final index = int.parse(normalized.substring('gateway_token_'.length));
|
|
return _gatewayTokenKeyForProfile(index);
|
|
}
|
|
if (_looksLikeGatewayProfileRef(normalized, 'gateway_password_')) {
|
|
final index = int.parse(normalized.substring('gateway_password_'.length));
|
|
return _gatewayPasswordKeyForProfile(index);
|
|
}
|
|
if (normalized == 'vault_token') {
|
|
return _vaultTokenKey;
|
|
}
|
|
if (normalized == 'ai_gateway_api_key') {
|
|
return _aiGatewayApiKeyKey;
|
|
}
|
|
if (normalized == 'ollama_cloud_api_key') {
|
|
return _ollamaCloudApiKeyKey;
|
|
}
|
|
if (isSupportedAccountManagedSecretTarget(normalized)) {
|
|
return _accountManagedSecretKey(normalized);
|
|
}
|
|
return _customSecretRefKey(normalized);
|
|
}
|
|
|
|
Future<Set<String>> _loadCustomSecretRefRegistryInternal() async {
|
|
final raw = await _readSecure(_customSecretRefRegistryKey);
|
|
if ((raw ?? '').trim().isEmpty) {
|
|
return <String>{};
|
|
}
|
|
try {
|
|
final decoded = jsonDecode(raw!);
|
|
if (decoded is! List) {
|
|
return <String>{};
|
|
}
|
|
return decoded
|
|
.map((item) => item.toString().trim())
|
|
.where((item) => item.isNotEmpty)
|
|
.toSet();
|
|
} catch (error) {
|
|
debugPrint('Custom secret ref registry decode failed: $error');
|
|
return <String>{};
|
|
}
|
|
}
|
|
|
|
Future<void> _saveCustomSecretRefRegistryInternal(Set<String> refs) async {
|
|
final normalized =
|
|
refs
|
|
.map((item) => item.trim())
|
|
.where((item) => item.isNotEmpty)
|
|
.toList(growable: false)
|
|
..sort();
|
|
if (normalized.isEmpty) {
|
|
await _deleteSecure(_customSecretRefRegistryKey);
|
|
return;
|
|
}
|
|
await _writeSecure(_customSecretRefRegistryKey, jsonEncode(normalized));
|
|
}
|
|
|
|
Future<String?> _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 (error) {
|
|
debugPrint('Secure read failed for $key: $error');
|
|
// Fall back to memory only when the secret path is unavailable.
|
|
}
|
|
}
|
|
final memoryValue = _memorySecure[key]?.trim() ?? '';
|
|
return memoryValue.isEmpty ? null : memoryValue;
|
|
}
|
|
|
|
Future<void> _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<void> _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<int> _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,
|
|
);
|
|
}
|
|
}
|