From 7adaf16bedc2ecb824a918ef45e13cf2adc35cdc Mon Sep 17 00:00:00 2001 From: Stefan Mitterrutzner Date: Tue, 18 May 2021 09:57:05 +0200 Subject: [PATCH 1/2] adds methods to ask for user consent seperate from sending the keys to the backend --- Sources/DP3TSDK/DP3TSDK.swift | 157 +++++++++++++++---------- Sources/DP3TSDK/DP3TTracing.swift | 26 ++++ Sources/DP3TSDK/DP3TTracingState.swift | 9 ++ Tests/DP3TSDKTests/DP3TSDKTests.swift | 2 +- 4 files changed, 128 insertions(+), 66 deletions(-) diff --git a/Sources/DP3TSDK/DP3TSDK.swift b/Sources/DP3TSDK/DP3TSDK.swift index 37dfe312..ce1f9523 100644 --- a/Sources/DP3TSDK/DP3TSDK.swift +++ b/Sources/DP3TSDK/DP3TSDK.swift @@ -236,93 +236,120 @@ class DP3TSDK { } } + /// Request the user permission to obtain all TEK's + /// This results with a IWasExposedState object which later can be used to transmit the filtered TEK's to the backend + /// - Parameter callback: a handler which receives the state object or an error + func requestTEKPermission(callback: @escaping (Result) -> Void) { + log.trace() + if case .infected = state.infectionStatus { + callback(.failure(DP3TTracingError.userAlreadyMarkedAsInfected)) + return + } + diagnosisKeysProvider.getDiagnosisKeys(onsetDate: nil, appDesc: applicationDescriptor, disableExposureNotificationAfterCompletion: false) { result in + switch result { + case let .success(keys): + callback(.success(.init(keys: keys, isFake: false))) + case let .failure(error): + callback(.failure(error)) + } + } + } + /// tell the SDK that the user was exposed - /// This will stop tracing /// - Parameters: /// - onset: Start date of the exposure - /// - authString: Authentication string for the exposure change + /// - authentication: Authentication method /// - isFakeRequest: indicates if the request should be a fake one. This method should be called regulary so people sniffing the networking traffic can no figure out if somebody is marking themself actually as exposed /// - callback: callback - func iWasExposed(onset: Date, - authentication: ExposeeAuthMethod, - isFakeRequest: Bool = false, - callback: @escaping (Result) -> Void) { + func sendTEKs(onset: Date, + iWasExposedState: IWasExposedState, + authentication: ExposeeAuthMethod, + callback: @escaping (Result) -> Void) { log.trace() - if !isFakeRequest, + if !iWasExposedState.isFake, case .infected = state.infectionStatus { callback(.failure(DP3TTracingError.userAlreadyMarkedAsInfected)) return } + var keys: [CodableDiagnosisKey] = iWasExposedState.keys.filter { $0.date > onset } - let group = DispatchGroup() + // always make sure we fill up the keys to defaults.parameters.crypto.numberOfKeysToSubmit + let fakeKeyCount = self.defaults.parameters.networking.numberOfKeysToSubmit - keys.count - var diagnosisKeysResult: Result<[CodableDiagnosisKey], DP3TTracingError> = .success([]) + let oldestRollingStartNumber = keys.min { (a, b) -> Bool in a.rollingStartNumber < b.rollingStartNumber }?.rollingStartNumber ?? DayDate(date: .init(timeIntervalSinceNow: -.day)).period - if isFakeRequest { - group.enter() - diagnosisKeysProvider.getFakeDiagnosisKeys { result in - diagnosisKeysResult = result - group.leave() - } - } else { - group.enter() - diagnosisKeysProvider.getDiagnosisKeys(onsetDate: onset, appDesc: applicationDescriptor, disableExposureNotificationAfterCompletion: false) { result in - diagnosisKeysResult = result - group.leave() - } - } + let startingFrom = Date(timeIntervalSince1970: Double(oldestRollingStartNumber) * 10 * .minute - .day) - group.notify(queue: .main) { [weak self] in - guard let self = self else { return } - switch diagnosisKeysResult { - case let .failure(error): - callback(.failure(error)) - case let .success(keys): + keys.append(contentsOf: self.diagnosisKeysProvider.getFakeKeys(count: fakeKeyCount, startingFrom: startingFrom)) - var mutableKeys = keys - // always make sure we fill up the keys to defaults.parameters.crypto.numberOfKeysToSubmit - let fakeKeyCount = self.defaults.parameters.networking.numberOfKeysToSubmit - mutableKeys.count + let withFederationGateway: Bool? + switch self.federationGateway { + case .yes: + withFederationGateway = true + case .no: + withFederationGateway = false + case .unspecified: + withFederationGateway = nil + } - let oldestRollingStartNumber = keys.min { (a, b) -> Bool in a.rollingStartNumber < b.rollingStartNumber }?.rollingStartNumber ?? DayDate(date: .init(timeIntervalSinceNow: -.day)).period + var oldestNonFakeKeyDate: Date? = nil + if !iWasExposedState.isFake { + oldestNonFakeKeyDate = Date(timeIntervalSince1970: Double(oldestRollingStartNumber) * 10 * .minute) + } - let startingFrom = Date(timeIntervalSince1970: Double(oldestRollingStartNumber) * 10 * .minute - .day) + let model = ExposeeListModel(gaenKeys: keys, + withFederationGateway: withFederationGateway, + fake: iWasExposedState.isFake) - mutableKeys.append(contentsOf: self.diagnosisKeysProvider.getFakeKeys(count: fakeKeyCount, startingFrom: startingFrom)) + self.service.addExposeeList(model, authentication: authentication) { [weak self] result in + guard let self = self else { return } + DispatchQueue.main.async { + switch result { + case .success: + if !iWasExposedState.isFake { + self.state.infectionStatus = .infected + self.tracer.setEnabled(false, completionHandler: nil) + } - let withFederationGateway: Bool? - switch self.federationGateway { - case .yes: - withFederationGateway = true - case .no: - withFederationGateway = false - case .unspecified: - withFederationGateway = nil + callback(.success(.init(oldestKeyDate: oldestNonFakeKeyDate))) + case let .failure(error): + callback(.failure(.networkingError(error: error))) } + } + } + } - var oldestNonFakeKeyDate: Date? = nil - if !isFakeRequest { - oldestNonFakeKeyDate = Date(timeIntervalSince1970: Double(oldestRollingStartNumber) * 10 * .minute) - } + /// tell the SDK that the user was exposed + /// This is a convenience method that for not fake requests internally first calls requestTEKPermission and then sendTEKs + /// This will stop tracing + /// - Parameters: + /// - onset: Start date of the exposure + /// - authString: Authentication string for the exposure change + /// - isFakeRequest: indicates if the request should be a fake one. This method should be called regulary so people sniffing the networking traffic can no figure out if somebody is marking themself actually as exposed + /// - callback: callback + func iWasExposed(onset: Date, + authentication: ExposeeAuthMethod, + isFakeRequest: Bool = false, + callback: @escaping (Result) -> Void) { + log.trace() - let model = ExposeeListModel(gaenKeys: mutableKeys, - withFederationGateway: withFederationGateway, - fake: isFakeRequest) - - self.service.addExposeeList(model, authentication: authentication) { [weak self] result in - guard let self = self else { return } - DispatchQueue.main.async { - switch result { - case .success: - if !isFakeRequest { - self.state.infectionStatus = .infected - self.tracer.setEnabled(false, completionHandler: nil) - } - - callback(.success(.init(oldestKeyDate: oldestNonFakeKeyDate))) - case let .failure(error): - callback(.failure(.networkingError(error: error))) - } - } + let handle: (_ state: IWasExposedState) -> Void = { [weak self] state in + guard let self = self else { return } + self.sendTEKs(onset: onset, + iWasExposedState: state, + authentication: authentication, + callback: callback) + } + + if isFakeRequest { + handle(.init(keys: [], isFake: true)) + } else { + requestTEKPermission { result in + switch result { + case let .success(state): + handle(state) + case let .failure(error): + callback(.failure(error)) } } } diff --git a/Sources/DP3TSDK/DP3TTracing.swift b/Sources/DP3TSDK/DP3TTracing.swift index 30fff9f5..9ef4279a 100644 --- a/Sources/DP3TSDK/DP3TTracing.swift +++ b/Sources/DP3TSDK/DP3TTracing.swift @@ -139,8 +139,34 @@ public enum DP3TTracing { public struct IWasExposedResult { public let oldestKeyDate: Date? } + + /// Request the user permission to obtain all TEK's + /// This results with a IWasExposedState object which later can be used to transmit the filtered TEK's to the backend by calling sendTEKs + /// - Parameter callback: a handler which receives the state object or an error + @available(iOS 12.5, *) + public static func requestTEKPermission(callback: @escaping (Result) -> Void) { + instancePrecondition() + instance.requestTEKPermission(callback: callback) + } + + /// Send the obtained keys to the backend. First a valid state has to be obtained by calling requestTEKPermission + /// - Parameters: + /// - onset: Start date of the exposure + /// - iWasExposedState: state object obtained by calling requestTEKPermission + /// - authentication: Authentication method + /// - callback: callback + @available(iOS 12.5, *) + public static func sendTEKs(onset: Date, + iWasExposedState: IWasExposedState, + authentication: ExposeeAuthMethod, + callback: @escaping (Result) -> Void) { + instancePrecondition() + instance.sendTEKs(onset: onset, iWasExposedState: iWasExposedState, authentication: authentication, callback: callback) + } /// tell the SDK that the user was exposed + /// This is a convenience method that for not fake requests internally first calls requestTEKPermission and then sendTEKs + /// This will stop tracing /// - Parameters: /// - onset: Start date of the exposure /// - authentication: Authentication method diff --git a/Sources/DP3TSDK/DP3TTracingState.swift b/Sources/DP3TSDK/DP3TTracingState.swift index 1651e25b..83bf8e3b 100644 --- a/Sources/DP3TSDK/DP3TTracingState.swift +++ b/Sources/DP3TSDK/DP3TTracingState.swift @@ -104,3 +104,12 @@ public enum SyncResult: Equatable { } } } + +public struct IWasExposedState { + internal let keys: [CodableDiagnosisKey] + internal let isFake: Bool + + public static var fake: Self { + return .init(keys: [], isFake: true) + } +} diff --git a/Tests/DP3TSDKTests/DP3TSDKTests.swift b/Tests/DP3TSDKTests/DP3TSDKTests.swift index 6aad76f4..64b4115a 100644 --- a/Tests/DP3TSDKTests/DP3TSDKTests.swift +++ b/Tests/DP3TSDKTests/DP3TSDKTests.swift @@ -102,7 +102,7 @@ class DP3TSDKTests: XCTestCase { .init(keyData: Data(count: 16), rollingPeriod: 144, rollingStartNumber: DayDate(date: oldestDate).period, transmissionRiskLevel: 0, fake: 0), .init(keyData: Data(count: 16), rollingPeriod: 144, rollingStartNumber: DayDate(date: oldestDate.addingTimeInterval(.day * 2)).period, transmissionRiskLevel: 0, fake: 0), ] - sdk.iWasExposed(onset: .init(timeIntervalSinceNow: -.day), authentication: .none) { (result) in + sdk.iWasExposed(onset: .distantPast, authentication: .none) { (result) in if case let Result.success(wrapper) = result { XCTAssertEqual(wrapper.oldestKeyDate, DayDate(date: oldestDate).dayMin) } else { From db241518de287f986dcb58d2b991692cb20419b8 Mon Sep 17 00:00:00 2001 From: Stefan Mitterrutzner Date: Tue, 18 May 2021 10:02:42 +0200 Subject: [PATCH 2/2] updates changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f6ff8a..25bf0e7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog for DP3T-SDK iOS +## next version +- adds methods which allows to seperatly ask for user consent from sending the keys to the backend (DP3TTracing.requestTEKPermission and DP3TTracing.sendTEKs) +- fixes retain cycle + ## Version 2.3.0 (30.04.2021) - expose oldest shared keydate when calling iWasExposed