132 lines
4.6 KiB
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');
|
|
}
|
|
}
|
|
} |