Files
waylume_server/lib/services/rolling_codes_service.dart

132 lines
4.6 KiB
Dart

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<void> 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<void> 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<void> _loadConfiguration() async {
try {
final file = File(configFile);
if (await file.exists()) {
final content = await file.readAsString();
final config = jsonDecode(content) as Map<String, dynamic>;
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<void> _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');
}
}
}