Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds method to request user consent independent from sending the keys to the backend when marking user as exposed #252

Merged
merged 2 commits into from
May 19, 2021
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 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