mirror of
https://github.com/seemoo-lab/openhaystack.git
synced 2026-05-20 07:22:45 +00:00
198 lines
6.5 KiB
Swift
Executable File
198 lines
6.5 KiB
Swift
Executable File
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||
//
|
||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||
// Copyright © 2021 The Open Wireless Link Project
|
||
//
|
||
// SPDX-License-Identifier: AGPL-3.0-only
|
||
|
||
// swiftlint:disable identifier_name
|
||
|
||
import Foundation
|
||
import CoreLocation
|
||
|
||
struct FindMyDevice: Codable, Hashable {
|
||
|
||
let deviceId: String
|
||
var keys = [FindMyKey]()
|
||
|
||
var catalinaBigSurKeyFiles: [Data]?
|
||
|
||
/// KeyHash: Report results
|
||
var reports: [FindMyReport]?
|
||
|
||
var decryptedReports: [FindMyLocationReport]?
|
||
|
||
func hash(into hasher: inout Hasher) {
|
||
hasher.combine(deviceId)
|
||
}
|
||
|
||
static func == (lhs: FindMyDevice, rhs: FindMyDevice) -> Bool {
|
||
lhs.deviceId == rhs.deviceId
|
||
}
|
||
}
|
||
|
||
struct FindMyKey: Codable {
|
||
internal init(advertisedKey: Data, hashedKey: Data, privateKey: Data, startTime: Date?, duration: Double?, pu: Data?, yCoordinate: Data?, fullKey: Data?) {
|
||
self.advertisedKey = advertisedKey
|
||
self.hashedKey = hashedKey
|
||
// The private key should only be 28 bytes long. If a 85 bytes full private public key is entered we truncate it here
|
||
if privateKey.count == 85 {
|
||
self.privateKey = privateKey.subdata(in: 57..<privateKey.endIndex)
|
||
} else {
|
||
self.privateKey = privateKey
|
||
}
|
||
|
||
self.startTime = startTime
|
||
self.duration = duration
|
||
self.pu = pu
|
||
self.yCoordinate = yCoordinate
|
||
self.fullKey = fullKey
|
||
}
|
||
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
self.advertisedKey = try container.decode(Data.self, forKey: .advertisedKey)
|
||
self.hashedKey = try container.decode(Data.self, forKey: .hashedKey)
|
||
let privateKey = try container.decode(Data.self, forKey: .privateKey)
|
||
if privateKey.count == 85 {
|
||
self.privateKey = privateKey.subdata(in: 57..<privateKey.endIndex)
|
||
} else {
|
||
self.privateKey = privateKey
|
||
}
|
||
|
||
self.startTime = try? container.decode(Date.self, forKey: .startTime)
|
||
self.duration = try? container.decode(Double.self, forKey: .duration)
|
||
self.pu = try? container.decode(Data.self, forKey: .pu)
|
||
self.yCoordinate = try? container.decode(Data.self, forKey: .yCoordinate)
|
||
self.fullKey = try? container.decode(Data.self, forKey: .fullKey)
|
||
}
|
||
|
||
/// The advertising key
|
||
let advertisedKey: Data
|
||
/// Hashed advertisement key using SHA256
|
||
let hashedKey: Data
|
||
/// The private key from which the advertisement keys can be derived
|
||
let privateKey: Data
|
||
/// When this key was used to send out BLE advertisements
|
||
let startTime: Date?
|
||
/// Duration from start time how long the key has been used to send out BLE advertisements
|
||
let duration: Double?
|
||
/// ?
|
||
let pu: Data?
|
||
|
||
/// As exported from Big Sur
|
||
let yCoordinate: Data?
|
||
/// As exported from BigSur
|
||
let fullKey: Data?
|
||
}
|
||
|
||
struct FindMyReportResults: Codable {
|
||
let results: [FindMyReport]
|
||
}
|
||
|
||
struct FindMyReport: Codable {
|
||
let datePublished: Date
|
||
let payload: Data
|
||
let id: String
|
||
let statusCode: Int
|
||
|
||
let confidence: UInt8
|
||
let timestamp: Date
|
||
|
||
enum CodingKeys: CodingKey {
|
||
case datePublished
|
||
case payload
|
||
case id
|
||
case statusCode
|
||
}
|
||
|
||
init(from decoder: Decoder) throws {
|
||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||
let dateTimestamp = try values.decode(Double.self, forKey: .datePublished)
|
||
// Convert from milis to time interval
|
||
let dP = Date(timeIntervalSince1970: dateTimestamp/1000)
|
||
let df = DateFormatter()
|
||
df.dateFormat = "YYYY-MM-dd"
|
||
|
||
if dP < df.date(from: "2020-01-01")! {
|
||
self.datePublished = Date(timeIntervalSince1970: dateTimestamp)
|
||
} else {
|
||
self.datePublished = dP
|
||
}
|
||
|
||
self.statusCode = try values.decode(Int.self, forKey: .statusCode)
|
||
let payloadBase64 = try values.decode(String.self, forKey: .payload)
|
||
|
||
guard let payload = Data(base64Encoded: payloadBase64) else {
|
||
throw DecodingError.dataCorruptedError(forKey: CodingKeys.payload, in: values, debugDescription: "")
|
||
}
|
||
self.payload = payload
|
||
|
||
var timestampData = payload.subdata(in: 0..<4)
|
||
let timestamp: Int32 = withUnsafeBytes(of: ×tampData) { (pointer) -> Int32 in
|
||
// Convert the endianness
|
||
pointer.load(as: Int32.self).bigEndian
|
||
}
|
||
|
||
// It's a cocoa time stamp (counting from 2001)
|
||
self.timestamp = Date(timeIntervalSinceReferenceDate: TimeInterval(timestamp))
|
||
self.confidence = payload[4]
|
||
|
||
self.id = try values.decode(String.self, forKey: .id)
|
||
}
|
||
|
||
func encode(to encoder: Encoder) throws {
|
||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||
try container.encode(self.datePublished.timeIntervalSince1970 * 1000, forKey: .datePublished)
|
||
try container.encode(self.payload.base64EncodedString(), forKey: .payload)
|
||
try container.encode(self.id, forKey: .id)
|
||
try container.encode(self.statusCode, forKey: .statusCode)
|
||
}
|
||
}
|
||
|
||
struct FindMyLocationReport: Codable {
|
||
let latitude: Double
|
||
let longitude: Double
|
||
let accuracy: UInt8
|
||
let datePublished: Date
|
||
let timestamp: Date?
|
||
let confidence: UInt8?
|
||
|
||
var location: CLLocation {
|
||
return CLLocation(latitude: latitude, longitude: longitude)
|
||
}
|
||
|
||
init(lat: Double, lng: Double, acc: UInt8, dP: Date, t: Date, c: UInt8) {
|
||
self.latitude = lat
|
||
self.longitude = lng
|
||
self.accuracy = acc
|
||
self.datePublished = dP
|
||
self.timestamp = t
|
||
self.confidence = c
|
||
}
|
||
|
||
init(from decoder: Decoder) throws {
|
||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||
|
||
self.latitude = try values.decode(Double.self, forKey: .latitude)
|
||
self.longitude = try values.decode(Double.self, forKey: .longitude)
|
||
|
||
do {
|
||
let uAcc = try values.decode(UInt8.self, forKey: .accuracy)
|
||
self.accuracy = uAcc
|
||
} catch {
|
||
let iAcc = try values.decode(Int8.self, forKey: .accuracy)
|
||
self.accuracy = UInt8(bitPattern: iAcc)
|
||
}
|
||
|
||
self.datePublished = try values.decode(Date.self, forKey: .datePublished)
|
||
self.timestamp = try? values.decode(Date.self, forKey: .timestamp)
|
||
self.confidence = try? values.decode(UInt8.self, forKey: .confidence)
|
||
}
|
||
|
||
}
|
||
|
||
enum FindMyError: Error {
|
||
case decryptionError(description: String)
|
||
}
|