Skip to content

Commit

Permalink
feat: implement filtering messages
Browse files Browse the repository at this point in the history
check if messages contain known data

DT-3038
  • Loading branch information
matejglejtek committed May 9, 2024
1 parent 845cac2 commit 9f4700f
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 1 deletion.
11 changes: 11 additions & 0 deletions lib/models/data_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 DataAuthenticityStatus {
// verified by authority/multilateration
verified,
// default
untrusted,
// signs if questionable authenticity exist
suspicious,
// high certainty of falsehood
counterfeit
}
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/data_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 DataAuthenticityStatus authenticityStatus;

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

MessageContainer copyWith({
Expand All @@ -44,6 +49,7 @@ class MessageContainer {
SelfIDMessage? selfIdMessage,
AuthMessage? authenticationMessage,
SystemMessage? systemDataMessage,
DataAuthenticityStatus? 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
13 changes: 13 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/data_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<DataAuthenticityStatus, String> _dataAuthenticityStatusConversionMap =
{
DataAuthenticityStatus.verified: 'Verified',
DataAuthenticityStatus.untrusted: 'Untrusted',
DataAuthenticityStatus.suspicious: 'Suspicious',
DataAuthenticityStatus.counterfeit: 'Counterfeit',
};

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

extension DataAuthenticityStatusConversion on DataAuthenticityStatus {
String? asString() => _dataAuthenticityStatusConversionMap[this];
}
211 changes: 211 additions & 0 deletions lib/utils/message_container_authenticator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import 'package:dart_opendroneid/dart_opendroneid.dart';
import 'package:flutter_opendroneid/models/constants.dart';
import 'package:flutter_opendroneid/models/data_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 [DataAuthenticityStatus] according to inverval:
/// < max/2 - untrusted
/// max/2, max/4*3 - suspicious
/// > max/4*3 - counterfeit.
static DataAuthenticityStatus determineAuthenticityStatus(
MessageContainer container) {
var score = 0.0;

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

return _scoreToStatus(score);
}

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

// if nothing can be decided, score is exactly half, use as limi
final noSuspisionScore = maxScore * 0.5;
final counterfeitScore = maxScore * 0.75;

if (score <= noSuspisionScore) return DataAuthenticityStatus.untrusted;
if (score <= counterfeitScore) return DataAuthenticityStatus.suspicious;
return DataAuthenticityStatus.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 does'nt 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;
}
}

0 comments on commit 9f4700f

Please sign in to comment.