├── Resources
└── Screenshot.png
├── BLE-Viewer
├── Assets.xcassets
│ ├── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── BLE_Viewer.entitlements
├── HandoffModels
│ ├── ScanModel.swift
│ ├── HandoffBLE.swift
│ ├── HandoffAdvertisement.swift
│ └── HandoffActivity.swift
├── ContentView.swift
├── Info.plist
├── Extensions
│ ├── HexToDataExtensions.swift
│ └── DataIntExtensions.swift
├── AppDelegate.swift
├── Logging
│ └── Log.swift
├── BLE
│ └── BLEReceiver.swift
├── Crypto
│ ├── BLEDecryptor.swift
│ └── KeychainAccess.swift
├── BLEScannerVC.swift
└── Base.lproj
│ └── Main.storyboard
├── BLE-Viewer.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
├── xcuserdata
│ └── Alex.xcuserdatad
│ │ ├── xcschemes
│ │ └── xcschememanagement.plist
│ │ └── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
└── project.pbxproj
├── BLE-ViewerTests
├── Info.plist
└── BLE_ViewerTests.swift
├── LICENSE
└── README.md
/Resources/Screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sn0wfreezeDev/Handoff-BLE-Viewer/HEAD/Resources/Screenshot.png
--------------------------------------------------------------------------------
/BLE-Viewer/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/BLE-Viewer/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/BLE-Viewer.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/BLE-Viewer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/BLE-Viewer/BLE_Viewer.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.device.bluetooth
8 |
9 | com.apple.security.files.user-selected.read-only
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/BLE-Viewer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "CryptoSwift",
6 | "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "3a2acbb32ab68215ee1596ee6004da8e90c3721b",
10 | "version": "1.0.0"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/BLE-Viewer.xcodeproj/xcuserdata/Alex.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | BLE-Viewer.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/BLE-Viewer/HandoffModels/ScanModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReceivedAdvertisements.swift
3 | // BLE-Viewer
4 | //
5 | // Created by Alexander Heinrich on 26.09.19.
6 | // Copyright © 2019 Alexander Heinrich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CoreBluetooth
11 |
12 | class ScanModel {
13 | let peripheral: CBPeripheral
14 | var advertisements: [HandoffAdvertisement] = []
15 |
16 | init(peripheral: CBPeripheral, advertisements: [HandoffAdvertisement] = []) {
17 | self.peripheral = peripheral
18 | self.advertisements = advertisements
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/BLE-Viewer/ContentView.swift:
--------------------------------------------------------------------------------
1 | ////
2 | //// ContentView.swift
3 | //// BLE-Viewer
4 | ////
5 | //// Created by Alexander Heinrich on 24.09.19.
6 | //// Copyright © 2019 Alexander Heinrich. All rights reserved.
7 | ////
8 | //
9 | //
10 | //import SwiftUI
11 | //
12 | //@available(macOS 10.15, *)
13 | //struct ContentView: View {
14 | //// var body: some View {
15 | // Text("Hello World")
16 | // .frame(maxWidth: .infinity, maxHeight: .infinity)
17 | // }
18 | //}
19 | //
20 | //@available(macOS 10.15, *)
21 | //struct ContentView_Previews: PreviewProvider {
22 | // static var previews: some View {
23 | // ContentView()
24 | // }
25 | //}
26 |
--------------------------------------------------------------------------------
/BLE-ViewerTests/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 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Alexander Heinrich
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/BLE-Viewer/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "size" : "16x16",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "size" : "16x16",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "size" : "32x32",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "size" : "32x32",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "size" : "128x128",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "size" : "128x128",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "size" : "256x256",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "size" : "256x256",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "size" : "512x512",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "size" : "512x512",
51 | "scale" : "2x"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
--------------------------------------------------------------------------------
/BLE-Viewer/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSApplicationCategoryType
24 | public.app-category.developer-tools
25 | LSMinimumSystemVersion
26 | $(MACOSX_DEPLOYMENT_TARGET)
27 | NSHumanReadableCopyright
28 | Copyright © 2019 Alexander Heinrich. All rights reserved.
29 | NSMainStoryboardFile
30 | Main
31 | NSPrincipalClass
32 | NSApplication
33 | NSSupportsAutomaticTermination
34 |
35 | NSSupportsSuddenTermination
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/BLE-Viewer/Extensions/HexToDataExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HexToDataExtensions.swift
3 | // ThesisTools
4 | //
5 | // Created by Alexander Heinrich on 08.05.19.
6 | // Copyright © 2019 Alexander Heinrich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension String {
12 |
13 | /// Create `Data` from hexadecimal string representation
14 | ///
15 | /// This creates a `Data` object from hex string. Note, if the string has any spaces or non-hex characters (e.g. starts with '<' and with a '>'), those are ignored and only hex characters are processed.
16 | ///
17 | /// - returns: Data represented by this hexadecimal string.
18 |
19 | var hexadecimal: Data? {
20 | var data = Data(capacity: count / 2)
21 |
22 | let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive)
23 | regex.enumerateMatches(in: self, range: NSRange(startIndex..., in: self)) { match, _, _ in
24 | let byteString = (self as NSString).substring(with: match!.range)
25 | let num = UInt8(byteString, radix: 16)!
26 | data.append(num)
27 | }
28 |
29 | guard data.count > 0 else { return nil }
30 |
31 | return data
32 | }
33 |
34 | }
35 |
36 | extension Data {
37 |
38 | /// Hexadecimal string representation of `Data` object.
39 |
40 | var hexadecimal: String {
41 | return map { String(format: "%02x", $0) }
42 | .joined()
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/BLE-Viewer/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // BLE-Viewer
4 | //
5 | // Created by Alexander Heinrich on 24.09.19.
6 | // Copyright © 2019 Alexander Heinrich. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | //#if canImport(SwiftUI)
11 | //import SwiftUI
12 | //#endif
13 |
14 | @NSApplicationMain
15 | class AppDelegate: NSObject, NSApplicationDelegate {
16 |
17 | var window: NSWindow!
18 |
19 |
20 | func applicationDidFinishLaunching(_ aNotification: Notification) {
21 | // Create the SwiftUI view that provides the window contents.
22 | window = NSWindow(
23 | contentRect: NSRect(x: 0, y: 0, width: 700, height: 480),
24 | styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
25 | backing: .buffered, defer: false)
26 |
27 | if #available(macOS 10.15, *) {
28 |
29 |
30 | window.center()
31 | window.setFrameAutosaveName("Main Window")
32 | window.makeKeyAndOrderFront(nil)
33 | // window.contentView = NSHostingView(rootView: ContentView())
34 | }else {
35 | let vc = NSStoryboard(name: "Main", bundle: nil).instantiateInitialController() as! NSViewController
36 |
37 | window.contentViewController = vc
38 | }
39 |
40 |
41 | }
42 |
43 |
44 | func applicationWillTerminate(_ aNotification: Notification) {
45 | // Insert code here to tear down your application
46 | }
47 |
48 |
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Handoff BLE-Viewer
2 |
3 | This project is part of my Master Thesis @ SEEMOO (TU Darmstadt).
4 | With iOS 8 Apple has released a feature called *Handoff*. It allows a user to start an activity on one device and continue on another device. For example you could start writing an email on your iPhone and continue the writing at the same spot with the same content on your Mac. All you need to do is to press one little button.
5 | Researching how this feature is implemented by Apple is my main goal of my Thesis.
6 |
7 | 
8 |
9 | One big part *Handoff* is Bluetooth Low Energy. With every *Handoff* activity (`NSUserActivity` for devs) the OS will automatically send out a BLE advertising packet. The BLE communication has been researched by Martin et. al. ([Paper]) in *Handoff All Your Privacy: A Review of Apple's Bluetooth Low Energy Continuity Protocol*. The paper does only cover the encrypted parts of the messages and does not actually decrypt any packets.
10 | Luckily most *Handoff* messages are encrypted, but this tool enables you to view the content of a *Handoff* BLE advertising packet.
11 |
12 | I may insert parts of my Thesis here later for further explanations.
13 |
14 | ## Installation
15 |
16 | * Use Xcode 11+
17 | * Make sure to use the CryptoSwift Swift Package
18 | * Run
19 |
20 | ## Privacy
21 |
22 | The App will request certain keys from your Keychain. This is necessary to decrypt BLE advertising packets. None of the keys will be uploaded somewhere. They will also not be saved outside of your keychain. They will stay in memory as long as the app is running.
23 | Checkout the code if you don't belive me.
24 |
25 |
26 |
27 |
28 | [Paper]: https://arxiv.org/abs/1904.10600
29 |
--------------------------------------------------------------------------------
/BLE-Viewer/HandoffModels/HandoffBLE.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HandoffBLE.swift
3 | // BLE-Viewer
4 | //
5 | // Created by Alexander Heinrich on 25.09.19.
6 | // Copyright © 2019 Alexander Heinrich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | /// This struct parses BLE data with Handoff to a format which can than be processed further
13 | /// It is used for parsing raw advertisements which are encrypted
14 | struct HandoffBLE {
15 | let statusByte: UInt8
16 | let counter: UInt16
17 | let counterIV: Array
18 | let authTag: Array
19 | let encryptedData: Data
20 |
21 | init(handoffData: Data) throws {
22 | guard handoffData.count >= 3 else {throw HandoffError.invalidFormat}
23 |
24 | let isAppleData = handoffData[handoffData.startIndex] == 0x4c
25 | let isHandoffData = handoffData[handoffData.startIndex.advanced(by: 2)] == 0x0c
26 |
27 | guard isAppleData && isHandoffData else {throw HandoffError.invalidFormat}
28 |
29 | let handoffContentLength = Int(handoffData[handoffData.startIndex.advanced(by: 3)])
30 | let contentStartIdx = handoffData.startIndex.advanced(by: 4)
31 | let handoffContent = handoffData[contentStartIdx...(contentStartIdx+handoffContentLength-1)]
32 | statusByte = handoffContent[handoffContent.startIndex]
33 |
34 | let counterData = (handoffContent[handoffContent.startIndex.advanced(by: 1)...handoffContent.startIndex.advanced(by: 2)])
35 | counter = counterData.uint16
36 | counterIV = Array(counterData)
37 |
38 | let authTagByte = handoffContent[handoffContent.startIndex.advanced(by: 3)]
39 | authTag = Array([authTagByte])
40 |
41 | encryptedData = handoffContent[handoffContent.startIndex.advanced(by: 4)...]
42 | }
43 |
44 | enum HandoffError: Error {
45 | case invalidFormat
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/BLE-Viewer/Extensions/DataIntExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataExtensions.swift
3 | //
4 | // Created by Boris Polania on 2/16/18.
5 | //
6 |
7 | import Foundation
8 |
9 | extension Data {
10 |
11 | var uint8: UInt8 {
12 | get {
13 | var number: UInt8 = 0
14 | self.copyBytes(to:&number, count: MemoryLayout.size)
15 | return number
16 | }
17 | }
18 |
19 | var uint16: UInt16 {
20 | get {
21 | let i16array = self.withUnsafeBytes {
22 | UnsafeBufferPointer(start: $0, count: self.count/2).map(UInt16.init(littleEndian:))
23 | }
24 | return i16array[0]
25 | }
26 | }
27 |
28 | var uint32: UInt32 {
29 | get {
30 | let i32array = self.withUnsafeBytes {
31 | UnsafeBufferPointer(start: $0, count: self.count/2).map(UInt32.init(littleEndian:))
32 | }
33 | return i32array[0]
34 | }
35 | }
36 |
37 | var uuid: NSUUID? {
38 | get {
39 | var bytes = [UInt8](repeating: 0, count: self.count)
40 | self.copyBytes(to:&bytes, count: self.count * MemoryLayout.size)
41 | return NSUUID(uuidBytes: bytes)
42 | }
43 | }
44 | var stringASCII: String? {
45 | get {
46 | return NSString(data: self, encoding: String.Encoding.ascii.rawValue) as String?
47 | }
48 | }
49 |
50 | var stringUTF8: String? {
51 | get {
52 | return NSString(data: self, encoding: String.Encoding.utf8.rawValue) as String?
53 | }
54 | }
55 |
56 | struct HexEncodingOptions: OptionSet {
57 | let rawValue: Int
58 | static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
59 | }
60 |
61 | func hexEncodedString(options: HexEncodingOptions = []) -> String {
62 | let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
63 | return map { String(format: format, $0) }.joined()
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/BLE-Viewer/Logging/Log.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Logging.swift
3 | // BLE-Viewer
4 | //
5 | // Created by Alexander Heinrich on 25.09.19.
6 | // Copyright © 2019 Alexander Heinrich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import OSLog
11 | @_exported import os.log
12 |
13 | struct Log {
14 | static func log(log: OSLog, type: OSLogType, message: StaticString,_ a: [CVarArg]) {
15 | switch a.count {
16 | case 5: os_log(message, log: log, type: type, a[0], a[1], a[2], a[3], a[4])
17 | case 4: os_log(message, log: log, type: type, a[0], a[1], a[2], a[3])
18 | case 3: os_log(message, log: log, type: type, a[0], a[1], a[2])
19 | case 2: os_log(message, log: log, type: type, a[0], a[1])
20 | case 1: os_log(message, log: log, type: type, a[0])
21 | case 0: os_log(message, log: log, type: type)
22 | default: os_log(message, log: log, type: type, a)
23 | }
24 | }
25 |
26 | static func info(system: LogSystem, message: StaticString,_ args: CVarArg...) {
27 | log(log: system.osLog, type: .info, message: message, args)
28 | }
29 |
30 | static func `default`(system: LogSystem, message: StaticString,_ args: CVarArg...) {
31 | log(log: system.osLog, type: .default, message: message, args)
32 | }
33 |
34 | static func debug(system: LogSystem, message: StaticString,_ args: CVarArg...) {
35 | log(log: system.osLog, type: .debug, message: message, args)
36 | }
37 |
38 | static func error(system: LogSystem, message: StaticString,_ args: CVarArg...) {
39 | log(log: system.osLog, type: .error, message: message, args)
40 | }
41 |
42 | struct LogSystem {
43 | let osLog: OSLog
44 |
45 | init(_ osLog: OSLog) {
46 | self.osLog = osLog
47 | }
48 |
49 | static let ble = LogSystem(OSLog(subsystem: "de.heinrich.alexander.BLE-Viewer", category: "BLE"))
50 | static let crypto = LogSystem(OSLog(subsystem: "de.heinrich.alexander.BLE-Viewer", category: "Crypto"))
51 |
52 | static let app = LogSystem(OSLog(subsystem: "de.heinrich.alexander.BLE-Viewer", category: "App"))
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/BLE-Viewer/BLE/BLEReceiver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BLEReceiver.swift
3 | // BLE-Viewer
4 | //
5 | // Created by Alexander Heinrich on 25.09.19.
6 | // Copyright © 2019 Alexander Heinrich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CoreBluetooth
11 | import os
12 |
13 | protocol BLEReceiverDelegate {
14 | func didReceive(handoffData: Data, fromDevice device: CBPeripheral)
15 | }
16 |
17 | class BLEReceiver: NSObject {
18 | var centralManager: CBCentralManager!
19 | var delegate: BLEReceiverDelegate?
20 |
21 | private var shouldScanForAdvertisements = false
22 |
23 | override init() {
24 | super.init()
25 | self.centralManager = CBCentralManager(delegate: self, queue: nil)
26 | }
27 |
28 | func scanForAdvertisements() {
29 | if self.centralManager.state == .poweredOn {
30 | self.centralManager.scanForPeripherals(withServices: nil, options: nil)
31 | }else {
32 | self.shouldScanForAdvertisements = true
33 | }
34 |
35 | }
36 |
37 | func isHandoff(data: Data) -> Bool {
38 | guard data.count >= 3 else {return false}
39 | let isAppleData = data[data.startIndex] == 0x4c
40 | let isHandoffData = data[data.startIndex.advanced(by: 2)] == 0x0c
41 |
42 | return isAppleData && isHandoffData
43 | }
44 | }
45 |
46 | extension BLEReceiver: CBCentralManagerDelegate {
47 | func centralManagerDidUpdateState(_ central: CBCentralManager) {
48 | print("CBCentralManager did Update state \(central.state)")
49 | if central.state == .poweredOn && self.shouldScanForAdvertisements {
50 | self.scanForAdvertisements()
51 | self.shouldScanForAdvertisements = false
52 | }
53 | }
54 |
55 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
56 |
57 |
58 | // Log.debug(system: .ble, message: "Received advertisement from %@", peripheral.identifier.uuidString)
59 | // Log.debug(system: .ble, message: "Advertisement data %@", advertisementData)
60 | guard let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data,
61 | self.isHandoff(data: manufacturerData) else { return }
62 |
63 | self.delegate?.didReceive(handoffData: manufacturerData, fromDevice: peripheral)
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/BLE-Viewer/Crypto/BLEDecryptor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BLEDecryptor.swift
3 | // BLE-Viewer
4 | //
5 | // Created by Alexander Heinrich on 25.09.19.
6 | // Copyright © 2019 Alexander Heinrich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CryptoSwift
11 |
12 | struct BLEDecryptor {
13 | var keys: [HandoffKey]
14 |
15 | init() throws {
16 | self.keys = try KeychainAccess.fetchHandoffKeys()
17 | }
18 |
19 | func decrypt(handoffBLE: HandoffBLE) throws -> Data {
20 | keys.forEach({ (key) in
21 |
22 | })
23 |
24 | for key in keys {
25 | do {
26 | let decrypted = try decrypt(handoffBLE: handoffBLE, withKey: key)
27 | // Log.info(system: .crypto,
28 | // message:
29 | // """
30 | // Successfully decrypted handoff message
31 | // Key used: %@
32 | // Decrypted message:
33 | // %@
34 | // """,
35 | // key.keyIdentifier.uuidString, decrypted.hexadecimal )
36 |
37 | return decrypted
38 |
39 | }catch (_) {
40 | //Should fail quite often
41 | // Log.debug(system: .crypto, message: "Failed decrypting with key %@.\nError %@", key.keyIdentifier.uuidString, error.localizedDescription)
42 | }
43 | }
44 |
45 | throw DecryptionError.noKeyFound
46 | }
47 |
48 | func decrypt(handoffBLE: HandoffBLE, withKey key: HandoffKey) throws -> Data {
49 | // The status byte is included in the GCM algorithm as part of the authendicated data
50 | let statusByte: [UInt8] = [handoffBLE.statusByte]
51 |
52 | let gcm = GCM(iv: handoffBLE.counterIV, authenticationTag: handoffBLE.authTag, additionalAuthenticatedData: statusByte, mode: .detached)
53 |
54 | gcm.authenticationTag = handoffBLE.authTag
55 |
56 | let keyArray = Array(key.keyData)
57 | let aes = try AES(key: keyArray, blockMode: gcm, padding: .noPadding)
58 |
59 | let encryptedData = Array(handoffBLE.encryptedData)
60 |
61 | let decryptedArray = try aes.decrypt(encryptedData)
62 | return Data(decryptedArray)
63 |
64 | }
65 |
66 | enum DecryptionError: Error {
67 | case noKeyFound
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/BLE-Viewer/Crypto/KeychainAccess.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeychainAccess.swift
3 | // BLE-Viewer
4 | //
5 | // Created by Alexander Heinrich on 24.09.19.
6 | // Copyright © 2019 Alexander Heinrich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct KeychainAccess {
12 |
13 | static func fetchHandoffKeys() throws -> [HandoffKey] {
14 | let query: [CFString: Any] = [
15 | kSecClass: kSecClassGenericPassword,
16 | kSecAttrService: "com.apple.continuity.encryption",
17 | kSecReturnAttributes: true,
18 | kSecMatchLimit: kSecMatchLimitAll
19 | ]
20 |
21 | var item: CFTypeRef?
22 | let status = SecItemCopyMatching(query as CFDictionary, &item)
23 |
24 | guard status == errSecSuccess else {throw KeychainError.secKeychain(err: NSError(domain: NSOSStatusErrorDomain, code: Int(status), userInfo: nil))}
25 |
26 | guard let itemArray = item as? [[CFString: Any]] else {throw KeychainError.keychainResponse}
27 |
28 | var responseArray = Array()
29 |
30 | for item in itemArray {
31 | let fetchedData = try fetchItem(withAttributes: item)
32 | responseArray.append(fetchedData)
33 | }
34 |
35 | //Fetch the NSDictionary
36 | let dictionaryResponses = try responseArray.compactMap({try PropertyListSerialization.propertyList(from: $0, options: [], format: nil) as? [String: Any]})
37 |
38 | return try dictionaryResponses.map({try HandoffKey(withDictionary: $0)})
39 | }
40 |
41 | static func fetchItem(withAttributes item: [CFString: Any]) throws -> Data {
42 | let itemQuery : [CFString: Any] = [
43 | kSecClass: kSecClassGenericPassword,
44 | kSecAttrAccount : item[kSecAttrAccount]!,
45 | kSecAttrLabel : item[kSecAttrLabel]!,
46 | kSecAttrService: item[kSecAttrService]!,
47 | kSecReturnData : true
48 | ]
49 |
50 | var fetchedItem: CFTypeRef?
51 | let status = SecItemCopyMatching(itemQuery as CFDictionary, &fetchedItem)
52 |
53 | guard status == errSecSuccess,
54 | let fetchedData = fetchedItem as? Data else {throw KeychainError.secKeychain(err: NSError(domain: NSOSStatusErrorDomain, code: Int(status), userInfo: nil))}
55 |
56 | return fetchedData
57 | }
58 | }
59 |
60 | enum KeychainError: Error {
61 | case secKeychain(err: NSError)
62 | case keychainResponse
63 | case keyParsing
64 | }
65 |
66 | struct HandoffKey {
67 | let dateCreated: Date
68 | let isWrappedKey: Bool
69 | let keyData: Data
70 | let keyIdentifier: UUID
71 | let lastUsedCounter: Int
72 |
73 | init(withDictionary dict: [String: Any]) throws {
74 | guard let dC = dict["dateCreated"] as? Date,
75 | let wrapped = dict["isWrappedKey"] as? Bool,
76 | let kD = dict["keyData"] as? Data,
77 | let keyId = dict["keyIdentifier"] as? String,
78 | let keyUUID = UUID(uuidString: keyId),
79 | let counter = dict["lastUsedCounter"] as? Int else {
80 | throw KeychainError.keyParsing
81 | }
82 |
83 | dateCreated = dC
84 | isWrappedKey = wrapped
85 | keyData = kD
86 | keyIdentifier = keyUUID
87 | lastUsedCounter = counter
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/BLE-Viewer/HandoffModels/HandoffAdvertisement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HandoffAdvertisement.swift
3 | // BLE-Viewer
4 | //
5 | // Created by Alexander Heinrich on 26.09.19.
6 | // Copyright © 2019 Alexander Heinrich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | #if os(macOS)
11 | import AppKit
12 | #endif
13 |
14 |
15 | /// This struct is used for parsing decrypted Handoff BLE Advertisements.
16 | /// It will reflect the content of the advertisement
17 | struct HandoffAdvertisement {
18 | let statusByte: UInt8
19 | let unknownByte: UInt8
20 | let handoffHash: Data
21 | private let flagByte: UInt8
22 | let flags: [HandoffFlags]
23 | let handoffActivity: HandoffActivity
24 |
25 | let dateReceived = Date()
26 |
27 | init(withRawAdvertisement handoffBLE: HandoffBLE, decryptedAdvertisementData advertisementData: Data) {
28 | self.statusByte = advertisementData[advertisementData.startIndex]
29 | self.handoffHash = advertisementData[advertisementData.startIndex.advanced(by: 1) ... advertisementData.startIndex.advanced(by: 7)]
30 | self.flagByte = advertisementData[advertisementData.startIndex.advanced(by: 8)]
31 | self.unknownByte = advertisementData[advertisementData.startIndex.advanced(by: 9)]
32 |
33 | var flags = [HandoffFlags]()
34 |
35 | //Parse the flags
36 | if flagByte & 0x1 != 0 {
37 | flags.append(.url)
38 | }
39 |
40 | if flagByte & 0x2 != 0 {
41 | flags.append(.fileProviderUrl)
42 | }
43 |
44 | if flagByte & 0x4 != 0 {
45 | flags.append(.cloudDocs)
46 | }
47 |
48 | if flagByte & 0x8 != 0 {
49 | flags.append(.pasteboardAvailable)
50 | }
51 |
52 | if flagByte & 0x10 != 0 {
53 | flags.append(.pasteboardVersionBit(versionBit: flagByte & 0x10))
54 | }
55 |
56 | if flagByte & 0x20 != 0 {
57 | flags.append(.activityAutoPullOnReceiver)
58 | }
59 |
60 | self.flags = flags
61 |
62 | //Match activity
63 | if flags.contains(.url) {
64 | handoffActivity = .viewWebpage
65 | }else {
66 | handoffActivity = HandoffActivity(withHash: handoffHash)
67 | }
68 |
69 | }
70 |
71 | func description() -> String {
72 | return(
73 | """
74 | Handoff Advertisement:
75 | Actvity: \(self.handoffActivity.activityName())
76 | App: \(self.handoffActivity.appBundleId())
77 | """
78 | )
79 | }
80 |
81 | #if os(macOS)
82 | func attributedDescription() -> NSAttributedString {
83 | let description = NSMutableAttributedString()
84 | description.append(NSAttributedString(string: "Activity:\t\t\t\t"))
85 | description.append(NSAttributedString(string: handoffActivity.activityName(), attributes: [NSAttributedString.Key.font : NSFont.boldSystemFont(ofSize: 13.0)]))
86 | description.append(NSAttributedString(string: "\n"))
87 |
88 | description.append(NSAttributedString(string: "App:\t\t\t\t"))
89 | description.append(NSAttributedString(string: handoffActivity.appBundleId(), attributes: [NSAttributedString.Key.font : NSFont.boldSystemFont(ofSize: 13.0)]))
90 | description.append(NSAttributedString(string: "\n"))
91 |
92 | description.append(NSAttributedString(string: "Contains URL:\t\t"))
93 | description.append(NSAttributedString(string: flags.contains(.url) ? "true" : "false", attributes: [NSAttributedString.Key.font : NSFont.boldSystemFont(ofSize: 13.0)]))
94 | description.append(NSAttributedString(string: "\n"))
95 |
96 | description.append(NSAttributedString(string: "Clipboard Available:\t"))
97 | description.append(NSAttributedString(string: flags.contains(.pasteboardAvailable) ? "true" : "false", attributes: [NSAttributedString.Key.font : NSFont.boldSystemFont(ofSize: 13.0)]))
98 |
99 | return description
100 | }
101 | #endif
102 | }
103 |
104 | enum HandoffFlags: Equatable {
105 | case url
106 | case fileProviderUrl
107 | case cloudDocs
108 | case pasteboardAvailable
109 | case pasteboardVersionBit(versionBit: UInt8)
110 | case activityAutoPullOnReceiver
111 |
112 | }
113 |
--------------------------------------------------------------------------------
/BLE-Viewer/HandoffModels/HandoffActivity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HandoffActivity.swift
3 | // BLE-Viewer
4 | //
5 | // Created by Alexander Heinrich on 26.09.19.
6 | // Copyright © 2019 Alexander Heinrich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum HandoffActivity: CaseIterable {
12 | case notesEditNote
13 | case mailViewMailbox
14 | case mailViewMessage
15 | case mailComposeMessage
16 | case applePodcast
17 | case keynoteEditPresentation
18 | case pagesEditDocument
19 | case numbersEditDocument
20 | case twodoSelecedList
21 | case twodoEditTask
22 | case viewWebpage
23 |
24 | case calendarDateSelection
25 | case calendarEventSelection
26 |
27 | case messages
28 |
29 | case home
30 |
31 | case unknown
32 |
33 | init(withHash hash: Data) {
34 | for activity in HandoffActivity.allCases {
35 | if activity.hash() == hash {
36 | self = activity
37 | return
38 | }
39 | }
40 | self = .unknown
41 | }
42 |
43 | func hash() -> Data {
44 | switch self {
45 | case .notesEditNote:
46 | return "88085c342dc9ed".hexadecimal!
47 | case .mailViewMailbox:
48 | return "9d98584545c05e".hexadecimal!
49 | case .mailViewMessage:
50 | return "86f6a0732b418e".hexadecimal!
51 | case .mailComposeMessage:
52 | return "a564c399758208".hexadecimal!
53 | case .applePodcast:
54 | return "a37b6b65fc4f5f".hexadecimal!
55 | case .keynoteEditPresentation:
56 | return "90ec0d1c7a00f7".hexadecimal!
57 | case .pagesEditDocument:
58 | return "a2f08c1c87dc98".hexadecimal!
59 | case .numbersEditDocument:
60 | return "855912d27f7828".hexadecimal!
61 | case .twodoSelecedList:
62 | return "99bee8c360dd13".hexadecimal!
63 | case .twodoEditTask:
64 | return "b2a2abdd925ef5".hexadecimal!
65 | case .viewWebpage:
66 | return "58c3379233c70b".hexadecimal!
67 | case .calendarDateSelection:
68 | return "b64729d1a4296b".hexadecimal!
69 | case .calendarEventSelection:
70 | return "88acfe99cad770".hexadecimal!
71 | case .messages:
72 | return "a32a9f48308fdc".hexadecimal!
73 | case .home:
74 | return "00000000000000".hexadecimal!
75 | case .unknown:
76 | return Data()
77 | }
78 | }
79 |
80 | func activityName() -> String {
81 | switch self {
82 | case .notesEditNote:
83 | return "com.apple.notes.activity.edit-note"
84 | case .mailViewMailbox:
85 | return "com.apple.mail.mailbox"
86 | case .mailViewMessage:
87 | return "com.apple.mail.message"
88 | case .mailComposeMessage:
89 | return "com.apple.mail.compose"
90 | case .applePodcast:
91 | //TODO: Check in Catalina with Podcasts app
92 | return "unknown"
93 | case .keynoteEditPresentation:
94 | return "com.apple.keynote.documentEditing"
95 | case .pagesEditDocument:
96 | return "com.apple.pages.documentEditing"
97 | case .numbersEditDocument:
98 | return "com.apple.numbers.documentEditing"
99 | case .twodoSelecedList:
100 | return "com.guidedways.2Do.SelectedList"
101 | case .twodoEditTask:
102 | return "com.guidedways.2Do.TaskEditing"
103 | case .viewWebpage:
104 | return "NSUserActivityTypeBrowsingWeb"
105 | case .calendarDateSelection:
106 | return "com.apple.calendar.continuity.date_selection"
107 | case .calendarEventSelection:
108 | return "com.apple.calendar.continuity.event_selection"
109 | case .messages:
110 | return "com.apple.Messages"
111 | case .home:
112 | return "Clear last Activity state"
113 | case .unknown:
114 | return "unkown"
115 | }
116 | }
117 |
118 | func appBundleId() -> String {
119 | switch self {
120 | case .notesEditNote:
121 | return "com.apple.Notes"
122 | case .mailViewMailbox, .mailViewMessage, .mailComposeMessage:
123 | return "com.apple.mail"
124 | case .applePodcast:
125 | return "com.apple.podcasts"
126 | case .keynoteEditPresentation:
127 | return "com.apple.iWork.Keynote"
128 | case .pagesEditDocument:
129 | return "com.apple.iWork.Pages"
130 | case .numbersEditDocument:
131 | return "com.apple.iWork.Numbers"
132 | case .twodoSelecedList, .twodoEditTask:
133 | return "com.guidedways.TodoMac"
134 | case .viewWebpage:
135 | return "com.apple.Safari"
136 | case .calendarDateSelection, .calendarEventSelection:
137 | return "com.apple.iCal"
138 | case .messages:
139 | return "com.apple.iChat"
140 | case .home:
141 | return "No app"
142 | case .unknown:
143 | return "Unkown"
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/BLE-Viewer/BLEScannerVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BLEScannerVC.swift
3 | // BLE-Viewer
4 | //
5 | // Created by Alexander Heinrich on 25.09.19.
6 | // Copyright © 2019 Alexander Heinrich. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import CoreBluetooth
11 |
12 | class BLEScannerVC: NSViewController {
13 | var decryptor: BLEDecryptor!
14 | var receiver: BLEReceiver!
15 |
16 | var scannedDevices = [ScanModel]()
17 | var selectedDevice: ScanModel? {
18 | didSet {
19 | self.contentTableView.reloadData()
20 | }
21 | }
22 |
23 | @IBOutlet weak var startButton: NSButton!
24 | @IBOutlet weak var scanOverlay: NSView!
25 | @IBOutlet weak var sidebar: NSScrollView!
26 | @IBOutlet weak var outlineView: NSOutlineView!
27 | @IBOutlet weak var contentTableView: NSTableView!
28 |
29 |
30 | override func viewDidLoad() {
31 | super.viewDidLoad()
32 | // Do view setup here.
33 |
34 | outlineView.delegate = self
35 | outlineView.dataSource = self
36 |
37 | contentTableView.delegate = self
38 | contentTableView.dataSource = self
39 |
40 | scanOverlay.isHidden = false
41 | }
42 | @IBAction func startScanning(_ sender: Any) {
43 | do {
44 | self.decryptor = try BLEDecryptor()
45 | self.receiver = BLEReceiver()
46 |
47 | self.receiver.delegate = self
48 | self.receiver.scanForAdvertisements()
49 |
50 | //Dismiss the scan overlay
51 | self.scanOverlay.isHidden = true
52 |
53 | }catch (let error) {
54 | Log.error(system: .app, message: "Error thrown during setup %@", error.localizedDescription)
55 | }
56 |
57 | }
58 |
59 | func addAdvertisement(_ advertisement: HandoffAdvertisement, fromDevice peripheral: CBPeripheral) {
60 | if var scanModel = self.scannedDevices.first(where: {$0.peripheral.identifier == peripheral.identifier}) {
61 |
62 | scanModel.advertisements.append(advertisement)
63 | guard scanModel.peripheral.identifier == self.selectedDevice?.peripheral.identifier else {return}
64 |
65 | contentTableView.insertRows(at: IndexSet(integer: scanModel.advertisements.count-1), withAnimation: .effectFade)
66 |
67 | }else {
68 | let scanModel = ScanModel(peripheral: peripheral, advertisements: [advertisement])
69 | self.scannedDevices.append(scanModel)
70 | outlineView.insertItems(at: IndexSet(integer: scannedDevices.count-1), inParent: nil, withAnimation: .slideLeft)
71 | }
72 |
73 | }
74 |
75 | }
76 |
77 | extension BLEScannerVC: BLEReceiverDelegate {
78 | func didReceive(handoffData: Data, fromDevice device: CBPeripheral) {
79 |
80 | do {
81 | //Decrypt the advertisements
82 | let handoff = try HandoffBLE(handoffData: handoffData)
83 | let handoffAdvData = try self.decryptor.decrypt(handoffBLE: handoff)
84 |
85 | let handoffAdv = HandoffAdvertisement(withRawAdvertisement: handoff, decryptedAdvertisementData: handoffAdvData)
86 |
87 | Log.info(system: .app, message: "\nRaw status byte %02x \nDecrypted status byte %02x", handoff.statusByte, handoffAdv.statusByte)
88 | Log.info(system: .app, message: "\nUnknown byte %02x", handoffAdv.unknownByte)
89 |
90 | Log.info(system: .app, message: "\nReceived advertisement from device: %@", device.name ?? device.identifier.uuidString)
91 | Log.info(system: .app, message: "%@", handoffAdv.description())
92 |
93 | self.addAdvertisement(handoffAdv, fromDevice: device)
94 |
95 |
96 | }catch (let error) {
97 | // Log.error(system: .app, message: "Failed parsing Handoff advertisement: \n%@", error.localizedDescription)
98 | }
99 |
100 | }
101 | }
102 |
103 | extension BLEScannerVC: NSOutlineViewDataSource {
104 | func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
105 | return scannedDevices[index]
106 | }
107 |
108 | func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
109 | return false
110 | }
111 |
112 | func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
113 | guard item == nil else {return 0}
114 |
115 | return scannedDevices.count
116 | }
117 |
118 | // func outlineView(_ outlineView: NSOutlineView, objectValueFor tableColumn: NSTableColumn?, byItem item: Any?) -> Any? {
119 | //
120 | // }
121 | }
122 |
123 | extension BLEScannerVC: NSOutlineViewDelegate {
124 | func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
125 | var view: NSTableCellView?
126 |
127 | guard let scan = item as? ScanModel else {return view}
128 |
129 | view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DeviceCell"), owner: self) as? NSTableCellView
130 |
131 | view?.textField?.stringValue = scan.peripheral.name ?? scan.peripheral.identifier.uuidString
132 | view?.textField?.sizeToFit()
133 |
134 | return view
135 | }
136 |
137 | func outlineViewSelectionDidChange(_ notification: Notification) {
138 | guard let outlineView = notification.object as? NSOutlineView else {
139 | return
140 | }
141 |
142 | let selectedIndex = outlineView.selectedRow
143 |
144 | guard let scannedDevice = outlineView.item(atRow: selectedIndex) as? ScanModel else {return}
145 |
146 | self.selectedDevice = scannedDevice
147 |
148 | }
149 | }
150 |
151 | extension BLEScannerVC: NSTableViewDataSource {
152 | func numberOfRows(in tableView: NSTableView) -> Int {
153 | return self.selectedDevice?.advertisements.count ?? 0
154 | }
155 |
156 | // func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
157 | // return self.selectedDevice?.advertisements[row] ?? nil
158 | // }
159 |
160 | }
161 |
162 | extension BLEScannerVC: NSTableViewDelegate {
163 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
164 | let adv = selectedDevice!.advertisements[row]
165 |
166 | if tableColumn?.identifier.rawValue == "DateColumn" {
167 | let view = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DateCell"), owner: self) as? NSTableCellView
168 |
169 | let df = DateFormatter()
170 | df.dateFormat = "HH:mm:ss dd.MM.yy"
171 |
172 | view?.textField?.stringValue = df.string(from: adv.dateReceived)
173 | view?.textField?.sizeToFit()
174 | return view
175 | }else {
176 | let view = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ActivityCell"), owner: self) as? NSTableCellView
177 |
178 | view?.textField?.attributedStringValue = adv.attributedDescription()
179 |
180 | return view
181 | }
182 | }
183 |
184 | func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
185 | return 68
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/BLE-ViewerTests/BLE_ViewerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BLE_ViewerTests.swift
3 | // BLE-ViewerTests
4 | //
5 | // Created by Alexander Heinrich on 24.09.19.
6 | // Copyright © 2019 Alexander Heinrich. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import BLE_Viewer
11 | import CryptoSwift
12 | import CoreBluetooth
13 |
14 | class BLE_ViewerTests: XCTestCase {
15 | var bleExpect: XCTestExpectation?
16 |
17 | override func setUp() {
18 | // Put setup code here. This method is called before the invocation of each test method in the class.
19 | }
20 |
21 | override func tearDown() {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testLoadingKeys() throws {
26 | do {
27 | let handoffKeys = try KeychainAccess.fetchHandoffKeys()
28 |
29 | XCTAssertGreaterThanOrEqual(handoffKeys.count, 1)
30 |
31 | var keyMatch = false
32 | handoffKeys.forEach { (key) in
33 | if key.keyData == "9cdbb22f 8dfc0198 bbf9b31e 7d64438e 1755094b ad224d32 d897c634 e71ffb9c".hexadecimal! {
34 | keyMatch = true
35 | }
36 | print(key.keyData.hexEncodedString())
37 | }
38 |
39 | XCTAssertTrue(keyMatch)
40 |
41 | }catch (let err) {
42 | print(err)
43 | XCTFail("Error thrown")
44 | }
45 |
46 | }
47 |
48 | func testBLE() throws {
49 | let receiver = BLEReceiver()
50 | receiver.scanForAdvertisements()
51 | receiver.delegate = self
52 |
53 | let expect = self.expectation(description: "Advertisements")
54 | self.bleExpect = expect
55 |
56 | self.wait(for: [expect], timeout: 10.0)
57 | }
58 |
59 | func testIsHandoff() {
60 | let handoffData = "<4c000c0e 0057cd3b 3cbd48f2 9e6bf563 f0921006 5b1e1d49 e249>".hexadecimal!
61 | let receiver = BLEReceiver()
62 |
63 | XCTAssert(receiver.isHandoff(data: handoffData), "Failed")
64 |
65 | }
66 |
67 | func testParsing() throws {
68 | let handoffData = "<4c000c0e 0057cd3b 3cbd48f2 9e6bf563 f0921006 5b1e1d49 e249>".hexadecimal!
69 | let handoffBLE = try HandoffBLE(handoffData: handoffData)
70 |
71 | XCTAssertEqual(handoffBLE.counter, UInt16(52567))
72 | XCTAssertEqual(handoffBLE.authTag, [0x3b])
73 | XCTAssertEqual(Array(handoffBLE.encryptedData), Array("3cbd48f2 9e6bf563 f092".hexadecimal!))
74 | }
75 |
76 | func testDecryption() throws {
77 | do {
78 | let handoffData = " <4c000c0e 00a9cd77 c546bb00 79902fbf 09371006 5b1ec335 0d71>".hexadecimal!
79 | let decryptor = try BLEDecryptor()
80 | let handoffBLE = try HandoffBLE(handoffData: handoffData)
81 | XCTAssertEqual(Array(handoffBLE.encryptedData), Array("".hexadecimal!))
82 |
83 | let decrypted = try decryptor.decrypt(handoffBLE: handoffBLE)
84 |
85 | XCTAssertNotNil(decrypted)
86 | XCTAssertEqual(decrypted, "<0088085c342dc9ed408e>".hexadecimal!)
87 |
88 | let u = NSUserActivity(activityType: "de.macoun.handoff")
89 | u.isEligibleForHandoff = true
90 |
91 | }catch(let error) {
92 | XCTFail(error.localizedDescription)
93 | }
94 |
95 |
96 | }
97 |
98 |
99 | func testDecryption2() {
100 | let encryptedData = Array("3d ce a5 d2 17 21 0f ec 20 3b".hexadecimal!)
101 | let counter: [UInt8] = [0xf2, 0xcd]
102 | let tag: [UInt8] = [0xfb]
103 | let version: [UInt8] = [0x0]
104 | let key = Array("9cdbb22f 8dfc0198 bbf9b31e 7d64438e 1755094b ad224d32 d897c634 e71ffb9c".hexadecimal!)
105 |
106 | let gcm = GCM(iv: counter, authenticationTag: tag, additionalAuthenticatedData: version, mode: .detached)
107 |
108 | do {
109 | let aes = try AES(key: key, blockMode: gcm, padding: .noPadding)
110 | let decrypted = try aes.decrypt(encryptedData)
111 |
112 | XCTAssertNotNil(decrypted)
113 | }catch {
114 | XCTFail()
115 | }
116 |
117 |
118 | }
119 |
120 | func testHandoffAdvertisementParsing() throws {
121 | let decryptedAdv = "0088085c342dc9ed198a".hexadecimal!
122 | let rawAdvertisement = "<4c000c0e 0057cd3b 3cbd48f2 9e6bf563 f0921006 5b1e1d49 e249>".hexadecimal!
123 | let handoffBLE = try HandoffBLE(handoffData: rawAdvertisement)
124 | let parsedAdv = HandoffAdvertisement(withRawAdvertisement: handoffBLE, decryptedAdvertisementData: decryptedAdv)
125 |
126 | XCTAssertEqual(parsedAdv.statusByte, 0x00)
127 | XCTAssertEqual(Array(parsedAdv.handoffHash), Array("88085c342dc9ed".hexadecimal!))
128 | XCTAssertEqual(parsedAdv.flags, [.url, .pasteboardAvailable ,.pasteboardVersionBit(versionBit: 0x10)])
129 | XCTAssertEqual(parsedAdv.handoffActivity, HandoffActivity.notesEditNote)
130 | }
131 |
132 | func testBleRandomNumbers() {
133 | var randomTable = Array(repeating: false, count: 0xffff + 1)
134 | for i in 0...0xffff {
135 | let random = arc4random_uniform(0xffff)
136 | XCTAssertFalse(randomTable[Int(random)], "Found doubled value")
137 | if randomTable[Int(random)] == true {
138 | print("Failed at \(random)")
139 | break
140 | }
141 | randomTable[Int(random)] = true
142 | }
143 | }
144 |
145 | var xs: UInt16 = 1
146 | func xorShift() -> UInt16 {
147 | xs ^= xs << 7;
148 | xs ^= xs >> 9;
149 | xs ^= xs << 8;
150 | return xs;
151 | }
152 |
153 | func testXorShift() {
154 | var randomTable = Array(repeating: false, count: 0xffff + 1)
155 | for i in 1...0xffff {
156 | let random = xorShift()
157 | if randomTable[Int(random)] == true {
158 | print("Failed at \(i) with \(random)")
159 | XCTFail("Found doubled value")
160 | break
161 | }
162 | randomTable[Int(random)] = true
163 | }
164 | }
165 |
166 |
167 | func xorShiftMul(x: UInt16, key: UInt16) -> UInt16 {
168 | var y = x
169 | y ^= y >> 7
170 | y ^= y << 9
171 | y ^= y << 8
172 |
173 | let z: UInt64 = UInt64(y) * UInt64(key)
174 |
175 | y = UInt16(z % 0x10000)
176 |
177 | return y
178 | }
179 |
180 | func testXorShiftMul() {
181 | var randomTable = Array(repeating: false, count: 0xffff + 1)
182 | var x: UInt16 = 1
183 | let key: UInt16 = 0x2362
184 |
185 | for i in 1...0xffff {
186 | let random = xorShiftMul(x: x, key: key)
187 | if randomTable[Int(random)] == true {
188 | print("Failed at \(i) with \(random)")
189 | XCTFail("Found doubled value")
190 | break
191 | }
192 | randomTable[Int(random)] = true
193 | x = random
194 | }
195 | }
196 |
197 |
198 | }
199 |
200 | extension BLE_ViewerTests: BLEReceiverDelegate {
201 | func didReceive(handoffData: Data, fromDevice device: CBPeripheral) {
202 | self.bleExpect?.fulfill()
203 | }
204 |
205 | func didReceive(handoffData: Data) {
206 | self.bleExpect?.fulfill()
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/BLE-Viewer.xcodeproj/xcuserdata/Alex.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
9 |
21 |
22 |
23 |
25 |
37 |
38 |
39 |
41 |
53 |
54 |
55 |
57 |
69 |
70 |
84 |
85 |
99 |
100 |
101 |
102 |
103 |
105 |
117 |
118 |
119 |
121 |
133 |
134 |
135 |
137 |
149 |
150 |
151 |
153 |
165 |
166 |
167 |
169 |
181 |
182 |
183 |
184 |
185 |
--------------------------------------------------------------------------------
/BLE-Viewer.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | FA058A0E2366DB2D002DB46D /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = FA058A0D2366DB2D002DB46D /* README.md */; };
11 | FA058A112366DE02002DB46D /* Screenshot.png in Resources */ = {isa = PBXBuildFile; fileRef = FA058A102366DE02002DB46D /* Screenshot.png */; };
12 | FA4D9E3F233CA654003E5D8F /* HandoffAdvertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4D9E3E233CA654003E5D8F /* HandoffAdvertisement.swift */; };
13 | FA4D9E41233CBF0A003E5D8F /* HandoffActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4D9E40233CBF0A003E5D8F /* HandoffActivity.swift */; };
14 | FA4D9E43233CDF36003E5D8F /* ScanModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4D9E42233CDF36003E5D8F /* ScanModel.swift */; };
15 | FA83551D233A756F00E4F46B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA83551C233A756F00E4F46B /* AppDelegate.swift */; };
16 | FA83551F233A756F00E4F46B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA83551E233A756F00E4F46B /* ContentView.swift */; };
17 | FA835521233A757100E4F46B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA835520233A757100E4F46B /* Assets.xcassets */; };
18 | FA835524233A757100E4F46B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA835523233A757100E4F46B /* Preview Assets.xcassets */; };
19 | FA835527233A757100E4F46B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FA835525233A757100E4F46B /* Main.storyboard */; };
20 | FA835533233A757100E4F46B /* BLE_ViewerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA835532233A757100E4F46B /* BLE_ViewerTests.swift */; };
21 | FA83553F233A75C700E4F46B /* KeychainAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA83553E233A75C700E4F46B /* KeychainAccess.swift */; };
22 | FA835541233B5F4900E4F46B /* BLEDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA835540233B5F4900E4F46B /* BLEDecryptor.swift */; };
23 | FA835544233B5F7400E4F46B /* BLEReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA835543233B5F7400E4F46B /* BLEReceiver.swift */; };
24 | FA835547233B62C500E4F46B /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA835546233B62C500E4F46B /* Log.swift */; };
25 | FA835549233B690800E4F46B /* HexToDataExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA835548233B690800E4F46B /* HexToDataExtensions.swift */; };
26 | FA83554B233B6FAA00E4F46B /* HandoffBLE.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA83554A233B6FAA00E4F46B /* HandoffBLE.swift */; };
27 | FA83554F233B718600E4F46B /* DataIntExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA83554E233B718600E4F46B /* DataIntExtensions.swift */; };
28 | FA835552233B82C800E4F46B /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FA835551233B82C800E4F46B /* CryptoSwift */; };
29 | FA835554233B86D900E4F46B /* BLEScannerVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA835553233B86D900E4F46B /* BLEScannerVC.swift */; };
30 | /* End PBXBuildFile section */
31 |
32 | /* Begin PBXContainerItemProxy section */
33 | FA83552F233A757100E4F46B /* PBXContainerItemProxy */ = {
34 | isa = PBXContainerItemProxy;
35 | containerPortal = FA835511233A756F00E4F46B /* Project object */;
36 | proxyType = 1;
37 | remoteGlobalIDString = FA835518233A756F00E4F46B;
38 | remoteInfo = "BLE-Viewer";
39 | };
40 | /* End PBXContainerItemProxy section */
41 |
42 | /* Begin PBXFileReference section */
43 | FA058A0D2366DB2D002DB46D /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
44 | FA058A102366DE02002DB46D /* Screenshot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Screenshot.png; sourceTree = ""; };
45 | FA4D9E3E233CA654003E5D8F /* HandoffAdvertisement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandoffAdvertisement.swift; sourceTree = ""; };
46 | FA4D9E40233CBF0A003E5D8F /* HandoffActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandoffActivity.swift; sourceTree = ""; };
47 | FA4D9E42233CDF36003E5D8F /* ScanModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanModel.swift; sourceTree = ""; };
48 | FA835519233A756F00E4F46B /* BLE-Viewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "BLE-Viewer.app"; sourceTree = BUILT_PRODUCTS_DIR; };
49 | FA83551C233A756F00E4F46B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
50 | FA83551E233A756F00E4F46B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
51 | FA835520233A757100E4F46B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
52 | FA835523233A757100E4F46B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
53 | FA835526233A757100E4F46B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
54 | FA835528233A757100E4F46B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
55 | FA835529233A757100E4F46B /* BLE_Viewer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BLE_Viewer.entitlements; sourceTree = ""; };
56 | FA83552E233A757100E4F46B /* BLE-ViewerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "BLE-ViewerTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
57 | FA835532233A757100E4F46B /* BLE_ViewerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLE_ViewerTests.swift; sourceTree = ""; };
58 | FA835534233A757100E4F46B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
59 | FA83553E233A75C700E4F46B /* KeychainAccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainAccess.swift; sourceTree = ""; };
60 | FA835540233B5F4900E4F46B /* BLEDecryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEDecryptor.swift; sourceTree = ""; };
61 | FA835543233B5F7400E4F46B /* BLEReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEReceiver.swift; sourceTree = ""; };
62 | FA835546233B62C500E4F46B /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; };
63 | FA835548233B690800E4F46B /* HexToDataExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HexToDataExtensions.swift; sourceTree = ""; };
64 | FA83554A233B6FAA00E4F46B /* HandoffBLE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandoffBLE.swift; sourceTree = ""; };
65 | FA83554E233B718600E4F46B /* DataIntExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataIntExtensions.swift; sourceTree = ""; };
66 | FA835553233B86D900E4F46B /* BLEScannerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEScannerVC.swift; sourceTree = ""; };
67 | /* End PBXFileReference section */
68 |
69 | /* Begin PBXFrameworksBuildPhase section */
70 | FA835516233A756F00E4F46B /* Frameworks */ = {
71 | isa = PBXFrameworksBuildPhase;
72 | buildActionMask = 2147483647;
73 | files = (
74 | FA835552233B82C800E4F46B /* CryptoSwift in Frameworks */,
75 | );
76 | runOnlyForDeploymentPostprocessing = 0;
77 | };
78 | FA83552B233A757100E4F46B /* Frameworks */ = {
79 | isa = PBXFrameworksBuildPhase;
80 | buildActionMask = 2147483647;
81 | files = (
82 | );
83 | runOnlyForDeploymentPostprocessing = 0;
84 | };
85 | /* End PBXFrameworksBuildPhase section */
86 |
87 | /* Begin PBXGroup section */
88 | FA058A0F2366DDEB002DB46D /* Resources */ = {
89 | isa = PBXGroup;
90 | children = (
91 | FA058A102366DE02002DB46D /* Screenshot.png */,
92 | );
93 | path = Resources;
94 | sourceTree = "";
95 | };
96 | FA4D9E3D233CA61A003E5D8F /* HandoffModels */ = {
97 | isa = PBXGroup;
98 | children = (
99 | FA4D9E42233CDF36003E5D8F /* ScanModel.swift */,
100 | FA83554A233B6FAA00E4F46B /* HandoffBLE.swift */,
101 | FA4D9E3E233CA654003E5D8F /* HandoffAdvertisement.swift */,
102 | FA4D9E40233CBF0A003E5D8F /* HandoffActivity.swift */,
103 | );
104 | path = HandoffModels;
105 | sourceTree = "";
106 | };
107 | FA835510233A756F00E4F46B = {
108 | isa = PBXGroup;
109 | children = (
110 | FA058A0D2366DB2D002DB46D /* README.md */,
111 | FA83551B233A756F00E4F46B /* BLE-Viewer */,
112 | FA835531233A757100E4F46B /* BLE-ViewerTests */,
113 | FA058A0F2366DDEB002DB46D /* Resources */,
114 | FA83551A233A756F00E4F46B /* Products */,
115 | );
116 | sourceTree = "";
117 | };
118 | FA83551A233A756F00E4F46B /* Products */ = {
119 | isa = PBXGroup;
120 | children = (
121 | FA835519233A756F00E4F46B /* BLE-Viewer.app */,
122 | FA83552E233A757100E4F46B /* BLE-ViewerTests.xctest */,
123 | );
124 | name = Products;
125 | sourceTree = "";
126 | };
127 | FA83551B233A756F00E4F46B /* BLE-Viewer */ = {
128 | isa = PBXGroup;
129 | children = (
130 | FA83551C233A756F00E4F46B /* AppDelegate.swift */,
131 | FA83551E233A756F00E4F46B /* ContentView.swift */,
132 | FA835553233B86D900E4F46B /* BLEScannerVC.swift */,
133 | FA835520233A757100E4F46B /* Assets.xcassets */,
134 | FA835525233A757100E4F46B /* Main.storyboard */,
135 | FA835528233A757100E4F46B /* Info.plist */,
136 | FA835529233A757100E4F46B /* BLE_Viewer.entitlements */,
137 | FA4D9E3D233CA61A003E5D8F /* HandoffModels */,
138 | FA83554D233B717700E4F46B /* Extensions */,
139 | FA835545233B62B700E4F46B /* Logging */,
140 | FA835542233B5F6200E4F46B /* BLE */,
141 | FA83553D233A757F00E4F46B /* Crypto */,
142 | FA835522233A757100E4F46B /* Preview Content */,
143 | );
144 | path = "BLE-Viewer";
145 | sourceTree = "";
146 | };
147 | FA835522233A757100E4F46B /* Preview Content */ = {
148 | isa = PBXGroup;
149 | children = (
150 | FA835523233A757100E4F46B /* Preview Assets.xcassets */,
151 | );
152 | path = "Preview Content";
153 | sourceTree = "";
154 | };
155 | FA835531233A757100E4F46B /* BLE-ViewerTests */ = {
156 | isa = PBXGroup;
157 | children = (
158 | FA835532233A757100E4F46B /* BLE_ViewerTests.swift */,
159 | FA835534233A757100E4F46B /* Info.plist */,
160 | );
161 | path = "BLE-ViewerTests";
162 | sourceTree = "";
163 | };
164 | FA83553D233A757F00E4F46B /* Crypto */ = {
165 | isa = PBXGroup;
166 | children = (
167 | FA83553E233A75C700E4F46B /* KeychainAccess.swift */,
168 | FA835540233B5F4900E4F46B /* BLEDecryptor.swift */,
169 | );
170 | path = Crypto;
171 | sourceTree = "";
172 | };
173 | FA835542233B5F6200E4F46B /* BLE */ = {
174 | isa = PBXGroup;
175 | children = (
176 | FA835543233B5F7400E4F46B /* BLEReceiver.swift */,
177 | );
178 | path = BLE;
179 | sourceTree = "";
180 | };
181 | FA835545233B62B700E4F46B /* Logging */ = {
182 | isa = PBXGroup;
183 | children = (
184 | FA835546233B62C500E4F46B /* Log.swift */,
185 | );
186 | path = Logging;
187 | sourceTree = "";
188 | };
189 | FA83554D233B717700E4F46B /* Extensions */ = {
190 | isa = PBXGroup;
191 | children = (
192 | FA835548233B690800E4F46B /* HexToDataExtensions.swift */,
193 | FA83554E233B718600E4F46B /* DataIntExtensions.swift */,
194 | );
195 | path = Extensions;
196 | sourceTree = "";
197 | };
198 | /* End PBXGroup section */
199 |
200 | /* Begin PBXNativeTarget section */
201 | FA835518233A756F00E4F46B /* BLE-Viewer */ = {
202 | isa = PBXNativeTarget;
203 | buildConfigurationList = FA835537233A757100E4F46B /* Build configuration list for PBXNativeTarget "BLE-Viewer" */;
204 | buildPhases = (
205 | FA835515233A756F00E4F46B /* Sources */,
206 | FA835516233A756F00E4F46B /* Frameworks */,
207 | FA835517233A756F00E4F46B /* Resources */,
208 | );
209 | buildRules = (
210 | );
211 | dependencies = (
212 | );
213 | name = "BLE-Viewer";
214 | packageProductDependencies = (
215 | FA835551233B82C800E4F46B /* CryptoSwift */,
216 | );
217 | productName = "BLE-Viewer";
218 | productReference = FA835519233A756F00E4F46B /* BLE-Viewer.app */;
219 | productType = "com.apple.product-type.application";
220 | };
221 | FA83552D233A757100E4F46B /* BLE-ViewerTests */ = {
222 | isa = PBXNativeTarget;
223 | buildConfigurationList = FA83553A233A757100E4F46B /* Build configuration list for PBXNativeTarget "BLE-ViewerTests" */;
224 | buildPhases = (
225 | FA83552A233A757100E4F46B /* Sources */,
226 | FA83552B233A757100E4F46B /* Frameworks */,
227 | FA83552C233A757100E4F46B /* Resources */,
228 | );
229 | buildRules = (
230 | );
231 | dependencies = (
232 | FA835530233A757100E4F46B /* PBXTargetDependency */,
233 | );
234 | name = "BLE-ViewerTests";
235 | productName = "BLE-ViewerTests";
236 | productReference = FA83552E233A757100E4F46B /* BLE-ViewerTests.xctest */;
237 | productType = "com.apple.product-type.bundle.unit-test";
238 | };
239 | /* End PBXNativeTarget section */
240 |
241 | /* Begin PBXProject section */
242 | FA835511233A756F00E4F46B /* Project object */ = {
243 | isa = PBXProject;
244 | attributes = {
245 | LastSwiftUpdateCheck = 1100;
246 | LastUpgradeCheck = 1100;
247 | ORGANIZATIONNAME = "Alexander Heinrich";
248 | TargetAttributes = {
249 | FA835518233A756F00E4F46B = {
250 | CreatedOnToolsVersion = 11.0;
251 | };
252 | FA83552D233A757100E4F46B = {
253 | CreatedOnToolsVersion = 11.0;
254 | TestTargetID = FA835518233A756F00E4F46B;
255 | };
256 | };
257 | };
258 | buildConfigurationList = FA835514233A756F00E4F46B /* Build configuration list for PBXProject "BLE-Viewer" */;
259 | compatibilityVersion = "Xcode 9.3";
260 | developmentRegion = en;
261 | hasScannedForEncodings = 0;
262 | knownRegions = (
263 | en,
264 | Base,
265 | );
266 | mainGroup = FA835510233A756F00E4F46B;
267 | packageReferences = (
268 | FA835550233B82C800E4F46B /* XCRemoteSwiftPackageReference "CryptoSwift" */,
269 | );
270 | productRefGroup = FA83551A233A756F00E4F46B /* Products */;
271 | projectDirPath = "";
272 | projectRoot = "";
273 | targets = (
274 | FA835518233A756F00E4F46B /* BLE-Viewer */,
275 | FA83552D233A757100E4F46B /* BLE-ViewerTests */,
276 | );
277 | };
278 | /* End PBXProject section */
279 |
280 | /* Begin PBXResourcesBuildPhase section */
281 | FA835517233A756F00E4F46B /* Resources */ = {
282 | isa = PBXResourcesBuildPhase;
283 | buildActionMask = 2147483647;
284 | files = (
285 | FA835527233A757100E4F46B /* Main.storyboard in Resources */,
286 | FA058A112366DE02002DB46D /* Screenshot.png in Resources */,
287 | FA835524233A757100E4F46B /* Preview Assets.xcassets in Resources */,
288 | FA058A0E2366DB2D002DB46D /* README.md in Resources */,
289 | FA835521233A757100E4F46B /* Assets.xcassets in Resources */,
290 | );
291 | runOnlyForDeploymentPostprocessing = 0;
292 | };
293 | FA83552C233A757100E4F46B /* Resources */ = {
294 | isa = PBXResourcesBuildPhase;
295 | buildActionMask = 2147483647;
296 | files = (
297 | );
298 | runOnlyForDeploymentPostprocessing = 0;
299 | };
300 | /* End PBXResourcesBuildPhase section */
301 |
302 | /* Begin PBXSourcesBuildPhase section */
303 | FA835515233A756F00E4F46B /* Sources */ = {
304 | isa = PBXSourcesBuildPhase;
305 | buildActionMask = 2147483647;
306 | files = (
307 | FA835544233B5F7400E4F46B /* BLEReceiver.swift in Sources */,
308 | FA835554233B86D900E4F46B /* BLEScannerVC.swift in Sources */,
309 | FA4D9E43233CDF36003E5D8F /* ScanModel.swift in Sources */,
310 | FA4D9E3F233CA654003E5D8F /* HandoffAdvertisement.swift in Sources */,
311 | FA835547233B62C500E4F46B /* Log.swift in Sources */,
312 | FA835541233B5F4900E4F46B /* BLEDecryptor.swift in Sources */,
313 | FA83551F233A756F00E4F46B /* ContentView.swift in Sources */,
314 | FA83554B233B6FAA00E4F46B /* HandoffBLE.swift in Sources */,
315 | FA83553F233A75C700E4F46B /* KeychainAccess.swift in Sources */,
316 | FA83554F233B718600E4F46B /* DataIntExtensions.swift in Sources */,
317 | FA83551D233A756F00E4F46B /* AppDelegate.swift in Sources */,
318 | FA835549233B690800E4F46B /* HexToDataExtensions.swift in Sources */,
319 | FA4D9E41233CBF0A003E5D8F /* HandoffActivity.swift in Sources */,
320 | );
321 | runOnlyForDeploymentPostprocessing = 0;
322 | };
323 | FA83552A233A757100E4F46B /* Sources */ = {
324 | isa = PBXSourcesBuildPhase;
325 | buildActionMask = 2147483647;
326 | files = (
327 | FA835533233A757100E4F46B /* BLE_ViewerTests.swift in Sources */,
328 | );
329 | runOnlyForDeploymentPostprocessing = 0;
330 | };
331 | /* End PBXSourcesBuildPhase section */
332 |
333 | /* Begin PBXTargetDependency section */
334 | FA835530233A757100E4F46B /* PBXTargetDependency */ = {
335 | isa = PBXTargetDependency;
336 | target = FA835518233A756F00E4F46B /* BLE-Viewer */;
337 | targetProxy = FA83552F233A757100E4F46B /* PBXContainerItemProxy */;
338 | };
339 | /* End PBXTargetDependency section */
340 |
341 | /* Begin PBXVariantGroup section */
342 | FA835525233A757100E4F46B /* Main.storyboard */ = {
343 | isa = PBXVariantGroup;
344 | children = (
345 | FA835526233A757100E4F46B /* Base */,
346 | );
347 | name = Main.storyboard;
348 | sourceTree = "";
349 | };
350 | /* End PBXVariantGroup section */
351 |
352 | /* Begin XCBuildConfiguration section */
353 | FA835535233A757100E4F46B /* Debug */ = {
354 | isa = XCBuildConfiguration;
355 | buildSettings = {
356 | ALWAYS_SEARCH_USER_PATHS = NO;
357 | CLANG_ANALYZER_NONNULL = YES;
358 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
359 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
360 | CLANG_CXX_LIBRARY = "libc++";
361 | CLANG_ENABLE_MODULES = YES;
362 | CLANG_ENABLE_OBJC_ARC = YES;
363 | CLANG_ENABLE_OBJC_WEAK = YES;
364 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
365 | CLANG_WARN_BOOL_CONVERSION = YES;
366 | CLANG_WARN_COMMA = YES;
367 | CLANG_WARN_CONSTANT_CONVERSION = YES;
368 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
369 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
370 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
371 | CLANG_WARN_EMPTY_BODY = YES;
372 | CLANG_WARN_ENUM_CONVERSION = YES;
373 | CLANG_WARN_INFINITE_RECURSION = YES;
374 | CLANG_WARN_INT_CONVERSION = YES;
375 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
376 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
377 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
378 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
379 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
380 | CLANG_WARN_STRICT_PROTOTYPES = YES;
381 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
382 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
383 | CLANG_WARN_UNREACHABLE_CODE = YES;
384 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
385 | COPY_PHASE_STRIP = NO;
386 | DEBUG_INFORMATION_FORMAT = dwarf;
387 | ENABLE_STRICT_OBJC_MSGSEND = YES;
388 | ENABLE_TESTABILITY = YES;
389 | GCC_C_LANGUAGE_STANDARD = gnu11;
390 | GCC_DYNAMIC_NO_PIC = NO;
391 | GCC_NO_COMMON_BLOCKS = YES;
392 | GCC_OPTIMIZATION_LEVEL = 0;
393 | GCC_PREPROCESSOR_DEFINITIONS = (
394 | "DEBUG=1",
395 | "$(inherited)",
396 | );
397 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
398 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
399 | GCC_WARN_UNDECLARED_SELECTOR = YES;
400 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
401 | GCC_WARN_UNUSED_FUNCTION = YES;
402 | GCC_WARN_UNUSED_VARIABLE = YES;
403 | MACOSX_DEPLOYMENT_TARGET = 10.14;
404 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
405 | MTL_FAST_MATH = YES;
406 | ONLY_ACTIVE_ARCH = YES;
407 | SDKROOT = macosx;
408 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
409 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
410 | };
411 | name = Debug;
412 | };
413 | FA835536233A757100E4F46B /* Release */ = {
414 | isa = XCBuildConfiguration;
415 | buildSettings = {
416 | ALWAYS_SEARCH_USER_PATHS = NO;
417 | CLANG_ANALYZER_NONNULL = YES;
418 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
419 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
420 | CLANG_CXX_LIBRARY = "libc++";
421 | CLANG_ENABLE_MODULES = YES;
422 | CLANG_ENABLE_OBJC_ARC = YES;
423 | CLANG_ENABLE_OBJC_WEAK = YES;
424 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
425 | CLANG_WARN_BOOL_CONVERSION = YES;
426 | CLANG_WARN_COMMA = YES;
427 | CLANG_WARN_CONSTANT_CONVERSION = YES;
428 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
429 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
430 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
431 | CLANG_WARN_EMPTY_BODY = YES;
432 | CLANG_WARN_ENUM_CONVERSION = YES;
433 | CLANG_WARN_INFINITE_RECURSION = YES;
434 | CLANG_WARN_INT_CONVERSION = YES;
435 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
436 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
437 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
438 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
439 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
440 | CLANG_WARN_STRICT_PROTOTYPES = YES;
441 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
442 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
443 | CLANG_WARN_UNREACHABLE_CODE = YES;
444 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
445 | COPY_PHASE_STRIP = NO;
446 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
447 | ENABLE_NS_ASSERTIONS = NO;
448 | ENABLE_STRICT_OBJC_MSGSEND = YES;
449 | GCC_C_LANGUAGE_STANDARD = gnu11;
450 | GCC_NO_COMMON_BLOCKS = YES;
451 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
452 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
453 | GCC_WARN_UNDECLARED_SELECTOR = YES;
454 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
455 | GCC_WARN_UNUSED_FUNCTION = YES;
456 | GCC_WARN_UNUSED_VARIABLE = YES;
457 | MACOSX_DEPLOYMENT_TARGET = 10.14;
458 | MTL_ENABLE_DEBUG_INFO = NO;
459 | MTL_FAST_MATH = YES;
460 | SDKROOT = macosx;
461 | SWIFT_COMPILATION_MODE = wholemodule;
462 | SWIFT_OPTIMIZATION_LEVEL = "-O";
463 | };
464 | name = Release;
465 | };
466 | FA835538233A757100E4F46B /* Debug */ = {
467 | isa = XCBuildConfiguration;
468 | buildSettings = {
469 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
470 | CODE_SIGN_ENTITLEMENTS = "BLE-Viewer/BLE_Viewer.entitlements";
471 | CODE_SIGN_IDENTITY = "Apple Development";
472 | CODE_SIGN_STYLE = Automatic;
473 | COMBINE_HIDPI_IMAGES = YES;
474 | DEVELOPMENT_ASSET_PATHS = "\"BLE-Viewer/Preview Content\"";
475 | DEVELOPMENT_TEAM = A5U65N39SD;
476 | ENABLE_HARDENED_RUNTIME = YES;
477 | ENABLE_PREVIEWS = YES;
478 | INFOPLIST_FILE = "BLE-Viewer/Info.plist";
479 | LD_RUNPATH_SEARCH_PATHS = (
480 | "$(inherited)",
481 | "@executable_path/../Frameworks",
482 | );
483 | MACOSX_DEPLOYMENT_TARGET = 10.14;
484 | PRODUCT_BUNDLE_IDENTIFIER = "de.heinrich.alexander.BLE-Viewer";
485 | PRODUCT_NAME = "$(TARGET_NAME)";
486 | PROVISIONING_PROFILE_SPECIFIER = "";
487 | SWIFT_VERSION = 5.0;
488 | };
489 | name = Debug;
490 | };
491 | FA835539233A757100E4F46B /* Release */ = {
492 | isa = XCBuildConfiguration;
493 | buildSettings = {
494 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
495 | CODE_SIGN_ENTITLEMENTS = "BLE-Viewer/BLE_Viewer.entitlements";
496 | CODE_SIGN_IDENTITY = "Apple Development";
497 | CODE_SIGN_STYLE = Automatic;
498 | COMBINE_HIDPI_IMAGES = YES;
499 | DEVELOPMENT_ASSET_PATHS = "\"BLE-Viewer/Preview Content\"";
500 | DEVELOPMENT_TEAM = A5U65N39SD;
501 | ENABLE_HARDENED_RUNTIME = YES;
502 | ENABLE_PREVIEWS = YES;
503 | INFOPLIST_FILE = "BLE-Viewer/Info.plist";
504 | LD_RUNPATH_SEARCH_PATHS = (
505 | "$(inherited)",
506 | "@executable_path/../Frameworks",
507 | );
508 | MACOSX_DEPLOYMENT_TARGET = 10.14;
509 | PRODUCT_BUNDLE_IDENTIFIER = "de.heinrich.alexander.BLE-Viewer";
510 | PRODUCT_NAME = "$(TARGET_NAME)";
511 | PROVISIONING_PROFILE_SPECIFIER = "";
512 | SWIFT_VERSION = 5.0;
513 | };
514 | name = Release;
515 | };
516 | FA83553B233A757100E4F46B /* Debug */ = {
517 | isa = XCBuildConfiguration;
518 | buildSettings = {
519 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
520 | BUNDLE_LOADER = "$(TEST_HOST)";
521 | CODE_SIGN_IDENTITY = "Mac Developer";
522 | CODE_SIGN_STYLE = Automatic;
523 | COMBINE_HIDPI_IMAGES = YES;
524 | DEVELOPMENT_TEAM = A5U65N39SD;
525 | INFOPLIST_FILE = "BLE-ViewerTests/Info.plist";
526 | LD_RUNPATH_SEARCH_PATHS = (
527 | "$(inherited)",
528 | "@executable_path/../Frameworks",
529 | "@loader_path/../Frameworks",
530 | );
531 | MACOSX_DEPLOYMENT_TARGET = 10.15;
532 | PRODUCT_BUNDLE_IDENTIFIER = "de.heinrich.alexander.BLE-ViewerTests";
533 | PRODUCT_NAME = "$(TARGET_NAME)";
534 | SWIFT_VERSION = 5.0;
535 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BLE-Viewer.app/Contents/MacOS/BLE-Viewer";
536 | };
537 | name = Debug;
538 | };
539 | FA83553C233A757100E4F46B /* Release */ = {
540 | isa = XCBuildConfiguration;
541 | buildSettings = {
542 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
543 | BUNDLE_LOADER = "$(TEST_HOST)";
544 | CODE_SIGN_IDENTITY = "Mac Developer";
545 | CODE_SIGN_STYLE = Automatic;
546 | COMBINE_HIDPI_IMAGES = YES;
547 | DEVELOPMENT_TEAM = A5U65N39SD;
548 | INFOPLIST_FILE = "BLE-ViewerTests/Info.plist";
549 | LD_RUNPATH_SEARCH_PATHS = (
550 | "$(inherited)",
551 | "@executable_path/../Frameworks",
552 | "@loader_path/../Frameworks",
553 | );
554 | MACOSX_DEPLOYMENT_TARGET = 10.15;
555 | PRODUCT_BUNDLE_IDENTIFIER = "de.heinrich.alexander.BLE-ViewerTests";
556 | PRODUCT_NAME = "$(TARGET_NAME)";
557 | SWIFT_VERSION = 5.0;
558 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BLE-Viewer.app/Contents/MacOS/BLE-Viewer";
559 | };
560 | name = Release;
561 | };
562 | /* End XCBuildConfiguration section */
563 |
564 | /* Begin XCConfigurationList section */
565 | FA835514233A756F00E4F46B /* Build configuration list for PBXProject "BLE-Viewer" */ = {
566 | isa = XCConfigurationList;
567 | buildConfigurations = (
568 | FA835535233A757100E4F46B /* Debug */,
569 | FA835536233A757100E4F46B /* Release */,
570 | );
571 | defaultConfigurationIsVisible = 0;
572 | defaultConfigurationName = Release;
573 | };
574 | FA835537233A757100E4F46B /* Build configuration list for PBXNativeTarget "BLE-Viewer" */ = {
575 | isa = XCConfigurationList;
576 | buildConfigurations = (
577 | FA835538233A757100E4F46B /* Debug */,
578 | FA835539233A757100E4F46B /* Release */,
579 | );
580 | defaultConfigurationIsVisible = 0;
581 | defaultConfigurationName = Release;
582 | };
583 | FA83553A233A757100E4F46B /* Build configuration list for PBXNativeTarget "BLE-ViewerTests" */ = {
584 | isa = XCConfigurationList;
585 | buildConfigurations = (
586 | FA83553B233A757100E4F46B /* Debug */,
587 | FA83553C233A757100E4F46B /* Release */,
588 | );
589 | defaultConfigurationIsVisible = 0;
590 | defaultConfigurationName = Release;
591 | };
592 | /* End XCConfigurationList section */
593 |
594 | /* Begin XCRemoteSwiftPackageReference section */
595 | FA835550233B82C800E4F46B /* XCRemoteSwiftPackageReference "CryptoSwift" */ = {
596 | isa = XCRemoteSwiftPackageReference;
597 | repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git";
598 | requirement = {
599 | kind = upToNextMajorVersion;
600 | minimumVersion = 1.0.0;
601 | };
602 | };
603 | /* End XCRemoteSwiftPackageReference section */
604 |
605 | /* Begin XCSwiftPackageProductDependency section */
606 | FA835551233B82C800E4F46B /* CryptoSwift */ = {
607 | isa = XCSwiftPackageProductDependency;
608 | package = FA835550233B82C800E4F46B /* XCRemoteSwiftPackageReference "CryptoSwift" */;
609 | productName = CryptoSwift;
610 | };
611 | /* End XCSwiftPackageProductDependency section */
612 | };
613 | rootObject = FA835511233A756F00E4F46B /* Project object */;
614 | }
615 |
--------------------------------------------------------------------------------
/BLE-Viewer/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 |
703 |
704 |
705 |
706 |
707 |
708 |
709 |
710 |
711 |
712 |
713 |
714 |
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 |
723 |
724 |
725 |
726 |
727 |
728 |
729 |
730 |
731 |
732 |
733 |
734 |
735 |
736 |
737 |
738 |
739 |
740 |
741 |
742 |
743 |
744 |
745 |
746 |
747 |
748 |
749 |
750 |
751 |
752 |
753 |
754 |
755 |
756 |
757 |
758 |
759 |
760 |
761 |
762 |
763 |
764 |
765 |
766 |
767 |
768 |
769 |
770 |
771 |
772 |
773 |
774 |
775 |
776 |
777 |
778 |
779 |
780 |
781 |
782 |
783 |
784 |
785 |
786 |
787 |
788 |
789 |
790 |
791 |
792 |
793 |
794 |
795 |
796 |
797 |
798 |
799 |
800 |
801 |
802 |
803 |
804 |
805 |
806 |
807 |
808 |
809 |
810 |
811 |
812 |
813 |
814 |
815 |
816 |
817 |
818 |
819 |
820 |
821 |
822 |
823 |
824 |
825 |
826 |
827 |
828 |
829 |
830 |
831 |
832 |
833 |
834 |
835 |
836 |
837 |
838 |
839 |
840 |
841 |
842 |
843 |
844 |
845 |
846 |
847 |
848 |
849 |
850 |
851 |
852 |
853 |
854 |
855 |
856 |
857 |
858 |
859 |
860 |
861 |
862 |
863 |
864 |
865 |
866 |
867 |
868 |
869 |
870 |
871 |
872 |
873 |
874 |
875 |
876 |
877 |
878 |
879 |
880 |
881 |
882 |
883 |
884 |
885 |
886 |
887 |
888 |
889 |
890 |
891 |
892 |
893 |
894 |
895 |
896 |
897 |
898 |
899 |
900 |
901 |
911 |
912 |
913 |
914 |
915 |
916 |
917 |
918 |
919 |
920 |
921 |
922 |
923 |
924 |
925 |
926 |
927 |
928 |
929 |
930 |
931 |
932 |
933 |
934 |
935 |
936 |
937 |
938 |
939 |
940 |
941 |
942 |
943 |
944 |
945 |
946 |
947 |
948 |
949 |
950 |
951 |
952 |
953 |
--------------------------------------------------------------------------------