import 'dart:convert'; import 'dart:typed_data'; import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:waylume_server/core/utils.dart'; import '../config/supabase_config.dart'; class RollingCodesService { // Shared constants (these should match edge function constants) static const String registrationSeed = String.fromEnvironment('REGISTRATION_SEED', defaultValue: 'default_registration_seed_change_in_production'); static const String secretSalt = String.fromEnvironment('SECRET_SALT', defaultValue: 'default_secret_salt_change_in_production'); static const int timeWindowSeconds = 300; // 5 minutes // Server configuration file path static const String configFile = '/tmp/waylume_server_config.json'; static String? _operationalSeed; static String? _serverId; /// Initialize service and load/generate configuration static Future initialize() async { _serverId = fromEnivronment('SERVER_ID'); if (_serverId == null) { throw Exception('SERVER_ID environment variable is required'); } await _loadConfiguration(); } /// Generate registration rolling code static String generateRegistrationCode([DateTime? timestamp]) { timestamp ??= DateTime.now(); final timeWindow = timestamp.millisecondsSinceEpoch ~/ 1000 ~/ timeWindowSeconds; return _generateHmacSha256(registrationSeed, timeWindow.toString()); } /// Generate operational rolling code static String generateOperationalCode([DateTime? timestamp]) { if (_operationalSeed == null) { throw Exception('Operational seed not set. Server needs to be registered first.'); } timestamp ??= DateTime.now(); final timeWindow = timestamp.millisecondsSinceEpoch ~/ 1000 ~/ timeWindowSeconds; return _generateHmacSha256(_operationalSeed!, timeWindow.toString() + secretSalt); } /// Validate rolling code (checks current and previous window) static bool validateOperationalCode(String receivedCode, [DateTime? timestamp]) { if (_operationalSeed == null) { return false; } timestamp ??= DateTime.now(); final currentWindow = timestamp.millisecondsSinceEpoch ~/ 1000 ~/ timeWindowSeconds; // Check current window final currentExpected = _generateHmacSha256(_operationalSeed!, currentWindow.toString() + secretSalt); if (receivedCode == currentExpected) { return true; } // Check previous window (10-minute tolerance total) final previousExpected = _generateHmacSha256(_operationalSeed!, (currentWindow - 1).toString() + secretSalt); return receivedCode == previousExpected; } /// Set operational seed after successful registration static Future setOperationalSeed(String seed) async { _operationalSeed = seed; await _saveConfiguration(); } /// Get current operational seed static String? get operationalSeed => _operationalSeed; /// Get server ID static String? get serverId => _serverId; /// Check if server is registered (has operational seed) static bool get isRegistered => _operationalSeed != null; /// Generate HMAC-SHA256 hash static String _generateHmacSha256(String key, String message) { final keyBytes = utf8.encode(key); final messageBytes = utf8.encode(key + message); final hmac = Hmac(sha256, keyBytes); final digest = hmac.convert(messageBytes); return digest.toString(); } /// Load configuration from persistent storage static Future _loadConfiguration() async { try { final file = File(configFile); if (await file.exists()) { final content = await file.readAsString(); final config = jsonDecode(content) as Map; if (config['server_id'] == _serverId) { _operationalSeed = config['operational_seed']; print('Loaded operational seed for server $_serverId'); } else { print('Configuration file exists but server_id does not match. Need to re-register.'); } } } catch (e) { print('Error loading configuration: $e'); } } /// Save configuration to persistent storage static Future _saveConfiguration() async { try { final config = { 'server_id': _serverId, 'operational_seed': _operationalSeed, 'last_updated': DateTime.now().toIso8601String(), }; final file = File(configFile); await file.writeAsString(jsonEncode(config)); print('Configuration saved for server $_serverId'); } catch (e) { print('Error saving configuration: $e'); throw Exception('Failed to save server configuration'); } } }