mirror of
https://github.com/seemoo-lab/openhaystack.git
synced 2026-08-23 21:46:15 +00:00
Adding OpenHaystack Mobile app
Co-Authored-By: Lukas Burg <lukas.burg@hemalu.de>
This commit is contained in:
committed by
Alexander Heinrich
co-authored by
Lukas Burg
parent
b65a6e6be0
commit
3d593a006c
@@ -0,0 +1,115 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:pointycastle/export.dart';
|
||||
import 'package:pointycastle/src/utils.dart' as pc_utils;
|
||||
import 'package:openhaystack_mobile/findMy/models.dart';
|
||||
|
||||
class DecryptReports {
|
||||
/// Decrypts a given [FindMyReport] with the given private key.
|
||||
static Future<FindMyLocationReport> decryptReport(
|
||||
FindMyReport report, Uint8List key) async {
|
||||
final curveDomainParam = ECCurve_secp224r1();
|
||||
|
||||
final payloadData = report.payload;
|
||||
final ephemeralKeyBytes = payloadData.sublist(5, 62);
|
||||
final encData = payloadData.sublist(62, 72);
|
||||
final tag = payloadData.sublist(72, payloadData.length);
|
||||
|
||||
_decodeTimeAndConfidence(payloadData, report);
|
||||
|
||||
final privateKey = ECPrivateKey(
|
||||
pc_utils.decodeBigIntWithSign(1, key),
|
||||
curveDomainParam);
|
||||
|
||||
final decodePoint = curveDomainParam.curve.decodePoint(ephemeralKeyBytes);
|
||||
final ephemeralPublicKey = ECPublicKey(decodePoint, curveDomainParam);
|
||||
|
||||
final Uint8List sharedKeyBytes = _ecdh(ephemeralPublicKey, privateKey);
|
||||
final Uint8List derivedKey = _kdf(sharedKeyBytes, ephemeralKeyBytes);
|
||||
|
||||
final decryptedPayload = _decryptPayload(encData, derivedKey, tag);
|
||||
final locationReport = _decodePayload(decryptedPayload, report);
|
||||
|
||||
return locationReport;
|
||||
}
|
||||
|
||||
/// Decodes the unencrypted timestamp and confidence
|
||||
static void _decodeTimeAndConfidence(Uint8List payloadData, FindMyReport report) {
|
||||
final seenTimeStamp = payloadData.sublist(0, 4).buffer.asByteData()
|
||||
.getInt32(0, Endian.big);
|
||||
final timestamp = DateTime(2001).add(Duration(seconds: seenTimeStamp));
|
||||
final confidence = payloadData.elementAt(4);
|
||||
report.timestamp = timestamp;
|
||||
report.confidence = confidence;
|
||||
}
|
||||
|
||||
/// Performs an Elliptic Curve Diffie-Hellman with the given keys.
|
||||
/// Returns the derived raw key data.
|
||||
static Uint8List _ecdh(ECPublicKey ephemeralPublicKey, ECPrivateKey privateKey) {
|
||||
final sharedKey = ephemeralPublicKey.Q! * privateKey.d;
|
||||
final sharedKeyBytes = pc_utils.encodeBigIntAsUnsigned(
|
||||
sharedKey!.x!.toBigInteger()!);
|
||||
print("Isolate:${Isolate.current.hashCode}: Shared Key (shared secret): ${base64Encode(sharedKeyBytes)}");
|
||||
|
||||
return sharedKeyBytes;
|
||||
}
|
||||
|
||||
/// Decodes the raw decrypted payload and constructs and returns
|
||||
/// the resulting [FindMyLocationReport].
|
||||
static FindMyLocationReport _decodePayload(
|
||||
Uint8List payload, FindMyReport report) {
|
||||
|
||||
final latitude = payload.buffer.asByteData(0, 4).getUint32(0, Endian.big);
|
||||
final longitude = payload.buffer.asByteData(4, 4).getUint32(0, Endian.big);
|
||||
final accuracy = payload.buffer.asByteData(8, 1).getUint8(0);
|
||||
|
||||
final latitudeDec = latitude / 10000000.0;
|
||||
final longitudeDec = longitude / 10000000.0;
|
||||
|
||||
return FindMyLocationReport(latitudeDec, longitudeDec, accuracy,
|
||||
report.datePublished, report.timestamp, report.confidence);
|
||||
}
|
||||
|
||||
/// Decrypts the given cipher text with the key data using an AES-GCM block cipher.
|
||||
/// Returns the decrypted raw data.
|
||||
static Uint8List _decryptPayload(
|
||||
Uint8List cipherText, Uint8List symmetricKey, Uint8List tag) {
|
||||
final decryptionKey = symmetricKey.sublist(0, 16);
|
||||
final iv = symmetricKey.sublist(16, symmetricKey.length);
|
||||
|
||||
final aesGcm = GCMBlockCipher(AESEngine())
|
||||
..init(false, AEADParameters(KeyParameter(decryptionKey),
|
||||
tag.lengthInBytes * 8, iv, tag));
|
||||
|
||||
final plainText = Uint8List(cipherText.length);
|
||||
var offset = 0;
|
||||
while (offset < cipherText.length) {
|
||||
offset += aesGcm.processBlock(cipherText, offset, plainText, offset);
|
||||
}
|
||||
|
||||
assert(offset == cipherText.length);
|
||||
return plainText;
|
||||
}
|
||||
|
||||
/// ANSI X.963 key derivation to calculate the actual (symmetric) advertisement
|
||||
/// key and returns the raw key data.
|
||||
static Uint8List _kdf(Uint8List secret, Uint8List ephemeralKey) {
|
||||
var shaDigest = SHA256Digest();
|
||||
shaDigest.update(secret, 0, secret.length);
|
||||
|
||||
var counter = 1;
|
||||
var counterData = ByteData(4)..setUint32(0, counter);
|
||||
var counterDataBytes = counterData.buffer.asUint8List();
|
||||
shaDigest.update(counterDataBytes, 0, counterDataBytes.lengthInBytes);
|
||||
|
||||
shaDigest.update(ephemeralKey, 0, ephemeralKey.lengthInBytes);
|
||||
|
||||
Uint8List out = Uint8List(shaDigest.digestSize);
|
||||
shaDigest.doFinal(out, 0);
|
||||
|
||||
print("Isolate:${Isolate.current.hashCode}: Derived key: ${base64Encode(out)}");
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:pointycastle/export.dart';
|
||||
import 'package:pointycastle/src/platform_check/platform_check.dart';
|
||||
import 'package:pointycastle/src/utils.dart' as pc_utils;
|
||||
import 'package:openhaystack_mobile/findMy/decrypt_reports.dart';
|
||||
import 'package:openhaystack_mobile/findMy/models.dart';
|
||||
import 'package:openhaystack_mobile/findMy/reports_fetcher.dart';
|
||||
|
||||
class FindMyController {
|
||||
static const _storage = FlutterSecureStorage();
|
||||
static final ECCurve_secp224r1 _curveParams = ECCurve_secp224r1();
|
||||
static HashMap _keyCache = HashMap();
|
||||
|
||||
/// Starts a new [Isolate], fetches and decrypts all location reports
|
||||
/// for the given [FindMyKeyPair].
|
||||
/// Returns a list of [FindMyLocationReport]'s.
|
||||
static Future<List<FindMyLocationReport>> computeResults(FindMyKeyPair keyPair) async{
|
||||
await _loadPrivateKey(keyPair);
|
||||
return compute(_getListedReportResults, keyPair);
|
||||
}
|
||||
|
||||
/// Fetches and decrypts the location reports for the given
|
||||
/// [FindMyKeyPair] from apples FindMy Network.
|
||||
/// Returns a list of [FindMyLocationReport].
|
||||
static Future<List<FindMyLocationReport>> _getListedReportResults(FindMyKeyPair keyPair) async{
|
||||
List<FindMyLocationReport> results = <FindMyLocationReport>[];
|
||||
final jsonResults = await ReportsFetcher.fetchLocationReports(keyPair.getHashedAdvertisementKey());
|
||||
for (var result in jsonResults) {
|
||||
results.add(await _decryptResult(result, keyPair, keyPair.privateKeyBase64!));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/// Loads the private key from the local cache or secure storage and adds it
|
||||
/// to the given [FindMyKeyPair].
|
||||
static Future<void> _loadPrivateKey(FindMyKeyPair keyPair) async {
|
||||
String? privateKey;
|
||||
if (!_keyCache.containsKey(keyPair.hashedPublicKey)) {
|
||||
privateKey = await _storage.read(key: keyPair.hashedPublicKey);
|
||||
final newKey = _keyCache.putIfAbsent(keyPair.hashedPublicKey, () => privateKey);
|
||||
assert(newKey == privateKey);
|
||||
} else {
|
||||
privateKey = _keyCache[keyPair.hashedPublicKey];
|
||||
}
|
||||
keyPair.privateKeyBase64 = privateKey!;
|
||||
}
|
||||
|
||||
/// Derives an [ECPublicKey] from a given [ECPrivateKey] on the given curve.
|
||||
static ECPublicKey _derivePublicKey(ECPrivateKey privateKey) {
|
||||
final pk = _curveParams.G * privateKey.d;
|
||||
final publicKey = ECPublicKey(pk, _curveParams);
|
||||
print("Isolate:${Isolate.current.hashCode}: Point Data: ${base64Encode(publicKey.Q!.getEncoded(false))}");
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
/// Decrypts the encrypted reports with the given [FindMyKeyPair] and private key.
|
||||
/// Returns the decrypted report as a [FindMyLocationReport].
|
||||
static Future<FindMyLocationReport> _decryptResult(dynamic result, FindMyKeyPair keyPair, String privateKey) async {
|
||||
assert (result["id"]! == keyPair.getHashedAdvertisementKey(),
|
||||
"Returned FindMyReport hashed key != requested hashed key");
|
||||
|
||||
final unixTimestampInMillis = result["datePublished"];
|
||||
final datePublished = DateTime.fromMillisecondsSinceEpoch(unixTimestampInMillis);
|
||||
FindMyReport report = FindMyReport(
|
||||
datePublished,
|
||||
base64Decode(result["payload"]),
|
||||
keyPair.getHashedAdvertisementKey(),
|
||||
result["statusCode"]);
|
||||
|
||||
FindMyLocationReport decryptedReport = await DecryptReports
|
||||
.decryptReport(report, base64Decode(privateKey));
|
||||
|
||||
return decryptedReport;
|
||||
}
|
||||
|
||||
/// Returns the to the base64 encoded given hashed public key
|
||||
/// corresponding [FindMyKeyPair] from the local [FlutterSecureStorage].
|
||||
static Future<FindMyKeyPair> getKeyPair(String base64HashedPublicKey) async {
|
||||
final privateKeyBase64 = await _storage.read(key: base64HashedPublicKey);
|
||||
|
||||
ECPrivateKey privateKey = ECPrivateKey(
|
||||
pc_utils.decodeBigIntWithSign(1, base64Decode(privateKeyBase64!)), _curveParams);
|
||||
ECPublicKey publicKey = _derivePublicKey(privateKey);
|
||||
|
||||
return FindMyKeyPair(publicKey, base64HashedPublicKey, privateKey, DateTime.now(), -1);
|
||||
}
|
||||
|
||||
/// Imports a base64 encoded private key to the local [FlutterSecureStorage].
|
||||
/// Returns a [FindMyKeyPair] containing the corresponding [ECPublicKey].
|
||||
static Future<FindMyKeyPair> importKeyPair(String privateKeyBase64) async {
|
||||
final privateKeyBytes = base64Decode(privateKeyBase64);
|
||||
final ECPrivateKey privateKey = ECPrivateKey(
|
||||
pc_utils.decodeBigIntWithSign(1, privateKeyBytes), _curveParams);
|
||||
final ECPublicKey publicKey = _derivePublicKey(privateKey);
|
||||
final hashedPublicKey = getHashedPublicKey(publicKey: publicKey);
|
||||
final keyPair = FindMyKeyPair(
|
||||
publicKey,
|
||||
hashedPublicKey,
|
||||
privateKey,
|
||||
DateTime.now(),
|
||||
-1);
|
||||
|
||||
await _storage.write(key: hashedPublicKey, value: keyPair.getBase64PrivateKey());
|
||||
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
/// Generates a [ECCurve_secp224r1] keypair.
|
||||
/// Returns the newly generated keypair as a [FindMyKeyPair] object.
|
||||
static Future<FindMyKeyPair> generateKeyPair() async {
|
||||
final ecCurve = ECCurve_secp224r1();
|
||||
final secureRandom = SecureRandom('Fortuna')
|
||||
..seed(KeyParameter(
|
||||
Platform.instance.platformEntropySource().getBytes(32)));
|
||||
ECKeyGenerator keyGen = ECKeyGenerator()
|
||||
..init(ParametersWithRandom(ECKeyGeneratorParameters(ecCurve), secureRandom));
|
||||
|
||||
final newKeyPair = keyGen.generateKeyPair();
|
||||
final ECPublicKey publicKey = newKeyPair.publicKey as ECPublicKey;
|
||||
final ECPrivateKey privateKey = newKeyPair.privateKey as ECPrivateKey;
|
||||
final hashedKey = getHashedPublicKey(publicKey: publicKey);
|
||||
final keyPair = FindMyKeyPair(publicKey, hashedKey, privateKey, DateTime.now(), -1);
|
||||
await _storage.write(key: hashedKey, value: keyPair.getBase64PrivateKey());
|
||||
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
/// Returns hashed, base64 encoded public key for given [publicKeyBytes]
|
||||
/// or for an [ECPublicKey] object [publicKey], if [publicKeyBytes] equals null.
|
||||
/// Returns the base64 encoded hashed public key as a [String].
|
||||
static String getHashedPublicKey({Uint8List? publicKeyBytes, ECPublicKey? publicKey}) {
|
||||
var pkBytes = publicKeyBytes ?? publicKey!.Q!.getEncoded(false);
|
||||
final shaDigest = SHA256Digest();
|
||||
shaDigest.update(pkBytes, 0, pkBytes.lengthInBytes);
|
||||
Uint8List out = Uint8List(shaDigest.digestSize);
|
||||
shaDigest.doFinal(out, 0);
|
||||
return base64Encode(out);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:pointycastle/ecc/api.dart';
|
||||
import 'package:pointycastle/src/utils.dart' as pc_utils;
|
||||
import 'package:openhaystack_mobile/findMy/find_my_controller.dart';
|
||||
|
||||
/// Represents a decrypted FindMyReport.
|
||||
class FindMyLocationReport {
|
||||
double latitude;
|
||||
double longitude;
|
||||
int accuracy;
|
||||
DateTime published;
|
||||
DateTime? timestamp;
|
||||
int? confidence;
|
||||
|
||||
FindMyLocationReport(this.latitude, this.longitude, this.accuracy,
|
||||
this.published, this.timestamp, this.confidence);
|
||||
|
||||
Location get location => Location(latitude, longitude);
|
||||
}
|
||||
|
||||
class Location {
|
||||
double latitude;
|
||||
double longitude;
|
||||
|
||||
Location(this.latitude, this.longitude);
|
||||
}
|
||||
|
||||
/// FindMy report returned by the FindMy Network
|
||||
class FindMyReport {
|
||||
DateTime datePublished;
|
||||
Uint8List payload;
|
||||
String id;
|
||||
int statusCode;
|
||||
|
||||
int? confidence;
|
||||
DateTime? timestamp;
|
||||
|
||||
FindMyReport(this.datePublished, this.payload, this.id, this.statusCode);
|
||||
|
||||
FindMyReport.completeInit(this.datePublished, this.payload, this.id, this.statusCode,
|
||||
this.confidence, this.timestamp);
|
||||
|
||||
}
|
||||
|
||||
class FindMyKeyPair {
|
||||
final ECPublicKey _publicKey;
|
||||
final ECPrivateKey _privateKey;
|
||||
final String hashedPublicKey;
|
||||
String? privateKeyBase64;
|
||||
|
||||
/// Time when this key was used to send BLE advertisements
|
||||
DateTime startTime;
|
||||
/// Duration from start time how long the key was used to send BLE advertisements
|
||||
double duration;
|
||||
|
||||
FindMyKeyPair(this._publicKey, this.hashedPublicKey, this._privateKey, this.startTime,
|
||||
this.duration);
|
||||
|
||||
String getBase64PublicKey() {
|
||||
return base64Encode(_publicKey.Q!.getEncoded(false));
|
||||
}
|
||||
|
||||
String getBase64PrivateKey() {
|
||||
return base64Encode(pc_utils.encodeBigIntAsUnsigned(_privateKey.d!));
|
||||
}
|
||||
|
||||
String getBase64AdvertisementKey() {
|
||||
return base64Encode(_getAdvertisementKey());
|
||||
}
|
||||
|
||||
Uint8List _getAdvertisementKey() {
|
||||
var pkBytes = _publicKey.Q!.getEncoded(true);
|
||||
//Drop first byte to get the 28byte version
|
||||
var key = pkBytes.sublist(1, pkBytes.length);
|
||||
return key;
|
||||
}
|
||||
|
||||
String getHashedAdvertisementKey() {
|
||||
var key = _getAdvertisementKey();
|
||||
return FindMyController.getHashedPublicKey(publicKeyBytes: key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class ReportsFetcher {
|
||||
static const _seemooEndpoint = "https://add-your-proxy-server-here/getLocationReports"
|
||||
|
||||
/// Fetches the location reports corresponding to the given hashed advertisement
|
||||
/// key.
|
||||
/// Throws [Exception] if no answer was received.
|
||||
static Future<List> fetchLocationReports(String hashedAdvertisementKey) async {
|
||||
final response = await http.post(Uri.parse(_seemooEndpoint),
|
||||
headers: <String, String>{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: jsonEncode(<String, dynamic>{
|
||||
"ids": [hashedAdvertisementKey],
|
||||
}));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return await jsonDecode(response.body)["results"];
|
||||
} else {
|
||||
throw Exception("Failed to fetch location reports with statusCode:${response.statusCode}\n\n Response:\n${response}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user