Commit be6529f0 authored by Lauri Eskor's avatar Lauri Eskor

Merge branch 'master' into feature/COVAPP-271-update-to-1.2.1

parents 77090ca7 2b376c92
......@@ -14,8 +14,8 @@ jobs:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Switch to Xcode 11.5
run: sudo xcode-select --switch /Applications/Xcode_11.5.app
- name: Switch to Xcode 11.6
run: sudo xcode-select --switch /Applications/Xcode_11.6.app
# Generate xcode project
- name: Generate xcodeproj
......@@ -32,8 +32,8 @@ jobs:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Switch to Xcode 11.5
run: sudo xcode-select --switch /Applications/Xcode_11.5.app
- name: Switch to Xcode 11.6
run: sudo xcode-select --switch /Applications/Xcode_11.6.app
# Compile sample app for iOS Simulator (no signing)
- name: Compile and run tests
......
......@@ -13,8 +13,8 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Switch to Xcode 11.5
run: sudo xcode-select --switch /Applications/Xcode_11.5.app
- name: Switch to Xcode 11.6
run: sudo xcode-select --switch /Applications/Xcode_11.6.app
- name: Install Cocoapods
run: gem install cocoapods
......
......@@ -2,7 +2,7 @@ name: distribute
on:
push:
branches: [ master, master-alpha ]
branches: [ master, master-alpha, develop ]
jobs:
appcenter:
runs-on: macOS-latest
......@@ -11,8 +11,8 @@ jobs:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Switch to Xcode 11.5
run: sudo xcode-select --switch /Applications/Xcode_11.5.app
- name: Switch to Xcode 11.6
run: sudo xcode-select --switch /Applications/Xcode_11.6.app
- name: Run fastlane build
env:
......
# Changelog for DP3T-SDK iOS
## Version 1.2.1 (31.08.2020)
- ensures that backgroundtask keeps running until outstandingPublishOperation is finished
## Version 1.2.0 (26.08.2020)
- resolves keychain issue with iOS 14
- adds iOS 14 info.plist entries for calibration app
- submitted keys are now always filled up to 30 instead of 14
- resolves detection issue for iOS 14 beta 5
## Version 1.1.1 (13.08.2020)
- DP3TNetworkingError.HTTPFailureResponse includes raw data
......
......@@ -2,7 +2,7 @@
Pod::Spec.new do |spec|
spec.name = "DP3TSDK"
spec.version = ENV['LIB_VERSION'] || '1.1.1'
spec.version = ENV['LIB_VERSION'] || '1.2.1'
spec.summary = "Open protocol for COVID-19 proximity tracing using Bluetooth Low Energy on mobile devices"
spec.description = <<-DESC
......
......@@ -74,7 +74,7 @@ DP3T-SDK is available through [Cocoapods](https://cocoapods.org/)
```ruby
pod 'DP3TSDK', => '1.1.1'
pod 'DP3TSDK', => '1.2.1'
```
......@@ -82,6 +82,11 @@ This version points to the HEAD of the `develop` branch and will always fetch th
## Using the SDK
In order to use the SDK with iOS 14 you need to specify the region for which the app works and the version of the [Exposure Notification](https://developer.apple.com/documentation/exposurenotification) Framework which should be used.
This is done by adding [`ENDeveloperRegion`](https://developer.apple.com/documentation/bundleresources/information_property_list/endeveloperregion) as an `Info.plist` property with the according ISO 3166-1 country code as its value.
The SDK currently works with EN Framework version 1 and therefore we need to specify [`ENAPIVersion`](https://developer.apple.com/documentation/bundleresources/information_property_list/enapiversion) with a value of 1 in the `Info.plist`.
### Initialization
In your AppDelegate in the `didFinishLaunchingWithOptions` function you have to initialize the SDK.
......
......@@ -251,7 +251,7 @@ class ControlViewController: UIViewController {
uploadKeysButton.setTitleColor(.blue, for: .normal)
uploadKeysButton.setTitleColor(.black, for: .highlighted)
}
uploadKeysButton.setTitle("Upload Keys for Debugging", for: .normal)
uploadKeysButton.setTitle("Upload Keys for Experiment", for: .normal)
uploadKeysButton.addTarget(self, action: #selector(uploadKeys), for: .touchUpInside)
stackView.addArrangedSubview(uploadKeysButton)
}
......@@ -340,16 +340,45 @@ class ControlViewController: UIViewController {
}
@objc func uploadKeys() {
let alert = UIAlertController(title: "Upload Keys", message: "Enter debug device name", preferredStyle: .alert)
let alert = UIAlertController(title: "Upload Keys", message: "Enter experiment name and device name", preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = "debug device name"
alert.addTextField { (textField) in
textField.placeholder = "experiment Name"
textField.text = ""
textField.tag = 1
}
alert.addAction(UIAlertAction(title: "Upload", style: .default, handler: { [weak alert] _ in
let textField = alert?.textFields![0]
self.uploadHelper.uploadDebugKeys(debugName: textField?.text ?? "noName") { result in
alert.addTextField { textField in
textField.placeholder = "debug device name"
textField.text = UIDevice.current.name
textField.tag = 0
}
alert.addAction(UIAlertAction(title: "Upload", style: .default, handler: { [weak alert, weak self] _ in
guard let self = self,
let deviceNameTextField = alert?.textFields?.first(where: { $0.tag == 0 }),
let experimentNameTextField = alert?.textFields?.first(where: { $0.tag == 1 }) else { return }
guard let deviceName = deviceNameTextField.text, !deviceName.isEmpty,
let experimentName = experimentNameTextField.text, !experimentName.isEmpty else {
var errors: [String] = []
if deviceNameTextField.text == nil ||
deviceNameTextField.text!.isEmpty {
errors.append("device name")
}
if experimentNameTextField.text == nil ||
experimentNameTextField.text!.isEmpty {
errors.append("experiment name")
}
let errorAlert = UIAlertController(title: "Error", message: "Please provide \(errors.joined(separator: " and "))", preferredStyle: .alert)
errorAlert.addAction(.init(title: "OK", style: .default) { _ in
self.uploadKeys()
})
self.present(errorAlert, animated: true, completion: nil)
return
}
let name = "experiment_\(experimentName)_\(deviceName)";
self.uploadHelper.uploadDebugKeys(debugName: name) { result in
print(result)
}
}))
......
......@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ENAPIVersion</key>
<string>1</string>
<key>ENDeveloperRegion</key>
<string>CH</string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>org.dpppt.exposure-notification</string>
......
......@@ -13,15 +13,28 @@ import ExposureNotification
import UIKit
import ZIPFoundation
class KeyDiffableDataSource: UITableViewDiffableDataSource<Date, NetworkingHelper.DebugZips> {
struct KeySection: Hashable {
let date: Date
let experimentName: String?
var title: String {
let dateString = Self.formatter.string(from: date)
if let experimentName = experimentName {
return "\(dateString) - Experiment: \(experimentName)"
}
return "\(dateString) - Single Device"
}
static var formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy"
return formatter
}()
}
class KeyDiffableDataSource: UITableViewDiffableDataSource<KeySection, NetworkingHelper.DebugZips> {
override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? {
return Self.formatter.string(from: snapshot().sectionIdentifiers[section])
return snapshot().sectionIdentifiers[section].title
}
}
......@@ -35,6 +48,10 @@ class KeysViewController: UIViewController {
private let networkingHelper = NetworkingHelper()
private let activityIndicator = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
private let nameRegex = try? NSRegularExpression(pattern: "key_export_experiment_([a-zA-Z0-9]+)_(.+)", options: .caseInsensitive)
init() {
super.init(nibName: nil, bundle: nil)
title = "Keys"
......@@ -69,30 +86,76 @@ class KeysViewController: UIViewController {
tableView.dataSource = dataSource
tableView.delegate = self
let date = Date().addingTimeInterval(60 * 60 * 24)
self.dataSource.apply(NSDiffableDataSourceSnapshot<KeySection, NetworkingHelper.DebugZips>(), animatingDifferences: true)
activityIndicator.hidesWhenStopped = true
let barButton = UIBarButtonItem(customView: activityIndicator)
self.navigationItem.setRightBarButton(barButton, animated: true)
activityIndicator.stopAnimating()
let ts = Date().timeIntervalSince1970
let roundendTs = Date(timeIntervalSince1970: ts - ts.truncatingRemainder(dividingBy: 60 * 60 * 24))
let date = roundendTs.addingTimeInterval(60 * 60 * 24)
datePicker.setDate(date, animated: false)
networkingHelper.getDebugKeys(day: date) { [weak self] result in
var snapshot = NSDiffableDataSourceSnapshot<Date, NetworkingHelper.DebugZips>()
snapshot.appendSections([date])
snapshot.appendItems(result, toSection: date)
self?.dataSource.apply(snapshot, animatingDifferences: true)
loadKeys(for: date)
}
private func parseZipName(name: String) -> (experimentName: String?, deviceName: String?) {
guard let regex = nameRegex else { return (nil, nil) }
var experimentName: String?
var deviceName: String?
if let match = regex.firstMatch(in: name, options: [], range: NSRange(location: 0, length: name.utf16.count)) {
if let experimentRange = Range(match.range(at: 1), in: name) {
experimentName = String(name[experimentRange])
}
if let deviceRange = Range(match.range(at: 2), in: name) {
deviceName = String(name[deviceRange])
}
}
return (experimentName, deviceName)
}
@objc func datePickerDidChange() {
let date = datePicker.date
guard !dataSource.snapshot().sectionIdentifiers.contains(date) else { return }
private func groupZips(zips: [NetworkingHelper.DebugZips], date: Date) -> [KeySection: [NetworkingHelper.DebugZips]] {
return zips.reduce([KeySection: [NetworkingHelper.DebugZips]]()) { (result, zip) -> [KeySection : [NetworkingHelper.DebugZips]] in
let (experimentName, _) = self.parseZipName(name: zip.name)
let section = KeySection(date: date, experimentName: experimentName)
var mutatingResult = result
mutatingResult[section, default: []].append(zip)
return mutatingResult
}
}
func loadKeys(for date: Date) {
activityIndicator.startAnimating()
networkingHelper.getDebugKeys(day: date) { [weak self] result in
guard let self = self else { return }
defer { self.activityIndicator.stopAnimating() }
var snapshot = self.dataSource.snapshot()
if !snapshot.sectionIdentifiers.contains(date) {
snapshot.appendSections([date])
let grouped = self.groupZips(zips: result, date: date)
for groupedItem in grouped {
let section = groupedItem.key
snapshot.insert(section: section, items: groupedItem.value)
}
if grouped.isEmpty {
let section = KeySection(date: date, experimentName: nil)
snapshot.insert(section: section, items: [])
}
snapshot.appendItems(result, toSection: date)
self.dataSource.apply(snapshot, animatingDifferences: true)
}
}
@objc func datePickerDidChange() {
let date = datePicker.date
loadKeys(for: date)
}
func makeDataSource() -> KeyDiffableDataSource {
let reuseIdentifier = cellReuseIdentifier
......@@ -171,3 +234,29 @@ extension ENExposureConfiguration {
return configuration
}
}
extension NSDiffableDataSourceSnapshot where SectionIdentifierType == KeySection,ItemIdentifierType == NetworkingHelper.DebugZips {
mutating func insert(section: KeySection, items: [NetworkingHelper.DebugZips]) {
if !sectionIdentifiers.contains(section) {
var inserted = false
for s in sectionIdentifiers {
if !inserted,
section.date > s.date,
(section.experimentName == nil || s.experimentName == nil) || section.experimentName! > s.experimentName! {
insertSections([section], beforeSection: s)
inserted = true
break
}
}
if !inserted {
appendSections([section])
}
}
let existingNames = itemIdentifiers(inSection: section).map(\.name)
for zip in items {
if !existingNames.contains(zip.name) {
appendItems([zip], toSection: section)
}
}
}
}
......@@ -13,6 +13,7 @@ import CommonCrypto
import ExposureNotification
import UIKit
import ZIPFoundation
import DP3TSDK
struct CodableDiagnosisKey: Codable, Equatable, Hashable {
let keyData: Data
......@@ -161,7 +162,7 @@ class NetworkingHelper {
}
var keys = keys?.map(CodableDiagnosisKey.init(key:)) ?? []
while keys.count < 14 {
while keys.count < DP3TTracing.parameters.crypto.numberOfKeysToSubmit {
let ts = Date().timeIntervalSince1970
let day = ts - ts.truncatingRemainder(dividingBy: 60 * 60 * 24)
keys.append(.init(keyData: Crypto.generateRandomKey(lenght: 16),
......
......@@ -74,7 +74,11 @@ extension ENManager: DiagnosisKeysProvider {
completionHandler(.failure(.exposureNotificationError(error: error)))
} else if let keys = keys {
logger.log("received %d keys", keys.count)
var filteredKeys = keys
let oldestDate = DayDate(date: Date().addingTimeInterval(-Default.shared.parameters.crypto.maxAgeOfKeyToRetreive)).dayMin
// make sure to never retreive keys older than maxNumberOfDaysToRetreive even if the onset date is older
var filteredKeys = keys.filter { $0.date > oldestDate }
// if a onsetDate was passed we filter the keys using it
if let onsetDate = onsetDate {
......
......@@ -12,7 +12,7 @@ import CoreBluetooth
import Foundation
public struct DP3TParameters: Codable {
static let parameterVersion: Int = 12
static let parameterVersion: Int = 15
let version: Int
......@@ -31,9 +31,11 @@ public struct DP3TParameters: Codable {
public var timeZone: TimeZone = TimeZone(identifier: "UTC")!
public var numberOfDaysToKeepMatchedContacts = 10
public var numberOfDaysToKeepMatchedContacts = 14
public var numberOfKeysToSubmit: Int = 14
public var maxAgeOfKeyToRetreive: TimeInterval = .day * 14
public var numberOfKeysToSubmit: Int = 30
}
public struct Networking: Codable {
......
......@@ -186,19 +186,20 @@ class DP3TSDK {
OperationQueue().addOperation(outstandingPublishOperation)
let sync = {
var storedResult: SyncResult?
// Skip sync when tracing is not active
if self.state.trackingState != .active {
self.log.error("Skip sync when tracking is not active")
callback?(.skipped)
return
storedResult = .skipped
} else {
group.enter()
self.synchronizer.sync { result in
storedResult = result
group.leave()
}
}
group.enter()
var storedResult: SyncResult?
self.synchronizer.sync { result in
storedResult = result
group.leave()
}
group.notify(queue: .main) { [weak self] in
guard let self = self else { return }
switch storedResult! {
......
......@@ -28,7 +28,7 @@ private var instance: DP3TSDK!
/// DP3TTracing
public enum DP3TTracing {
/// The current version of the SDK
public static let frameworkVersion: String = "1.1.1"
public static let frameworkVersion: String = "1.2.1"
/// sets global parameter values which are used throughout the sdk
public static var parameters: DP3TParameters {
......
......@@ -35,11 +35,13 @@ class ExposureNotificationMatcher: Matcher {
logger.trace()
return try synchronousQueue.sync {
var urls: [URL] = []
let tempDirectory = FileManager.default
.urls(for: .cachesDirectory, in: .userDomainMask).first!
.appendingPathComponent(UUID().uuidString)
if let archive = Archive(data: data, accessMode: .read) {
logger.debug("unarchived archive")
for entry in archive {
let localURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
.appendingPathComponent(UUID().uuidString).appendingPathComponent(entry.path)
let localURL = tempDirectory.appendingPathComponent(entry.path)
do {
_ = try archive.extract(entry, to: localURL)
} catch {
......@@ -83,7 +85,7 @@ class ExposureNotificationMatcher: Matcher {
timingManager?.addDetection(timestamp: now)
try? urls.forEach(deleteDiagnosisKeyFile(at:))
try? FileManager.default.removeItem(at: tempDirectory)
if let summary = exposureSummary {
let computedThreshold: Double = (Double(truncating: summary.attenuationDurations[0]) * defaults.parameters.contactMatching.factorLow + Double(truncating: summary.attenuationDurations[1]) * defaults.parameters.contactMatching.factorHigh) / TimeInterval.minute
......
......@@ -82,8 +82,8 @@ class Keychain: KeychainProtocol {
/// - Returns: a result which either contain the error or the object
public func get<T: Codable>(for key: KeychainKey<T>) -> Result<T, KeychainError> {
var query = self.query(for: key)
query[kSecReturnData] = kCFBooleanTrue
query[kSecMatchLimit] = kSecMatchLimitOne
query[kSecReturnData as String] = kCFBooleanTrue
query[kSecMatchLimit as String] = kSecMatchLimitOne
var item: CFTypeRef?
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &item)
......@@ -120,7 +120,7 @@ class Keychain: KeychainProtocol {
return .failure(.encodingError(error))
}
var query = self.query(for: key)
query[kSecValueData] = data
query[kSecValueData as String] = data
var status: OSStatus = SecItemCopyMatching(query as CFDictionary, nil)
......@@ -166,11 +166,11 @@ class Keychain: KeychainProtocol {
/// helpermethod to construct the keychain query
/// - Parameter key: key to use
/// - Returns: the keychain query
private func query<T>(for key: KeychainKey<T>) -> [CFString: Any] {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword as String,
kSecAttrAccount: key.key,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
private func query<T>(for key: KeychainKey<T>) -> [String: Any] {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String: key.key,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
]
return query
}
......
......@@ -52,7 +52,11 @@ class DP3TSDKTests: XCTestCase {
fileprivate var service: MockService!
fileprivate var defaults: MockDefaults!
fileprivate var keyProvider: MockKeyProvider!
fileprivate var descriptor: ApplicationDescriptor!
var descriptor: ApplicationDescriptor {
MockService.descriptor
}
fileprivate var backgroundTaskManager: DP3TBackgroundTaskManager!
fileprivate var manager: MockENManager!
fileprivate var exposureDayStorage: ExposureDayStorage!
......@@ -71,7 +75,6 @@ class DP3TSDKTests: XCTestCase {
service = MockService()
keyProvider = MockKeyProvider()
descriptor = ApplicationDescriptor(appId: "org.dpppt", bucketBaseUrl: URL(string: "http://google.com")!, reportBaseUrl: URL(string: "http://google.com")!)
backgroundTaskManager = DP3TBackgroundTaskManager(handler: nil, keyProvider: keyProvider, serviceClient: service, tracer: tracer)
sdk = DP3TSDK(applicationDescriptor: descriptor,
urlSession: MockSession(data: nil, urlResponse: nil, error: nil),
......
/*
* Copyright (c) 2020 Ubique Innovation AG <https://www.ubique.ch>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*/
@testable import DP3TSDK
import Foundation
import XCTest
import ExposureNotification
class DiagnosisKeysProviderTests: XCTestCase {
var manager: MockENManager!
var descriptor: ApplicationDescriptor {
MockService.descriptor
}
override func setUp() {
manager = MockENManager()
}
func testFilteringOfOldTests(){
let onset = Date().addingTimeInterval(.day * -100)
let day = DayDate(date: Date().addingTimeInterval(.day * (-15)))
manager.keys = [.initialize(rollingStartNumber: day.period)]
let exp = expectation(description: "getDiagnosisKeys")
manager.getDiagnosisKeys(onsetDate: onset, appDesc: descriptor) { (result) in
switch result {
case .failure(_):
XCTFail()
case let .success(keys):
XCTAssert(keys.isEmpty)
}
exp.fulfill()
}
wait(for: [exp], timeout: 0.1)
}
func testNotFilteringNewTests(){
let onset = Date().addingTimeInterval(.day * -14)
let day = DayDate(date: Date().addingTimeInterval(.day * (-13)))
manager.keys = [.initialize(rollingStartNumber: day.period)]
let exp = expectation(description: "getDiagnosisKeys")
manager.getDiagnosisKeys(onsetDate: onset, appDesc: descriptor) { (result) in
switch result {
case .failure(_):
XCTFail()
case let .success(keys):
XCTAssert(!keys.isEmpty)
XCTAssertEqual(keys.count, 1)
}
exp.fulfill()
}
wait(for: [exp], timeout: 0.1)
}
func testFilteringOnsetDate(){
let onset = Date().addingTimeInterval(.day * -13)
let day = DayDate(date: Date().addingTimeInterval(.day * (-14)))
manager.keys = [.initialize(rollingStartNumber: day.period)]
let exp = expectation(description: "getDiagnosisKeys")
manager.getDiagnosisKeys(onsetDate: onset, appDesc: descriptor) { (result) in
switch result {
case <