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
15 changes: 11 additions & 4 deletions MeshCore/Sources/MeshCore/Session/MeshCoreSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1331,12 +1331,19 @@ public actor MeshCoreSession: MeshCoreSessionProtocol {
/// This is a sensitive operation that exposes the device's cryptographic identity.
/// The exported key can be imported into another device to clone its identity.
///
/// - Returns: The 32-byte private key, or `nil` if export is disabled.
/// - Throws: ``MeshCoreError/timeout`` if the device doesn't respond.
/// - Returns: The 32-byte private key.
/// - Throws: ``MeshCoreError/featureDisabled`` if private key export is disabled on the device,
/// or ``MeshCoreError/timeout`` if the device doesn't respond.
public func exportPrivateKey() async throws -> Data {
try await sendAndWait(PacketBuilder.exportPrivateKey()) { event in
try await sendAndWaitWithError(
PacketBuilder.exportPrivateKey()
) { event in
if case .privateKey(let key) = event { return key }
if case .disabled = event { return nil }
return nil
} errorMatcher: { event in
if case .disabled = event {
return MeshCoreError.featureDisabled
}
return nil
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,64 @@ struct MeshCoreSessionCommandCorrelationTests {
await session.stop()
}

@Test("exportPrivateKey throws featureDisabled on disabled response")
func exportPrivateKeyThrowsFeatureDisabledOnDisabledResponse() async throws {
let transport = MockTransport()
let session = MeshCoreSession(
transport: transport,
configuration: SessionConfiguration(defaultTimeout: 0.2, clientIdentifier: "MCTst")
)

try await startSession(session, transport: transport)

let exportTask = Task {
try await session.exportPrivateKey()
}

try await waitUntil("exportPrivateKey should be sent") {
await transport.sentData.count == 2
}

await transport.simulateReceive(Data([ResponseCode.disabled.rawValue]))

let error = await #expect(throws: MeshCoreError.self) {
try await exportTask.value
}
guard case .featureDisabled? = error else {
Issue.record("Expected featureDisabled, got \(String(describing: error))")
await session.stop()
return
}

await session.stop()
}

@Test("disabled responses do not break unrelated requests")
func disabledResponsesDoNotBreakUnrelatedRequests() async throws {
let transport = MockTransport()
let session = MeshCoreSession(
transport: transport,
configuration: SessionConfiguration(defaultTimeout: 0.2, clientIdentifier: "MCTst")
)

try await startSession(session, transport: transport)

let batteryTask = Task {
try await session.getBattery()
}

try await waitUntil("getBattery should be sent") {
await transport.sentData.count == 2
}

await transport.simulateReceive(Data([ResponseCode.disabled.rawValue]))
await transport.simulateReceive(makeBatteryPacket(level: 4018))

let battery = try await batteryTask.value
#expect(battery.level == 4018)
await session.stop()
}

@Test("requestStatus fails fast on device error before messageSent")
func requestStatusFailsFastOnDeviceErrorBeforeMessageSent() async throws {
let transport = MockTransport()
Expand Down