Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: filter-out known spoofed data #34

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion lib/models/message_container.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'package:dart_opendroneid/dart_opendroneid.dart';
import 'package:flutter_opendroneid/extensions/compare_extension.dart';
import 'package:flutter_opendroneid/models/constants.dart';
import 'package:flutter_opendroneid/models/message_container_authenticity_status.dart';
import 'package:flutter_opendroneid/pigeon.dart' as pigeon;
import 'package:flutter_opendroneid/utils/message_container_authenticator.dart';
import 'package:flutter_opendroneid/utils/conversions.dart';

/// The [MessageContainer] groups together messages of different types
Expand All @@ -20,6 +22,8 @@ class MessageContainer {
final AuthMessage? authenticationMessage;
final SystemMessage? systemDataMessage;

final MessageContainerAuthenticityStatus authenticityStatus;

MessageContainer({
required this.macAddress,
required this.lastUpdate,
Expand All @@ -31,6 +35,7 @@ class MessageContainer {
this.selfIdMessage,
this.authenticationMessage,
this.systemDataMessage,
this.authenticityStatus = MessageContainerAuthenticityStatus.untrusted,
});

MessageContainer copyWith({
Expand All @@ -44,6 +49,7 @@ class MessageContainer {
SelfIDMessage? selfIdMessage,
AuthMessage? authenticationMessage,
SystemMessage? systemDataMessage,
MessageContainerAuthenticityStatus? authenticityStatus,
}) =>
MessageContainer(
macAddress: macAddress ?? this.macAddress,
Expand All @@ -57,11 +63,13 @@ class MessageContainer {
authenticationMessage:
authenticationMessage ?? this.authenticationMessage,
systemDataMessage: systemDataMessage ?? this.systemDataMessage,
authenticityStatus: authenticityStatus ?? this.authenticityStatus,
);

/// Returns new MessageContainer updated with message.
/// Null is returned if update is refused, because it contains duplicate or
/// corrupted data.
/// Check data authenticity status using [MessageContainerAuthenticator].
MessageContainer? update({
required ODIDMessage message,
required int receivedTimestamp,
Expand All @@ -83,7 +91,7 @@ class MessageContainer {
return result;
}
// update pack only if new data differ from saved ones
return switch (message.runtimeType) {
final updatedPack = switch (message.runtimeType) {
LocationMessage => locationMessage != null &&
locationMessage!.containsEqualData(message as LocationMessage)
? null
Expand Down Expand Up @@ -142,6 +150,14 @@ class MessageContainer {
),
_ => null
};

if (updatedPack != null) {
final dataStatus =
MessageContainerAuthenticator.determineAuthenticityStatus(
updatedPack);
return updatedPack.copyWith(authenticityStatus: dataStatus);
}
return null;
}

pigeon.MessageSource get packSource => source;
Expand Down
11 changes: 11 additions & 0 deletions lib/models/message_container_authenticity_status.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// Represents the level of trust in the authenticity of collected data.
enum MessageContainerAuthenticityStatus {
// verified by authority/multilateration
verified,
// default
untrusted,
// signs if questionable authenticity exist
suspicious,
// high certainty of falsehood
counterfeit
}
14 changes: 14 additions & 0 deletions lib/utils/conversions.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:dart_opendroneid/dart_opendroneid.dart';
import 'package:flutter_opendroneid/extensions/list_extension.dart';
import 'package:flutter_opendroneid/models/message_container_authenticity_status.dart';

/// Conversions extensions
// TODO move to dart-opendroneid in the future(?)
Expand Down Expand Up @@ -100,6 +101,14 @@ const Map<OperationalStatus, String> _operationalStatusConversionMap = {
OperationalStatus.none: 'Unknown',
};

const Map<MessageContainerAuthenticityStatus, String>
_MessageContainerAuthenticityStatusConversionMap = {
MessageContainerAuthenticityStatus.verified: 'Verified',
MessageContainerAuthenticityStatus.untrusted: 'Untrusted',
MessageContainerAuthenticityStatus.suspicious: 'Suspicious',
MessageContainerAuthenticityStatus.counterfeit: 'Counterfeit',
};

extension HorizontalAccuracyConversion on HorizontalAccuracy {
double? toMeters() => _horizontalAccuracyConversionMap[this];
}
Expand Down Expand Up @@ -159,3 +168,8 @@ extension HeightTypeConversion on HeightType {
extension OperationalStatusConversion on OperationalStatus {
String? asString() => _operationalStatusConversionMap[this];
}

extension MessageContainerAuthenticityStatusConversion
on MessageContainerAuthenticityStatus {
String? asString() => _MessageContainerAuthenticityStatusConversionMap[this];
}
214 changes: 214 additions & 0 deletions lib/utils/message_container_authenticator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import 'package:dart_opendroneid/dart_opendroneid.dart';
import 'package:flutter_opendroneid/models/constants.dart';
import 'package:flutter_opendroneid/models/message_container_authenticity_status.dart';
import 'package:flutter_opendroneid/models/message_container.dart';

class MessageContainerAuthenticator {
/// Detectors that contribute to final score.
static final detectors = [
MacAddressSpooferDetector(),
TimestampSpooferDetector(),
AuthDataSpooferDetector(),
BasicIdSpooferDetector(),
OperatorIdSpooferDetector(),
LocationSpooferDetector(),
SelfIdSpooferDetector(),
SystemDataSpooferDetector(),
];

/// Check contents of message container using [detectors].
/// Test known spoofer data traits, each detector returns probability that
/// data were spoofed. Sum up the probablities and make final decision.
/// Convert score into [MessageContainerAuthenticityStatus] according to inverval:
/// < max/2 - untrusted
/// max/2, max/4*3 - suspicious
/// > max/4*3 - counterfeit.
static MessageContainerAuthenticityStatus determineAuthenticityStatus(
MessageContainer container) {
var score = 0.0;

detectors.forEach((element) {
score += element.calculateSpoofedProbability(container);
});

return _scoreToStatus(score);
}

static 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
final noSuspisionScore = maxScore * 0.5;
final counterfeitScore = maxScore * 0.75;

if (score <= noSuspisionScore)
return MessageContainerAuthenticityStatus.untrusted;
if (score <= counterfeitScore)
return MessageContainerAuthenticityStatus.suspicious;
return MessageContainerAuthenticityStatus.counterfeit;
}
}

abstract class SpooferDetector {
const SpooferDetector();

/// Return a probability that data are spoofed.
/// 0 -> data are real
/// 0.5 -> cannot decide
/// 1 -> data are counterfeit
double calculateSpoofedProbability(MessageContainer container);
}

class MacAddressSpooferDetector implements SpooferDetector {
// spoofed data MAC addr always starts with a zero
// if starts with 0, still doesn't have to be spoofed
@override
double calculateSpoofedProbability(MessageContainer container) =>
container.macAddress.startsWith('0') ? 0.75 : 0;
}

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

bool _isNearSpooferTimestamp(DateTime timestamp) =>
timestamp.isAfter(spooferTimestamp) &&
timestamp.isBefore(spooferTimestamp.add(maxUptime));

// returns true if any of timestamps within the interval
// (spooferTimestamp, spooferTimestamp + maxUptime).
@override
double calculateSpoofedProbability(MessageContainer container) {
final authTimestamp = container.authenticationMessage?.timestamp;
final systemTimestamp = container.systemDataMessage?.timestamp;

if (authTimestamp == null && systemTimestamp == null) return 0.5;

return (authTimestamp != null && _isNearSpooferTimestamp(authTimestamp)) ||
systemTimestamp != null && _isNearSpooferTimestamp(systemTimestamp)
? 1
: 0;
}
}

/// Following detectors check the contents of messages and compare them with
/// known values for spoofed data.
class AuthDataSpooferDetector implements SpooferDetector {
@override
double calculateSpoofedProbability(MessageContainer container) {
final message = container.authenticationMessage;
// message not received, cannot decide
if (message == null) return 0.5;

// if len and type are not as expected, data are not spoofed
if (message.authData.authData.length != maxAuthDataPages ||
message.authType != AuthType.none) return 0.1;

// spoofed auth data contain indexes
for (var index = 0; index < message.authData.authData.length; index++) {
if (message.authData.authData[index] != index) return 0.2;
}
return 0.8;
}
}

class BasicIdSpooferDetector implements SpooferDetector {
@override
double calculateSpoofedProbability(MessageContainer container) {
final messages = container.basicIdMessages;

if (messages == null || messages.isEmpty) return 0.5;

// spofed data always have 1 basic id with types set to .none
return (messages.length == 1 &&
messages.values.first.uaType == UAType.none &&
messages.values.first.uasID.type == IDType.none)
? 0.8
: 0.2;
}
}

class OperatorIdSpooferDetector implements SpooferDetector {
@override
double calculateSpoofedProbability(MessageContainer container) {
final message = container.operatorIdMessage;
// message not received, cannot decide
if (message == null) return 0.5;

// spofed DATA have 16 random chars in Operator IDm
// [OperatorIDTypeOperatorID] type and len 16
if (message.operatorIDType is! OperatorIDTypeOperatorID ||
message.operatorID.length != 16) return 0.3;
// check that country code cantains only capital letters
// if not, data are spoofed
final countryCode = message.operatorID.substring(0, 3);
if (countryCode != countryCode.toUpperCase()) return 0.9;
// country code contains capital letters,
// data can still be spoofed
return 0.4;
}
}

class LocationSpooferDetector implements SpooferDetector {
@override
double calculateSpoofedProbability(MessageContainer container) {
final message = container.locationMessage;
// message not received, cannot decide
if (message == null) return 0.5;

// spoofed if all values are set as expected
final spoofed = message.status == OperationalStatus.none &&
(message.verticalSpeed == null ||
message.verticalSpeed == INV_SPEED_V) &&
message.heightType == HeightType.aboveTakeoff &&
message.horizontalAccuracy == HorizontalAccuracy.meters_10 &&
message.verticalAccuracy == VerticalAccuracy.meters_10 &&
message.baroAltitudeAccuracy == VerticalAccuracy.meters_10 &&
message.speedAccuracy == SpeedAccuracy.meterPerSecond_10 &&
// spoofed data have acc 1.5
message.timestampAccuracy?.compareTo(Duration(milliseconds: 1500)) == 0;
return spoofed ? 1 : 0.2;
}
}

class SelfIdSpooferDetector implements SpooferDetector {
@override
double calculateSpoofedProbability(MessageContainer container) {
final message = container.selfIdMessage;
// message not received, cannot decide
if (message == null) return 0.5;

// spoofed if all values are set as expected
return message.descriptionType is DescriptionTypeText &&
message.description == "Recreational"
? 1
: 0.2;
}
}

class SystemDataSpooferDetector implements SpooferDetector {
@override
double calculateSpoofedProbability(MessageContainer container) {
final message = container.systemDataMessage;
// message not received, cannot decide
if (message == null) return 0.5;

// spoofed if all values are set as expected
final spoofed = message.operatorLocationType ==
OperatorLocationType.takeOff &&
message.uaClassification is UAClassificationEurope &&
(message.uaClassification as UAClassificationEurope).uaCategoryEurope ==
UACategoryEurope.EUOpen &&
(message.uaClassification as UAClassificationEurope).uaClassEurope ==
UAClassEurope.EUClass_4 &&
message.areaCount == 1 &&
message.areaRadius == 500 &&
message.areaCeiling == null &&
message.areaFloor == null;
return spoofed ? 1 : 0.2;
}
}
Loading