Skip to content

Commit

Permalink
Merge pull request #252 from DP-3T/feature/split-permission-and-backe…
Browse files Browse the repository at this point in the history
…nd-request

Adds method to request user consent independent from sending the keys to the backend when marking user as exposed
  • Loading branch information
stmitt authored May 19, 2021
2 parents e0f2149 + db24151 commit d1b0e8e
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 66 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
157 changes: 92 additions & 65 deletions Sources/DP3TSDK/DP3TSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<IWasExposedState, DP3TTracingError>) -> 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<DP3TTracing.IWasExposedResult, DP3TTracingError>) -> Void) {
func sendTEKs(onset: Date,
iWasExposedState: IWasExposedState,
authentication: ExposeeAuthMethod,
callback: @escaping (Result<DP3TTracing.IWasExposedResult, DP3TTracingError>) -> 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<DP3TTracing.IWasExposedResult, DP3TTracingError>) -> 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))
}
}
}
Expand Down
26 changes: 26 additions & 0 deletions Sources/DP3TSDK/DP3TTracing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<IWasExposedState, DP3TTracingError>) -> 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<IWasExposedResult, DP3TTracingError>) -> 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
Expand Down
9 changes: 9 additions & 0 deletions Sources/DP3TSDK/DP3TTracingState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion Tests/DP3TSDKTests/DP3TSDKTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit d1b0e8e

Please sign in to comment.