Skip to content
Merged
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
4 changes: 4 additions & 0 deletions LibreLoop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
A2BF7E31EE7A24E1130871FE /* LibreLoop.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B556C4EB4504CA4CFCF51EA2 /* LibreLoop.framework */; };
AE16E0F3A62344D974DF129C /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2509954985674E6150F6C160 /* LoopKit.framework */; };
A1B2C3D4E5F6A7B8C9D0E102 /* LibreLoopRuntimeTables.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E101 /* LibreLoopRuntimeTables.swift */; };
5A1B3C5D7E9F0A2B4C6D8E02 /* LibreLoopSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1B3C5D7E9F0A2B4C6D8E01 /* LibreLoopSimulator.swift */; };
B04E3C5ED32E80EEC213D178 /* LibreLoopPairingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693CAA1DD7F8B8662B3ED42B /* LibreLoopPairingService.swift */; };
B793A348DB4300FC386AF780 /* LibreLoopCGMManagerStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E9D5D87695F004FCFDA5075 /* LibreLoopCGMManagerStateTests.swift */; };
B87069E2100DD9DD3B3B897D /* LibreLoopPairingProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 659E4212E046544D5827E2D1 /* LibreLoopPairingProgressView.swift */; };
Expand Down Expand Up @@ -127,6 +128,7 @@
651E3C856170F6BD369EA0DD /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; path = README.md; sourceTree = "<group>"; };
659E4212E046544D5827E2D1 /* LibreLoopPairingProgressView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreLoopPairingProgressView.swift; sourceTree = "<group>"; };
A1B2C3D4E5F6A7B8C9D0E101 /* LibreLoopRuntimeTables.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreLoopRuntimeTables.swift; sourceTree = "<group>"; };
5A1B3C5D7E9F0A2B4C6D8E01 /* LibreLoopSimulator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreLoopSimulator.swift; sourceTree = "<group>"; };
693CAA1DD7F8B8662B3ED42B /* LibreLoopPairingService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreLoopPairingService.swift; sourceTree = "<group>"; };
6A64C620E258CF55CE0B0E11 /* LibreLoopCGMManager+Observers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "LibreLoopCGMManager+Observers.swift"; sourceTree = "<group>"; };
6D0763A875B13A9543390BC5 /* LibreLoopSensorLifecycle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreLoopSensorLifecycle.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -366,6 +368,7 @@
D912C6E4A9CF07D06EEF98BA /* LibreLoopKeychain.swift */,
693CAA1DD7F8B8662B3ED42B /* LibreLoopPairingService.swift */,
A1B2C3D4E5F6A7B8C9D0E101 /* LibreLoopRuntimeTables.swift */,
5A1B3C5D7E9F0A2B4C6D8E01 /* LibreLoopSimulator.swift */,
);
name = Pairing;
path = Pairing;
Expand Down Expand Up @@ -604,6 +607,7 @@
93AC995063491BFA4B6D22C3 /* LibreLoopKeychain.swift in Sources */,
B04E3C5ED32E80EEC213D178 /* LibreLoopPairingService.swift in Sources */,
A1B2C3D4E5F6A7B8C9D0E102 /* LibreLoopRuntimeTables.swift in Sources */,
5A1B3C5D7E9F0A2B4C6D8E02 /* LibreLoopSimulator.swift in Sources */,
C558A41FBB49E4CFE8C0CD6B /* LibreLoopGlucoseSample.swift in Sources */,
70BCAB4E71383256521B6B16 /* LibreLoopSensorLifecycle.swift in Sources */,
AAEC520180AAB1FC572D3EFE /* LibreLoopExpiryAlerts.swift in Sources */,
Expand Down
10 changes: 10 additions & 0 deletions LibreLoop/Pairing/LibreLoopPairingService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,11 @@ public final class LibreLoopPairingService {
// matches what the sensor remembers; pass-through here, the sensor
// validates server-side.
onStage(.nfcScanning)
#if targetEnvironment(simulator)
let nfcReader = LibreLoopSimulatedNFCReader()
#else
let nfcReader = Libre3NFCActivationReader()
#endif
let scanResult: Libre3NFCScanResult
let receiverID: UInt32
do {
Expand Down Expand Up @@ -719,10 +723,16 @@ public final class LibreLoopPairingService {
blePIN: Data
) async throws -> (FirstPairHandshakeResult, authKey: Data) {
let phoneEph = EphemeralKeyPair()
#if targetEnvironment(simulator)
let signingKeys = try await LibreLoopSimulator.patchSigningKeys()
#else
let signingKeys = Libre3PatchSigningKey.known
#endif
let flow = PairingFlow(
transport: SensorSessionTransport(session: session),
phoneCert: identity.certificate,
phoneEph: phoneEph,
sensorCertSigningKeys: signingKeys,
eventLogger: { message in llog(message) }
)
var authKey = Data()
Expand Down
5 changes: 5 additions & 0 deletions LibreLoop/Pairing/LibreLoopRuntimeTables.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public enum LibreLoopAppIdentities {
}

private static func install() async throws {
#if targetEnvironment(simulator)
try AppIdentities.install(json: await LibreLoopSimulator.appCredentials())
llog("app credentials: installed libre3sim's")
#else
if let saved = try? Data(contentsOf: fileURL) {
do {
try installBlob(saved)
Expand Down Expand Up @@ -59,6 +63,7 @@ public enum LibreLoopAppIdentities {
}
}
throw lastError
#endif
}

private static func installBlob(_ blob: Data) throws {
Expand Down
119 changes: 119 additions & 0 deletions LibreLoop/Pairing/LibreLoopSimulator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#if targetEnvironment(simulator)
import Foundation
import RoundWhiteDiscKit

/// The iOS Simulator has no NFC and no Libre 3, so pairing there talks to
/// libre3sim (https://github.com/LoopKit/libre3sim), which reaches the app's
/// Bluetooth through ImpossiBLE. Its HTTP API stands in for the sensor's NFC
/// tag and supplies the app credential and the key that signs its sensors'
/// certificates. Set LIBRE3SIM_URL to reach it somewhere other than port 8080.
enum LibreLoopSimulator {
struct Failure: LocalizedError {
let message: String
var errorDescription: String? { message }
}

static var baseURL: URL {
URL(string: ProcessInfo.processInfo.environment["LIBRE3SIM_URL"] ?? "http://127.0.0.1:8080")!
}

static func appCredentials() async throws -> Data {
try await get("app-credentials")
}

static func patchSigningKeys() async throws -> [Data] {
struct Key: Decodable { let publicKey: String }
let key = try JSONDecoder().decode(Key.self, from: try await get("patch-signing-key"))
guard let data = Data(simulatorHex: key.publicKey) else {
throw Failure(message: "libre3sim sent a malformed patch-signing key")
}
return [data]
}

/// An ISO 15693 custom command, answered as CoreNFC would return it.
static func nfc(code: UInt8, parameters: Data) async throws -> Data {
struct Reply: Decodable {
let response: String?
let error: String?
}
var request = URLRequest(url: baseURL.appendingPathComponent("nfc"))
request.httpMethod = "POST"
request.httpBody = try JSONSerialization.data(withJSONObject: [
"code": Int(code),
"parameters": parameters.map { String(format: "%02x", $0) }.joined(),
])
let reply = try JSONDecoder().decode(Reply.self, from: try await load(request))
guard let hex = reply.response, let data = Data(simulatorHex: hex) else {
throw Failure(message: reply.error ?? "libre3sim gave no NFC response")
}
return data
}

private static func get(_ path: String) async throws -> Data {
try await load(URLRequest(url: baseURL.appendingPathComponent(path)))
}

private static func load(_ request: URLRequest) async throws -> Data {
do {
let (data, _) = try await URLSession.shared.data(for: request)
return data
} catch {
throw Failure(message: "libre3sim is not running at \(baseURL.absoluteString): \(error.localizedDescription)")
}
}
}

/// Libre3NFCActivationReader's scan, with libre3sim as the tag.
final class LibreLoopSimulatedNFCReader {
func scan(mode: Libre3NFCScanMode) async throws -> Libre3NFCScanResult {
let patchInfo = try Libre3NFCPatchInfo(raw: await LibreLoopSimulator.nfc(code: 0xa1, parameters: Data()))
let commandCode: NFCActivationCommandCode
let receiverID: UInt32
let explicitTime: UInt32?
switch mode {
case .readPatchInfo:
return Libre3NFCScanResult(patchInfo: patchInfo)
case .activateFreshSensor(let id, let time):
guard patchInfo.recommendedCommandCode == .activate else {
throw Libre3NFCError.unexpectedSensorState(patchInfo: patchInfo)
}
(commandCode, receiverID, explicitTime) = (.activate, id, time)
case .switchReceiver(let id, let time):
guard patchInfo.recommendedCommandCode == .switchReceiver else {
throw Libre3NFCError.unexpectedSensorState(patchInfo: patchInfo)
}
(commandCode, receiverID, explicitTime) = (.switchReceiver, id, time)
case .activateOrSwitchReceiver(let id, let time):
(commandCode, receiverID, explicitTime) = (patchInfo.recommendedCommandCode, id, time)
case .forceActivationCommand(let code, let id, let time):
(commandCode, receiverID, explicitTime) = (code, id, time)
}
let parameters = NFCActivationCommand.customRequestParameters(
timeSeconds: explicitTime ?? UInt32(Date().timeIntervalSince1970) - 1,
receiverID: receiverID
)
let raw = try await LibreLoopSimulator.nfc(code: commandCode.rawValue, parameters: parameters)
return Libre3NFCScanResult(
patchInfo: patchInfo,
commandCode: commandCode,
commandParameters: parameters,
activationResponse: try Libre3NFCActivationResponse(raw: raw)
)
}
}

private extension Data {
init?(simulatorHex hex: String) {
guard hex.count % 2 == 0 else { return nil }
var bytes = [UInt8]()
var index = hex.startIndex
while index < hex.endIndex {
let next = hex.index(index, offsetBy: 2)
guard let byte = UInt8(hex[index..<next], radix: 16) else { return nil }
bytes.append(byte)
index = next
}
self.init(bytes)
}
}
#endif