Skip to content

Commit

Permalink
feat: positive spoofer detectors (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
matejglejtek committed Jun 8, 2024
2 parents afa47cf + 3f25d28 commit cf02b54
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 49 deletions.
10 changes: 7 additions & 3 deletions lib/bloc/aircraft/aircraft_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class AircraftCubit extends Cubit<AircraftState> {
Timer? _refreshTimer;
final AircraftExpirationCubit expirationCubit;

final MessageContainerAuthenticator messageContainerAuthenticator;

static const uiUpdateIntervalMs = 200;

// data for showcase
Expand Down Expand Up @@ -71,8 +73,10 @@ class AircraftCubit extends Cubit<AircraftState> {
),
];

AircraftCubit({required this.expirationCubit})
: super(AircraftState(packHistory: {}, dataAuthenticityStatuses: {})) {
AircraftCubit({
required this.expirationCubit,
required this.messageContainerAuthenticator,
}) : super(AircraftState(packHistory: {}, dataAuthenticityStatuses: {})) {
expirationCubit.deleteCallback = deletePack;
}

Expand Down Expand Up @@ -147,7 +151,7 @@ class AircraftCubit extends Cubit<AircraftState> {
dataAuthenticityStatuses: state.dataAuthenticityStatuses
..addAll({
pack.macAddress:
MessageContainerAuthenticator.determineAuthenticityStatus(
messageContainerAuthenticator.determineAuthenticityStatus(
pack,
)
}),
Expand Down
33 changes: 25 additions & 8 deletions lib/bloc/aircraft/aircraft_metadata_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:http/http.dart';
import 'package:localstorage/localstorage.dart';
import 'package:logging/logging.dart';

import '../../exceptions/invalid_country_code_exception.dart';
import '../../models/aircraft_model_info.dart';
import '../../services/flag_rest_client.dart';
import '../../services/ornithology_rest_client.dart';
Expand Down Expand Up @@ -144,21 +145,24 @@ class AircraftMetadataCubit extends Cubit<AircraftMetadataState> {
if (state.countryCodeFlags.containsKey(alpha3CountryCode)) {
return;
}

try {
emit(state.copyWith(fetchInProgress: true));
final alpha2CountryCode = _countryCodeMapping[alpha3CountryCode];

if (alpha2CountryCode == null) {
throw Exception('Invalid country code parameter: $alpha3CountryCode');
throw InvalidCountryCodeException(
'Invalid country code: $alpha3CountryCode');
}

final flag =
await flagRestClient.fetchFlag(countryCode: alpha2CountryCode);
if (flag == null) {
Logger.root
.warning('Flag for country code $alpha2CountryCode does not exist');
Logger.root.warning(
'Unable to fetch flag for country code $alpha2CountryCode');
return;
}
// save also empty flag so it does not have to be fetched again

emit(
state.copyWith(
countryCodeFlags: {
Expand All @@ -169,10 +173,23 @@ class AircraftMetadataCubit extends Cubit<AircraftMetadataState> {
),
);
await _saveFlags();
} on ClientException catch (err) {
Logger.root.warning(
'Failed to fetch flag model info for $alpha3CountryCode, $err');
emit(state.copyWith(fetchInProgress: false));
} catch (err) {
Logger.root.warning('Failed to fetch flag for $alpha3CountryCode, $err');

// if InvalidCountryCodeException is thrown, save emptry flag for code
// and proceed normally
if (err is! InvalidCountryCodeException) rethrow;

emit(
state.copyWith(
countryCodeFlags: {
...state.countryCodeFlags,
alpha3CountryCode: null,
},
fetchInProgress: false,
),
);
await _saveFlags();
}
}

Expand Down
5 changes: 5 additions & 0 deletions lib/exceptions/invalid_country_code_exception.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class InvalidCountryCodeException implements Exception {
final String message;

InvalidCountryCodeException([this.message = "Invalid country code"]);
}
9 changes: 8 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import 'services/geocoding_rest_client.dart';
import 'services/location_service.dart';
import 'services/notification_service.dart';
import 'services/ornithology_rest_client.dart';
import 'utils/message_container_authenticator.dart';
import 'widgets/app/app.dart';

const sentryDsn = String.fromEnvironment(
Expand Down Expand Up @@ -68,13 +69,19 @@ void main() async {
// init local notifications
final notificationService = NotificationService();
await notificationService.setup();
// Init Google services
// init location services
final locationService = LocationService();

await locationService.enableService();

final mapCubit = MapCubit(locationService);
final messageContainerAuthenticator =
MessageContainerAuthenticator(locationService: locationService);
final selectedCubit = SelectedAircraftCubit();
final aircraftExpirationCubit = AircraftExpirationCubit();
final aircraftCubit = AircraftCubit(
expirationCubit: aircraftExpirationCubit,
messageContainerAuthenticator: messageContainerAuthenticator,
);
final aircraftMetadataCubit = await AircraftMetadataCubit(
ornithologyRestClient: OrnithologyRestClient(),
Expand Down
14 changes: 3 additions & 11 deletions lib/models/message_container_authenticity_status.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,12 @@
enum MessageContainerAuthenticityStatus {
// verified by authority/multilateration
verified,
// reasonable signs of authenticity exist
trusted,
// default
untrusted,
// signs if questionable authenticity exist
// signs of questionable authenticity exist
suspicious,
// high certainty of falsehood
counterfeit
}

// only suspicious and counterfeit statuses are displayed in UI,
// temporary measure until verification is complete
extension MessageContainerAuthenticityStatusExtension
on MessageContainerAuthenticityStatus {
bool get shouldBeDisplayed {
return this == MessageContainerAuthenticityStatus.suspicious ||
this == MessageContainerAuthenticityStatus.counterfeit;
}
}
14 changes: 12 additions & 2 deletions lib/services/location_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class LocationService {
bool settingsSet = false;
loc.PermissionStatus permissions = loc.PermissionStatus.denied;

LocationService() {
location.onLocationChanged.listen(_onLocationUpdated);
}

bool get missingGrantedPermission =>
permissions != loc.PermissionStatus.granted;

Expand Down Expand Up @@ -79,11 +83,17 @@ class LocationService {

final locationData = await location.getLocation();

_onLocationUpdated(locationData);

return lastLocation;
}

void _onLocationUpdated(locationData) {
if (locationData.latitude == null || locationData.longitude == null) {
return null;
return;
}

return lastLocation = Location(
lastLocation = Location(
latitude: locationData.latitude!,
longitude: locationData.longitude!,
);
Expand Down
111 changes: 102 additions & 9 deletions lib/utils/message_container_authenticator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,28 @@ import 'package:flutter_opendroneid/models/constants.dart';
import 'package:flutter_opendroneid/models/message_container.dart';

import '../models/message_container_authenticity_status.dart';
import '../services/location_service.dart';
import 'utils.dart';

class MessageContainerAuthenticator {
final LocationService locationService;

/// Detectors that contribute to final score.
static final detectors = [
late final detectors = [
MacAddressSpooferDetector(),
TimestampSpooferDetector(),
LocationTimestampSpooferDetector(),
AuthAndSystemTimestampSpooferDetector(),
AuthDataSpooferDetector(),
BasicIdSpooferDetector(),
OperatorIdSpooferDetector(),
LocationSpooferDetector(),
LocationMessageSpooferDetector(),
SelfIdSpooferDetector(),
SystemDataSpooferDetector(),
LocationSpooferDetector(locationService: locationService),
];

MessageContainerAuthenticator({required this.locationService});

/// Check contents of message container using [detectors].
/// Check if messages contain values used in RemoteIDSpoofer.
/// Each detector returns probability that data were spoofed.
Expand All @@ -26,7 +34,7 @@ class MessageContainerAuthenticator {
/// < max/2 - untrusted
/// max/2, max/4*3 - suspicious
/// > max/4*3 - counterfeit.
static MessageContainerAuthenticityStatus determineAuthenticityStatus(
MessageContainerAuthenticityStatus determineAuthenticityStatus(
MessageContainer container) {
var score = 0.0;

Expand All @@ -37,14 +45,19 @@ class MessageContainerAuthenticator {
return _scoreToStatus(score);
}

static MessageContainerAuthenticityStatus _scoreToStatus(double score) {
MessageContainerAuthenticityStatus _scoreToStatus(double score) {
final maxScore = detectors.length;

// if nothing can be decided, score is exactly half, when score is bigger
// than half, at least one detector noticed suspisious data
// than half, at least one detector noticed suspisious data.
// Score bellow 1/4 is considered trusted.
final untrustedScore = maxScore * 0.25;
final noSuspisionScore = maxScore * 0.5;
final counterfeitScore = maxScore * 0.75;

if (score <= untrustedScore) {
return MessageContainerAuthenticityStatus.trusted;
}
if (score <= noSuspisionScore) {
return MessageContainerAuthenticityStatus.untrusted;
}
Expand Down Expand Up @@ -73,10 +86,50 @@ class MacAddressSpooferDetector implements SpooferDetector {
container.macAddress.startsWith('0') ? 0.75 : 0;
}

/// Compare received location timestamps with system time.
class LocationTimestampSpooferDetector implements SpooferDetector {
// time difference thresholds in seconds
static const suspiciousTimeDifference = 10;
static const counterfeitTimeDifference = 60;

@override
double calculateSpoofedProbability(MessageContainer container) {
final locTimestamp = container.locationMessage?.timestamp;

if (locTimestamp == null) return 0.5;

final currentTimestamp = DateTime.now().toLocal();

// subtract last full hour from current timestamp to get duration passed
// from the last full hour
final currentTimestampDurationSinceLastHour = currentTimestamp.difference(
DateTime(currentTimestamp.year, currentTimestamp.month,
currentTimestamp.day, currentTimestamp.hour),
);

// calculate absulute difference in seconds
final locTimestampDifference = ((locTimestamp.inMilliseconds -
currentTimestampDurationSinceLastHour.inMilliseconds) /
1000)
.abs();

// if time difference is small, data can be still spoofed because spoofer
// could be started at time that matches system timestamp but probability
// is small
if (locTimestampDifference < suspiciousTimeDifference) {
return 0.1;
} else if (locTimestampDifference < counterfeitTimeDifference) {
return 0.75;
}
// big difference btw location and system timestamp means data are spoofed
return 1;
}
}

/// RemoteIDSpoofer starts counting time from known timestamp.
/// If received timestamp is in short interval after that timestamp,
/// data are probably spoofed.
class TimestampSpooferDetector implements SpooferDetector {
class AuthAndSystemTimestampSpooferDetector implements SpooferDetector {
static final spooferTimestamp = DateTime(2022, 11, 16, 10);
static const maxUptime = Duration(days: 10);

Expand Down Expand Up @@ -155,11 +208,11 @@ class OperatorIdSpooferDetector implements SpooferDetector {
if (countryCode != countryCode.toUpperCase()) return 0.9;
// country code contains capital letters,
// data can still be spoofed
return 0.4;
return 0.25;
}
}

class LocationSpooferDetector implements SpooferDetector {
class LocationMessageSpooferDetector implements SpooferDetector {
@override
double calculateSpoofedProbability(MessageContainer container) {
final message = container.locationMessage;
Expand Down Expand Up @@ -220,3 +273,43 @@ class SystemDataSpooferDetector implements SpooferDetector {
return spoofed ? 1 : 0.2;
}
}

/// [LocationSpooferDetector] compares location of detected aircraft with
/// phone location.
class LocationSpooferDetector implements SpooferDetector {
// distance thresholds in meters
static const suspiciousDistance = 3000;
static const counterfeitDistance = 10000;

LocationService locationService;

LocationSpooferDetector({required this.locationService});

@override
double calculateSpoofedProbability(MessageContainer container) {
final phoneLocation = locationService.lastLocation;
final aircraftLocation = container.locationMessage?.location;

// when one of locations if null, detector cannot decide
if (phoneLocation == null || aircraftLocation == null) return 0.5;

// calc distance and convert to meters
final distance = calculateDistance(
phoneLocation.latitude,
phoneLocation.longitude,
aircraftLocation.latitude,
aircraftLocation.longitude) *
1000;

// if distance is shorter than suspicious threshold,
// there is still some change that data are spoofed.
// Location is configurable in RemoteIDSpoofer.
if (distance < suspiciousDistance) {
return 0.2;
} else if (distance < counterfeitDistance) {
return 0.75;
}
// distance bigger than counterfeitDistance, spoofed for sure
return 1;
}
}
10 changes: 5 additions & 5 deletions lib/widgets/sliders/aircraft/aircraft_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,11 @@ class AircraftCard extends StatelessWidget {
AircraftCardCustomText(
messagePack: messagePack,
),
if (authenticityStatus.shouldBeDisplayed)
Text(
authenticityStatus.name.capitalize(),
textScaler: const TextScaler.linear(0.9),
)

Text(
authenticityStatus.name.capitalize(),
textScaler: const TextScaler.linear(0.9),
)
],
);
}
Expand Down
16 changes: 7 additions & 9 deletions lib/widgets/sliders/aircraft/detail/aircraft_detail_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -307,14 +307,12 @@ class AircraftDetailHeader extends StatelessWidget {
cubit.state.dataAuthenticityStatuses[macAddress] ??
MessageContainerAuthenticityStatus.untrusted,
);
if (authenticityStatus.shouldBeDisplayed) {
return Text(
authenticityStatus.name.capitalize(),
style: const TextStyle(
color: Colors.white,
),
);
}
return const SizedBox.shrink();

return Text(
authenticityStatus.name.capitalize(),
style: const TextStyle(
color: Colors.white,
),
);
}
}
Loading

0 comments on commit cf02b54

Please sign in to comment.