Implement authentication middleware and rolling codes service for secure API access

This commit is contained in:
ImBenji
2025-08-19 20:30:43 +01:00
parent f829bd5fe1
commit 17091bcc95
4 changed files with 380 additions and 44 deletions

View File

@@ -0,0 +1,132 @@
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');
}
}
}

View File

@@ -1,28 +1,32 @@
import 'dart:io'; import 'dart:io';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:waylume_server/config/supabase_config.dart'; import 'package:waylume_server/config/supabase_config.dart';
import 'package:waylume_server/core/utils.dart'; import 'package:waylume_server/core/utils.dart';
import 'package:waylume_server/services/rolling_codes_service.dart';
class ServerService { class ServerService {
static Future<void> registerServer() async { static Future<void> registerServer() async {
await RollingCodesService.initialize();
// If already registered and has operational seed, skip registration
if (RollingCodesService.isRegistered) {
print('Server already registered with operational seed');
return;
}
GeolocationData geolocationData = await getGeolocationData(); GeolocationData geolocationData = await getGeolocationData();
String ip = "${geolocationData.ip}:${fromEnivronment("EXTERNAL_PORT") ?? "3000"}"; // Generate registration rolling code
String registrationAuth = RollingCodesService.generateRegistrationCode();
var existsCheck = await SUPABASE_CLIENT // Call server-manager registration endpoint
.from("waylume_servers") String serverManagerUrl = '${fromEnivronment("SUPABASE_URL")}/functions/v1/server-manager/register';
.select()
.eq("id", fromEnivronment('SERVER_ID'))
.eq("host_ip", ip);
if (existsCheck.isEmpty) { Map<String, dynamic> requestBody = {
await SUPABASE_CLIENT 'server_id': fromEnivronment('SERVER_ID')!,
.from("waylume_servers") 'registration_auth': registrationAuth,
.insert({ 'geolocation_data': {
"id": fromEnivronment('SERVER_ID')!,
"last_heartbeat": DateTime.now().toUtc().toIso8601String(),
"host_ip": ip,
"geolocation": {
"country": geolocationData.countryName, "country": geolocationData.countryName,
"country_code": geolocationData.countryCode, "country_code": geolocationData.countryCode,
"city": geolocationData.city, "city": geolocationData.city,
@@ -31,7 +35,33 @@ class ServerService {
geolocationData.longitude geolocationData.longitude
], ],
} }
}); };
try {
final response = await http.post(
Uri.parse(serverManagerUrl),
headers: {
'Content-Type': 'application/json',
'apikey': fromEnivronment('SUPABASE_ANON_KEY')!,
},
body: jsonEncode(requestBody),
);
if (response.statusCode == 200) {
final responseData = jsonDecode(response.body);
if (responseData['success']) {
// Store operational seed
await RollingCodesService.setOperationalSeed(responseData['operational_seed']);
print('Server registered successfully with server-manager');
} else {
throw Exception('Registration failed: ${responseData['error']}');
}
} else {
throw Exception('Registration request failed: ${response.statusCode}');
}
} catch (e) {
print('Error registering server: $e');
throw Exception('Failed to register server: $e');
} }
} }

View File

@@ -4,9 +4,10 @@
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'dart:convert';
import 'package:supabase/supabase.dart'; import 'package:http/http.dart' as http;
import 'package:waylume_server/config/supabase_config.dart'; import 'package:waylume_server/config/supabase_config.dart';
import 'package:waylume_server/services/rolling_codes_service.dart';
void initHeartbeat() { void initHeartbeat() {
@@ -19,17 +20,121 @@ void initHeartbeat() {
while (true) { while (true) {
DateTime now = DateTime.now().toUtc(); DateTime now = DateTime.now().toUtc();
await SUPABASE_CLIENT try {
.from("waylume_servers") // Wait until rolling codes service is initialized and registered
.update({ "last_heartbeat": now.toIso8601String() }) if (!RollingCodesService.isRegistered) {
.eq("id", fromEnivronment("SERVER_ID")!); print("Server not registered yet, skipping heartbeat");
print("Heartbeat sent to Supabase at ${now.toIso8601String()}");
// Wait 30 seconds before sending the next heartbeat
await Future.delayed(Duration(seconds: 30)); await Future.delayed(Duration(seconds: 30));
continue;
}
// Generate operational rolling code for heartbeat authentication
String authCode = RollingCodesService.generateOperationalCode();
// Get server status (CPU, memory, active connections)
Map<String, dynamic> serverStatus = await _getServerStatus();
// Send heartbeat to server-manager endpoint
String serverManagerUrl = '${fromEnivronment("SUPABASE_URL")}/functions/v1/server-manager/heartbeat';
Map<String, dynamic> heartbeatData = {
'server_id': fromEnivronment("SERVER_ID")!,
'timestamp': now.toIso8601String(),
'auth_code': authCode,
'status': serverStatus
};
final response = await http.post(
Uri.parse(serverManagerUrl),
headers: {
'Content-Type': 'application/json',
'apikey': fromEnivronment('SUPABASE_ANON_KEY')!,
},
body: jsonEncode(heartbeatData),
);
if (response.statusCode == 200) {
final responseData = jsonDecode(response.body);
if (responseData['success']) {
print("Heartbeat sent successfully at ${now.toIso8601String()}");
} else {
print("Heartbeat failed: ${responseData['error']}");
}
} else {
print("Heartbeat request failed: ${response.statusCode}");
}
} catch (e) {
print("Error sending heartbeat: $e");
}
// Wait 120 seconds (2 minutes) before sending the next heartbeat
await Future.delayed(Duration(seconds: 120));
} }
}, null); }, null);
} }
Future<Map<String, dynamic>> _getServerStatus() async {
// Get basic server status information
// This is a simplified implementation - could be enhanced with actual metrics
int activeConnections = 0;
double cpuUsage = 0.0;
double memoryUsage = 0.0;
try {
// Get active WireGuard connections count
ProcessResult wgShowResult = await Process.run('wg', ['show', 'wg0']);
if (wgShowResult.exitCode == 0) {
// Count peer entries (simplified - each peer has multiple lines)
String output = wgShowResult.stdout as String;
activeConnections = 'peer:'.allMatches(output).length;
}
} catch (e) {
print("Error getting WireGuard status: $e");
}
try {
// Get basic system info (simplified)
ProcessResult uptimeResult = await Process.run('uptime', []);
if (uptimeResult.exitCode == 0) {
String output = uptimeResult.stdout as String;
// Parse load average as a rough CPU usage indicator
RegExp loadRegex = RegExp(r'load average: (\d+\.?\d*),');
Match? match = loadRegex.firstMatch(output);
if (match != null) {
cpuUsage = double.tryParse(match.group(1) ?? '0') ?? 0.0;
cpuUsage = (cpuUsage * 100).clamp(0, 100); // Convert to percentage
}
}
} catch (e) {
print("Error getting CPU usage: $e");
}
try {
// Get memory usage (simplified)
ProcessResult freeResult = await Process.run('free', ['-m']);
if (freeResult.exitCode == 0) {
String output = freeResult.stdout as String;
List<String> lines = output.split('\n');
if (lines.length > 1) {
List<String> memLine = lines[1].split(RegExp(r'\s+'));
if (memLine.length > 2) {
int total = int.tryParse(memLine[1]) ?? 1;
int used = int.tryParse(memLine[2]) ?? 0;
memoryUsage = (used / total * 100).clamp(0, 100);
}
}
}
} catch (e) {
print("Error getting memory usage: $e");
}
return {
'active_connections': activeConnections,
'cpu_usage': cpuUsage,
'memory_usage': memoryUsage,
};
}

View File

@@ -7,22 +7,91 @@ import 'package:waylume_server/wireguard/utils.dart';
import 'package:waylume_server/core/utils.dart'; import 'package:waylume_server/core/utils.dart';
import 'package:waylume_server/services/vpn_session_service.dart'; import 'package:waylume_server/services/vpn_session_service.dart';
import 'package:waylume_server/services/bandwidth_service.dart'; import 'package:waylume_server/services/bandwidth_service.dart';
import 'package:waylume_server/services/rolling_codes_service.dart';
class PeerRoutes { class PeerRoutes {
Router get router { Router get router {
final router = Router(); final router = Router();
router.get('/peers', _getPeers); router.get('/peers', _authMiddleware(_getPeers));
router.post('/peer', _createPeer); router.post('/peer', _authMiddleware(_createPeer));
router.delete('/peer/<publicKey>', _deletePeer); router.delete('/peer/<publicKey>', _authMiddleware(_deletePeer));
router.get('/peer/<publicKey>/config', _getPeerConfig); router.get('/peer/<publicKey>/config', _authMiddleware(_getPeerConfig));
router.patch('/peer/<publicKey>/speed-limit', _setSpeedLimit); router.patch('/peer/<publicKey>/speed-limit', _authMiddleware(_setSpeedLimit));
router.patch('/peer/<publicKey>/data-cap', _setDataCap); router.patch('/peer/<publicKey>/data-cap', _authMiddleware(_setDataCap));
router.get('/bandwidth-stats', _getBandwidthStats); router.get('/bandwidth-stats', _authMiddleware(_getBandwidthStats));
return router; return router;
} }
/// Authentication middleware for API endpoints
Handler _authMiddleware(Handler handler) {
return (Request request) async {
try {
// Check if server is registered
if (!RollingCodesService.isRegistered) {
return Response(401,
body: jsonEncode({
'success': false,
'error': 'Server not registered - missing operational seed'
}),
headers: {'Content-Type': 'application/json'}
);
}
// Get authorization header
String? authHeader = request.headers['authorization'];
if (authHeader == null) {
return Response(401,
body: jsonEncode({
'success': false,
'error': 'Authorization header required'
}),
headers: {'Content-Type': 'application/json'}
);
}
// Validate WaylumeAuth format
if (!authHeader.startsWith('WaylumeAuth ')) {
return Response(401,
body: jsonEncode({
'success': false,
'error': 'Invalid authorization format - expected WaylumeAuth'
}),
headers: {'Content-Type': 'application/json'}
);
}
// Extract auth code
String authCode = authHeader.substring('WaylumeAuth '.length);
// Validate rolling code
bool isValid = RollingCodesService.validateOperationalCode(authCode);
if (!isValid) {
return Response(401,
body: jsonEncode({
'success': false,
'error': 'Invalid authentication code'
}),
headers: {'Content-Type': 'application/json'}
);
}
// Authentication successful, proceed to handler
return await handler(request);
} catch (e) {
return Response(500,
body: jsonEncode({
'success': false,
'error': 'Authentication error: $e'
}),
headers: {'Content-Type': 'application/json'}
);
}
};
}
Future<Response> _getPeers(Request request) async { Future<Response> _getPeers(Request request) async {
try { try {
final statusParam = request.url.queryParameters['status']?.toUpperCase(); final statusParam = request.url.queryParameters['status']?.toUpperCase();