├── .github └── workflows │ ├── ci-sdk.yml │ └── distribute.yml ├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── SampleApp ├── Gemfile ├── Gemfile.lock ├── STARSampleApp.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ ├── Debug.xcscheme │ │ └── Release.xcscheme ├── STARSampleApp │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── ControlViewController.swift │ ├── Defaults.swift │ ├── HandshakeViewController.swift │ ├── Info.plist │ ├── LogsViewController.swift │ ├── ParametersViewController.swift │ ├── RootViewController.swift │ └── StackView+Spacing.swift └── fastlane │ ├── Appfile │ ├── Fastfile │ └── Pluginfile ├── Sources ├── Config.xcconfig ├── STARSDK │ ├── Bluetooth │ │ ├── BluetoothBroadcastService.swift │ │ ├── BluetoothConstants.swift │ │ ├── BluetoothDelegate.swift │ │ └── BluetoothDiscoveryService.swift │ ├── Cryptography │ │ ├── Crypto.swift │ │ ├── CryptoConstants.swift │ │ ├── Epoch.swift │ │ ├── STARCryptoModule.swift │ │ └── SecretKey.swift │ ├── Database │ │ ├── ApplicationStorage.swift │ │ ├── Database.swift │ │ ├── Defaults.swift │ │ ├── HandshakesStorage.swift │ │ ├── KnownCasesStorage.swift │ │ ├── LoggingStorage.swift │ │ ├── PeripheralStorage.swift │ │ ├── SQLite+URL.swift │ │ └── SecretKeyStorage.swift │ ├── LoggingDelegate.swift │ ├── Models │ │ ├── APIModel.swift │ │ ├── ExposeeAuthData.swift │ │ ├── ExposeeModel.swift │ │ ├── HandshakeModel.swift │ │ └── KnownCaseModel.swift │ ├── Networking │ │ ├── ApplicationsSynchronizer.swift │ │ ├── Endpoints.swift │ │ ├── Enviroment.swift │ │ ├── ExposeeServiceClient.swift │ │ └── KnownCasesSynchronizer.swift │ ├── STARErrors.swift │ ├── STARMatching.swift │ ├── STARMode.swift │ ├── STARSDK.swift │ ├── STARTracing.swift │ └── STARTracingState.swift └── STARSDK_CALIBRATION └── Tests ├── LinuxMain.swift └── STARSDKTests ├── CovidTracingTests.swift ├── CryptoTest.swift └── XCTestManifests.swift /.github/workflows/ci-sdk.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master, develop ] 8 | 9 | jobs: 10 | sdk: 11 | runs-on: macOS-latest 12 | 13 | steps: 14 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 15 | - uses: actions/checkout@v2 16 | 17 | - name: Switch to Xcode 11.4 18 | run: sudo xcode-select --switch /Applications/Xcode_11.4.app 19 | 20 | # Generate xcode project 21 | - name: Generate xcodeproj 22 | run: swift package generate-xcodeproj --xcconfig-overrides Sources/Config.xcconfig 23 | 24 | # Compile project and run tests 25 | - name: Compile and run tests 26 | run: fastlane scan 27 | 28 | sampleapp: 29 | runs-on: macOS-latest 30 | 31 | steps: 32 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 33 | - uses: actions/checkout@v2 34 | 35 | - name: Switch to Xcode 11.4 36 | run: sudo xcode-select --switch /Applications/Xcode_11.4.app 37 | 38 | # Compile sample app for iOS Simulator (no signing) 39 | - name: Compile and run tests 40 | run: fastlane gym --project SampleApp/STARSampleApp.xcodeproj --scheme "Debug" --skip_package_ipa true --destination "generic/platform=iOS Simulator" 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/distribute.yml: -------------------------------------------------------------------------------- 1 | name: distribute 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | 7 | jobs: 8 | appcenter: 9 | runs-on: macOS-latest 10 | 11 | steps: 12 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 13 | - uses: actions/checkout@v2 14 | 15 | - name: Switch to Xcode 11.4 16 | run: sudo xcode-select --switch /Applications/Xcode_11.4.app 17 | 18 | - name: Run fastlane build 19 | env: 20 | MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }} 21 | MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} 22 | MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} 23 | FASTLANE_USER: ${{ secrets.FASTLANE_USER }} 24 | FASTLANE_TEAM_ID: ${{ secrets.TEAM_ID }} 25 | APPCENTER_APP_NAME: ${{ secrets.APPCENTER_APP_NAME }} 26 | APPCENTER_OWNER_NAME: ${{ secrets.APPCENTER_OWNER_NAME }} 27 | APPCENTER_API_TOKEN: ${{ secrets.APPCENTER_API_TOKEN }} 28 | APP_IDENTIFIER: ${{ secrets.APP_IDENTIFIER }} 29 | run: | 30 | fastlane distribute 31 | working-directory: SampleApp 32 | 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SQLite.swift", 6 | "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "0a9893ec030501a3956bee572d6b4fdd3ae158a1", 10 | "version": "0.12.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "STARSDK", 8 | platforms: [ 9 | // Add support for all platforms starting from a specific version. 10 | .iOS(.v10), 11 | ], 12 | products: [ 13 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 14 | .library( 15 | name: "STARSDK", 16 | targets: ["STARSDK"] 17 | ), 18 | .library( 19 | name: "STARSDK_CALIBRATION", 20 | targets: ["STARSDK_CALIBRATION"] 21 | ), 22 | ], 23 | dependencies: [ 24 | // Dependencies declare other packages that this package depends on. 25 | // .package(url: /* package url */, from: "1.0.0"), 26 | .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.12.0"), 27 | ], 28 | targets: [ 29 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 30 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 31 | .target( 32 | name: "STARSDK", 33 | dependencies: ["SQLite"] 34 | ), 35 | .target( 36 | name: "STARSDK_CALIBRATION", 37 | dependencies: ["SQLite"], 38 | swiftSettings: [.define("CALIBRATION")] 39 | ), 40 | .testTarget( 41 | name: "STARSDKTests", 42 | dependencies: ["STARSDK"] 43 | ), 44 | ] 45 | ) 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # STARSDK 2 | 3 | > #### Moved to DP-3T! 4 | > 5 | > As of May 2020, all of our efforts are transitioning to [DP-3T](https://github.com/DP-3T). 6 | > 7 | 8 | ## Introduction 9 | This is the iOS version of the Secure Tag for Approach Recognition (STAR) SDK. The idea of the sdk is, to provide a SDK, which enables an easy way to provide methods for contact tracing. This project was built within 71 hours at the HackZurich Hackathon 2020. 10 | 11 | ## Architecture 12 | There exists a central discovery server on [Github](https://raw.githubusercontent.com/SecureTagForApproachRecognition/discovery/master/discovery.json). This server provides the necessary information for the SDK to initialize itself. After the SDK loaded the base url for its own backend it will load the infected list from there, as well as post if a user is infected. 13 | 14 | The backend should hence gather all the infected list from other backends and provide a collected list from all sources. As long as the keys are generated with the SDK we can validate them across different apps. 15 | 16 | ## Further Documentation 17 | 18 | There exists a documentation repository in the [STAR](https://github.com/SecureTagForApproachRecognition) Organization. It includes Swager YAMLs for the backend API definitions, as well as some more technical details on how the keys are generated and how the validation mechanism works 19 | 20 | 21 | ## Documentation 22 | Please find in the project a Documentation folder with an **index.html** file. 23 | 24 | ## Installation 25 | ### Swift Package Manager 26 | 27 | STARSDK is available through [Swift Package Manager][https://swift.org/package-manager] 28 | 29 | 1. Add the following to your `Package.swift` file: 30 | 31 | ```swift 32 | 33 | dependencies: [ 34 | .package(url: "https://github.com/SecureTagForApproachRecognition/star-sdk-ios.git", branch: "master") 35 | ] 36 | 37 | ``` 38 | -------------------------------------------------------------------------------- /SampleApp/Gemfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | source "https://rubygems.org" 6 | 7 | gem 'fastlane' 8 | 9 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 10 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 11 | -------------------------------------------------------------------------------- /SampleApp/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | addressable (2.7.0) 6 | public_suffix (>= 2.0.2, < 5.0) 7 | atomos (0.1.3) 8 | aws-eventstream (1.1.0) 9 | aws-partitions (1.296.0) 10 | aws-sdk-core (3.94.0) 11 | aws-eventstream (~> 1, >= 1.0.2) 12 | aws-partitions (~> 1, >= 1.239.0) 13 | aws-sigv4 (~> 1.1) 14 | jmespath (~> 1.0) 15 | aws-sdk-kms (1.30.0) 16 | aws-sdk-core (~> 3, >= 3.71.0) 17 | aws-sigv4 (~> 1.1) 18 | aws-sdk-s3 (1.61.2) 19 | aws-sdk-core (~> 3, >= 3.83.0) 20 | aws-sdk-kms (~> 1) 21 | aws-sigv4 (~> 1.1) 22 | aws-sigv4 (1.1.1) 23 | aws-eventstream (~> 1.0, >= 1.0.2) 24 | babosa (1.0.3) 25 | claide (1.0.3) 26 | colored (1.2) 27 | colored2 (3.1.2) 28 | commander-fastlane (4.4.6) 29 | highline (~> 1.7.2) 30 | declarative (0.0.10) 31 | declarative-option (0.1.0) 32 | digest-crc (0.5.1) 33 | domain_name (0.5.20190701) 34 | unf (>= 0.0.5, < 1.0.0) 35 | dotenv (2.7.5) 36 | emoji_regex (1.0.1) 37 | excon (0.73.0) 38 | faraday (0.17.3) 39 | multipart-post (>= 1.2, < 3) 40 | faraday-cookie_jar (0.0.6) 41 | faraday (>= 0.7.4) 42 | http-cookie (~> 1.0.0) 43 | faraday_middleware (0.13.1) 44 | faraday (>= 0.7.4, < 1.0) 45 | fastimage (2.1.7) 46 | fastlane (2.145.0) 47 | CFPropertyList (>= 2.3, < 4.0.0) 48 | addressable (>= 2.3, < 3.0.0) 49 | aws-sdk-s3 (~> 1.0) 50 | babosa (>= 1.0.2, < 2.0.0) 51 | bundler (>= 1.12.0, < 3.0.0) 52 | colored 53 | commander-fastlane (>= 4.4.6, < 5.0.0) 54 | dotenv (>= 2.1.1, < 3.0.0) 55 | emoji_regex (>= 0.1, < 2.0) 56 | excon (>= 0.71.0, < 1.0.0) 57 | faraday (~> 0.17) 58 | faraday-cookie_jar (~> 0.0.6) 59 | faraday_middleware (~> 0.13.1) 60 | fastimage (>= 2.1.0, < 3.0.0) 61 | gh_inspector (>= 1.1.2, < 2.0.0) 62 | google-api-client (>= 0.29.2, < 0.37.0) 63 | google-cloud-storage (>= 1.15.0, < 2.0.0) 64 | highline (>= 1.7.2, < 2.0.0) 65 | json (< 3.0.0) 66 | jwt (~> 2.1.0) 67 | mini_magick (>= 4.9.4, < 5.0.0) 68 | multi_xml (~> 0.5) 69 | multipart-post (~> 2.0.0) 70 | plist (>= 3.1.0, < 4.0.0) 71 | public_suffix (~> 2.0.0) 72 | rubyzip (>= 1.3.0, < 2.0.0) 73 | security (= 0.1.3) 74 | simctl (~> 1.6.3) 75 | slack-notifier (>= 2.0.0, < 3.0.0) 76 | terminal-notifier (>= 2.0.0, < 3.0.0) 77 | terminal-table (>= 1.4.5, < 2.0.0) 78 | tty-screen (>= 0.6.3, < 1.0.0) 79 | tty-spinner (>= 0.8.0, < 1.0.0) 80 | word_wrap (~> 1.0.0) 81 | xcodeproj (>= 1.13.0, < 2.0.0) 82 | xcpretty (~> 0.3.0) 83 | xcpretty-travis-formatter (>= 0.0.3) 84 | fastlane-plugin-appcenter (1.8.0) 85 | gh_inspector (1.1.3) 86 | google-api-client (0.36.4) 87 | addressable (~> 2.5, >= 2.5.1) 88 | googleauth (~> 0.9) 89 | httpclient (>= 2.8.1, < 3.0) 90 | mini_mime (~> 1.0) 91 | representable (~> 3.0) 92 | retriable (>= 2.0, < 4.0) 93 | signet (~> 0.12) 94 | google-cloud-core (1.5.0) 95 | google-cloud-env (~> 1.0) 96 | google-cloud-errors (~> 1.0) 97 | google-cloud-env (1.3.1) 98 | faraday (>= 0.17.3, < 2.0) 99 | google-cloud-errors (1.0.0) 100 | google-cloud-storage (1.26.0) 101 | addressable (~> 2.5) 102 | digest-crc (~> 0.4) 103 | google-api-client (~> 0.33) 104 | google-cloud-core (~> 1.2) 105 | googleauth (~> 0.9) 106 | mini_mime (~> 1.0) 107 | googleauth (0.11.0) 108 | faraday (>= 0.17.3, < 2.0) 109 | jwt (>= 1.4, < 3.0) 110 | memoist (~> 0.16) 111 | multi_json (~> 1.11) 112 | os (>= 0.9, < 2.0) 113 | signet (~> 0.12) 114 | highline (1.7.10) 115 | http-cookie (1.0.3) 116 | domain_name (~> 0.5) 117 | httpclient (2.8.3) 118 | jmespath (1.4.0) 119 | json (2.3.0) 120 | jwt (2.1.0) 121 | memoist (0.16.2) 122 | mini_magick (4.10.1) 123 | mini_mime (1.0.2) 124 | multi_json (1.14.1) 125 | multi_xml (0.6.0) 126 | multipart-post (2.0.0) 127 | nanaimo (0.2.6) 128 | naturally (2.2.0) 129 | os (1.1.0) 130 | plist (3.5.0) 131 | public_suffix (2.0.5) 132 | representable (3.0.4) 133 | declarative (< 0.1.0) 134 | declarative-option (< 0.2.0) 135 | uber (< 0.2.0) 136 | retriable (3.1.2) 137 | rouge (2.0.7) 138 | rubyzip (1.3.0) 139 | security (0.1.3) 140 | signet (0.14.0) 141 | addressable (~> 2.3) 142 | faraday (>= 0.17.3, < 2.0) 143 | jwt (>= 1.5, < 3.0) 144 | multi_json (~> 1.10) 145 | simctl (1.6.8) 146 | CFPropertyList 147 | naturally 148 | slack-notifier (2.3.2) 149 | terminal-notifier (2.0.0) 150 | terminal-table (1.8.0) 151 | unicode-display_width (~> 1.1, >= 1.1.1) 152 | tty-cursor (0.7.1) 153 | tty-screen (0.7.1) 154 | tty-spinner (0.9.3) 155 | tty-cursor (~> 0.7) 156 | uber (0.1.0) 157 | unf (0.1.4) 158 | unf_ext 159 | unf_ext (0.0.7.7) 160 | unicode-display_width (1.7.0) 161 | word_wrap (1.0.0) 162 | xcodeproj (1.15.0) 163 | CFPropertyList (>= 2.3.3, < 4.0) 164 | atomos (~> 0.1.3) 165 | claide (>= 1.0.2, < 2.0) 166 | colored2 (~> 3.1) 167 | nanaimo (~> 0.2.6) 168 | xcpretty (0.3.0) 169 | rouge (~> 2.0.7) 170 | xcpretty-travis-formatter (1.0.0) 171 | xcpretty (~> 0.2, >= 0.0.7) 172 | 173 | PLATFORMS 174 | ruby 175 | 176 | DEPENDENCIES 177 | fastlane 178 | fastlane-plugin-appcenter 179 | 180 | BUNDLED WITH 181 | 2.0.2 182 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F812AC56243DF459005F26AE /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F812AC55243DF459005F26AE /* RootViewController.swift */; }; 11 | F812AC59243E02A1005F26AE /* ControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F812AC58243E02A1005F26AE /* ControlViewController.swift */; }; 12 | F812AC5B243E02AF005F26AE /* ParametersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F812AC5A243E02AF005F26AE /* ParametersViewController.swift */; }; 13 | F812AC5D243E02B9005F26AE /* HandshakeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F812AC5C243E02B9005F26AE /* HandshakeViewController.swift */; }; 14 | F812AC5F243E08BF005F26AE /* STARSDK_CALIBRATION in Frameworks */ = {isa = PBXBuildFile; productRef = F812AC5E243E08BF005F26AE /* STARSDK_CALIBRATION */; }; 15 | F83BE13A242DDC450043FA1E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83BE139242DDC450043FA1E /* AppDelegate.swift */; }; 16 | F83BE143242DDC460043FA1E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F83BE142242DDC460043FA1E /* Assets.xcassets */; }; 17 | F83BE146242DDC460043FA1E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F83BE144242DDC460043FA1E /* LaunchScreen.storyboard */; }; 18 | F83BE150242DDFF60043FA1E /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = F83BE14F242DDFF60043FA1E /* SnapKit */; }; 19 | F85D4F40243EFC6A00529168 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4F3F243EFC6A00529168 /* Defaults.swift */; }; 20 | F85D4F42243F0D8B00529168 /* StackView+Spacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4F41243F0D8B00529168 /* StackView+Spacing.swift */; }; 21 | F8F528F3243E2090000ABACC /* LogsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F528F2243E2090000ABACC /* LogsViewController.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | F812AC55243DF459005F26AE /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; 26 | F812AC58243E02A1005F26AE /* ControlViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlViewController.swift; sourceTree = ""; }; 27 | F812AC5A243E02AF005F26AE /* ParametersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParametersViewController.swift; sourceTree = ""; }; 28 | F812AC5C243E02B9005F26AE /* HandshakeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandshakeViewController.swift; sourceTree = ""; }; 29 | F83BE136242DDC450043FA1E /* STARSampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = STARSampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | F83BE139242DDC450043FA1E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 31 | F83BE142242DDC460043FA1E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 32 | F83BE145242DDC460043FA1E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 33 | F83BE147242DDC460043FA1E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 34 | F83BE17C242DE1BF0043FA1E /* covid-tracing-ios-sdk */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "covid-tracing-ios-sdk"; path = ..; sourceTree = ""; }; 35 | F85D4F3F243EFC6A00529168 /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; 36 | F85D4F41243F0D8B00529168 /* StackView+Spacing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StackView+Spacing.swift"; sourceTree = ""; }; 37 | F8F528F2243E2090000ABACC /* LogsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsViewController.swift; sourceTree = ""; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | F83BE133242DDC450043FA1E /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | F812AC5F243E08BF005F26AE /* STARSDK_CALIBRATION in Frameworks */, 46 | F83BE150242DDFF60043FA1E /* SnapKit in Frameworks */, 47 | ); 48 | runOnlyForDeploymentPostprocessing = 0; 49 | }; 50 | /* End PBXFrameworksBuildPhase section */ 51 | 52 | /* Begin PBXGroup section */ 53 | F83BE12D242DDC450043FA1E = { 54 | isa = PBXGroup; 55 | children = ( 56 | F83BE138242DDC450043FA1E /* STARSampleApp */, 57 | F83BE137242DDC450043FA1E /* Products */, 58 | F83BE151242DE0C00043FA1E /* Frameworks */, 59 | ); 60 | sourceTree = ""; 61 | }; 62 | F83BE137242DDC450043FA1E /* Products */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | F83BE136242DDC450043FA1E /* STARSampleApp.app */, 66 | ); 67 | name = Products; 68 | sourceTree = ""; 69 | }; 70 | F83BE138242DDC450043FA1E /* STARSampleApp */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | F83BE139242DDC450043FA1E /* AppDelegate.swift */, 74 | F85D4F3F243EFC6A00529168 /* Defaults.swift */, 75 | F812AC55243DF459005F26AE /* RootViewController.swift */, 76 | F8F528F2243E2090000ABACC /* LogsViewController.swift */, 77 | F812AC58243E02A1005F26AE /* ControlViewController.swift */, 78 | F812AC5A243E02AF005F26AE /* ParametersViewController.swift */, 79 | F812AC5C243E02B9005F26AE /* HandshakeViewController.swift */, 80 | F85D4F41243F0D8B00529168 /* StackView+Spacing.swift */, 81 | F83BE142242DDC460043FA1E /* Assets.xcassets */, 82 | F83BE144242DDC460043FA1E /* LaunchScreen.storyboard */, 83 | F83BE147242DDC460043FA1E /* Info.plist */, 84 | ); 85 | path = STARSampleApp; 86 | sourceTree = ""; 87 | }; 88 | F83BE151242DE0C00043FA1E /* Frameworks */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | F83BE17C242DE1BF0043FA1E /* covid-tracing-ios-sdk */, 92 | ); 93 | name = Frameworks; 94 | sourceTree = ""; 95 | }; 96 | /* End PBXGroup section */ 97 | 98 | /* Begin PBXNativeTarget section */ 99 | F83BE135242DDC450043FA1E /* STARSampleApp */ = { 100 | isa = PBXNativeTarget; 101 | buildConfigurationList = F83BE14A242DDC460043FA1E /* Build configuration list for PBXNativeTarget "STARSampleApp" */; 102 | buildPhases = ( 103 | F83BE132242DDC450043FA1E /* Sources */, 104 | F83BE133242DDC450043FA1E /* Frameworks */, 105 | F83BE134242DDC450043FA1E /* Resources */, 106 | ); 107 | buildRules = ( 108 | ); 109 | dependencies = ( 110 | ); 111 | name = STARSampleApp; 112 | packageProductDependencies = ( 113 | F83BE14F242DDFF60043FA1E /* SnapKit */, 114 | F812AC5E243E08BF005F26AE /* STARSDK_CALIBRATION */, 115 | ); 116 | productName = CovidTracingTestApp; 117 | productReference = F83BE136242DDC450043FA1E /* STARSampleApp.app */; 118 | productType = "com.apple.product-type.application"; 119 | }; 120 | /* End PBXNativeTarget section */ 121 | 122 | /* Begin PBXProject section */ 123 | F83BE12E242DDC450043FA1E /* Project object */ = { 124 | isa = PBXProject; 125 | attributes = { 126 | LastSwiftUpdateCheck = 1130; 127 | LastUpgradeCheck = 1130; 128 | ORGANIZATIONNAME = Ubique; 129 | TargetAttributes = { 130 | F83BE135242DDC450043FA1E = { 131 | CreatedOnToolsVersion = 11.3.1; 132 | }; 133 | }; 134 | }; 135 | buildConfigurationList = F83BE131242DDC450043FA1E /* Build configuration list for PBXProject "STARSampleApp" */; 136 | compatibilityVersion = "Xcode 9.3"; 137 | developmentRegion = en; 138 | hasScannedForEncodings = 0; 139 | knownRegions = ( 140 | en, 141 | Base, 142 | ); 143 | mainGroup = F83BE12D242DDC450043FA1E; 144 | packageReferences = ( 145 | F83BE14E242DDFF60043FA1E /* XCRemoteSwiftPackageReference "SnapKit" */, 146 | ); 147 | productRefGroup = F83BE137242DDC450043FA1E /* Products */; 148 | projectDirPath = ""; 149 | projectRoot = ""; 150 | targets = ( 151 | F83BE135242DDC450043FA1E /* STARSampleApp */, 152 | ); 153 | }; 154 | /* End PBXProject section */ 155 | 156 | /* Begin PBXResourcesBuildPhase section */ 157 | F83BE134242DDC450043FA1E /* Resources */ = { 158 | isa = PBXResourcesBuildPhase; 159 | buildActionMask = 2147483647; 160 | files = ( 161 | F83BE146242DDC460043FA1E /* LaunchScreen.storyboard in Resources */, 162 | F83BE143242DDC460043FA1E /* Assets.xcassets in Resources */, 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | /* End PBXResourcesBuildPhase section */ 167 | 168 | /* Begin PBXSourcesBuildPhase section */ 169 | F83BE132242DDC450043FA1E /* Sources */ = { 170 | isa = PBXSourcesBuildPhase; 171 | buildActionMask = 2147483647; 172 | files = ( 173 | F83BE13A242DDC450043FA1E /* AppDelegate.swift in Sources */, 174 | F85D4F40243EFC6A00529168 /* Defaults.swift in Sources */, 175 | F812AC59243E02A1005F26AE /* ControlViewController.swift in Sources */, 176 | F85D4F42243F0D8B00529168 /* StackView+Spacing.swift in Sources */, 177 | F812AC5B243E02AF005F26AE /* ParametersViewController.swift in Sources */, 178 | F812AC56243DF459005F26AE /* RootViewController.swift in Sources */, 179 | F8F528F3243E2090000ABACC /* LogsViewController.swift in Sources */, 180 | F812AC5D243E02B9005F26AE /* HandshakeViewController.swift in Sources */, 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | /* End PBXSourcesBuildPhase section */ 185 | 186 | /* Begin PBXVariantGroup section */ 187 | F83BE144242DDC460043FA1E /* LaunchScreen.storyboard */ = { 188 | isa = PBXVariantGroup; 189 | children = ( 190 | F83BE145242DDC460043FA1E /* Base */, 191 | ); 192 | name = LaunchScreen.storyboard; 193 | sourceTree = ""; 194 | }; 195 | /* End PBXVariantGroup section */ 196 | 197 | /* Begin XCBuildConfiguration section */ 198 | F83BE148242DDC460043FA1E /* Debug */ = { 199 | isa = XCBuildConfiguration; 200 | buildSettings = { 201 | ALWAYS_SEARCH_USER_PATHS = NO; 202 | BUILD_NUMBER = 1; 203 | CLANG_ANALYZER_NONNULL = YES; 204 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 205 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 206 | CLANG_CXX_LIBRARY = "libc++"; 207 | CLANG_ENABLE_MODULES = YES; 208 | CLANG_ENABLE_OBJC_ARC = YES; 209 | CLANG_ENABLE_OBJC_WEAK = YES; 210 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 211 | CLANG_WARN_BOOL_CONVERSION = YES; 212 | CLANG_WARN_COMMA = YES; 213 | CLANG_WARN_CONSTANT_CONVERSION = YES; 214 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 215 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 216 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 217 | CLANG_WARN_EMPTY_BODY = YES; 218 | CLANG_WARN_ENUM_CONVERSION = YES; 219 | CLANG_WARN_INFINITE_RECURSION = YES; 220 | CLANG_WARN_INT_CONVERSION = YES; 221 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 222 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 223 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 224 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 225 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 226 | CLANG_WARN_STRICT_PROTOTYPES = YES; 227 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 228 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 229 | CLANG_WARN_UNREACHABLE_CODE = YES; 230 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 231 | COPY_PHASE_STRIP = NO; 232 | DEBUG_INFORMATION_FORMAT = dwarf; 233 | ENABLE_STRICT_OBJC_MSGSEND = YES; 234 | ENABLE_TESTABILITY = YES; 235 | GCC_C_LANGUAGE_STANDARD = gnu11; 236 | GCC_DYNAMIC_NO_PIC = NO; 237 | GCC_NO_COMMON_BLOCKS = YES; 238 | GCC_OPTIMIZATION_LEVEL = 0; 239 | GCC_PREPROCESSOR_DEFINITIONS = ( 240 | "DEBUG=1", 241 | "$(inherited)", 242 | ); 243 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 244 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 245 | GCC_WARN_UNDECLARED_SELECTOR = YES; 246 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 247 | GCC_WARN_UNUSED_FUNCTION = YES; 248 | GCC_WARN_UNUSED_VARIABLE = YES; 249 | IPHONEOS_DEPLOYMENT_TARGET = 12.4; 250 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 251 | MTL_FAST_MATH = YES; 252 | ONLY_ACTIVE_ARCH = YES; 253 | SDKROOT = iphoneos; 254 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG DEV_ENV DEBUG_NOTIFICATONS"; 255 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 256 | }; 257 | name = Debug; 258 | }; 259 | F83BE14B242DDC460043FA1E /* Debug */ = { 260 | isa = XCBuildConfiguration; 261 | buildSettings = { 262 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 263 | CODE_SIGN_STYLE = Automatic; 264 | CURRENT_PROJECT_VERSION = 1; 265 | DEVELOPMENT_TEAM = JH7CQD4MLU; 266 | INFOPLIST_FILE = STARSampleApp/Info.plist; 267 | IPHONEOS_DEPLOYMENT_TARGET = 12.4; 268 | LD_RUNPATH_SEARCH_PATHS = ( 269 | "$(inherited)", 270 | "@executable_path/Frameworks", 271 | ); 272 | OTHER_SWIFT_FLAGS = ""; 273 | PRODUCT_BUNDLE_IDENTIFIER = ch.ubique.starsampleapp; 274 | PRODUCT_NAME = "$(TARGET_NAME)"; 275 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 276 | SWIFT_VERSION = 5.0; 277 | TARGETED_DEVICE_FAMILY = "1,2"; 278 | }; 279 | name = Debug; 280 | }; 281 | F85D4F43243F1C1800529168 /* Release */ = { 282 | isa = XCBuildConfiguration; 283 | buildSettings = { 284 | ALWAYS_SEARCH_USER_PATHS = NO; 285 | BUILD_NUMBER = 1; 286 | CLANG_ANALYZER_NONNULL = YES; 287 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 288 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 289 | CLANG_CXX_LIBRARY = "libc++"; 290 | CLANG_ENABLE_MODULES = YES; 291 | CLANG_ENABLE_OBJC_ARC = YES; 292 | CLANG_ENABLE_OBJC_WEAK = YES; 293 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 294 | CLANG_WARN_BOOL_CONVERSION = YES; 295 | CLANG_WARN_COMMA = YES; 296 | CLANG_WARN_CONSTANT_CONVERSION = YES; 297 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 298 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 299 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 300 | CLANG_WARN_EMPTY_BODY = YES; 301 | CLANG_WARN_ENUM_CONVERSION = YES; 302 | CLANG_WARN_INFINITE_RECURSION = YES; 303 | CLANG_WARN_INT_CONVERSION = YES; 304 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 305 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 306 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 307 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 308 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 309 | CLANG_WARN_STRICT_PROTOTYPES = YES; 310 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 311 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 312 | CLANG_WARN_UNREACHABLE_CODE = YES; 313 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 314 | COPY_PHASE_STRIP = NO; 315 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 316 | ENABLE_NS_ASSERTIONS = NO; 317 | ENABLE_STRICT_OBJC_MSGSEND = YES; 318 | GCC_C_LANGUAGE_STANDARD = gnu11; 319 | GCC_NO_COMMON_BLOCKS = YES; 320 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 321 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 322 | GCC_WARN_UNDECLARED_SELECTOR = YES; 323 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 324 | GCC_WARN_UNUSED_FUNCTION = YES; 325 | GCC_WARN_UNUSED_VARIABLE = YES; 326 | IPHONEOS_DEPLOYMENT_TARGET = 12.4; 327 | MTL_ENABLE_DEBUG_INFO = NO; 328 | MTL_FAST_MATH = YES; 329 | SDKROOT = iphoneos; 330 | SWIFT_COMPILATION_MODE = wholemodule; 331 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 332 | VALIDATE_PRODUCT = YES; 333 | }; 334 | name = Release; 335 | }; 336 | F85D4F44243F1C1800529168 /* Release */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 340 | CODE_SIGN_STYLE = Automatic; 341 | CURRENT_PROJECT_VERSION = 1; 342 | DEVELOPMENT_TEAM = JH7CQD4MLU; 343 | INFOPLIST_FILE = STARSampleApp/Info.plist; 344 | IPHONEOS_DEPLOYMENT_TARGET = 12.4; 345 | LD_RUNPATH_SEARCH_PATHS = ( 346 | "$(inherited)", 347 | "@executable_path/Frameworks", 348 | ); 349 | OTHER_SWIFT_FLAGS = ""; 350 | PRODUCT_BUNDLE_IDENTIFIER = ch.ubique.starsampleapp; 351 | PRODUCT_NAME = "$(TARGET_NAME)"; 352 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; 353 | SWIFT_VERSION = 5.0; 354 | TARGETED_DEVICE_FAMILY = "1,2"; 355 | }; 356 | name = Release; 357 | }; 358 | /* End XCBuildConfiguration section */ 359 | 360 | /* Begin XCConfigurationList section */ 361 | F83BE131242DDC450043FA1E /* Build configuration list for PBXProject "STARSampleApp" */ = { 362 | isa = XCConfigurationList; 363 | buildConfigurations = ( 364 | F83BE148242DDC460043FA1E /* Debug */, 365 | F85D4F43243F1C1800529168 /* Release */, 366 | ); 367 | defaultConfigurationIsVisible = 0; 368 | defaultConfigurationName = Debug; 369 | }; 370 | F83BE14A242DDC460043FA1E /* Build configuration list for PBXNativeTarget "STARSampleApp" */ = { 371 | isa = XCConfigurationList; 372 | buildConfigurations = ( 373 | F83BE14B242DDC460043FA1E /* Debug */, 374 | F85D4F44243F1C1800529168 /* Release */, 375 | ); 376 | defaultConfigurationIsVisible = 0; 377 | defaultConfigurationName = Debug; 378 | }; 379 | /* End XCConfigurationList section */ 380 | 381 | /* Begin XCRemoteSwiftPackageReference section */ 382 | F83BE14E242DDFF60043FA1E /* XCRemoteSwiftPackageReference "SnapKit" */ = { 383 | isa = XCRemoteSwiftPackageReference; 384 | repositoryURL = "https://github.com/SnapKit/SnapKit"; 385 | requirement = { 386 | kind = upToNextMajorVersion; 387 | minimumVersion = 5.0.1; 388 | }; 389 | }; 390 | /* End XCRemoteSwiftPackageReference section */ 391 | 392 | /* Begin XCSwiftPackageProductDependency section */ 393 | F812AC5E243E08BF005F26AE /* STARSDK_CALIBRATION */ = { 394 | isa = XCSwiftPackageProductDependency; 395 | productName = STARSDK_CALIBRATION; 396 | }; 397 | F83BE14F242DDFF60043FA1E /* SnapKit */ = { 398 | isa = XCSwiftPackageProductDependency; 399 | package = F83BE14E242DDFF60043FA1E /* XCRemoteSwiftPackageReference "SnapKit" */; 400 | productName = SnapKit; 401 | }; 402 | /* End XCSwiftPackageProductDependency section */ 403 | }; 404 | rootObject = F83BE12E242DDC450043FA1E /* Project object */; 405 | } 406 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SnapKit", 6 | "repositoryURL": "https://github.com/SnapKit/SnapKit", 7 | "state": { 8 | "branch": null, 9 | "revision": "d458564516e5676af9c70b4f4b2a9178294f1bc6", 10 | "version": "5.0.1" 11 | } 12 | }, 13 | { 14 | "package": "SQLite.swift", 15 | "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "0a9893ec030501a3956bee572d6b4fdd3ae158a1", 19 | "version": "0.12.2" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp.xcodeproj/xcshareddata/xcschemes/Debug.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp.xcodeproj/xcshareddata/xcschemes/Release.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import os 2 | import STARSDK_CALIBRATION 3 | import UIKit 4 | 5 | @UIApplicationMain 6 | class AppDelegate: UIResponder, UIApplicationDelegate { 7 | var window: UIWindow? 8 | 9 | func application(_ application: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 10 | STARTracing.reconnectionDelay = Default.shared.reconnectionDelay 11 | try! STARTracing.initialize(with: "ch.ubique.starsdk.sample", enviroment: .dev, mode: .calibration(identifierPrefix: Default.shared.identifierPrefix ?? "AAAA")) 12 | 13 | if application.applicationState != .background { 14 | initWindow() 15 | } 16 | 17 | switch Default.shared.tracingMode { 18 | case .none: 19 | break 20 | case .active: 21 | try? STARTracing.startTracing() 22 | case .activeAdvertising: 23 | try? STARTracing.startAdvertising() 24 | case .activeReceiving: 25 | try? STARTracing.startReceiving() 26 | } 27 | 28 | return true 29 | } 30 | 31 | func initWindow() { 32 | window = UIWindow(frame: UIScreen.main.bounds) 33 | window?.makeKey() 34 | window?.rootViewController = RootViewController() 35 | window?.makeKeyAndVisible() 36 | } 37 | 38 | func applicationWillEnterForeground(_: UIApplication) { 39 | if window == nil { 40 | initWindow() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /SampleApp/STARSampleApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SampleApp/STARSampleApp/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp/ControlViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import SnapKit 4 | import STARSDK_CALIBRATION 5 | import UIKit 6 | 7 | class ControlViewController: UIViewController { 8 | let segmentedControl = UISegmentedControl(items: ["On", "Off"]) 9 | 10 | let startAdvertisingButton = UIButton() 11 | let startReceivingButton = UIButton() 12 | 13 | let statusLabel = UILabel() 14 | 15 | let stackView = UIStackView() 16 | 17 | let identifierInput = UITextField() 18 | 19 | init() { 20 | super.init(nibName: nil, bundle: nil) 21 | title = "Controls" 22 | if #available(iOS 13.0, *) { 23 | tabBarItem = UITabBarItem(title: title, image: UIImage(systemName: "doc.text"), tag: 0) 24 | } 25 | segmentedControl.selectedSegmentIndex = 1 26 | segmentedControl.addTarget(self, action: #selector(segmentedControlChanges), for: .valueChanged) 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | if #available(iOS 13.0, *) { 32 | self.view.backgroundColor = .systemBackground 33 | } else { 34 | view.backgroundColor = .white 35 | } 36 | view.addSubview(stackView) 37 | stackView.snp.makeConstraints { make in 38 | make.left.right.bottom.equalTo(self.view.layoutMarginsGuide) 39 | make.top.equalTo(self.view.layoutMarginsGuide).inset(12) 40 | } 41 | stackView.axis = .vertical 42 | 43 | statusLabel.font = .systemFont(ofSize: 18) 44 | statusLabel.textAlignment = .center 45 | statusLabel.numberOfLines = 0 46 | if #available(iOS 13.0, *) { 47 | statusLabel.backgroundColor = .systemGroupedBackground 48 | } else { 49 | statusLabel.backgroundColor = .lightGray 50 | } 51 | STARTracing.status { result in 52 | switch result { 53 | case let .success(state): 54 | self.updateUI(state) 55 | case .failure: 56 | break 57 | } 58 | } 59 | 60 | stackView.addArrangedSubview(statusLabel) 61 | stackView.addSpacerView(18) 62 | 63 | do { 64 | let label = UILabel() 65 | label.text = "Start / Stop Bluetooth Service" 66 | stackView.addArrangedSubview(label) 67 | stackView.addArrangedSubview(segmentedControl) 68 | 69 | if #available(iOS 13.0, *) { 70 | startAdvertisingButton.setTitleColor(.systemBlue, for: .normal) 71 | startAdvertisingButton.setTitleColor(.systemGray, for: .highlighted) 72 | startAdvertisingButton.setTitleColor(.systemGray2, for: .disabled) 73 | } else { 74 | startAdvertisingButton.setTitleColor(.blue, for: .normal) 75 | startAdvertisingButton.setTitleColor(.black, for: .highlighted) 76 | startAdvertisingButton.setTitleColor(.lightGray, for: .disabled) 77 | } 78 | startAdvertisingButton.setTitle("Start Advertising", for: .normal) 79 | startAdvertisingButton.addTarget(self, action: #selector(startAdvertising), for: .touchUpInside) 80 | 81 | stackView.addArrangedSubview(startAdvertisingButton) 82 | 83 | if #available(iOS 13.0, *) { 84 | startReceivingButton.setTitleColor(.systemBlue, for: .normal) 85 | startReceivingButton.setTitleColor(.systemGray, for: .highlighted) 86 | startReceivingButton.setTitleColor(.systemGray2, for: .disabled) 87 | } else { 88 | startReceivingButton.setTitleColor(.blue, for: .normal) 89 | startReceivingButton.setTitleColor(.black, for: .highlighted) 90 | startReceivingButton.setTitleColor(.lightGray, for: .disabled) 91 | } 92 | startReceivingButton.setTitle("Start Receiving", for: .normal) 93 | startReceivingButton.addTarget(self, action: #selector(startReceiving), for: .touchUpInside) 94 | 95 | stackView.addArrangedSubview(startReceivingButton) 96 | } 97 | 98 | stackView.addSpacerView(12) 99 | 100 | do { 101 | let button = UIButton() 102 | if #available(iOS 13.0, *) { 103 | button.setTitleColor(.systemBlue, for: .normal) 104 | button.setTitleColor(.systemGray, for: .highlighted) 105 | } else { 106 | button.setTitleColor(.blue, for: .normal) 107 | button.setTitleColor(.black, for: .highlighted) 108 | } 109 | button.setTitle("Reset", for: .normal) 110 | button.addTarget(self, action: #selector(reset), for: .touchUpInside) 111 | stackView.addArrangedSubview(button) 112 | } 113 | 114 | stackView.addSpacerView(12) 115 | 116 | do { 117 | let button = UIButton() 118 | if #available(iOS 13.0, *) { 119 | button.setTitleColor(.systemBlue, for: .normal) 120 | button.setTitleColor(.systemGray, for: .highlighted) 121 | } else { 122 | button.setTitleColor(.blue, for: .normal) 123 | button.setTitleColor(.black, for: .highlighted) 124 | } 125 | button.setTitle("Set Infected", for: .normal) 126 | button.addTarget(self, action: #selector(setExposed), for: .touchUpInside) 127 | stackView.addArrangedSubview(button) 128 | } 129 | stackView.addSpacerView(12) 130 | 131 | do { 132 | let button = UIButton() 133 | if #available(iOS 13.0, *) { 134 | button.setTitleColor(.systemBlue, for: .normal) 135 | button.setTitleColor(.systemGray, for: .highlighted) 136 | } else { 137 | button.setTitleColor(.blue, for: .normal) 138 | button.setTitleColor(.black, for: .highlighted) 139 | } 140 | button.setTitle("Synchronize with Backend", for: .normal) 141 | button.addTarget(self, action: #selector(sync), for: .touchUpInside) 142 | stackView.addArrangedSubview(button) 143 | } 144 | 145 | stackView.addSpacerView(12) 146 | 147 | do { 148 | let label = UILabel() 149 | label.text = "Set ID Prefix" 150 | stackView.addArrangedSubview(label) 151 | 152 | identifierInput.text = Default.shared.identifierPrefix ?? "AAAA" 153 | identifierInput.delegate = self 154 | identifierInput.font = UIFont.systemFont(ofSize: 15) 155 | identifierInput.borderStyle = UITextField.BorderStyle.roundedRect 156 | identifierInput.autocorrectionType = UITextAutocorrectionType.no 157 | identifierInput.keyboardType = UIKeyboardType.default 158 | identifierInput.returnKeyType = UIReturnKeyType.done 159 | identifierInput.clearButtonMode = UITextField.ViewMode.whileEditing 160 | identifierInput.contentVerticalAlignment = UIControl.ContentVerticalAlignment.center 161 | identifierInput.delegate = self 162 | stackView.addArrangedSubview(identifierInput) 163 | 164 | let button = UIButton() 165 | if #available(iOS 13.0, *) { 166 | button.setTitleColor(.systemBlue, for: .normal) 167 | button.setTitleColor(.systemGray, for: .highlighted) 168 | } else { 169 | button.setTitleColor(.blue, for: .normal) 170 | button.setTitleColor(.black, for: .highlighted) 171 | } 172 | button.setTitle("Update", for: .normal) 173 | button.addTarget(self, action: #selector(updateIdentifier), for: .touchUpInside) 174 | stackView.addArrangedSubview(button) 175 | } 176 | stackView.addSpacerView(12) 177 | 178 | do { 179 | let button = UIButton() 180 | if #available(iOS 13.0, *) { 181 | button.setTitleColor(.systemBlue, for: .normal) 182 | button.setTitleColor(.systemGray, for: .highlighted) 183 | } else { 184 | button.setTitleColor(.blue, for: .normal) 185 | button.setTitleColor(.black, for: .highlighted) 186 | } 187 | button.setTitle("Share Database", for: .normal) 188 | button.addTarget(self, action: #selector(shareDatabase), for: .touchUpInside) 189 | stackView.addArrangedSubview(button) 190 | } 191 | 192 | stackView.addArrangedSubview(UIView()) 193 | } 194 | 195 | required init?(coder _: NSCoder) { 196 | fatalError("init(coder:) has not been implemented") 197 | } 198 | 199 | @objc func updateIdentifier() { 200 | identifierInput.resignFirstResponder() 201 | Default.shared.identifierPrefix = identifierInput.text 202 | reset() 203 | } 204 | 205 | @objc func sync() { 206 | STARTracing.sync { _ in } 207 | } 208 | 209 | @objc func setExposed() { 210 | STARTracing.iWasExposed(onset: Date(), authString: "") { _ in 211 | STARTracing.status { result in 212 | switch result { 213 | case let .success(state): 214 | self.updateUI(state) 215 | case .failure: 216 | break 217 | } 218 | } 219 | } 220 | } 221 | 222 | @objc func shareDatabase() { 223 | let acv = UIActivityViewController(activityItems: [Self.getDatabasePath()], applicationActivities: nil) 224 | if let popoverController = acv.popoverPresentationController { 225 | popoverController.sourceView = view 226 | } 227 | present(acv, animated: true) 228 | } 229 | 230 | @objc func reset() { 231 | STARTracing.stopTracing() 232 | try? STARTracing.reset() 233 | NotificationCenter.default.post(name: Notification.Name("ClearData"), object: nil) 234 | try! STARTracing.initialize(with: "ch.ubique.starsdk.sample", enviroment: .dev, mode: .calibration(identifierPrefix: Default.shared.identifierPrefix ?? "AAAA")) 235 | STARTracing.delegate = navigationController?.tabBarController as? STARTracingDelegate 236 | STARTracing.status { result in 237 | switch result { 238 | case let .success(state): 239 | self.updateUI(state) 240 | case .failure: 241 | break 242 | } 243 | } 244 | } 245 | 246 | @objc func segmentedControlChanges() { 247 | if segmentedControl.selectedSegmentIndex == 0 { 248 | try? STARTracing.startTracing() 249 | Default.shared.tracingMode = .active 250 | } else { 251 | STARTracing.stopTracing() 252 | Default.shared.tracingMode = .none 253 | } 254 | } 255 | 256 | @objc func startAdvertising() { 257 | try? STARTracing.startAdvertising() 258 | Default.shared.tracingMode = .activeAdvertising 259 | } 260 | 261 | @objc func startReceiving() { 262 | try? STARTracing.startReceiving() 263 | Default.shared.tracingMode = .activeReceiving 264 | } 265 | 266 | func updateUI(_ state: TracingState) { 267 | var elements: [String] = [] 268 | elements.append(state.trackingState.stringValue) 269 | switch state.trackingState { 270 | case .active, .activeReceiving, .activeAdvertising: 271 | segmentedControl.selectedSegmentIndex = 0 272 | startReceivingButton.isEnabled = false 273 | startAdvertisingButton.isEnabled = false 274 | default: 275 | segmentedControl.selectedSegmentIndex = 1 276 | startReceivingButton.isEnabled = true 277 | startAdvertisingButton.isEnabled = true 278 | } 279 | if let lastSync = state.lastSync { 280 | elements.append(lastSync.stringVal) 281 | } 282 | 283 | switch state.infectionStatus { 284 | case .exposed: 285 | elements.append("InfectionStatus: EXPOSED") 286 | case .infected: 287 | elements.append("InfectionStatus: INFECTED") 288 | case .healthy: 289 | elements.append("InfectionStatus: HEALTHY") 290 | } 291 | elements.append("Handshakes: \(state.numberOfHandshakes)") 292 | 293 | statusLabel.text = elements.joined(separator: "\n") 294 | } 295 | 296 | private static func getDatabasePath() -> URL { 297 | let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) 298 | let documentsDirectory = paths[0] 299 | return documentsDirectory.appendingPathComponent("STAR_tracing_db").appendingPathExtension("sqlite") 300 | } 301 | } 302 | 303 | extension ControlViewController: STARTracingDelegate { 304 | func STARTracingStateChanged(_ state: TracingState) { 305 | updateUI(state) 306 | } 307 | } 308 | 309 | extension ControlViewController: UITextFieldDelegate { 310 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 311 | guard let textFieldText = textField.text, 312 | let rangeOfTextToReplace = Range(range, in: textFieldText) else { 313 | return false 314 | } 315 | let substringToReplace = textFieldText[rangeOfTextToReplace] 316 | let count = textFieldText.count - substringToReplace.count + string.count 317 | return count <= 4 318 | } 319 | 320 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 321 | textField.resignFirstResponder() 322 | return true 323 | } 324 | } 325 | 326 | private extension TrackingState { 327 | var stringValue: String { 328 | switch self { 329 | case .active: 330 | return "active" 331 | case .activeAdvertising: 332 | return "activeAdvertising" 333 | case .activeReceiving: 334 | return "activeReceiving" 335 | case let .inactive(error): 336 | return "inactive \(error.localizedDescription)" 337 | case .stopped: 338 | return "stopped" 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp/Defaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | 5 | /// UserDefaults Storage Singleton 6 | class Default { 7 | static var shared = Default() 8 | var store = UserDefaults.standard 9 | 10 | /// Current infection status 11 | var identifierPrefix: String? { 12 | get { 13 | return store.string(forKey: "ch.ubique.starsdk.sampleapp.identifierPrefix") 14 | } 15 | set(newValue) { 16 | store.set(newValue, forKey: "ch.ubique.starsdk.sampleapp.identifierPrefix") 17 | } 18 | } 19 | 20 | var reconnectionDelay: Int { 21 | get { 22 | return (store.object(forKey: "ch.ubique.starsdk.sampleapp.reconnectionDelay") as? Int) ?? 60 * 5 23 | } 24 | set(newValue) { 25 | store.set(newValue, forKey: "ch.ubique.starsdk.sampleapp.reconnectionDelay") 26 | } 27 | } 28 | 29 | enum TracingMode: Int { 30 | case none = 0 31 | case active = 1 32 | case activeReceiving = 2 33 | case activeAdvertising = 3 34 | } 35 | 36 | var tracingMode: TracingMode { 37 | get { 38 | let mode = (store.object(forKey: "ch.ubique.starsdk.sampleapp.tracingMode") as? Int) ?? 0 39 | return TracingMode(rawValue: mode) ?? .none 40 | } 41 | set(newValue) { 42 | store.set(newValue.rawValue, forKey: "ch.ubique.starsdk.sampleapp.tracingMode") 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp/HandshakeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import SnapKit 4 | import STARSDK_CALIBRATION 5 | import UIKit 6 | 7 | class HandshakeViewController: UIViewController { 8 | private var tableView: UITableView? 9 | 10 | private let MAX_NUMBER_OF_MISSING_HANDSHAKES = 3 11 | private var cachedHandshakeIntervals: [HandshakeInterval] = [] 12 | private var cachedHandshakes: [HandshakeModel] = [] 13 | private var nextRequest: HandshakeRequest? 14 | private let dateFormatter = DateFormatter() 15 | 16 | private enum Mode { 17 | case raw, grouped 18 | } 19 | 20 | private var mode: Mode = .raw { 21 | didSet { 22 | reloadModel() 23 | } 24 | } 25 | 26 | private var didLoadHandshakes = false 27 | 28 | init() { 29 | super.init(nibName: nil, bundle: nil) 30 | title = "HandShakes" 31 | dateFormatter.dateFormat = "dd.MM HH:mm:ss " 32 | if #available(iOS 13.0, *) { 33 | tabBarItem = UITabBarItem(title: title, image: UIImage(systemName: "person.3.fill"), tag: 0) 34 | } 35 | NotificationCenter.default.addObserver(self, selector: #selector(didClearData(notification:)), name: Notification.Name("ClearData"), object: nil) 36 | } 37 | 38 | required init?(coder _: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | tableView = UITableView(frame: .zero, style: .plain) 45 | tableView!.dataSource = self 46 | tableView!.delegate = self 47 | view.addSubview(tableView!) 48 | tableView!.snp.makeConstraints { make in 49 | make.edges.equalToSuperview() 50 | } 51 | 52 | let refreshControl = UIRefreshControl() 53 | refreshControl.attributedTitle = NSAttributedString(string: "Pull to refresh") 54 | refreshControl.addTarget(self, action: #selector(refresh(sender:)), for: UIControl.Event.valueChanged) 55 | tableView!.addSubview(refreshControl) 56 | 57 | let segmentedControl = UISegmentedControl(items: ["Raw", "Grouped"]) 58 | segmentedControl.addTarget(self, action: #selector(groupingChanged(sender:)), for: .valueChanged) 59 | segmentedControl.selectedSegmentIndex = 0 60 | navigationItem.titleView = segmentedControl 61 | 62 | NotificationCenter.default.addObserver(self, selector: #selector(didClearData(notification:)), name: Notification.Name("ClearData"), object: nil) 63 | } 64 | 65 | @objc func didClearData(notification _: Notification) { 66 | cachedHandshakes.removeAll() 67 | cachedHandshakeIntervals.removeAll() 68 | nextRequest = nil 69 | didLoadHandshakes = false 70 | tableView?.reloadData() 71 | } 72 | 73 | override func viewWillAppear(_ animated: Bool) { 74 | super.viewWillAppear(animated) 75 | if !didLoadHandshakes { 76 | didLoadHandshakes = true 77 | reloadModel() 78 | } 79 | } 80 | 81 | @objc func groupingChanged(sender: UISegmentedControl) { 82 | if sender.selectedSegmentIndex == 0 { 83 | mode = .raw 84 | } else { 85 | mode = .grouped 86 | } 87 | } 88 | 89 | @objc func refresh(sender: UIRefreshControl) { 90 | reloadModel() 91 | sender.endRefreshing() 92 | } 93 | 94 | private func reloadModel() { 95 | cachedHandshakeIntervals.removeAll() 96 | cachedHandshakes.removeAll() 97 | nextRequest = nil 98 | switch mode { 99 | case .raw: 100 | loadHandshakes(request: HandshakeRequest(offset: 0, limit: 30), clear: true) 101 | case .grouped: 102 | do { 103 | let response = try STARTracing.getHandshakes(request: HandshakeRequest()) 104 | let groupped = groupHandshakes(response.handshakes) 105 | let intervals = generateIntervalsFrom(grouppedHandshakes: groupped) 106 | cachedHandshakeIntervals = intervals 107 | tableView?.reloadData() 108 | } catch { 109 | let alert = UIAlertController(title: "Error Fetching Handshakes", message: error.localizedDescription, preferredStyle: .alert) 110 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 111 | present(alert, animated: true, completion: nil) 112 | } 113 | } 114 | } 115 | 116 | private func loadNextHandshakes() { 117 | guard let nextRequest = nextRequest else { 118 | return 119 | } 120 | loadHandshakes(request: nextRequest, clear: false) 121 | } 122 | 123 | private func loadHandshakes(request: HandshakeRequest, clear: Bool) { 124 | do { 125 | let response = try STARTracing.getHandshakes(request: request) 126 | nextRequest = response.nextRequest 127 | if clear { 128 | cachedHandshakes = response.handshakes 129 | tableView?.reloadData() 130 | } else if response.handshakes.isEmpty == false { 131 | var indexPathes: [IndexPath] = [] 132 | let base = response.handshakes.count 133 | let target = base + response.handshakes.count 134 | for rowIndex in base ..< target { 135 | indexPathes.append(IndexPath(row: rowIndex, section: 0)) 136 | } 137 | cachedHandshakes.append(contentsOf: response.handshakes) 138 | tableView?.insertRows(at: indexPathes, with: .bottom) 139 | } 140 | } catch { 141 | let alert = UIAlertController(title: "Error Fetching Handshakes", message: error.localizedDescription, preferredStyle: .alert) 142 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 143 | present(alert, animated: true, completion: nil) 144 | } 145 | } 146 | 147 | private func groupHandshakes(_ handshakes: [HandshakeModel]) -> [String: [HandshakeModel]] { 148 | var grouppedHandshakes: [String: [HandshakeModel]] = [:] 149 | for handshake in handshakes { 150 | guard let identifier = handshake.star.STARHeadIndentifier else { 151 | continue 152 | } 153 | var group = grouppedHandshakes[identifier, default: []] 154 | group.append(handshake) 155 | grouppedHandshakes[identifier] = group 156 | } 157 | return grouppedHandshakes 158 | } 159 | 160 | private func generateIntervalsFrom(grouppedHandshakes: [String: [HandshakeModel]]) -> [HandshakeInterval] { 161 | var intervals: [HandshakeInterval] = [] 162 | for (_, group) in grouppedHandshakes.enumerated() { 163 | let sortedGroup = group.value.sorted(by: { $0.timestamp < $1.timestamp }) 164 | var start = 0 165 | var end = 1 166 | while end < sortedGroup.count { 167 | let timeDelay = abs(sortedGroup[end].timestamp.timeIntervalSince(sortedGroup[end - 1].timestamp)) 168 | if timeDelay > Double(MAX_NUMBER_OF_MISSING_HANDSHAKES * STARTracing.reconnectionDelay) { 169 | let startTime = sortedGroup[start].timestamp 170 | let endTime = sortedGroup[end - 1].timestamp 171 | let elapsedTime = abs(startTime.timeIntervalSince(endTime)) 172 | let expectedCount: Int = 1 + Int(ceil(elapsedTime) / Double(STARTracing.reconnectionDelay)) 173 | let interval = HandshakeInterval(identifier: group.key, start: startTime, end: endTime, count: end - start, expectedCount: expectedCount) 174 | intervals.append(interval) 175 | start = end 176 | } 177 | end += 1 178 | } 179 | let startTime = sortedGroup[start].timestamp 180 | let endTime = sortedGroup[end - 1].timestamp 181 | let elapsedTime = abs(startTime.timeIntervalSince(endTime)) 182 | let expectedCount: Int = 1 + Int(ceil(elapsedTime) / Double(STARTracing.reconnectionDelay)) 183 | let interval = HandshakeInterval(identifier: group.key, start: startTime, end: endTime, count: end - start, expectedCount: expectedCount) 184 | intervals.append(interval) 185 | } 186 | return intervals 187 | } 188 | 189 | private struct HandshakeInterval { 190 | let identifier: String 191 | let start: Date 192 | let end: Date 193 | let count: Int 194 | let expectedCount: Int 195 | } 196 | } 197 | 198 | extension HandshakeViewController: UITableViewDataSource { 199 | func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { 200 | switch mode { 201 | case .grouped: 202 | return cachedHandshakeIntervals.count 203 | case .raw: 204 | return cachedHandshakes.count 205 | } 206 | } 207 | 208 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 209 | let cell: UITableViewCell 210 | if let dequeuedCell = tableView.dequeueReusableCell(withIdentifier: "TVCID") { 211 | cell = dequeuedCell 212 | } else { 213 | cell = UITableViewCell(style: .subtitle, reuseIdentifier: "TVCID") 214 | cell.textLabel?.numberOfLines = 0 215 | cell.detailTextLabel?.numberOfLines = 0 216 | cell.selectionStyle = .none 217 | } 218 | 219 | switch mode { 220 | case .grouped: 221 | let interval = cachedHandshakeIntervals[indexPath.row] 222 | cell.textLabel?.text = "\(dateFormatter.string(from: interval.start)) -> \(dateFormatter.string(from: interval.end))" 223 | cell.detailTextLabel?.text = "\(interval.identifier) - \(interval.count) / \(interval.expectedCount)" 224 | case .raw: 225 | let handshake = cachedHandshakes[indexPath.row] 226 | let star = handshake.star 227 | cell.textLabel?.text = (star.STARHeadIndentifier ?? "Unknown") + " - " + star.hexEncodedString 228 | let distance: String = handshake.distance == nil ? "--" : String(format: "%.2fm", handshake.distance!) 229 | let tx: String = handshake.TXPowerlevel == nil ? " -- " : String(format: "%.2f", handshake.TXPowerlevel!) 230 | let rssi: String = handshake.RSSI == nil ? " -- " : String(format: "%.2f", handshake.RSSI!) 231 | cell.detailTextLabel?.text = "\(dateFormatter.string(from: handshake.timestamp)), distance: est. \(distance), TX: \(tx), RSSI: \(rssi), \(handshake.knownCaseId != nil ? "Exposed" : "Not Exposed")" 232 | } 233 | 234 | return cell 235 | } 236 | } 237 | 238 | extension HandshakeViewController: UITableViewDelegate { 239 | func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity _: CGPoint, targetContentOffset: UnsafeMutablePointer) { 240 | let targetOffset = CGFloat(targetContentOffset.pointee.y) 241 | let maximumOffset = scrollView.adjustedContentInset.bottom + scrollView.contentSize.height - scrollView.frame.size.height 242 | 243 | if maximumOffset - targetOffset <= 40.0 { 244 | loadNextHandshakes() 245 | } 246 | } 247 | } 248 | 249 | extension HandshakeViewController: STARTracingDelegate { 250 | func STARTracingStateChanged(_: TracingState) {} 251 | 252 | func didAddHandshake(_ handshake: HandshakeModel) { 253 | switch mode { 254 | case .raw: 255 | cachedHandshakes.insert(handshake, at: 0) 256 | tableView?.insertRows(at: [IndexPath(row: 0, section: 0)], with: .top) 257 | case .grouped: 258 | reloadModel() 259 | } 260 | } 261 | } 262 | 263 | extension Data { 264 | var hexEncodedString: String { 265 | return map { String(format: "%02hhx ", $0) }.joined() 266 | } 267 | 268 | var STARHeadIndentifier: String? { 269 | let head = self.prefix(4) 270 | guard let identifier = String(data: head, encoding: .utf8) else { 271 | return nil 272 | } 273 | return identifier 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | LSRequiresIPhoneOS 22 | 23 | NSBluetoothAlwaysUsageDescription 24 | I need the bluetooth 25 | NSBluetoothPeripheralUsageDescription 26 | i need the bluetooth 27 | UIBackgroundModes 28 | 29 | bluetooth-central 30 | bluetooth-peripheral 31 | 32 | UIFileSharingEnabled 33 | 34 | UILaunchStoryboardName 35 | LaunchScreen 36 | UIRequiredDeviceCapabilities 37 | 38 | armv7 39 | 40 | UISupportedInterfaceOrientations 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UISupportedInterfaceOrientations~ipad 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationPortraitUpsideDown 50 | UIInterfaceOrientationLandscapeLeft 51 | UIInterfaceOrientationLandscapeRight 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp/LogsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogsViewController.swift 3 | // STARSampleApp 4 | // 5 | // Created by Stefan Mitterrutzner on 08.04.20. 6 | // Copyright © 2020 Ubique. All rights reserved. 7 | // 8 | 9 | import STARSDK_CALIBRATION 10 | import UIKit 11 | 12 | class LogCell: UITableViewCell { 13 | override init(style _: UITableViewCell.CellStyle, reuseIdentifier: String?) { 14 | super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) 15 | textLabel?.numberOfLines = 0 16 | textLabel?.font = .boldSystemFont(ofSize: 12.0) 17 | detailTextLabel?.numberOfLines = 0 18 | selectionStyle = .none 19 | } 20 | 21 | required init?(coder _: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | } 25 | 26 | class LogsViewController: UIViewController { 27 | let tableView = UITableView() 28 | 29 | let refreshControl = UIRefreshControl() 30 | 31 | var logs: [LogEntry] = [] 32 | 33 | var nextRequest: LogRequest? 34 | 35 | init() { 36 | super.init(nibName: nil, bundle: nil) 37 | title = "Logs" 38 | if #available(iOS 13.0, *) { 39 | tabBarItem = UITabBarItem(title: title, image: UIImage(systemName: "list.bullet"), tag: 0) 40 | } 41 | loadLogs() 42 | NotificationCenter.default.addObserver(self, selector: #selector(didClearData(notification:)), name: Notification.Name("ClearData"), object: nil) 43 | } 44 | 45 | required init?(coder _: NSCoder) { 46 | fatalError("init(coder:) has not been implemented") 47 | } 48 | 49 | override func loadView() { 50 | view = tableView 51 | } 52 | 53 | override func viewDidLoad() { 54 | super.viewDidLoad() 55 | tableView.register(LogCell.self, forCellReuseIdentifier: "logCell") 56 | tableView.refreshControl = refreshControl 57 | tableView.dataSource = self 58 | refreshControl.addTarget(self, action: #selector(reloadLogs), for: .allEvents) 59 | } 60 | 61 | @objc func didClearData(notification _: Notification) { 62 | logs = [] 63 | tableView.reloadData() 64 | } 65 | 66 | @objc 67 | func reloadLogs() { 68 | loadLogs() 69 | } 70 | 71 | func loadLogs(request: LogRequest = LogRequest(sorting: .desc, offset: 0, limit: 200)) { 72 | DispatchQueue.global(qos: .background).async { 73 | if let resp = try? STARTracing.getLogs(request: request) { 74 | self.nextRequest = resp.nextRequest 75 | DispatchQueue.main.async { 76 | self.refreshControl.endRefreshing() 77 | if request.offset == 0 { 78 | self.logs = resp.logs 79 | } else { 80 | self.logs.append(contentsOf: resp.logs) 81 | self.tableView.reloadData() 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | extension LogsViewController: UITableViewDataSource { 90 | func numberOfSections(in _: UITableView) -> Int { 91 | return 1 92 | } 93 | 94 | func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { 95 | return logs.count 96 | } 97 | 98 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 99 | if indexPath.row == (logs.count - 1), 100 | let nextRequest = self.nextRequest { 101 | loadLogs(request: nextRequest) 102 | } 103 | let cell = tableView.dequeueReusableCell(withIdentifier: "logCell", for: indexPath) as! LogCell 104 | let log = logs[indexPath.row] 105 | cell.textLabel?.text = "\(log.timestamp.stringVal) \(log.type.description)" 106 | cell.detailTextLabel?.text = log.message 107 | switch log.type { 108 | case .sender: 109 | cell.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 0.1) 110 | case .receiver: 111 | cell.backgroundColor = UIColor(red: 0, green: 1, blue: 0, alpha: 0.1) 112 | default: 113 | cell.backgroundColor = .clear 114 | } 115 | return cell 116 | } 117 | } 118 | 119 | extension LogsViewController: STARTracingDelegate { 120 | func STARTracingStateChanged(_: TracingState) {} 121 | 122 | func didAddLog(_ entry: LogEntry) { 123 | logs.insert(entry, at: 0) 124 | if view.superview != nil { 125 | tableView.reloadData() 126 | } 127 | nextRequest?.offset += 1 128 | } 129 | } 130 | 131 | extension Date { 132 | var stringVal: String { 133 | let dateFormatter = DateFormatter() 134 | dateFormatter.dateFormat = "dd.MM HH:mm:ss " 135 | return dateFormatter.string(from: self) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp/ParametersViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import STARSDK_CALIBRATION 4 | import UIKit 5 | 6 | class ParametersViewController: UIViewController { 7 | let stackView = UIStackView() 8 | 9 | let reconnectionDelayInput = UITextField() 10 | 11 | init() { 12 | super.init(nibName: nil, bundle: nil) 13 | title = "Parameters" 14 | if #available(iOS 13.0, *) { 15 | tabBarItem = UITabBarItem(title: title, image: UIImage(systemName: "wrench.fill"), tag: 0) 16 | } 17 | } 18 | 19 | required init?(coder _: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | if #available(iOS 13.0, *) { 26 | self.view.backgroundColor = .systemBackground 27 | } else { 28 | view.backgroundColor = .white 29 | } 30 | view.addSubview(stackView) 31 | stackView.snp.makeConstraints { make in 32 | make.left.right.bottom.equalTo(self.view.layoutMarginsGuide) 33 | make.top.equalTo(self.view.layoutMarginsGuide).inset(12) 34 | } 35 | stackView.axis = .vertical 36 | 37 | do { 38 | let label = UILabel() 39 | label.text = "Set Reconnection Delay (seconds)" 40 | stackView.addArrangedSubview(label) 41 | 42 | reconnectionDelayInput.text = "\(Default.shared.reconnectionDelay)" 43 | reconnectionDelayInput.delegate = self 44 | reconnectionDelayInput.font = UIFont.systemFont(ofSize: 15) 45 | reconnectionDelayInput.borderStyle = UITextField.BorderStyle.roundedRect 46 | reconnectionDelayInput.autocorrectionType = UITextAutocorrectionType.no 47 | reconnectionDelayInput.keyboardType = UIKeyboardType.numberPad 48 | reconnectionDelayInput.returnKeyType = UIReturnKeyType.done 49 | reconnectionDelayInput.clearButtonMode = UITextField.ViewMode.whileEditing 50 | reconnectionDelayInput.contentVerticalAlignment = UIControl.ContentVerticalAlignment.center 51 | reconnectionDelayInput.delegate = self 52 | stackView.addArrangedSubview(reconnectionDelayInput) 53 | 54 | let button = UIButton() 55 | if #available(iOS 13.0, *) { 56 | button.setTitleColor(.systemBlue, for: .normal) 57 | button.setTitleColor(.systemGray, for: .highlighted) 58 | } else { 59 | button.setTitleColor(.blue, for: .normal) 60 | button.setTitleColor(.black, for: .highlighted) 61 | } 62 | button.setTitle("Update", for: .normal) 63 | button.addTarget(self, action: #selector(updateReconnectionDelay), for: .touchUpInside) 64 | stackView.addArrangedSubview(button) 65 | } 66 | 67 | stackView.addArrangedView(UIView()) 68 | } 69 | 70 | @objc func updateReconnectionDelay() { 71 | let delay = reconnectionDelayInput.text ?? "0" 72 | let intDelay = Int(delay) ?? 0 73 | Default.shared.reconnectionDelay = intDelay 74 | reconnectionDelayInput.text = "\(Default.shared.reconnectionDelay)" 75 | STARTracing.reconnectionDelay = Default.shared.reconnectionDelay 76 | reconnectionDelayInput.resignFirstResponder() 77 | } 78 | } 79 | 80 | extension ParametersViewController: STARTracingDelegate { 81 | func STARTracingStateChanged(_: TracingState) {} 82 | } 83 | 84 | extension ParametersViewController: UITextFieldDelegate { 85 | func textField(_: UITextField, shouldChangeCharactersIn _: NSRange, replacementString string: String) -> Bool { 86 | let allowedCharacters = CharacterSet.decimalDigits 87 | let characterSet = CharacterSet(charactersIn: string) 88 | return allowedCharacters.isSuperset(of: characterSet) 89 | } 90 | 91 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 92 | textField.resignFirstResponder() 93 | return true 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp/RootViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import STARSDK_CALIBRATION 4 | import UIKit 5 | 6 | class RootViewController: UITabBarController { 7 | var logsViewController = LogsViewController() 8 | var controlsViewController = ControlViewController() 9 | var parameterViewController = ParametersViewController() 10 | var handshakeViewController = HandshakeViewController() 11 | 12 | lazy var tabs: [UIViewController] = [controlsViewController, 13 | logsViewController, 14 | parameterViewController, 15 | handshakeViewController] 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | viewControllers = tabs.map(UINavigationController.init(rootViewController:)) 20 | 21 | STARTracing.delegate = self 22 | } 23 | } 24 | 25 | extension RootViewController: STARTracingDelegate { 26 | func STARTracingStateChanged(_ state: TracingState) { 27 | tabs 28 | .compactMap { $0 as? STARTracingDelegate } 29 | .forEach { $0.STARTracingStateChanged(state) } 30 | } 31 | 32 | func didAddLog(_ entry: LogEntry) { 33 | tabs 34 | .compactMap { $0 as? STARTracingDelegate } 35 | .forEach { $0.didAddLog(entry) } 36 | } 37 | 38 | func didAddHandshake(_ handshake: HandshakeModel) { 39 | tabs 40 | .compactMap { $0 as? STARTracingDelegate } 41 | .forEach { $0.didAddHandshake(handshake) } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SampleApp/STARSampleApp/StackView+Spacing.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import UIKit 4 | 5 | extension UIStackView { 6 | func addArrangedView(_ view: UIView, size: CGFloat? = nil, insets: UIEdgeInsets? = nil) { 7 | if let h = size, axis == .vertical { 8 | view.snp.makeConstraints { make in 9 | make.height.equalTo(h) 10 | } 11 | } else if let w = size, axis == .horizontal { 12 | view.snp.makeConstraints { make in 13 | make.width.equalTo(w) 14 | } 15 | } 16 | 17 | addArrangedSubview(view) 18 | 19 | if let insets = insets { 20 | view.snp.makeConstraints { make in 21 | if axis == .vertical { 22 | make.leading.trailing.equalToSuperview().inset(insets) 23 | } else { 24 | make.top.bottom.equalToSuperview().inset(insets) 25 | } 26 | } 27 | } 28 | } 29 | 30 | func addSpacerView(_ size: CGFloat, color: UIColor? = nil, insets: UIEdgeInsets? = nil) { 31 | let extraSpacer = UIView() 32 | extraSpacer.backgroundColor = color 33 | addArrangedView(extraSpacer, size: size) 34 | if let insets = insets { 35 | extraSpacer.snp.makeConstraints { make in 36 | if axis == .vertical { 37 | make.leading.trailing.equalToSuperview().inset(insets) 38 | } else { 39 | make.top.bottom.equalToSuperview().inset(insets) 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SampleApp/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier ENV["APP_IDENTIFIER"] 2 | -------------------------------------------------------------------------------- /SampleApp/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:ios) 2 | 3 | platform :ios do 4 | desc "Push a new build to AppCenter" 5 | lane :distribute do 6 | 7 | create_keychain( 8 | name: "githubactionci", 9 | password: "githubkeychain", 10 | default_keychain: true, 11 | unlock: true 12 | ) 13 | 14 | match( 15 | type: "enterprise", 16 | readonly: is_ci, 17 | keychain_name: "githubactionci", 18 | keychain_password: "githubkeychain" 19 | ) 20 | 21 | update_code_signing_settings( 22 | use_automatic_signing: false, 23 | path: ENV["XCODE_PROJ"], 24 | profile_name: ENV["sigh_"+ENV["APP_IDENTIFIER"] +"_enterprise_profile-name"], 25 | code_sign_identity: "iPhone Distribution", 26 | bundle_identifier: ENV["APP_IDENTIFIER"] 27 | ) 28 | 29 | build_app( 30 | scheme: "Release", 31 | ) 32 | 33 | appcenter_upload( 34 | owner_type: "organization", 35 | file: ENV["IPA_OUTPUT_PATH"], 36 | upload_build_only: true, 37 | destinations: "Collaborators,Internal" 38 | ) 39 | end 40 | end 41 | 42 | 43 | -------------------------------------------------------------------------------- /SampleApp/fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'fastlane-plugin-appcenter' 6 | -------------------------------------------------------------------------------- /Sources/Config.xcconfig: -------------------------------------------------------------------------------- 1 | IPHONEOS_DEPLOYMENT_TARGET = 10.0 2 | -------------------------------------------------------------------------------- /Sources/STARSDK/Bluetooth/BluetoothBroadcastService.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import CoreBluetooth 4 | import Foundation 5 | import UIKit 6 | 7 | /// A service to broadcast bluetooth packets containing the STAR token 8 | class BluetoothBroadcastService: NSObject { 9 | /// The peripheral manager 10 | private var peripheralManager: CBPeripheralManager? 11 | /// The broadcasted service 12 | private var service: CBMutableService? 13 | 14 | /// The STAR crypto algorithm 15 | private weak var starCrypto: STARCryptoModule? 16 | 17 | /// Random device name for enhanced privacy 18 | private var localName: String = UUID().uuidString 19 | 20 | /// An object that can handle bluetooth permission requests and errors 21 | public weak var permissionDelegate: BluetoothPermissionDelegate? 22 | 23 | #if CALIBRATION 24 | /// A logger to output messages 25 | public weak var logger: LoggingDelegate? 26 | #endif 27 | 28 | 29 | /// Create a Bluetooth broadcaster with a STAR crypto algorithm 30 | /// - Parameter starCrypto: The STAR crypto algorithm 31 | public init(starCrypto: STARCryptoModule) { 32 | self.starCrypto = starCrypto 33 | super.init() 34 | } 35 | 36 | 37 | /// Start the broadcast service 38 | public func startService() { 39 | guard peripheralManager == nil else { 40 | #if CALIBRATION 41 | logger?.log(type: .sender, "startService service already started") 42 | #endif 43 | return 44 | } 45 | peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: [ 46 | CBPeripheralManagerOptionShowPowerAlertKey: NSNumber(booleanLiteral: true), 47 | CBPeripheralManagerOptionRestoreIdentifierKey: "STARTracingPeripheralManagerIdentifier", 48 | ]) 49 | } 50 | 51 | /// Stops the broadcast service 52 | public func stopService() { 53 | #if CALIBRATION 54 | logger?.log(type: .sender, "stopping Services") 55 | #endif 56 | 57 | peripheralManager?.removeAllServices() 58 | peripheralManager?.stopAdvertising() 59 | service = nil 60 | peripheralManager = nil 61 | } 62 | 63 | /// Adds a bluetooth service and broadcast it 64 | private func addService() { 65 | guard peripheralManager?.state == .some(.poweredOn) else { 66 | return 67 | } 68 | service = CBMutableService(type: BluetoothConstants.serviceCBUUID, 69 | primary: true) 70 | let characteristic = CBMutableCharacteristic(type: BluetoothConstants.characteristicsCBUUID, 71 | properties: [.read, .notify], 72 | value: nil, 73 | permissions: .readable) 74 | service?.characteristics = [characteristic] 75 | peripheralManager?.add(service!) 76 | 77 | #if CALIBRATION 78 | logger?.log(type: .sender, "added Service with \(BluetoothConstants.serviceCBUUID.uuidString)") 79 | #endif 80 | } 81 | } 82 | 83 | // MARK: CBPeripheralManagerDelegate implementation 84 | 85 | extension BluetoothBroadcastService: CBPeripheralManagerDelegate { 86 | func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { 87 | #if CALIBRATION 88 | logger?.log(type: .sender, state: peripheral.state, prefix: "peripheralManagerDidUpdateState") 89 | #endif 90 | 91 | switch peripheral.state { 92 | case .poweredOn where service == nil: 93 | addService() 94 | case .poweredOff: 95 | permissionDelegate?.deviceTurnedOff() 96 | case .unauthorized: 97 | permissionDelegate?.unauthorized() 98 | default: 99 | break 100 | } 101 | } 102 | 103 | func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error _: Error?) { 104 | #if CALIBRATION 105 | logger?.log(type: .sender, state: peripheral.state, prefix: "peripheralManagerdidAddservice") 106 | #endif 107 | 108 | peripheralManager?.startAdvertising([ 109 | CBAdvertisementDataServiceUUIDsKey: [BluetoothConstants.serviceCBUUID], 110 | CBAdvertisementDataLocalNameKey: "", 111 | ]) 112 | } 113 | 114 | #if CALIBRATION 115 | func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) { 116 | logger?.log(type: .sender, state: peripheral.state, prefix: "peripheralManagerDidStartAdvertising") 117 | if let error = error { 118 | logger?.log(type: .sender, "peripheralManagerDidStartAdvertising error: \(error.localizedDescription)") 119 | } 120 | } 121 | #endif 122 | 123 | func peripheralManager(_: CBPeripheralManager, didReceiveRead request: CBATTRequest) { 124 | #if CALIBRATION 125 | logger?.log(type: .sender, "didReceiveRead") 126 | #endif 127 | do { 128 | let data = try starCrypto!.getCurrentEphId() 129 | 130 | switch STARMode.current { 131 | #if CALIBRATION 132 | case let .calibration(identifierPrefix) where identifierPrefix != "": 133 | request.value = identifierPrefix.data(using: .utf8)! + data.prefix(22) 134 | #endif 135 | default: 136 | request.value = data 137 | } 138 | 139 | peripheralManager?.respond(to: request, withResult: .success) 140 | #if CALIBRATION 141 | logger?.log(type: .sender, "← ✅ didReceiveRead: Responded with new token: \(data.hexEncodedString)") 142 | #endif 143 | } catch { 144 | peripheralManager?.respond(to: request, withResult: .unlikelyError) 145 | #if CALIBRATION 146 | logger?.log(type: .sender, "← ❌ didReceiveRead: Could not respond because token was not generated \(error)") 147 | #endif 148 | } 149 | } 150 | 151 | func peripheralManager(_: CBPeripheralManager, willRestoreState dict: [String: Any]) { 152 | if let services: [CBMutableService] = dict[CBPeripheralManagerRestoredStateServicesKey] as? [CBMutableService], 153 | let service = services.first(where: { $0.uuid == BluetoothConstants.serviceCBUUID }) { 154 | self.service = service 155 | #if CALIBRATION 156 | logger?.log(type: .sender, "PeripheralManager#willRestoreState services :\(services.count)") 157 | #endif 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Sources/STARSDK/Bluetooth/BluetoothConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import CoreBluetooth 4 | import Foundation 5 | 6 | /// Some constats used for configuring the bluetooth services 7 | enum BluetoothConstants { 8 | /// Predefined Service CBUUID 9 | static var serviceCBUUID = CBUUID(string: "8c8494e3-bab5-1848-40a0-1b06991c0000") 10 | 11 | /// Predefined Characteristics CBUUID 12 | static var characteristicsCBUUID = CBUUID(string: "8c8494e3-bab5-1848-40a0-1b06991c0001") 13 | 14 | /// The delay after what we reconnect to a device 15 | static var peripheralReconnectDelay: Int = 60 * 5 16 | 17 | /// If we weren't able to connect to a peripheral since x seconds we dont keep track of it 18 | /// This is needed because peripheralId's are roatating 19 | static var peripheralDisposeInterval: TimeInterval = 60 * 60 20 | static var peripheralDisposeIntervalSinceDiscovery: TimeInterval = 10 * 60 21 | 22 | static var androidManufacturerId: UInt16 = 0xABBA 23 | } 24 | -------------------------------------------------------------------------------- /Sources/STARSDK/Bluetooth/BluetoothDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | 5 | /// A delegate to respond to bluetooth discovery callbacks 6 | protocol BluetoothDiscoveryDelegate: class { 7 | /// The discovery service did discover some data and calculated the distance of the source 8 | /// - Parameters: 9 | /// - data: The data received 10 | /// - TXPowerlevel: The TX Power level of both connection devices 11 | /// - RSSI: The RSSI of both connection devices 12 | func didDiscover(data: Data, TXPowerlevel: Double?, RSSI: Double?) throws 13 | } 14 | 15 | /// A delegate that can react to bluetooth permission requests 16 | protocol BluetoothPermissionDelegate: class { 17 | /// The Bluetooth device is turned off 18 | func deviceTurnedOff() 19 | /// The app is not authorized to use bluetooth 20 | func unauthorized() 21 | } 22 | -------------------------------------------------------------------------------- /Sources/STARSDK/Bluetooth/BluetoothDiscoveryService.swift: -------------------------------------------------------------------------------- 1 | import CoreBluetooth 2 | import Foundation 3 | import UIKit.UIApplication 4 | 5 | /// The discovery service responsible of scanning for nearby bluetooth devices offering the STAR service 6 | class BluetoothDiscoveryService: NSObject { 7 | /// The manager 8 | private var manager: CBCentralManager? 9 | 10 | /// A delegate for receiving the discovery callbacks 11 | public weak var delegate: BluetoothDiscoveryDelegate? 12 | 13 | /// A delegate capable of responding to permission requests 14 | public weak var permissionDelegate: BluetoothPermissionDelegate? 15 | 16 | /// The storage for last connecting dates of peripherals 17 | private let storage: PeripheralStorage 18 | 19 | /// A logger for debugging 20 | #if CALIBRATION 21 | public weak var logger: LoggingDelegate? 22 | #endif 23 | 24 | /// A list of peripherals pending for retriving info 25 | private var pendingPeripherals: [CBPeripheral] = [] { 26 | didSet { 27 | if pendingPeripherals.isEmpty { 28 | endBackgroundTask() 29 | } else { 30 | beginBackgroundTask() 31 | } 32 | } 33 | } 34 | 35 | /// A list of peripherals that are about to be discarded 36 | private var peripheralsToDiscard: [CBPeripheral]? 37 | 38 | /// Transmission power levels per discovered peripheral 39 | private var powerLevelsCache: [UUID: Double] = [:] 40 | 41 | /// The computed distance from the discovered peripherals 42 | private var RSSICache: [UUID: Double] = [:] 43 | 44 | /// Identifier of the background task 45 | private var backgroundTask: UIBackgroundTaskIdentifier? 46 | 47 | /// Initialize the discovery object with a storage. 48 | /// - Parameters: 49 | /// - storage: The storage. 50 | init(storage: PeripheralStorage) { 51 | self.storage = storage 52 | super.init() 53 | } 54 | 55 | /// Starts a background task 56 | private func beginBackgroundTask() { 57 | guard backgroundTask == nil else { return } 58 | #if CALIBRATION 59 | logger?.log(type: .receiver, "Starting Background Task") 60 | #endif 61 | backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "ch.ubique.bluetooth.backgroundtask") { 62 | self.endBackgroundTask() 63 | #if CALIBRATION 64 | self.logger?.log(type: .receiver, "Background Task ended") 65 | #endif 66 | } 67 | } 68 | 69 | /// Terminates a Backgroundtask if one is running 70 | private func endBackgroundTask() { 71 | guard let identifier = backgroundTask else { return } 72 | #if CALIBRATION 73 | logger?.log(type: .receiver, "Terminating background Task") 74 | #endif 75 | UIApplication.shared.endBackgroundTask(identifier) 76 | backgroundTask = nil 77 | } 78 | 79 | /// Update all services 80 | private func updateServices() { 81 | guard manager?.state == .some(.poweredOn) else { return } 82 | manager?.scanForPeripherals(withServices: [BluetoothConstants.serviceCBUUID], options: [ 83 | CBCentralManagerOptionShowPowerAlertKey: NSNumber(booleanLiteral: true), 84 | ]) 85 | #if CALIBRATION 86 | DispatchQueue.main.async { 87 | self.logger?.log(type: .receiver, " scanning for \(BluetoothConstants.serviceCBUUID.uuidString)") 88 | } 89 | #endif 90 | } 91 | 92 | /// Start the scanning service for nearby devices 93 | public func startScanning() { 94 | #if CALIBRATION 95 | logger?.log(type: .receiver, " start Scanning") 96 | #endif 97 | if manager != nil { 98 | manager?.stopScan() 99 | manager?.scanForPeripherals(withServices: [BluetoothConstants.serviceCBUUID], options: [ 100 | CBCentralManagerOptionShowPowerAlertKey: NSNumber(booleanLiteral: true), 101 | ]) 102 | #if CALIBRATION 103 | logger?.log(type: .receiver, " scanning for \(BluetoothConstants.serviceCBUUID.uuidString)") 104 | #endif 105 | } else { 106 | manager = CBCentralManager(delegate: self, queue: nil, options: [ 107 | CBCentralManagerOptionShowPowerAlertKey: NSNumber(booleanLiteral: true), 108 | CBCentralManagerOptionRestoreIdentifierKey: "STARTracingCentralManagerIdentifier", 109 | ]) 110 | } 111 | } 112 | 113 | /// Stop scanning for nearby devices 114 | public func stopScanning() { 115 | #if CALIBRATION 116 | logger?.log(type: .receiver, "stop Scanning") 117 | logger?.log(type: .receiver, "going to sleep with \(pendingPeripherals) peripherals \n") 118 | #endif 119 | manager?.stopScan() 120 | manager = nil 121 | pendingPeripherals.removeAll() 122 | endBackgroundTask() 123 | } 124 | } 125 | 126 | // MARK: CBCentralManagerDelegate implementation 127 | 128 | extension BluetoothDiscoveryService: CBCentralManagerDelegate { 129 | func centralManagerDidUpdateState(_ central: CBCentralManager) { 130 | #if CALIBRATION 131 | logger?.log(type: .receiver, state: central.state, prefix: "centralManagerDidUpdateState") 132 | #endif 133 | switch central.state { 134 | case .poweredOn: 135 | #if CALIBRATION 136 | logger?.log(type: .receiver, " scanning for \(BluetoothConstants.serviceCBUUID.uuidString)") 137 | #endif 138 | manager?.scanForPeripherals(withServices: [BluetoothConstants.serviceCBUUID], options: [ 139 | CBCentralManagerOptionShowPowerAlertKey: NSNumber(booleanLiteral: true), 140 | ]) 141 | peripheralsToDiscard?.forEach { peripheral in 142 | try? self.storage.discard(uuid: peripheral.identifier.uuidString) 143 | self.manager?.cancelPeripheralConnection(peripheral) 144 | } 145 | peripheralsToDiscard = nil 146 | case .poweredOff: 147 | permissionDelegate?.deviceTurnedOff() 148 | case .unauthorized: 149 | permissionDelegate?.unauthorized() 150 | default: 151 | break 152 | } 153 | } 154 | 155 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { 156 | #if CALIBRATION 157 | logger?.log(type: .receiver, " didDiscover: \(peripheral), rssi: \(RSSI)db") 158 | #endif 159 | if let power = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? Double { 160 | #if CALIBRATION 161 | logger?.log(type: .receiver, " found TX-Power in Advertisment data: \(power)") 162 | #endif 163 | powerLevelsCache[peripheral.identifier] = power 164 | } else { 165 | #if CALIBRATION 166 | logger?.log(type: .receiver, " TX-Power not available") 167 | #endif 168 | } 169 | RSSICache[peripheral.identifier] = Double(truncating: RSSI) 170 | 171 | if let manuData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data, 172 | manuData.count == CryptoConstants.keyLenght + 2, 173 | manuData[0 ..< 2].withUnsafeBytes({ $0.load(as: UInt16.self) }) == BluetoothConstants.androidManufacturerId { 174 | // drop manufacturer identifier 175 | let data = manuData.dropFirst(2) 176 | 177 | let id = peripheral.identifier 178 | try? delegate?.didDiscover(data: data, TXPowerlevel: powerLevelsCache[id], RSSI: RSSICache[id]) 179 | 180 | #if CALIBRATION 181 | logger?.log(type: .receiver, "got Manufacturer Data \(data.hexEncodedString)") 182 | let identifier = String(data: data[..<4], encoding: .utf8) ?? "Unable to decode" 183 | logger?.log(type: .receiver, " → ✅ Received (identifier over ScanRSP: \(identifier)) (\(data.count) bytes) from \(peripheral.identifier) at \(Date()): \(data.hexEncodedString)") 184 | #endif 185 | 186 | // Cancel connection if it was already made 187 | pendingPeripherals.removeAll(where: { $0.identifier == peripheral.identifier }) 188 | try? storage.discard(uuid: peripheral.identifier.uuidString) 189 | manager?.cancelPeripheralConnection(peripheral) 190 | 191 | } else { 192 | // Only connect if we didn't got manufacturer data 193 | // we only get the manufacturer if iOS is activly scanning 194 | // otherwise we have to connect to the peripheral and read the characteristics 195 | try? storage.setDiscovery(uuid: peripheral.identifier) 196 | pendingPeripherals.append(peripheral) 197 | central.connect(peripheral, options: nil) 198 | } 199 | } 200 | 201 | func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) { 202 | #if CALIBRATION 203 | logger?.log(type: .receiver, " didConnect: \(peripheral)") 204 | #endif 205 | try? storage.setConnection(uuid: peripheral.identifier) 206 | peripheral.delegate = self 207 | peripheral.discoverServices([BluetoothConstants.serviceCBUUID]) 208 | peripheral.readRSSI() 209 | } 210 | 211 | func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { 212 | if let entity = try? storage.get(uuid: peripheral.identifier), 213 | let lastConnection = entity.lastConnection { 214 | if Date().timeIntervalSince(lastConnection) > BluetoothConstants.peripheralDisposeInterval { 215 | #if CALIBRATION 216 | logger?.log(type: .receiver, " didDisconnectPeripheral dispose because last connection was \(Date().timeIntervalSince(lastConnection))seconds ago") 217 | #endif 218 | pendingPeripherals.removeAll(where: { $0.identifier == peripheral.identifier }) 219 | try? storage.discard(uuid: peripheral.identifier.uuidString) 220 | return 221 | } 222 | } 223 | 224 | var delay = 0 225 | if let error = error { 226 | #if CALIBRATION 227 | logger?.log(type: .receiver, " didDisconnectPeripheral (unexpected): \(peripheral) with error: \(error)") 228 | #endif 229 | } else { 230 | #if CALIBRATION 231 | logger?.log(type: .receiver, " didDisconnectPeripheral (successful): \(peripheral)") 232 | #endif 233 | 234 | // Do not re-connect to the same (iOS) peripheral right away again to save battery 235 | delay = BluetoothConstants.peripheralReconnectDelay 236 | } 237 | 238 | central.connect(peripheral, options: [ 239 | CBConnectPeripheralOptionStartDelayKey: NSNumber(integerLiteral: delay), 240 | ]) 241 | } 242 | 243 | func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { 244 | #if CALIBRATION 245 | logger?.log(type: .receiver, " didFailToConnect: \(peripheral)") 246 | logger?.log(type: .receiver, " didFailToConnect error: \(error.debugDescription)") 247 | #endif 248 | 249 | if let entity = try? storage.get(uuid: peripheral.identifier) { 250 | if let lastConnection = entity.lastConnection, 251 | Date().timeIntervalSince(lastConnection) > BluetoothConstants.peripheralDisposeInterval { 252 | #if CALIBRATION 253 | logger?.log(type: .receiver, " didFailToConnect dispose because last connection was \(Date().timeIntervalSince(lastConnection))seconds ago") 254 | #endif 255 | pendingPeripherals.removeAll(where: { $0.identifier == peripheral.identifier }) 256 | try? storage.discard(uuid: peripheral.identifier.uuidString) 257 | return 258 | } else if Date().timeIntervalSince(entity.discoverTime) > BluetoothConstants.peripheralDisposeIntervalSinceDiscovery { 259 | #if CALIBRATION 260 | logger?.log(type: .receiver, " didFailToConnect dispose because connection never suceeded and was \(Date().timeIntervalSince(entity.discoverTime))seconds ago") 261 | #endif 262 | pendingPeripherals.removeAll(where: { $0.identifier == peripheral.identifier }) 263 | try? storage.discard(uuid: peripheral.identifier.uuidString) 264 | return 265 | } 266 | } 267 | 268 | central.connect(peripheral, options: nil) 269 | } 270 | 271 | func centralManager(_: CBCentralManager, willRestoreState dict: [String: Any]) { 272 | #if CALIBRATION 273 | logger?.log(type: .receiver, " CentralManager#willRestoreState") 274 | #endif 275 | if let peripherals: [CBPeripheral] = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { 276 | peripheralsToDiscard = [] 277 | 278 | try? storage.loopThrough(block: { (entity) -> Bool in 279 | var toDiscard: String? 280 | if let lastConnection = entity.lastConnection, 281 | Date().timeIntervalSince(lastConnection) > BluetoothConstants.peripheralDisposeInterval { 282 | toDiscard = entity.uuid 283 | } else if Date().timeIntervalSince(entity.discoverTime) > BluetoothConstants.peripheralDisposeIntervalSinceDiscovery { 284 | toDiscard = entity.uuid 285 | } 286 | if let toDiscard = toDiscard, 287 | let peripheralToDiscard = peripherals.first(where: { $0.identifier.uuidString == toDiscard }) { 288 | peripheralsToDiscard?.append(peripheralToDiscard) 289 | } 290 | return true 291 | }) 292 | 293 | pendingPeripherals.append(contentsOf: peripherals.filter { !(peripheralsToDiscard?.contains($0) ?? false) }) 294 | #if CALIBRATION 295 | logger?.log(type: .receiver, "CentralManager#willRestoreState restoring peripherals \(pendingPeripherals) discarded \(peripheralsToDiscard.debugDescription) \n") 296 | #endif 297 | } 298 | } 299 | 300 | func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error _: Error?) { 301 | RSSICache[peripheral.identifier] = Double(truncating: RSSI) 302 | } 303 | } 304 | 305 | extension BluetoothDiscoveryService: CBPeripheralDelegate { 306 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { 307 | if let error = error { 308 | #if CALIBRATION 309 | logger?.log(type: .receiver, " didDiscoverCharacteristicsFor" + error.localizedDescription) 310 | #endif 311 | return 312 | } 313 | let cbuuid = BluetoothConstants.characteristicsCBUUID 314 | guard let characteristic = service.characteristics?.first(where: { $0.uuid == cbuuid }) else { 315 | return 316 | } 317 | peripheral.readValue(for: characteristic) 318 | #if CALIBRATION 319 | logger?.log(type: .receiver, " found characteristic \(peripheral.name.debugDescription)") 320 | #endif 321 | } 322 | 323 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { 324 | if let error = error { 325 | #if CALIBRATION 326 | logger?.log(type: .receiver, " didUpdateValueFor " + error.localizedDescription) 327 | #endif 328 | manager?.cancelPeripheralConnection(peripheral) 329 | return 330 | } 331 | 332 | guard let data = characteristic.value else { 333 | #if CALIBRATION 334 | logger?.log(type: .receiver, " → ❌ Could not read data from characteristic of \(peripheral.identifier) at \(Date())") 335 | #endif 336 | manager?.cancelPeripheralConnection(peripheral) 337 | return 338 | } 339 | 340 | guard data.count == CryptoConstants.keyLenght else { 341 | #if CALIBRATION 342 | logger?.log(type: .receiver, " → ❌ Received wrong number of bytes (\(data.count) bytes) from \(peripheral.identifier) at \(Date())") 343 | #endif 344 | manager?.cancelPeripheralConnection(peripheral) 345 | return 346 | } 347 | #if CALIBRATION 348 | let identifier = String(data: data[0 ..< 4], encoding: .utf8) ?? "Unable to decode" 349 | logger?.log(type: .receiver, " → ✅ Received (identifier: \(identifier)) (\(data.count) bytes) from \(peripheral.identifier) at \(Date()): \(data.hexEncodedString)") 350 | #endif 351 | manager?.cancelPeripheralConnection(peripheral) 352 | 353 | let id = peripheral.identifier 354 | try? delegate?.didDiscover(data: data, TXPowerlevel: powerLevelsCache[id], RSSI: RSSICache[id]) 355 | } 356 | 357 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { 358 | #if CALIBRATION 359 | logger?.log(type: .receiver, " didDiscoverServices for \(peripheral.identifier)") 360 | #endif 361 | if let error = error { 362 | #if CALIBRATION 363 | logger?.log(type: .receiver, error.localizedDescription) 364 | #endif 365 | return 366 | } 367 | if let service = peripheral.services?.first(where: { $0.uuid == BluetoothConstants.serviceCBUUID }) { 368 | peripheral.discoverCharacteristics([BluetoothConstants.characteristicsCBUUID], for: service) 369 | } else { 370 | #if CALIBRATION 371 | logger?.log(type: .receiver, " No service found 🤬") 372 | #endif 373 | try? storage.discard(uuid: peripheral.identifier.uuidString) 374 | manager?.cancelPeripheralConnection(peripheral) 375 | } 376 | } 377 | } 378 | 379 | extension Data { 380 | var hexEncodedString: String { 381 | return map { String(format: "%02hhx ", $0) }.joined() 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /Sources/STARSDK/Cryptography/Crypto.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import CommonCrypto 4 | import Foundation 5 | 6 | public class Crypto { 7 | public static func sha256(_ data: Data) -> Data { 8 | var digest = Data(count: Int(CC_SHA256_DIGEST_LENGTH)) 9 | digest.withUnsafeMutableBytes { rawMutableBufferPointer in 10 | let bufferPointer = rawMutableBufferPointer.bindMemory(to: UInt8.self) 11 | _ = data.withUnsafeBytes { 12 | CC_SHA256($0.baseAddress, UInt32(data.count), bufferPointer.baseAddress) 13 | } 14 | } 15 | return digest 16 | } 17 | 18 | /// Perform an HMAC function on a message using a secret key 19 | /// - Parameters: 20 | /// - msg: The message to be hashed 21 | /// - key: The key to use for the hash 22 | public static func hmac(msg: Data, key: Data) -> Data { 23 | var macData = Data(count: Int(CC_SHA256_DIGEST_LENGTH)) 24 | macData.withUnsafeMutableBytes { macBytes in 25 | msg.withUnsafeBytes { msgBytes in 26 | key.withUnsafeBytes { keyBytes in 27 | guard let keyAddress = keyBytes.baseAddress, 28 | let msgAddress = msgBytes.baseAddress, 29 | let macAddress = macBytes.baseAddress 30 | else { return } 31 | CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), 32 | keyAddress, key.count, msgAddress, 33 | msg.count, macAddress) 34 | return 35 | } 36 | } 37 | } 38 | return macData 39 | } 40 | 41 | class AESCTREncrypt { 42 | let keyData: Data 43 | 44 | let keyLength: Int 45 | 46 | var cryptor: CCCryptorRef? 47 | 48 | init(keyData: Data) throws { 49 | self.keyData = keyData 50 | 51 | keyLength = keyData.count 52 | 53 | let status = keyData.withUnsafeBytes { keyBytes -> CCCryptorStatus in 54 | let keyBuffer: UnsafeRawPointer = keyBytes.baseAddress! 55 | return CCCryptorCreateWithMode(CCOperation(kCCEncrypt), 56 | CCMode(kCCModeCTR), 57 | CCAlgorithm(kCCAlgorithmAES), 58 | CCPadding(ccNoPadding), 59 | nil, 60 | keyBuffer, 61 | keyLength, 62 | nil, 63 | 0, 64 | 0, 65 | CCOptions(kCCModeOptionCTR_BE), 66 | &cryptor) 67 | } 68 | if status != 0 { 69 | throw CrypoError.AESError 70 | } 71 | } 72 | 73 | deinit { 74 | CCCryptorRelease(cryptor) 75 | } 76 | 77 | func encrypt(data: Data) throws -> Data { 78 | var cryptData = Data(count: data.count) 79 | 80 | var numBytesEncrypted: size_t = 0 81 | 82 | let cryptStatus = cryptData.withUnsafeMutableBytes { cryptBytes -> CCCryptorStatus in 83 | let cryptBuffer: UnsafeMutableRawPointer = cryptBytes.baseAddress! 84 | return data.withUnsafeBytes { dataBytes -> CCCryptorStatus in 85 | let dataBuffer: UnsafeRawPointer = dataBytes.baseAddress! 86 | return CCCryptorUpdate(cryptor, 87 | dataBuffer, 88 | data.count, 89 | cryptBuffer, 90 | data.count, 91 | &numBytesEncrypted) 92 | } 93 | } 94 | 95 | if UInt32(cryptStatus) != UInt32(kCCSuccess) { 96 | throw CrypoError.AESError 97 | } 98 | 99 | return cryptData 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/STARSDK/Cryptography/CryptoConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | 5 | extension TimeInterval { 6 | static let second = 1.0 7 | static let minute = TimeInterval.second * 60 8 | static let hour = TimeInterval.minute * 60 9 | static let day = TimeInterval.hour * 24 10 | } 11 | 12 | enum CryptoConstants { 13 | static let keyLenght: Int = 16 14 | static let numberOfDaysToKeepData: Int = 21 15 | static let numberOfEpochsPerDay: Int = 24 * 12 16 | static let millisecondsPerEpoch = 24 * 60 * 60 * 1000 / CryptoConstants.numberOfDaysToKeepData 17 | // TODO: set correct broadcast key 18 | static let broadcastKey: Data = "broadcast key".data(using: .utf8)! 19 | } 20 | 21 | enum CrypoError: Error { 22 | case dataIntegrity 23 | case IVError 24 | case AESError 25 | } 26 | -------------------------------------------------------------------------------- /Sources/STARSDK/Cryptography/Epoch.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | 5 | struct Epoch: Codable, CustomStringConvertible, Equatable { 6 | let timestamp: TimeInterval 7 | 8 | init(date: Date = Date()) { 9 | var calendar = Calendar.current 10 | calendar.timeZone = TimeZone(identifier: "UTC")! 11 | let components = calendar.dateComponents([.year, .day, .month], from: date) 12 | timestamp = calendar.date(from: components)!.timeIntervalSince1970 13 | } 14 | 15 | public func getNext() -> Epoch { 16 | let nextDay = Date(timeIntervalSince1970: timestamp).addingTimeInterval(.day) 17 | return Epoch(date: nextDay) 18 | } 19 | 20 | public func isBefore(other: Date) -> Bool { 21 | return timestamp < other.timeIntervalSince1970 22 | } 23 | 24 | public func isBefore(other: Epoch) -> Bool { 25 | return timestamp < other.timestamp 26 | } 27 | 28 | var description: String { 29 | return "" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/STARSDK/Cryptography/STARCryptoModule.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import CommonCrypto 4 | import Foundation 5 | 6 | class STARCryptoModule { 7 | private let store: SecretKeyStorageProtocol 8 | 9 | init?(store: SecretKeyStorageProtocol = SecretKeyStorage.shared) { 10 | self.store = store 11 | do { 12 | let keys = try store.get() 13 | if keys.isEmpty { 14 | try generateInitialSecretKey() 15 | } 16 | } catch KeychainError.notFound { 17 | do { 18 | try generateInitialSecretKey() 19 | } catch { 20 | return nil 21 | } 22 | } catch KeychainError.cannotAccess { 23 | return nil 24 | } catch { 25 | return nil 26 | } 27 | } 28 | 29 | private func getSKt1(SKt0: Data) -> Data { 30 | return Crypto.sha256(SKt0) 31 | } 32 | 33 | private func rotateSK() throws { 34 | var keys = try store.get() 35 | guard let firstKey = keys.first else { 36 | throw CrypoError.dataIntegrity 37 | } 38 | let nextEpoch = firstKey.epoch.getNext() 39 | let sKt1 = getSKt1(SKt0: firstKey.keyData) 40 | keys.insert(SecretKey(epoch: nextEpoch, keyData: sKt1), at: 0) 41 | while keys.count > CryptoConstants.numberOfDaysToKeepData { 42 | _ = keys.popLast() 43 | } 44 | try store.set(keys) 45 | } 46 | 47 | public func getCurrentSK(day: Epoch) throws -> Data { 48 | var keys = try store.get() 49 | while keys.first!.epoch.isBefore(other: day) { 50 | try rotateSK() 51 | keys = try store.get() 52 | } 53 | guard let firstKey = keys.first else { 54 | throw CrypoError.dataIntegrity 55 | } 56 | assert(firstKey.epoch.timestamp == day.timestamp) 57 | return firstKey.keyData 58 | } 59 | 60 | public func createEphIds(secretKey: Data) throws -> [Data] { 61 | let hmac = Crypto.hmac(msg: CryptoConstants.broadcastKey, key: secretKey) 62 | 63 | let zeroData = Data(count: CryptoConstants.keyLenght * CryptoConstants.numberOfEpochsPerDay) 64 | 65 | let aes = try Crypto.AESCTREncrypt(keyData: hmac) 66 | 67 | var ephIds = [Data]() 68 | let prgData = try aes.encrypt(data: zeroData) 69 | for i in 0 ..< CryptoConstants.numberOfEpochsPerDay { 70 | let pos = i * CryptoConstants.keyLenght 71 | ephIds.append(prgData[pos ..< pos + CryptoConstants.keyLenght]) 72 | } 73 | 74 | return ephIds 75 | } 76 | 77 | public func getCurrentEphId() throws -> Data { 78 | let currentEpoch = Epoch() 79 | let currentSk = try getCurrentSK(day: currentEpoch) 80 | let counter = Int((Date().timeIntervalSince1970 - currentEpoch.timestamp) / Double(CryptoConstants.millisecondsPerEpoch)) 81 | return try createEphIds(secretKey: currentSk)[counter] 82 | } 83 | 84 | public func checkContacts(secretKey: Data, onsetDate: Epoch, bucketDate: Epoch, getHandshake: (Date) -> ([HandshakeModel])) throws -> HandshakeModel? { 85 | var dayToTest: Epoch = onsetDate 86 | var secretKeyForDay: Data = secretKey 87 | while dayToTest.timestamp <= bucketDate.timestamp { 88 | let handshakesOnDay = getHandshake(Date(timeIntervalSince1970: dayToTest.timestamp)) 89 | guard !handshakesOnDay.isEmpty else { 90 | dayToTest = dayToTest.getNext() 91 | secretKeyForDay = getSKt1(SKt0: secretKeyForDay) 92 | continue 93 | } 94 | 95 | // generate all ephIds for day 96 | let ephIds = try createEphIds(secretKey: secretKeyForDay) 97 | // check all handshakes if they match any of the ephIds 98 | for handshake in handshakesOnDay { 99 | for ephId in ephIds { 100 | if handshake.star == ephId { 101 | return handshake 102 | } 103 | } 104 | } 105 | 106 | // update day to next day and rotate sk accordingly 107 | dayToTest = dayToTest.getNext() 108 | secretKeyForDay = getSKt1(SKt0: secretKeyForDay) 109 | } 110 | return nil 111 | } 112 | 113 | public func getSecretKeyForPublishing(onsetDate: Date) throws -> Data? { 114 | let keys = try store.get() 115 | let epoch = Epoch(date: onsetDate) 116 | for key in keys { 117 | if key.epoch == epoch { 118 | return key.keyData 119 | } 120 | } 121 | if let last = keys.last, 122 | epoch.isBefore(other: last.epoch) { 123 | return last.keyData 124 | } 125 | return nil 126 | } 127 | 128 | public func reset() { 129 | store.removeAllObject() 130 | } 131 | 132 | private func generateInitialSecretKey() throws { 133 | let keyData = try generateRandomKey() 134 | try store.set([SecretKey(epoch: Epoch(), keyData: keyData)]) 135 | } 136 | 137 | private func generateRandomKey() throws -> Data { 138 | var keyData = Data(count: Int(CC_SHA256_DIGEST_LENGTH)) 139 | let result = keyData.withUnsafeMutableBytes { 140 | SecRandomCopyBytes(kSecRandomDefault, Int(CC_SHA256_DIGEST_LENGTH), $0.baseAddress!) 141 | } 142 | guard result == errSecSuccess else { 143 | throw KeychainError.cannotAccess 144 | } 145 | return keyData 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Sources/STARSDK/Cryptography/SecretKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | 5 | struct SecretKey: Codable, CustomStringConvertible { 6 | let epoch: Epoch 7 | let keyData: Data 8 | 9 | var description: String { 10 | return "" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/STARSDK/Database/ApplicationStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | import SQLite 5 | 6 | /// Storage used to persist application from the STAR discovery 7 | class ApplicationStorage { 8 | /// Database connection 9 | private let database: Connection 10 | 11 | /// Name of the table 12 | let table = Table("applications") 13 | 14 | /// Column definitions 15 | let appIdColumn = Expression("app_id") 16 | let descriptionColumn = Expression("description") 17 | let backendBaseUrlColumn = Expression("backend_base_url") 18 | let listBaseUrlColumn = Expression("list_base_url") 19 | let contactColumn = Expression("contact") 20 | 21 | /// Initializer 22 | /// - Parameter database: database connection 23 | init(database: Connection) throws { 24 | self.database = database 25 | try createTable() 26 | } 27 | 28 | /// Create the table 29 | private func createTable() throws { 30 | try database.run(table.create(ifNotExists: true) { t in 31 | t.column(appIdColumn, primaryKey: true) 32 | t.column(descriptionColumn) 33 | t.column(backendBaseUrlColumn) 34 | t.column(listBaseUrlColumn) 35 | t.column(contactColumn) 36 | }) 37 | } 38 | 39 | /// Add a application descriptro 40 | /// - Parameter ad: The descriptor to add 41 | func add(appDescriptor ad: TracingApplicationDescriptor) throws { 42 | let insert = table.insert(or: .replace, 43 | appIdColumn <- ad.appId, 44 | descriptionColumn <- ad.description, 45 | backendBaseUrlColumn <- ad.backendBaseUrl, 46 | listBaseUrlColumn <- ad.listBaseUrl, 47 | contactColumn <- ad.contact) 48 | try database.run(insert) 49 | } 50 | 51 | /// Retreive the descriptor for a specific application 52 | /// - Parameter appid: the application to look for 53 | func descriptor(for appid: String) throws -> TracingApplicationDescriptor? { 54 | let query = table.filter(appIdColumn == appid) 55 | guard let row = try database.pluck(query) else { return nil } 56 | return TracingApplicationDescriptor(appId: row[appIdColumn], 57 | description: row[descriptionColumn], 58 | backendBaseUrl: row[backendBaseUrlColumn], 59 | listBaseUrl: row[listBaseUrlColumn], 60 | contact: row[contactColumn]) 61 | } 62 | 63 | /// Delete all entries 64 | func emptyStorage() throws { 65 | try database.run(table.delete()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/STARSDK/Database/Database.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | import SQLite 5 | 6 | /// Wrapper class for all Databases 7 | class STARDatabase { 8 | /// Database connection 9 | private let connection: Connection 10 | 11 | /// flag used to set Database as destroyed 12 | private(set) var isDestroyed = false 13 | 14 | #if CALIBRATION 15 | public weak var logger: LoggingDelegate? 16 | #endif 17 | 18 | /// application Storage 19 | private let _applicationStorage: ApplicationStorage 20 | var applicationStorage: ApplicationStorage { 21 | guard !isDestroyed else { fatalError("Database is destroyed") } 22 | return _applicationStorage 23 | } 24 | 25 | /// handshaked Storage 26 | private let _handshakesStorage: HandshakesStorage 27 | var handshakesStorage: HandshakesStorage { 28 | guard !isDestroyed else { fatalError("Database is destroyed") } 29 | return _handshakesStorage 30 | } 31 | 32 | /// knowncase Storage 33 | private let _knownCasesStorage: KnownCasesStorage 34 | var knownCasesStorage: KnownCasesStorage { 35 | guard !isDestroyed else { fatalError("Database is destroyed") } 36 | return _knownCasesStorage 37 | } 38 | 39 | /// peripheral Storage 40 | private let _peripheralStorage: PeripheralStorage 41 | var peripheralStorage: PeripheralStorage { 42 | guard !isDestroyed else { fatalError("Database is destroyed") } 43 | return _peripheralStorage 44 | } 45 | 46 | #if CALIBRATION 47 | /// logging Storage 48 | private let _logggingStorage: LoggingStorage 49 | var loggingStorage: LoggingStorage { 50 | guard !isDestroyed else { fatalError("Database is destroyed") } 51 | return _logggingStorage 52 | } 53 | #endif 54 | 55 | /// Initializer 56 | init() throws { 57 | let fileName = STARDatabase.getDatabasePath() 58 | connection = try Connection(fileName, readonly: false) 59 | _knownCasesStorage = try KnownCasesStorage(database: connection) 60 | _handshakesStorage = try HandshakesStorage(database: connection, knownCasesStorage: _knownCasesStorage) 61 | _peripheralStorage = try PeripheralStorage(database: connection) 62 | _applicationStorage = try ApplicationStorage(database: connection) 63 | #if CALIBRATION 64 | _logggingStorage = try LoggingStorage(database: connection) 65 | #endif 66 | } 67 | 68 | /// Discard all data 69 | func emptyStorage() throws { 70 | guard !isDestroyed else { fatalError("Database is destroyed") } 71 | try connection.transaction { 72 | try handshakesStorage.emptyStorage() 73 | try knownCasesStorage.emptyStorage() 74 | try peripheralStorage.emptyStorage() 75 | #if CALIBRATION 76 | try loggingStorage.emptyStorage() 77 | #endif 78 | } 79 | } 80 | 81 | /// delete Database 82 | func destroyDatabase() throws { 83 | let path = STARDatabase.getDatabasePath() 84 | if FileManager.default.fileExists(atPath: path) { 85 | try FileManager.default.removeItem(atPath: path) 86 | } 87 | isDestroyed = true 88 | } 89 | 90 | /// get database path 91 | private static func getDatabasePath() -> String { 92 | let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) 93 | let documentsDirectory = paths[0] 94 | return documentsDirectory.appendingPathComponent("STAR_tracing_db").appendingPathExtension("sqlite").absoluteString 95 | } 96 | } 97 | 98 | extension STARDatabase: CustomDebugStringConvertible { 99 | var debugDescription: String { 100 | return "DB at path <\(STARDatabase.getDatabasePath())>" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/STARSDK/Database/Defaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | 5 | /// UserDefaults Storage Singleton 6 | class Default { 7 | static var shared = Default() 8 | var store = UserDefaults.standard 9 | 10 | /// Last date a backend sync happend 11 | var lastSync: Date? { 12 | get { 13 | return store.object(forKey: "ch.ubique.starsdk.lastsync") as? Date 14 | } 15 | set(newValue) { 16 | store.set(newValue, forKey: "ch.ubique.starsdk.lastsync") 17 | } 18 | } 19 | 20 | /// Current infection status 21 | var infectionStatus: InfectionStatus { 22 | get { 23 | return InfectionStatus(rawValue: store.integer(forKey: "ch.ubique.starsdk.InfectionStatus")) ?? .healthy 24 | } 25 | set(newValue) { 26 | store.set(newValue.rawValue, forKey: "ch.ubique.starsdk.InfectionStatus") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/STARSDK/Database/HandshakesStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | import SQLite 5 | 6 | /// Storage used to persist STAR handshakes 7 | class HandshakesStorage { 8 | /// Database connection 9 | private let database: Connection 10 | 11 | /// Name of the table 12 | let table = Table("handshakes") 13 | 14 | /// Column definitions 15 | let idColumn = Expression("id") 16 | let timestampColumn = Expression("timestamp") 17 | let starColumn = Expression("star") 18 | let TXPowerlevelColumn = Expression("tx_power_level") 19 | let RSSIColumn = Expression("rssi") 20 | let associatedKnownCaseColumn = Expression("associated_known_case") 21 | 22 | /// Initializer 23 | /// - Parameters: 24 | /// - database: database Connection 25 | /// - knownCasesStorage: knownCases Storage 26 | init(database: Connection, knownCasesStorage: KnownCasesStorage) throws { 27 | self.database = database 28 | try createTable(knownCasesStorage: knownCasesStorage) 29 | } 30 | 31 | /// Create the table 32 | private func createTable(knownCasesStorage: KnownCasesStorage) throws { 33 | try database.run(table.create(ifNotExists: true) { t in 34 | t.column(idColumn, primaryKey: .autoincrement) 35 | t.column(timestampColumn) 36 | t.column(starColumn) 37 | t.column(associatedKnownCaseColumn) 38 | t.column(TXPowerlevelColumn) 39 | t.column(RSSIColumn) 40 | t.foreignKey(associatedKnownCaseColumn, references: knownCasesStorage.table, knownCasesStorage.idColumn, delete: .setNull) 41 | }) 42 | } 43 | 44 | /// returns the known Case Id for a star 45 | func starExists(star: Data) throws -> Int? { 46 | let query = table.filter(starColumn == star) 47 | let row = try database.pluck(query) 48 | return row?[associatedKnownCaseColumn] 49 | } 50 | 51 | /// count of entries 52 | func count() throws -> Int { 53 | try database.scalar(table.count) 54 | } 55 | 56 | /// add a Handshake 57 | /// - Parameter h: handshake 58 | func add(handshake h: HandshakeModel) throws { 59 | let insert = table.insert( 60 | timestampColumn <- h.timestamp, 61 | starColumn <- h.star, 62 | associatedKnownCaseColumn <- h.knownCaseId, 63 | TXPowerlevelColumn <- h.TXPowerlevel, 64 | RSSIColumn <- h.RSSI 65 | ) 66 | try database.run(insert) 67 | } 68 | 69 | /// Add a known case to the handshake 70 | /// - Parameters: 71 | /// - knownCaseId: identifier of known case 72 | /// - handshakeId: identifier of handshake 73 | func addKnownCase(_ knownCaseId: Int, to handshakeId: Int) throws { 74 | let handshakeRow = table.filter(idColumn == handshakeId) 75 | try database.run(handshakeRow.update(associatedKnownCaseColumn <- knownCaseId)) 76 | } 77 | 78 | /// helper function to loop through all entries 79 | func getBy(day: Date) throws -> [HandshakeModel] { 80 | let query = table.filter(timestampColumn >= day.dayMin && timestampColumn <= day.dayMax) 81 | var models = [HandshakeModel]() 82 | for row in try database.prepare(query) { 83 | guard row[associatedKnownCaseColumn] == nil else { continue } 84 | var model = HandshakeModel(timestamp: row[timestampColumn], 85 | star: row[starColumn], 86 | TXPowerlevel: row[TXPowerlevelColumn], 87 | RSSI: row[RSSIColumn], 88 | knownCaseId: nil) 89 | model.identifier = row[idColumn] 90 | models.append(model) 91 | } 92 | return models 93 | } 94 | 95 | /// Delete all entries 96 | func emptyStorage() throws { 97 | try database.run(table.delete()) 98 | } 99 | 100 | func numberOfHandshakes() throws -> Int { 101 | try database.scalar(table.count) 102 | } 103 | 104 | func getHandshakes(_ request: HandshakeRequest) throws -> HandshakeResponse { 105 | var query = table 106 | 107 | // Limit 108 | if let limit = request.limit { 109 | assert(limit > 0, "Limits should be at least one") 110 | assert(request.offset >= 0, "Offset must be positive") 111 | query = query.limit(limit, offset: request.offset) 112 | } 113 | 114 | // Sorting 115 | switch request.sortingOption { 116 | case .ascendingTimestamp: 117 | query = query.order(timestampColumn.asc) 118 | case .descendingTimestamp: 119 | query = query.order(timestampColumn.desc) 120 | } 121 | 122 | // Filtering 123 | if request.filterOption.contains(.hasKnownCaseAssociated) { 124 | query = query.filter(associatedKnownCaseColumn != nil) 125 | } 126 | 127 | var handshakes = [HandshakeModel]() 128 | for row in try database.prepare(query) { 129 | let model = HandshakeModel(timestamp: row[timestampColumn], 130 | star: row[starColumn], 131 | TXPowerlevel: row[TXPowerlevelColumn], 132 | RSSI: row[RSSIColumn], 133 | knownCaseId: row[associatedKnownCaseColumn]) 134 | handshakes.append(model) 135 | } 136 | 137 | let previousRequest: HandshakeRequest? 138 | if request.offset > 0, let limit = request.limit { 139 | let diff = request.offset - limit 140 | let previousOffset = max(0, diff) 141 | let previousLimit = limit + min(0, diff) 142 | previousRequest = HandshakeRequest(filterOption: request.filterOption, offset: previousOffset, limit: previousLimit) 143 | } else { 144 | previousRequest = nil 145 | } 146 | 147 | let nextRequest: HandshakeRequest? 148 | if request.limit == nil || handshakes.count < request.limit! { 149 | nextRequest = nil 150 | } else { 151 | let nextOffset = request.offset + request.limit! 152 | nextRequest = HandshakeRequest(filterOption: request.filterOption, offset: nextOffset, limit: request.limit) 153 | } 154 | 155 | return HandshakeResponse(handshakes: handshakes, offset: request.offset, limit: request.limit, previousRequest: previousRequest, nextRequest: nextRequest) 156 | } 157 | } 158 | 159 | public struct HandshakeRequest { 160 | public struct FilterOption: OptionSet { 161 | public let rawValue: Int 162 | public static let hasKnownCaseAssociated = FilterOption(rawValue: 1 << 0) 163 | public init(rawValue: Int) { 164 | self.rawValue = rawValue 165 | } 166 | } 167 | 168 | public enum SortingOption { 169 | case ascendingTimestamp 170 | case descendingTimestamp 171 | } 172 | 173 | public let filterOption: FilterOption 174 | public let sortingOption: SortingOption 175 | public let offset: Int 176 | public let limit: Int? 177 | public init(filterOption: FilterOption = [], sortingOption: SortingOption = .descendingTimestamp, offset: Int = 0, limit: Int? = nil) { 178 | self.filterOption = filterOption 179 | self.sortingOption = sortingOption 180 | self.offset = offset 181 | self.limit = limit 182 | } 183 | } 184 | 185 | public struct HandshakeResponse { 186 | public let offset: Int 187 | public let limit: Int? 188 | public let handshakes: [HandshakeModel] 189 | public let previousRequest: HandshakeRequest? 190 | public let nextRequest: HandshakeRequest? 191 | fileprivate init(handshakes: [HandshakeModel], offset: Int, limit: Int?, previousRequest: HandshakeRequest?, nextRequest: HandshakeRequest?) { 192 | self.handshakes = handshakes 193 | self.previousRequest = previousRequest 194 | self.nextRequest = nextRequest 195 | self.offset = offset 196 | self.limit = limit 197 | } 198 | } 199 | 200 | private extension Date { 201 | var dayMax: Date { 202 | var calendar = Calendar.current 203 | calendar.timeZone = TimeZone(identifier: "UTC")! 204 | var components = calendar.dateComponents([.year, .day, .month, .hour, .minute, .second], from: self) 205 | components.hour = 23 206 | components.minute = 59 207 | components.second = 59 208 | return calendar.date(from: components)! 209 | } 210 | 211 | var dayMin: Date { 212 | var calendar = Calendar.current 213 | calendar.timeZone = TimeZone(identifier: "UTC")! 214 | let components = calendar.dateComponents([.year, .day, .month], from: self) 215 | return calendar.date(from: components)! 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Sources/STARSDK/Database/KnownCasesStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | import SQLite 5 | 6 | /// Storage used to persist STAR known cases 7 | class KnownCasesStorage { 8 | /// Database connection 9 | private let database: Connection 10 | 11 | /// Name of the table 12 | let table = Table("known_cases") 13 | 14 | /// Column definitions 15 | let idColumn = Expression("id") 16 | let dayColumn = Expression("day") 17 | let onsetColumn = Expression("onset") 18 | let keyColumn = Expression("key") 19 | 20 | /// Initializer 21 | /// - Parameter database: database connection 22 | init(database: Connection) throws { 23 | self.database = database 24 | try createTable() 25 | } 26 | 27 | /// Create the table 28 | private func createTable() throws { 29 | try database.run(table.create(ifNotExists: true) { t in 30 | t.column(idColumn, primaryKey: .autoincrement) 31 | t.column(dayColumn) 32 | t.column(onsetColumn) 33 | t.column(keyColumn) 34 | }) 35 | } 36 | 37 | /// update the list of known cases 38 | /// - Parameter kcs: known cases 39 | /// - Parameter day: day identifier 40 | func update(knownCases kcs: [KnownCaseModel], day: String) throws { 41 | // Remove old values 42 | let casesToRemove = table.filter(dayColumn == day) 43 | try database.run(casesToRemove.delete()) 44 | 45 | try database.transaction { 46 | try kcs.forEach { try add(knownCase: $0, day: day) } 47 | } 48 | } 49 | 50 | func getId(for key: Data) throws -> Int? { 51 | let query = table.filter(keyColumn == key) 52 | guard let row = try database.pluck(query) else { return nil } 53 | return row[idColumn] 54 | } 55 | 56 | /// add a known case 57 | /// - Parameter kc: known case 58 | /// - Parameter day: day identifier 59 | private func add(knownCase kc: KnownCaseModel, day: String) throws { 60 | let insert = table.insert( 61 | dayColumn <- day, 62 | onsetColumn <- kc.onset, 63 | keyColumn <- kc.key 64 | ) 65 | 66 | try database.run(insert) 67 | } 68 | 69 | /// Delete all entries 70 | func emptyStorage() throws { 71 | try database.run(table.delete()) 72 | } 73 | 74 | /// helper function to loop through all entries 75 | /// - Parameter block: execution block should return false to break looping 76 | func loopThrough(block: (KnownCaseModel) -> Bool) throws { 77 | for row in try database.prepare(table) { 78 | let model = KnownCaseModel(id: row[idColumn], key: row[keyColumn], onset: row[onsetColumn]) 79 | if !block(model) { 80 | break 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/STARSDK/Database/LoggingStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | import SQLite 5 | 6 | #if CALIBRATION 7 | public struct LogEntry: Identifiable { 8 | public let id: Int 9 | public let timestamp: Date 10 | public let type: LogType 11 | public let message: String 12 | } 13 | 14 | public struct LogRequest { 15 | public enum Sorting { 16 | case asc 17 | case desc 18 | } 19 | 20 | public let sorting: Sorting 21 | public var offset: Int 22 | public var limit: Int 23 | 24 | public init(sorting: Sorting = .asc, offset: Int = 0, limit: Int = 1000) { 25 | self.sorting = sorting 26 | self.offset = offset 27 | self.limit = limit 28 | } 29 | } 30 | 31 | public struct LogResponse { 32 | public let logs: [LogEntry] 33 | public let nextRequest: LogRequest? 34 | fileprivate init(logs: [LogEntry], nextRequest: LogRequest?) { 35 | self.logs = logs 36 | self.nextRequest = nextRequest 37 | } 38 | } 39 | 40 | /// Storage used to persist Logs 41 | class LoggingStorage { 42 | /// Database connection 43 | private let database: Connection 44 | 45 | /// Name of the table 46 | let table = Table("logs") 47 | 48 | /// Column definitions 49 | let idColumn = Expression("id") 50 | let timestampColumn = Expression("timestamp") 51 | let typeColumn = Expression("type") 52 | let messageColumn = Expression("message") 53 | 54 | /// Initializer 55 | /// - Parameters: 56 | /// - database: database Connection 57 | init(database: Connection) throws { 58 | self.database = database 59 | try createTable() 60 | } 61 | 62 | /// Create the table 63 | private func createTable() throws { 64 | try database.run(table.create(ifNotExists: true) { t in 65 | t.column(idColumn, primaryKey: .autoincrement) 66 | t.column(timestampColumn) 67 | t.column(typeColumn) 68 | t.column(messageColumn) 69 | }) 70 | } 71 | 72 | func log(type: LogType, message: String) throws -> LogEntry { 73 | let timestamp = Date() 74 | let insert = table.insert( 75 | timestampColumn <- timestamp, 76 | typeColumn <- type.rawValue, 77 | messageColumn <- message 78 | ) 79 | try database.run(insert) 80 | return LogEntry(id: 0, timestamp: timestamp, type: type, message: message) 81 | } 82 | 83 | /// Delete all entries 84 | func emptyStorage() throws { 85 | try database.run(table.delete()) 86 | } 87 | 88 | /// count of entries 89 | func count() throws -> Int { 90 | try database.scalar(table.count) 91 | } 92 | 93 | func getLogs(_ request: LogRequest) throws -> LogResponse { 94 | assert(request.limit > 0, "Limits should be at least one") 95 | assert(request.offset >= 0, "Offset must be positive") 96 | 97 | var query = table 98 | 99 | switch request.sorting { 100 | case .asc: 101 | query = query.order(timestampColumn.asc) 102 | case .desc: 103 | query = query.order(timestampColumn.desc) 104 | } 105 | 106 | query = query.limit(request.limit, offset: request.offset) 107 | 108 | var logs: [LogEntry] = [] 109 | for row in try database.prepare(query) { 110 | logs.append(LogEntry(id: row[idColumn], timestamp: row[timestampColumn], type: LogType(rawValue: row[typeColumn]) ?? .none, message: row[messageColumn])) 111 | } 112 | 113 | var nextRequest: LogRequest? 114 | if logs.count < request.limit { 115 | nextRequest = nil 116 | } else { 117 | let nextOffset = request.offset + request.limit 118 | nextRequest = LogRequest(sorting: request.sorting, offset: nextOffset, limit: request.limit) 119 | } 120 | 121 | return LogResponse(logs: logs, nextRequest: nextRequest) 122 | } 123 | } 124 | #endif 125 | -------------------------------------------------------------------------------- /Sources/STARSDK/Database/PeripheralStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | import SQLite 5 | 6 | /// Wrapper to return Peripheral data 7 | struct PeripheralWrapper { 8 | let uuid: String 9 | let discoverTime: Date 10 | let lastConnection: Date? 11 | } 12 | 13 | class PeripheralStorage { 14 | /// Database connection 15 | private let database: Connection 16 | 17 | /// Name of the table 18 | let table = Table("peripheral_last_connection") 19 | 20 | /// Column definitions 21 | let peripheralUUID = Expression("peripheral_uuid") 22 | let discoverTime = Expression("discover_time") 23 | let lastConnection = Expression("last_connection") 24 | 25 | /// Initializer 26 | /// - Parameter database: database connection 27 | init(database: Connection) throws { 28 | self.database = database 29 | try createTable() 30 | } 31 | 32 | /// Create the table 33 | private func createTable() throws { 34 | try database.run(table.create(ifNotExists: true) { t in 35 | t.column(peripheralUUID, primaryKey: true) 36 | t.column(lastConnection) 37 | t.column(discoverTime) 38 | }) 39 | } 40 | 41 | /// inserts a discovery for a peripheral 42 | /// - Parameter uuid: peripheral identifier 43 | func setDiscovery(uuid: UUID) throws { 44 | let query = table.insert(or: .replace, peripheralUUID <- uuid.uuidString, 45 | discoverTime <- Date()) 46 | try database.run(query) 47 | } 48 | 49 | /// sets the lastconnection time for a peripheral 50 | /// - Parameter uuid: peripheral identifier 51 | func setConnection(uuid: UUID) throws { 52 | let query = table.filter(peripheralUUID == uuid.uuidString) 53 | try database.run(query.update(lastConnection <- Date())) 54 | } 55 | 56 | /// gets the periphal by identifier 57 | /// - Parameter uuid: peripheral identifier 58 | func get(uuid: UUID) throws -> PeripheralWrapper? { 59 | let query = table.filter(peripheralUUID == uuid.uuidString) 60 | guard let row = try database.pluck(query) else { return nil } 61 | return PeripheralWrapper(uuid: row[peripheralUUID], discoverTime: row[discoverTime], lastConnection: row[lastConnection]) 62 | } 63 | 64 | /// helper function to loop through all entries 65 | /// - Parameter block: execution block should return false to break looping 66 | func loopThrough(block: (PeripheralWrapper) -> Bool) throws { 67 | for row in try database.prepare(table) { 68 | let model = PeripheralWrapper(uuid: row[peripheralUUID], discoverTime: row[discoverTime], lastConnection: row[lastConnection]) 69 | if !block(model) { 70 | break 71 | } 72 | } 73 | } 74 | 75 | /// discard periphal by identifier 76 | /// - Parameter uuid: peripheral identifier 77 | func discard(uuid: String) throws { 78 | let query = table.filter(peripheralUUID == uuid) 79 | try database.run(query.delete()) 80 | } 81 | 82 | /// Delete all entries 83 | func emptyStorage() throws { 84 | try database.run(table.delete()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/STARSDK/Database/SQLite+URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | import SQLite 5 | 6 | /// URL extension to store it in Sqlite as String 7 | extension URL: Value { 8 | /// :nodoc: 9 | public typealias Datatype = String 10 | 11 | /// :nodoc: 12 | public static let declaredDatatype = "TEXT" 13 | 14 | /// :nodoc: 15 | public static func fromDatatypeValue(_ datatypeValue: String) -> URL { 16 | URL(string: datatypeValue)! 17 | } 18 | 19 | /// :nodoc: 20 | public var datatypeValue: String { 21 | absoluteString 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/STARSDK/Database/SecretKeyStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | 5 | enum KeychainError: Error { 6 | case notFound 7 | case cannotAccess 8 | } 9 | 10 | protocol SecretKeyStorageProtocol { 11 | func get() throws -> [SecretKey] 12 | func set(_ object: [SecretKey]) throws 13 | func removeAllObject() 14 | } 15 | 16 | class SecretKeyStorage: SecretKeyStorageProtocol { 17 | static let shared = SecretKeyStorage() 18 | 19 | private let key: String = "ch.ubique.keylist" 20 | 21 | private let encoder = JSONEncoder() 22 | private let decoder = JSONDecoder() 23 | 24 | init() {} 25 | 26 | func get() throws -> [SecretKey] { 27 | let query: [String: Any] = [ 28 | kSecClass as String: kSecClassGenericPassword, 29 | kSecAttrAccount as String: key, 30 | kSecReturnData as String: true, 31 | kSecMatchLimit as String: kSecMatchLimitOne, 32 | ] 33 | var item: CFTypeRef? 34 | let status = SecItemCopyMatching(query as CFDictionary, &item) 35 | guard status != errSecItemNotFound else { 36 | throw KeychainError.notFound 37 | } 38 | guard status == errSecSuccess else { 39 | throw KeychainError.cannotAccess 40 | } 41 | let data = (item as! CFData) as Data 42 | return try decoder.decode([SecretKey].self, from: data) 43 | } 44 | 45 | func set(_ object: [SecretKey]) throws { 46 | let data = try encoder.encode(object) 47 | 48 | let query: [String: Any] = [ 49 | kSecClass as String: kSecClassGenericPassword as String, 50 | kSecAttrAccount as String: key, 51 | kSecValueData as String: data, 52 | kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock, 53 | ] 54 | SecItemAdd(query as CFDictionary, nil) 55 | } 56 | 57 | func removeAllObject() { 58 | let query: [String: Any] = [ 59 | kSecClass as String: kSecClassGenericPassword, 60 | kSecAttrAccount as String: key, 61 | ] 62 | SecItemDelete(query as CFDictionary) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/STARSDK/LoggingDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import CoreBluetooth 4 | import Foundation 5 | #if CALIBRATION 6 | public enum LogType: Int, CustomStringConvertible { 7 | case none = 0 8 | case receiver = 1 9 | case sender = 2 10 | case crypto = 3 11 | case sdk = 4 12 | case database = 5 13 | 14 | public var description: String { 15 | switch self { 16 | case .none: 17 | return "[]" 18 | case .receiver: 19 | return "[Receiver]" 20 | case .sender: 21 | return "[Sender]" 22 | case .sdk: 23 | return "[SDK]" 24 | case .crypto: 25 | return "[Crypo]" 26 | case .database: 27 | return "[Database]" 28 | } 29 | } 30 | } 31 | 32 | /// A logging delegate 33 | protocol LoggingDelegate: class { 34 | /// Log a string 35 | /// - Parameter LogType: the type of log 36 | /// - Parameter string: The string to log 37 | func log(type: LogType, _ string: String) 38 | } 39 | 40 | extension LoggingDelegate { 41 | /// Log 42 | /// - Parameters: 43 | /// - state: The state 44 | /// - prefix: A prefix 45 | func log(type: LogType, state: CBManagerState, prefix: String = "") { 46 | switch state { 47 | case .poweredOff: 48 | log(type: type, "\(prefix): poweredOff") 49 | case .poweredOn: 50 | log(type: type, "\(prefix): poweredOn") 51 | case .resetting: 52 | log(type: type, "\(prefix): resetting") 53 | case .unauthorized: 54 | log(type: type, "\(prefix): unauthorized") 55 | case .unknown: 56 | log(type: type, "\(prefix): unknown") 57 | case .unsupported: 58 | log(type: type, "\(prefix): unsupported") 59 | @unknown default: 60 | fatalError() 61 | } 62 | } 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /Sources/STARSDK/Models/APIModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Model of the known cases 4 | struct KnownCasesResponse: Decodable { 5 | /// All exposed known cases 6 | let exposed: [KnownCaseModel] 7 | } 8 | 9 | /// Model of the discovery of services 10 | struct DiscoveryServiceResponse: Codable { 11 | /// All available applications 12 | let applications: [TracingApplicationDescriptor] 13 | } 14 | 15 | /// Model for a record in the published services 16 | struct TracingApplicationDescriptor: Codable { 17 | /// The app ID 18 | var appId: String 19 | /// A description of the service 20 | var description: String 21 | /// The backend base URL 22 | var backendBaseUrl: URL 23 | /// A list of base url 24 | var listBaseUrl: URL 25 | /// The contact person for the record 26 | var contact: String 27 | } 28 | -------------------------------------------------------------------------------- /Sources/STARSDK/Models/ExposeeAuthData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Model for the authentication data provided by health institutes to verify test results 4 | struct ExposeeAuthData: Encodable { 5 | /// Authentication data used to verify the test result (base64 encoded) 6 | let value: String 7 | } 8 | -------------------------------------------------------------------------------- /Sources/STARSDK/Models/ExposeeModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Model of the exposed person 4 | struct ExposeeModel: Encodable { 5 | /// Secret key used to generate EphID (base64 encoded) 6 | let key: Data 7 | 8 | /// The onset date of the secret key (format: yyyy-MM-dd) 9 | let onset: String 10 | 11 | /// Authentication data provided by health institutes to verify test results 12 | let authData: ExposeeAuthData 13 | } 14 | -------------------------------------------------------------------------------- /Sources/STARSDK/Models/HandshakeModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A model for the digital handshake 4 | public struct HandshakeModel { 5 | public var identifier: Int? 6 | 7 | /// The timestamp of the handshake 8 | public let timestamp: Date 9 | /// The STAR token exchanged during the handshake 10 | public let star: Data 11 | /// The TX Power Level of both handshaking parties 12 | public let TXPowerlevel: Double? 13 | /// The RSSI of both handshaking parties 14 | public let RSSI: Double? 15 | /// If the handshake is associated with a known exposed case 16 | public let knownCaseId: Int? 17 | 18 | // iOS sends at 12bm? Android seems to vary between -1dbm (HIGH_POWER) and -21dbm (LOW_POWER) 19 | private let defaultPower = 12.0 20 | 21 | /// Calcualte an estimation of the distance separating the two devices when a handshake happens 22 | /// - Parameters: 23 | /// - peripheral: The peripheral in question 24 | /// - RSSI: The RSSI 25 | public var distance: Double? { 26 | guard let RSSI = RSSI else { 27 | return nil 28 | } 29 | let power = TXPowerlevel ?? defaultPower 30 | let distance = pow(10, (power - RSSI) / 20) 31 | return distance / 1000 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/STARSDK/Models/KnownCaseModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A model for known cases 4 | struct KnownCaseModel: Decodable { 5 | /// The identifier of the case 6 | let id: Int? 7 | /// The private key of the case 8 | let key: Data 9 | /// The day the known case was set as exposed 10 | let onset: String 11 | 12 | enum CodingKeys: String, CodingKey { 13 | case id, key, onset 14 | } 15 | } 16 | 17 | // MARK: Codable implementation 18 | 19 | extension KnownCaseModel { 20 | init(from decoder: Decoder) throws { 21 | let values = try decoder.container(keyedBy: CodingKeys.self) 22 | id = nil 23 | key = try values.decode(Data.self, forKey: .key) 24 | onset = try values.decode(String.self, forKey: .onset) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/STARSDK/Networking/ApplicationsSynchronizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | 5 | /// Fetch the discovery data and stores it 6 | class ApplicationSynchronizer { 7 | /// The storage of the data 8 | let storage: ApplicationStorage 9 | /// The enviroment 10 | let enviroment: Enviroment 11 | 12 | /// Create a synchronizer 13 | /// - Parameters: 14 | /// - enviroment: The environment of the synchronizer 15 | /// - storage: The storage 16 | init(enviroment: Enviroment, storage: ApplicationStorage) { 17 | self.storage = storage 18 | self.enviroment = enviroment 19 | } 20 | 21 | /// Synchronize the local and remote data. 22 | /// - Parameter callback: A callback with the sync result 23 | func sync(callback: @escaping (Result) -> Void) throws { 24 | ExposeeServiceClient.getAvailableApplicationDescriptors(enviroment: enviroment) { [weak self] result in 25 | guard let self = self else { return } 26 | switch result { 27 | case let .success(ad): 28 | do { 29 | try ad.forEach(self.storage.add(appDescriptor:)) 30 | callback(.success(())) 31 | } catch { 32 | callback(.failure(STARTracingErrors.DatabaseError(error: error))) 33 | } 34 | case let .failure(error): 35 | callback(.failure(STARTracingErrors.NetworkingError(error: error))) 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/STARSDK/Networking/Endpoints.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | 5 | /// An endpoint for exposed people 6 | struct ExposeeEndpoint { 7 | /// The base URL to derive the url from 8 | let baseURL: URL 9 | /// A version of the API 10 | let version: String 11 | /// Initialize the endpoint 12 | /// - Parameters: 13 | /// - baseURL: The base URL of the endpoint 14 | /// - version: The version of the API 15 | init(baseURL: URL, version: String = "v1") { 16 | self.baseURL = baseURL 17 | self.version = version 18 | } 19 | 20 | /// A versionned base URL 21 | private var baseURLVersionned: URL { 22 | baseURL.appendingPathComponent(version) 23 | } 24 | 25 | /// Get the URL for the exposed people endpoint at a day 26 | /// - Parameter forDay: The day to fetch 27 | func getExposee(forDay: String) -> URL { 28 | baseURLVersionned.appendingPathComponent("exposed").appendingPathComponent(forDay) 29 | } 30 | } 31 | 32 | /// An endpoint for adding and removing exposed people 33 | struct ManagingExposeeEndpoint { 34 | /// The base URL to derive the url from 35 | let baseURL: URL 36 | /// A version of the API 37 | let version: String 38 | /// Initialize the endpoint 39 | /// - Parameters: 40 | /// - baseURL: The base URL of the endpoint 41 | /// - version: The version of the API 42 | init(baseURL: URL, version: String = "v1") { 43 | self.baseURL = baseURL 44 | self.version = version 45 | } 46 | 47 | /// A versionned base URL 48 | private var baseURLVersionned: URL { 49 | baseURL.appendingPathComponent(version) 50 | } 51 | 52 | /// Get the add exposee endpoint URL 53 | func addExposee() -> URL { 54 | baseURLVersionned.appendingPathComponent("exposed") 55 | } 56 | 57 | /// Get the remove exposee endpoint URL 58 | func removeExposee() -> URL { 59 | baseURLVersionned.appendingPathComponent("removeexposed") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/STARSDK/Networking/Enviroment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The environment of the app 4 | public enum Enviroment { 5 | /// Production environment 6 | case prod 7 | /// A development environment 8 | case dev 9 | 10 | /// The endpoint for the discovery 11 | var discoveryEndpoint: URL { 12 | switch self { 13 | case .prod: 14 | return URL(string: "https://raw.githubusercontent.com/SecureTagForApproachRecognition/discovery/master/discovery.json")! 15 | case .dev: 16 | return URL(string: "https://raw.githubusercontent.com/SecureTagForApproachRecognition/discovery/master/discovery_dev.json")! 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/STARSDK/Networking/ExposeeServiceClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | /// The client for managing and fetching exposee 5 | class ExposeeServiceClient { 6 | /// The descriptor to use for the fetch 7 | private let descriptor: TracingApplicationDescriptor 8 | /// The endpoint for getting exposee 9 | private let exposeeEndpoint: ExposeeEndpoint 10 | /// The endpoint for adding and removing exposee 11 | private let managingExposeeEndpoint: ManagingExposeeEndpoint 12 | 13 | /// The user agent to send with the requests 14 | private var userAgent: String { 15 | let appId = descriptor.appId 16 | let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "0.0" 17 | let systemVersion = UIDevice.current.systemVersion 18 | 19 | return [appId, appVersion, "iOS", systemVersion].joined(separator: ";") 20 | } 21 | 22 | /// Initialize the client with a descriptor 23 | /// - Parameter descriptor: The descriptor to use 24 | public init(descriptor: TracingApplicationDescriptor) { 25 | self.descriptor = descriptor 26 | exposeeEndpoint = ExposeeEndpoint(baseURL: descriptor.listBaseUrl) 27 | managingExposeeEndpoint = ManagingExposeeEndpoint(baseURL: descriptor.backendBaseUrl) 28 | } 29 | 30 | /// Get all exposee for a known day 31 | /// - Parameters: 32 | /// - dayIdentifier: The day identifier 33 | /// - completion: The completion block 34 | func getExposee(dayIdentifier: String, completion: @escaping (Result<[KnownCaseModel], STARTracingErrors>) -> Void) { 35 | let url = exposeeEndpoint.getExposee(forDay: dayIdentifier) 36 | 37 | let task = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in 38 | guard error == nil else { 39 | completion(.failure(.NetworkingError(error: error))) 40 | return 41 | } 42 | guard let responseData = data else { 43 | completion(.failure(.NetworkingError(error: nil))) 44 | return 45 | } 46 | guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { 47 | completion(.failure(.NetworkingError(error: nil))) 48 | return 49 | } 50 | if statusCode == 404 { 51 | // 404 not found response means there is no data for this day 52 | completion(.success([])) 53 | return 54 | } 55 | guard statusCode == 200 else { 56 | completion(.failure(.NetworkingError(error: nil))) 57 | return 58 | } 59 | do { 60 | let decoder = JSONDecoder() 61 | 62 | let dayData = try decoder.decode(KnownCasesResponse.self, from: responseData) 63 | 64 | completion(.success(dayData.exposed)) 65 | } catch { 66 | completion(.failure(.NetworkingError(error: error))) 67 | } 68 | }) 69 | task.resume() 70 | } 71 | 72 | /// Adds an exposee 73 | /// - Parameters: 74 | /// - exposee: The exposee to add 75 | /// - completion: The completion block 76 | func addExposee(_ exposee: ExposeeModel, completion: @escaping (Result) -> Void) { 77 | exposeeEndpointRequest(exposee, action: .add) { result in 78 | switch result { 79 | case let .failure(error): 80 | completion(.failure(.NetworkingError(error: error))) 81 | case .success: 82 | completion(.success(())) 83 | } 84 | } 85 | } 86 | 87 | /// Removes an exposee 88 | /// - Parameters: 89 | /// - exposee: The exposee to remove 90 | /// - completion: The completion block 91 | func removeExposee(_ exposee: ExposeeModel, completion: @escaping (Result) -> Void) { 92 | exposeeEndpointRequest(exposee, action: .remove) { result in 93 | switch result { 94 | case let .failure(error): 95 | completion(.failure(.NetworkingError(error: error))) 96 | case .success: 97 | completion(.success(())) 98 | } 99 | } 100 | } 101 | 102 | private enum ExposeeEndpointAction { case add, remove } 103 | /// Executes a managing exposee request 104 | /// - Parameters: 105 | /// - exposee: The exposee to manage 106 | /// - action: The action to perform 107 | /// - completion: The completion block 108 | private func exposeeEndpointRequest(_ exposee: ExposeeModel, action: ExposeeEndpointAction, completion: @escaping (Result) -> Void) { 109 | // addExposee endpoint 110 | let url: URL 111 | switch action { 112 | case .add: 113 | url = managingExposeeEndpoint.addExposee() 114 | case .remove: 115 | url = managingExposeeEndpoint.removeExposee() 116 | } 117 | 118 | guard let payload = try? JSONEncoder().encode(exposee) else { 119 | completion(.failure(.NetworkingError(error: nil))) 120 | return 121 | } 122 | 123 | var request = URLRequest(url: url) 124 | request.httpMethod = "POST" 125 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 126 | request.addValue(String(payload.count), forHTTPHeaderField: "Content-Length") 127 | request.addValue(userAgent, forHTTPHeaderField: "User-Agent") 128 | request.httpBody = payload 129 | 130 | let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in 131 | guard error == nil else { 132 | completion(.failure(.NetworkingError(error: error))) 133 | return 134 | } 135 | guard let responseData = data else { 136 | completion(.failure(.NetworkingError(error: nil))) 137 | return 138 | } 139 | guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { 140 | completion(.failure(.NetworkingError(error: nil))) 141 | return 142 | } 143 | guard statusCode == 200 else { 144 | completion(.failure(.NetworkingError(error: nil))) 145 | return 146 | } 147 | // string response 148 | _ = String(data: responseData, encoding: .utf8) 149 | completion(.success(())) 150 | }) 151 | task.resume() 152 | } 153 | 154 | /// Returns the list of all available application descriptors registered with the backend 155 | /// - Parameters: 156 | /// - enviroment: The environment to use 157 | /// - completion: The completion block 158 | static func getAvailableApplicationDescriptors(enviroment: Enviroment, completion: @escaping (Result<[TracingApplicationDescriptor], STARTracingErrors>) -> Void) { 159 | let url = enviroment.discoveryEndpoint 160 | let request = URLRequest(url: url) 161 | 162 | let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in 163 | guard error == nil else { 164 | completion(.failure(.NetworkingError(error: error))) 165 | return 166 | } 167 | guard let responseData = data else { 168 | completion(.failure(.NetworkingError(error: nil))) 169 | return 170 | } 171 | guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { 172 | completion(.failure(.NetworkingError(error: nil))) 173 | return 174 | } 175 | guard statusCode == 200 else { 176 | completion(.failure(.NetworkingError(error: nil))) 177 | return 178 | } 179 | do { 180 | let discoveryResponse = try JSONDecoder().decode(DiscoveryServiceResponse.self, from: responseData) 181 | return completion(.success(discoveryResponse.applications)) 182 | } catch { 183 | completion(.failure(.NetworkingError(error: error))) 184 | return 185 | } 186 | }) 187 | task.resume() 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Sources/STARSDK/Networking/KnownCasesSynchronizer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | Synchronizes data on known cases for the time period of the past 14 days and stores them to the local database. 5 | 6 | Use a fresh instance for every synchronization call. 7 | */ 8 | class KnownCasesSynchronizer { 9 | /// The app id to use 10 | private let appId: String 11 | /// A database to store the known cases 12 | private let database: KnownCasesStorage 13 | 14 | // keep track of errors and successes with regard to individual requests (networking or database errors) 15 | private var errors = [(String, Error)]() 16 | // a list of temporary known cases 17 | private var knownCases = [String: [KnownCaseModel]]() 18 | 19 | // keep track of the number of issued and fulfilled requests 20 | private var numberOfIssuedRequests: Int = 0 21 | private var numberOfFulfilledRequests: Int = 0 22 | 23 | /// A STAR matcher 24 | private weak var matcher: STARMatcher? 25 | 26 | /// Create a known case synchronizer 27 | /// - Parameters: 28 | /// - appId: The app id to use 29 | /// - database: The database for storage 30 | /// - matcher: The matcher for STAR resolution and checks 31 | init(appId: String, database: STARDatabase, matcher: STARMatcher) { 32 | self.appId = appId 33 | self.database = database.knownCasesStorage 34 | self.matcher = matcher 35 | } 36 | 37 | /// A callback result of async operations 38 | typealias Callback = (Result) -> Void 39 | 40 | /// Synchronizes the local database with the remote one 41 | /// - Parameters: 42 | /// - service: The service to use for synchronization 43 | /// - callback: The callback once the task if finished 44 | func sync(service: ExposeeServiceClient, callback: Callback?) { 45 | errors.removeAll() 46 | knownCases.removeAll() 47 | // compute day identifiers (formatted dates) for the last 14 days 48 | let dayIdentifierFormatter = DateFormatter() 49 | dayIdentifierFormatter.dateFormat = "yyyy-MM-dd" 50 | let dayIdentifiers = (0 ..< 14).reversed().map { days -> String in 51 | let date = Calendar.current.date(byAdding: .day, value: -1 * days, to: Date())! 52 | return dayIdentifierFormatter.string(from: date) 53 | } 54 | 55 | for dayIdentifier in dayIdentifiers { 56 | service.getExposee(dayIdentifier: dayIdentifier, 57 | completion: dayResultHandler(dayIdentifier, callback: callback)) 58 | } 59 | } 60 | 61 | /// Handle a single day 62 | /// - Parameters: 63 | /// - dayIdentifier: The day identifier 64 | /// - callback: The callback once the task is finished 65 | private func dayResultHandler(_ dayIdentifier: String, callback: Callback?) -> (Result<[KnownCaseModel], STARTracingErrors>) -> Void { 66 | numberOfIssuedRequests += 1 67 | return { result in 68 | switch result { 69 | case let .failure(error): 70 | self.errors.append((dayIdentifier, error)) 71 | case let .success(data): 72 | self.knownCases[dayIdentifier] = data 73 | } 74 | self.numberOfFulfilledRequests += 1 75 | self.checkForCompletion(callback: callback) 76 | } 77 | } 78 | 79 | /** Checks whether all issued requests have completed and then invokes the subsuming completion handler */ 80 | private func checkForCompletion(callback: Callback?) { 81 | guard numberOfFulfilledRequests == numberOfIssuedRequests else { 82 | return 83 | } 84 | 85 | if errors.count == numberOfIssuedRequests { // all requests failed 86 | callback?(Result.failure(.CaseSynchronizationError)) 87 | } else if errors.count > 0 { // some requests failed 88 | callback?(Result.failure(.CaseSynchronizationError)) 89 | } else { // all requests were successful 90 | processDayResults(callback: callback) 91 | } 92 | } 93 | 94 | /** Process all received day data. */ 95 | private func processDayResults(callback: Callback?) { 96 | // TODO: Handle db errors 97 | for (day, knownCases) in knownCases { 98 | try? database.update(knownCases: knownCases, day: day) 99 | for knownCase in knownCases { 100 | try? matcher?.checkNewKnownCase(knownCase, bucketDay: day) 101 | } 102 | } 103 | 104 | callback?(Result.success(())) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/STARSDK/STARErrors.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | 5 | /// SDK Errors 6 | public enum STARTracingErrors: Error { 7 | /// Networking Error 8 | case NetworkingError(error: Error?) 9 | 10 | /// Error happend during known case synchronization 11 | case CaseSynchronizationError 12 | 13 | /// Cryptography Error 14 | case CryptographyError(error: String) 15 | 16 | /// Databse Error 17 | case DatabaseError(error: Error) 18 | 19 | /// Bluetooth device turned off 20 | case BluetoothTurnedOff 21 | 22 | /// Bluetooth permission error 23 | case PermissonError 24 | } 25 | -------------------------------------------------------------------------------- /Sources/STARSDK/STARMatching.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A delegate used to respond on STAR events 4 | protocol STARMatcherDelegate: class { 5 | /// We found a match 6 | func didFindMatch() 7 | 8 | /// A new handshake occured 9 | func handShakeAdded(_ handshake: HandshakeModel) 10 | } 11 | 12 | /// matcher for STAR tokens 13 | class STARMatcher { 14 | /// The STAR crypto algorithm 15 | private let starCrypto: STARCryptoModule 16 | 17 | /// Databse 18 | private weak var database: STARDatabase! 19 | 20 | /// Delegate to notify on STAR events 21 | public weak var delegate: STARMatcherDelegate! 22 | 23 | /// Initializer 24 | /// - Parameters: 25 | /// - database: databse 26 | /// - starCrypto: star algorithm 27 | init(database: STARDatabase, starCrypto: STARCryptoModule) throws { 28 | self.database = database 29 | self.starCrypto = starCrypto 30 | } 31 | 32 | /// check for new known case 33 | /// - Parameter knownCase: known Case 34 | func checkNewKnownCase(_ knownCase: KnownCaseModel, bucketDay: String) throws { 35 | let dateFormatter = DateFormatter() 36 | dateFormatter.timeZone = TimeZone(identifier: "UTC")! 37 | dateFormatter.dateFormat = "yyyy-MM-dd" 38 | let onset = dateFormatter.date(from: knownCase.onset)! 39 | let bucketDayDate = dateFormatter.date(from: bucketDay)! 40 | 41 | let handshake = try starCrypto.checkContacts(secretKey: knownCase.key, onsetDate: Epoch(date: onset), bucketDate: Epoch(date: bucketDayDate)) { (day) -> ([HandshakeModel]) in 42 | return (try? database.handshakesStorage.getBy(day: day)) ?? [] 43 | } 44 | 45 | if let handshakeid = handshake?.identifier, 46 | let knownCaseId = try? database.knownCasesStorage.getId(for: knownCase.key) { 47 | try database.handshakesStorage.addKnownCase(knownCaseId, to: handshakeid) 48 | delegate.didFindMatch() 49 | } 50 | } 51 | } 52 | 53 | // MARK: BluetoothDiscoveryDelegate implementation 54 | 55 | extension STARMatcher: BluetoothDiscoveryDelegate { 56 | func didDiscover(data : Data, TXPowerlevel : Double?, RSSI : Double?) throws { 57 | // Do no realtime matching 58 | let handshake = HandshakeModel(timestamp: Date(), 59 | star: data, 60 | TXPowerlevel: TXPowerlevel, 61 | RSSI: RSSI, 62 | knownCaseId: nil) 63 | try database.handshakesStorage.add(handshake: handshake) 64 | 65 | delegate.handShakeAdded(handshake) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/STARSDK/STARMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | /// This is used to differentiate between production and calibration mode 4 | public enum STARMode: Equatable { 5 | case production 6 | #if CALIBRATION 7 | case calibration(identifierPrefix: String) 8 | #endif 9 | 10 | static var current: STARMode = .production 11 | } 12 | -------------------------------------------------------------------------------- /Sources/STARSDK/STARSDK.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | import os 5 | import UIKit 6 | 7 | /// Main class for handling SDK logic 8 | class STARSDK { 9 | /// appId of this instance 10 | private let appId: String 11 | 12 | /// A service to broadcast bluetooth packets containing the STAR token 13 | private let broadcaster: BluetoothBroadcastService 14 | 15 | /// The discovery service responsible of scanning for nearby bluetooth devices offering the STAR service 16 | private let discoverer: BluetoothDiscoveryService 17 | 18 | /// matcher for STAR tokens 19 | private let matcher: STARMatcher 20 | 21 | /// databsase 22 | private let database: STARDatabase 23 | 24 | /// The STAR crypto algorithm 25 | private let starCrypto: STARCryptoModule 26 | 27 | /// Fetch the discovery data and stores it 28 | private let applicationSynchronizer: ApplicationSynchronizer 29 | 30 | /// Synchronizes data on known cases 31 | private let synchronizer: KnownCasesSynchronizer 32 | 33 | /// tracing service client 34 | private var cachedTracingServiceClient: ExposeeServiceClient? 35 | 36 | /// enviroemnt of this instance 37 | private let enviroment: Enviroment 38 | 39 | /// delegate 40 | public weak var delegate: STARTracingDelegate? 41 | 42 | #if CALIBRATION 43 | /// getter for identifier prefix for calibration mode 44 | private(set) var identifierPrefix: String { 45 | get { 46 | switch STARMode.current { 47 | case let .calibration(identifierPrefix): 48 | return identifierPrefix 49 | default: 50 | fatalError("identifierPrefix is only usable in calibration mode") 51 | } 52 | } 53 | set {} 54 | } 55 | #endif 56 | 57 | /// keeps track of SDK state 58 | private var state: TracingState { 59 | didSet { 60 | Default.shared.infectionStatus = state.infectionStatus 61 | Default.shared.lastSync = state.lastSync 62 | DispatchQueue.main.async { 63 | self.delegate?.STARTracingStateChanged(self.state) 64 | } 65 | } 66 | } 67 | 68 | /// Initializer 69 | /// - Parameters: 70 | /// - appId: application identifer to use for discovery call 71 | /// - enviroment: enviroment to use 72 | init(appId: String, enviroment: Enviroment) throws { 73 | self.enviroment = enviroment 74 | self.appId = appId 75 | database = try STARDatabase() 76 | starCrypto = STARCryptoModule()! 77 | matcher = try STARMatcher(database: database, starCrypto: starCrypto) 78 | synchronizer = KnownCasesSynchronizer(appId: appId, database: database, matcher: matcher) 79 | applicationSynchronizer = ApplicationSynchronizer(enviroment: enviroment, storage: database.applicationStorage) 80 | broadcaster = BluetoothBroadcastService(starCrypto: starCrypto) 81 | discoverer = BluetoothDiscoveryService(storage: database.peripheralStorage) 82 | state = TracingState(numberOfHandshakes: (try? database.handshakesStorage.count()) ?? 0, 83 | trackingState: .stopped, 84 | lastSync: Default.shared.lastSync, 85 | infectionStatus: Default.shared.infectionStatus) 86 | 87 | broadcaster.permissionDelegate = self 88 | discoverer.permissionDelegate = self 89 | discoverer.delegate = matcher 90 | matcher.delegate = self 91 | 92 | #if CALIBRATION 93 | broadcaster.logger = self 94 | discoverer.logger = self 95 | database.logger = self 96 | #endif 97 | 98 | print(database) 99 | 100 | try applicationSynchronizer.sync { [weak self] result in 101 | guard let self = self else { return } 102 | switch result { 103 | case .success: 104 | if let desc = try? self.database.applicationStorage.descriptor(for: self.appId) { 105 | let client = ExposeeServiceClient(descriptor: desc) 106 | self.cachedTracingServiceClient = client 107 | } 108 | case let .failure(error): 109 | DispatchQueue.main.async { 110 | self.state.trackingState = .inactive(error: error) 111 | self.stopTracing() 112 | } 113 | } 114 | } 115 | } 116 | 117 | /// start tracing 118 | func startTracing() throws { 119 | state.trackingState = .active 120 | discoverer.startScanning() 121 | broadcaster.startService() 122 | } 123 | 124 | /// stop tracing 125 | func stopTracing() { 126 | discoverer.stopScanning() 127 | broadcaster.stopService() 128 | state.trackingState = .stopped 129 | } 130 | 131 | #if CALIBRATION 132 | func startAdvertising() throws { 133 | state.trackingState = .activeAdvertising 134 | broadcaster.startService() 135 | } 136 | 137 | func startReceiving() throws { 138 | state.trackingState = .activeReceiving 139 | discoverer.startScanning() 140 | } 141 | #endif 142 | 143 | /// Perform a new sync 144 | /// - Parameter callback: callback 145 | func sync(callback: ((Result) -> Void)?) { 146 | getATracingServiceClient(forceRefresh: true) { [weak self] result in 147 | switch result { 148 | case let .failure(error): 149 | callback?(.failure(error)) 150 | return 151 | case let .success(service): 152 | self?.synchronizer.sync(service: service) { [weak self] result in 153 | if case .success = result { 154 | self?.state.lastSync = Date() 155 | } 156 | callback?(result) 157 | } 158 | } 159 | } 160 | } 161 | 162 | /// get the current status of the SDK 163 | /// - Parameter callback: callback 164 | func status(callback: (Result) -> Void) { 165 | try? state.numberOfHandshakes = database.handshakesStorage.count() 166 | callback(.success(state)) 167 | } 168 | 169 | /// tell the SDK that the user was exposed 170 | /// - Parameters: 171 | /// - onset: Start date of the exposure 172 | /// - authString: Authentication string for the exposure change 173 | /// - callback: callback 174 | func iWasExposed(onset: Date, authString: String, callback: @escaping (Result) -> Void) { 175 | setExposed(onset: onset, authString: authString, callback: callback) 176 | } 177 | 178 | /// used to construct a new tracing service client 179 | private func getATracingServiceClient(forceRefresh: Bool, callback: @escaping (Result) -> Void) { 180 | if forceRefresh == false, let cachedTracingServiceClient = cachedTracingServiceClient { 181 | callback(.success(cachedTracingServiceClient)) 182 | return 183 | } 184 | try? applicationSynchronizer.sync { [weak self] result in 185 | guard let self = self else { return } 186 | switch result { 187 | case .success: 188 | if let desc = try? self.database.applicationStorage.descriptor(for: self.appId) { 189 | let client = ExposeeServiceClient(descriptor: desc) 190 | self.cachedTracingServiceClient = client 191 | callback(.success(client)) 192 | } else { 193 | callback(.failure(STARTracingErrors.CaseSynchronizationError)) 194 | } 195 | case let .failure(error): 196 | callback(.failure(error)) 197 | } 198 | } 199 | } 200 | 201 | /// update the backend with the new exposure state 202 | /// - Parameters: 203 | /// - onset: Start date of the exposure 204 | /// - authString: Authentication string for the exposure change 205 | /// - callback: callback 206 | private func setExposed(onset: Date, authString: String, callback: @escaping (Result) -> Void) { 207 | getATracingServiceClient(forceRefresh: false) { [weak self] result in 208 | guard let self = self else { 209 | return 210 | } 211 | switch result { 212 | case let .failure(error): 213 | DispatchQueue.main.async { 214 | callback(.failure(error)) 215 | } 216 | case let .success(service): 217 | do { 218 | let block: ((Result) -> Void) = { [weak self] result in 219 | if case .success = result { 220 | self?.state.infectionStatus = .infected 221 | } 222 | DispatchQueue.main.async { 223 | callback(result) 224 | } 225 | } 226 | let dateFormatter = DateFormatter() 227 | dateFormatter.dateFormat = "yyyy-MM-dd" 228 | let model = ExposeeModel(key: try self.starCrypto.getSecretKeyForPublishing(onsetDate: onset)!, onset: dateFormatter.string(from: onset), authData: ExposeeAuthData(value: authString)) 229 | service.addExposee(model, completion: block) 230 | 231 | } catch let error as STARTracingErrors { 232 | DispatchQueue.main.async { 233 | callback(.failure(error)) 234 | } 235 | } catch { 236 | DispatchQueue.main.async { 237 | callback(.failure(STARTracingErrors.CryptographyError(error: "Cannot get secret key"))) 238 | } 239 | } 240 | } 241 | } 242 | } 243 | 244 | /// reset the SDK 245 | func reset() throws { 246 | stopTracing() 247 | Default.shared.lastSync = nil 248 | Default.shared.infectionStatus = .healthy 249 | try database.emptyStorage() 250 | try database.destroyDatabase() 251 | starCrypto.reset() 252 | } 253 | 254 | #if CALIBRATION 255 | func getHandshakes(request: HandshakeRequest) throws -> HandshakeResponse { 256 | try database.handshakesStorage.getHandshakes(request) 257 | } 258 | 259 | func numberOfHandshakes() throws -> Int { 260 | try database.handshakesStorage.numberOfHandshakes() 261 | } 262 | 263 | func getLogs(request: LogRequest) throws -> LogResponse { 264 | return try database.loggingStorage.getLogs(request) 265 | } 266 | #endif 267 | } 268 | 269 | // MARK: STARMatcherDelegate implementation 270 | 271 | extension STARSDK: STARMatcherDelegate { 272 | func didFindMatch() { 273 | state.infectionStatus = .exposed 274 | } 275 | 276 | func handShakeAdded(_ handshake: HandshakeModel) { 277 | if let newHandshaked = try? database.handshakesStorage.count() { 278 | state.numberOfHandshakes = newHandshaked 279 | } 280 | #if CALIBRATION 281 | delegate?.didAddHandshake(handshake) 282 | #endif 283 | } 284 | } 285 | 286 | // MARK: BluetoothPermissionDelegate implementation 287 | 288 | extension STARSDK: BluetoothPermissionDelegate { 289 | func deviceTurnedOff() { 290 | state.trackingState = .inactive(error: .BluetoothTurnedOff) 291 | } 292 | 293 | func unauthorized() { 294 | state.trackingState = .inactive(error: .PermissonError) 295 | } 296 | } 297 | 298 | #if CALIBRATION 299 | extension STARSDK: LoggingDelegate { 300 | func log(type: LogType, _ string: String) { 301 | os_log("%@: %@", type.description, string) 302 | if let entry = try? database.loggingStorage.log(type: type, message: string) { 303 | delegate?.didAddLog(entry) 304 | } 305 | } 306 | } 307 | #endif 308 | -------------------------------------------------------------------------------- /Sources/STARSDK/STARTracing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A delegate for the STAR tracing 4 | public protocol STARTracingDelegate: AnyObject { 5 | /// The state has changed 6 | /// - Parameter state: The new state 7 | func STARTracingStateChanged(_ state: TracingState) 8 | 9 | #if CALIBRATION 10 | func didAddLog(_ entry: LogEntry) 11 | func didAddHandshake(_ handshake: HandshakeModel) 12 | #endif 13 | } 14 | 15 | #if CALIBRATION 16 | public extension STARTracingDelegate { 17 | func didAddLog(_: LogEntry) {} 18 | func didAddHandshake(_: HandshakeModel) {} 19 | } 20 | #endif 21 | 22 | private var instance: STARSDK! 23 | 24 | /// STARTracing 25 | public enum STARTracing { 26 | /// initialize the SDK 27 | /// - Parameter appId: application identifier used for the discovery call 28 | /// - Parameter enviroment: enviroment to use 29 | public static func initialize(with appId: String, enviroment: Enviroment, mode: STARMode = .production) throws { 30 | guard instance == nil else { 31 | fatalError("STARSDK already initialized") 32 | } 33 | STARMode.current = mode 34 | instance = try STARSDK(appId: appId, enviroment: enviroment) 35 | } 36 | 37 | /// The delegate 38 | public static var delegate: STARTracingDelegate? { 39 | set { 40 | guard instance != nil else { 41 | fatalError("STARSDK not initialized") 42 | } 43 | instance.delegate = newValue 44 | } 45 | get { 46 | instance.delegate 47 | } 48 | } 49 | 50 | /// Starts Bluetooth tracing 51 | public static func startTracing() throws { 52 | guard let instance = instance else { 53 | fatalError("STARSDK not initialized call `initialize(with:delegate:)`") 54 | } 55 | try instance.startTracing() 56 | } 57 | 58 | /// Stops Bluetooth tracing 59 | public static func stopTracing() { 60 | guard let instance = instance else { 61 | fatalError("STARSDK not initialized call `initialize(with:delegate:)`") 62 | } 63 | instance.stopTracing() 64 | } 65 | 66 | /// Triggers sync with the backend to refresh the exposed list 67 | /// - Parameter callback: callback 68 | public static func sync(callback: ((Result) -> Void)?) { 69 | guard let instance = instance else { 70 | fatalError("STARSDK not initialized call `initialize(with:delegate:)`") 71 | } 72 | instance.sync { result in 73 | DispatchQueue.main.async { 74 | callback?(result) 75 | } 76 | } 77 | } 78 | 79 | /// get the current status of the SDK 80 | /// - Parameter callback: callback 81 | public static func status(callback: (Result) -> Void) { 82 | guard let instance = instance else { 83 | fatalError("STARSDK not initialized call `initialize(with:delegate:)`") 84 | } 85 | instance.status(callback: callback) 86 | } 87 | 88 | /// tell the SDK that the user was exposed 89 | /// - Parameters: 90 | /// - onset: Start date of the exposure 91 | /// - authString: Authentication string for the exposure change 92 | /// - callback: callback 93 | public static func iWasExposed(onset: Date, authString: String, callback: @escaping (Result) -> Void) { 94 | guard let instance = instance else { 95 | fatalError("STARSDK not initialized call `initialize(with:delegate:)`") 96 | } 97 | instance.iWasExposed(onset: onset, authString: authString, callback: callback) 98 | } 99 | 100 | /// reset the SDK 101 | public static func reset() throws { 102 | guard instance != nil else { 103 | fatalError("STARSDK not initialized call `initialize(with:delegate:)`") 104 | } 105 | try instance.reset() 106 | instance = nil 107 | } 108 | 109 | #if CALIBRATION 110 | public static func startAdvertising() throws { 111 | guard let instance = instance else { 112 | fatalError("STARSDK not initialized call `initialize(with:delegate:)`") 113 | } 114 | try instance.startAdvertising() 115 | } 116 | 117 | public static func startReceiving() throws { 118 | guard let instance = instance else { 119 | fatalError("STARSDK not initialized call `initialize(with:delegate:)`") 120 | } 121 | try instance.startReceiving() 122 | } 123 | 124 | public static func getHandshakes(request: HandshakeRequest) throws -> HandshakeResponse { 125 | try instance.getHandshakes(request: request) 126 | } 127 | 128 | public static func getLogs(request: LogRequest) throws -> LogResponse { 129 | guard let instance = instance else { 130 | fatalError("STARSDK not initialized call `initialize(with:delegate:)`") 131 | } 132 | return try instance.getLogs(request: request) 133 | } 134 | 135 | public static func numberOfHandshakes() throws -> Int { 136 | try instance.numberOfHandshakes() 137 | } 138 | 139 | public static var isInitialized: Bool { 140 | return instance != nil 141 | } 142 | 143 | public static var reconnectionDelay: Int { 144 | get { 145 | return BluetoothConstants.peripheralReconnectDelay 146 | } 147 | set { 148 | BluetoothConstants.peripheralReconnectDelay = newValue 149 | } 150 | } 151 | #endif 152 | } 153 | -------------------------------------------------------------------------------- /Sources/STARSDK/STARTracingState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The infection status of the user 4 | public enum InfectionStatus: Int { 5 | /// The user is healthy and had no contact with any infected person 6 | case healthy 7 | /// The user is infected and has signaled it himself 8 | case infected 9 | /// The user was in contact with a person that was flagged as infected 10 | case exposed 11 | } 12 | 13 | /// The tracking state of the bluetooth and the other networking api 14 | public enum TrackingState { 15 | /// The tracking is active and working fine 16 | case active 17 | 18 | #if CALIBRATION 19 | case activeReceiving 20 | case activeAdvertising 21 | #endif 22 | 23 | /// The tracking is stopped by the user 24 | case stopped 25 | /// The tracking is facing some issues that needs to be solved 26 | case inactive(error: STARTracingErrors) 27 | } 28 | 29 | /// The state of the API 30 | public struct TracingState { 31 | /// The number of encounters with other people 32 | public var numberOfHandshakes: Int 33 | /// The tracking state of the bluetooth and the other networking api 34 | public var trackingState: TrackingState 35 | /// The last syncronization when the list of infected people was fetched 36 | public var lastSync: Date? 37 | /// The infection status of the user 38 | public var infectionStatus: InfectionStatus 39 | } 40 | -------------------------------------------------------------------------------- /Sources/STARSDK_CALIBRATION: -------------------------------------------------------------------------------- 1 | STARSDK -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import STARTracingTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += STARTracingTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/STARSDKTests/CovidTracingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | @testable import STARSDK 4 | import XCTest 5 | 6 | final class STARTracingTests: XCTestCase { 7 | func testExample() { 8 | // This is an example of a functional test case. 9 | // Use XCTAssert and related functions to verify your tests produce the correct 10 | // results. 11 | } 12 | 13 | static var allTests = [ 14 | ("testExample", testExample), 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /Tests/STARSDKTests/CryptoTest.swift: -------------------------------------------------------------------------------- 1 | @testable import STARSDK 2 | import XCTest 3 | 4 | private class KeyStore: SecretKeyStorageProtocol { 5 | var keys: [SecretKey] = [] 6 | 7 | func get() throws -> [SecretKey] { 8 | return keys 9 | } 10 | 11 | func set(_ object: [SecretKey]) throws { 12 | keys = object 13 | } 14 | 15 | func removeAllObject() { 16 | keys = [] 17 | } 18 | } 19 | 20 | final class STARTracingCryptoTests: XCTestCase { 21 | func testSha256() { 22 | let string = "COVID19" 23 | let strData = string.data(using: .utf8)! 24 | let digest = Crypto.sha256(strData) 25 | let hex = digest.base64EncodedString() 26 | XCTAssertEqual(hex, "wdvvalTpy3jExBEyO6iIHps+HUsrnwgCtMGpi86eq4c=") 27 | } 28 | 29 | func testHmac() { 30 | let secretKey = "pcu8RDQQhvzL7oCOZjLCBdAodQfNK406m1x9JXugxoY=" 31 | let secretKeyData = Data(base64Encoded: secretKey)! 32 | let expected = "M+AgJ345G+6AZYu1Cx2IGD6VL1YigLmFrG0roTmIlQA=" 33 | let real = Crypto.hmac(msg: CryptoConstants.broadcastKey, key: secretKeyData) 34 | XCTAssertEqual(real.base64EncodedString(), expected) 35 | } 36 | 37 | func testGenerateEphIds() { 38 | let store = KeyStore() 39 | let star: STARCryptoModule = STARCryptoModule(store: store)! 40 | let allEphsOfToday = try! star.createEphIds(secretKey: star.getSecretKeyForPublishing(onsetDate: Date())!) 41 | let currentEphId = try! star.getCurrentEphId() 42 | var matchingCount = 0 43 | for ephId in allEphsOfToday { 44 | XCTAssert(ephId.count == CryptoConstants.keyLenght) 45 | if ephId == currentEphId { 46 | matchingCount += 1 47 | } 48 | } 49 | XCTAssert(matchingCount == 1) 50 | } 51 | 52 | func testGenerationEphsIdsWithAndorid() { 53 | let store = KeyStore() 54 | let star: STARCryptoModule = STARCryptoModule(store: store)! 55 | let base64SecretKey = "MZbZmgsA+9b0A8mkkcAQJcww727M8tlI1zO/2eGZ/DA=" 56 | let base64EncodedEphId = "IYiXz8YZcqTUGNhmHk422UlogB6bQAFGr6Q=" 57 | let base64EncodedEph1Id = "iavGGZym0MwjmWhJP8vk4Fmer2sO/YHGgmg=" 58 | let allEphId: [Data] = try! star.createEphIds(secretKey: Data(base64Encoded: base64SecretKey)!) 59 | var matchingCount = 0 60 | for ephId in allEphId { 61 | if ephId.base64EncodedString() == base64EncodedEphId { 62 | matchingCount += 1 63 | } 64 | if ephId.base64EncodedString() == base64EncodedEph1Id { 65 | matchingCount += 1 66 | } 67 | } 68 | XCTAssert(matchingCount == 2) 69 | } 70 | 71 | func testReset() { 72 | let store = KeyStore() 73 | var star: STARCryptoModule? = STARCryptoModule(store: store)! 74 | let ephId = try! star!.getCurrentEphId() 75 | 76 | star!.reset() 77 | star = nil 78 | star = STARCryptoModule(store: store)! 79 | 80 | let newEphId = try! star!.getCurrentEphId() 81 | 82 | XCTAssertNotEqual(ephId, newEphId) 83 | } 84 | 85 | func testTokenToday() { 86 | let key = "lTSYc/ER08HD1/ucwBJOiDLDEYiJruKqTHCiOFavzwA=" 87 | let token = "yJNfwAP8UaF+BZKbUiVwhUghLz60SOqPE0I=" 88 | testKeyAndTokenToday(key, token, found: true) 89 | } 90 | 91 | func testWrongTokenToday() { 92 | let key = "yJNfwAP8UaF+BZKbUiVwhUghLz60SOqPE0I=" 93 | let token = "lTSYc/ER08HD1/ucwBJOiDLDEYiJruKqTHCiOFavzwA=" 94 | testKeyAndTokenToday(key, token, found: false) 95 | } 96 | 97 | func testSecretKeyPushlishing() { 98 | let store1 = KeyStore() 99 | let star1: STARCryptoModule = STARCryptoModule(store: store1)! 100 | let token = try! star1.getCurrentEphId() 101 | _ = try! star1.getCurrentSK(day: Epoch(date: Date().addingTimeInterval(1 * .day))) 102 | _ = try! star1.getCurrentSK(day: Epoch(date: Date().addingTimeInterval(2 * .day))) 103 | _ = try! star1.getCurrentSK(day: Epoch(date: Date().addingTimeInterval(3 * .day))) 104 | 105 | let key = (try! star1.getSecretKeyForPublishing(onsetDate: Date()))! 106 | 107 | var handshakes: [HandshakeModel] = [] 108 | handshakes.append(HandshakeModel(identifier: 0, timestamp: Date(), star: token, TXPowerlevel: nil, RSSI: nil, knownCaseId: nil)) 109 | 110 | let store2 = KeyStore() 111 | let star2: STARCryptoModule = STARCryptoModule(store: store2)! 112 | 113 | let h = try! star2.checkContacts(secretKey: key, onsetDate: Epoch(date: Date()), bucketDate: Epoch(date: Date().addingTimeInterval(.day)), getHandshake: { (_) -> ([HandshakeModel]) in 114 | handshakes 115 | }) 116 | 117 | XCTAssertNotNil(h) 118 | } 119 | 120 | func testSecretKeyPushlishingOnsetAfterContact() { 121 | let store1 = KeyStore() 122 | let star1: STARCryptoModule = STARCryptoModule(store: store1)! 123 | let token = try! star1.getCurrentEphId() 124 | _ = try! star1.getCurrentSK(day: Epoch(date: Date().addingTimeInterval(1 * .day))) 125 | _ = try! star1.getCurrentSK(day: Epoch(date: Date().addingTimeInterval(2 * .day))) 126 | _ = try! star1.getCurrentSK(day: Epoch(date: Date().addingTimeInterval(3 * .day))) 127 | 128 | let key = (try! star1.getSecretKeyForPublishing(onsetDate: Date().addingTimeInterval(.day)))! 129 | 130 | var handshakes: [HandshakeModel] = [] 131 | handshakes.append(HandshakeModel(identifier: 0, timestamp: Date(), star: token, TXPowerlevel: nil, RSSI: nil, knownCaseId: nil)) 132 | 133 | let store2 = KeyStore() 134 | let star2: STARCryptoModule = STARCryptoModule(store: store2)! 135 | 136 | let h = try! star2.checkContacts(secretKey: key, onsetDate: Epoch(date: Date()), bucketDate: Epoch(date: Date().addingTimeInterval(.day)), getHandshake: { (_) -> ([HandshakeModel]) in 137 | handshakes 138 | }) 139 | 140 | XCTAssertNil(h) 141 | } 142 | 143 | func testKeyAndTokenToday(_ key: String, _ token: String, found: Bool) { 144 | let store = KeyStore() 145 | let star: STARCryptoModule? = STARCryptoModule(store: store)! 146 | 147 | var handshakes: [HandshakeModel] = [] 148 | handshakes.append(HandshakeModel(identifier: 0, timestamp: Date(), star: Data(base64Encoded: token)!, TXPowerlevel: nil, RSSI: nil, knownCaseId: nil)) 149 | 150 | let keyData = Data(base64Encoded: key)! 151 | let h = try! star?.checkContacts(secretKey: keyData, onsetDate: Epoch(date: Date().addingTimeInterval(-1 * .day)), bucketDate: Epoch(), getHandshake: { (_) -> ([HandshakeModel]) in 152 | handshakes 153 | }) 154 | XCTAssertEqual(h != nil, found) 155 | } 156 | 157 | static var allTests = [ 158 | ("sha256", testSha256), 159 | ("generateEphIds", testGenerateEphIds), 160 | ("generateEphIdsAndroid", testGenerationEphsIdsWithAndorid), 161 | ("testHmac", testHmac), 162 | ("testReset", testReset), 163 | ("testTokenToday", testTokenToday), 164 | ("testWrongTokenToday", testWrongTokenToday), 165 | ("testSecretKeyPushlishing", testSecretKeyPushlishing), 166 | ("testSecretKeyPushlishingOnsetAfterContact", testSecretKeyPushlishingOnsetAfterContact), 167 | ] 168 | } 169 | -------------------------------------------------------------------------------- /Tests/STARSDKTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(STARTracingTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------