92 lines
3.2 KiB
Dart
92 lines
3.2 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 (hardcoded to match edge function constants)
|
|
static const String registrationSeed = 'waylume_rolling_registration_seed_2025';
|
|
static const String secretSalt = 'waylume_operational_secret_salt_2025';
|
|
static const int timeWindowSeconds = 300; // 5 minutes
|
|
|
|
static String? _operationalSeed;
|
|
static String? _serverId;
|
|
|
|
/// Initialize service
|
|
static Future<void> initialize() async {
|
|
_serverId = fromEnivronment('SERVER_ID');
|
|
if (_serverId == null) {
|
|
throw Exception('SERVER_ID environment variable is required');
|
|
}
|
|
|
|
// No need to load configuration - server re-registers on every restart
|
|
print('Rolling codes service initialized for server $_serverId');
|
|
}
|
|
|
|
/// 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 void setOperationalSeed(String seed) {
|
|
_operationalSeed = seed;
|
|
print('Operational seed set for server $_serverId');
|
|
}
|
|
|
|
/// 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();
|
|
}
|
|
} |