Adding OpenHaystack Mobile app

Co-Authored-By: Lukas Burg <lukas.burg@hemalu.de>
This commit is contained in:
MaxGranzow
2022-05-12 15:28:06 +02:00
committed by Alexander Heinrich
co-authored by Lukas Burg
parent b65a6e6be0
commit 3d593a006c
182 changed files with 10499 additions and 0 deletions
@@ -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}");
}
}
}