diff --git a/.gitignore b/.gitignore
index 16931720..965c5408 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,7 +65,9 @@ fastlane/test_output
# SPM Generated
/*.xcodeproj
+.swiftpm/*
# R.swift
*.generated.swift
iOS/rswift
+
diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d98100..00000000
--- a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/CoreLock.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/CoreLock.xcscheme
deleted file mode 100644
index e4570b78..00000000
--- a/.swiftpm/xcode/xcshareddata/xcschemes/CoreLock.xcscheme
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Lock-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Lock-Package.xcscheme
deleted file mode 100644
index f52c10bf..00000000
--- a/.swiftpm/xcode/xcshareddata/xcschemes/Lock-Package.xcscheme
+++ /dev/null
@@ -1,152 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/lockd.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/lockd.xcscheme
deleted file mode 100644
index 25193a40..00000000
--- a/.swiftpm/xcode/xcshareddata/xcschemes/lockd.xcscheme
+++ /dev/null
@@ -1,127 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Package.resolved b/Package.resolved
index 4c1abafb..786fb0f4 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -1,6 +1,42 @@
{
"object": {
"pins": [
+ {
+ "package": "BigInt",
+ "repositoryURL": "https://github.com/Boilertalk/BigInt.swift.git",
+ "state": {
+ "branch": null,
+ "revision": "fd9b2ebff53e7fd211a7d2b2ea682db300a8571b",
+ "version": "1.0.0"
+ }
+ },
+ {
+ "package": "Cryptor",
+ "repositoryURL": "https://github.com/IBM-Swift/BlueCryptor.git",
+ "state": {
+ "branch": null,
+ "revision": "12d2bf3ec7207ec3cd004b9582f69ef5fae1da3b",
+ "version": "1.0.32"
+ }
+ },
+ {
+ "package": "Socket",
+ "repositoryURL": "https://github.com/IBM-Swift/BlueSocket.git",
+ "state": {
+ "branch": null,
+ "revision": "f82e1401f04f62b2e8ce4670456d104466957810",
+ "version": "1.0.50"
+ }
+ },
+ {
+ "package": "SSLService",
+ "repositoryURL": "https://github.com/IBM-Swift/BlueSSLService.git",
+ "state": {
+ "branch": null,
+ "revision": "d6616def31a12088034f0a63ed4646772eff5bce",
+ "version": "1.0.50"
+ }
+ },
{
"package": "Bluetooth",
"repositoryURL": "https://github.com/PureSwift/Bluetooth.git",
@@ -28,15 +64,42 @@
"version": null
}
},
+ {
+ "package": "Bonjour",
+ "repositoryURL": "https://github.com/PureSwift/Bonjour.git",
+ "state": {
+ "branch": "master",
+ "revision": "0907af223d72b1a4be14b805a8b5edd586f28721",
+ "version": null
+ }
+ },
+ {
+ "package": "Cdns_sd",
+ "repositoryURL": "https://github.com/Bouke/Cdns_sd.git",
+ "state": {
+ "branch": null,
+ "revision": "b6dce342895400d0cd86726ad0fcdcad7abc6137",
+ "version": "2.0.0"
+ }
+ },
{
"package": "CryptoSwift",
"repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift",
"state": {
"branch": "master",
- "revision": "4ccfe9034a07af4b153657ba1cdac702ffcf8f2f",
+ "revision": "7c94c0bc9d222f9c88d9f6ed8f71926f372d30a8",
"version": null
}
},
+ {
+ "package": "Evergreen",
+ "repositoryURL": "https://github.com/Bouke/Evergreen.git",
+ "state": {
+ "branch": null,
+ "revision": "4d9d17d6efca12398de2b0a4c9c5a4eacc556e03",
+ "version": "2.0.0"
+ }
+ },
{
"package": "GATT",
"repositoryURL": "https://github.com/PureSwift/GATT.git",
@@ -46,6 +109,123 @@
"version": null
}
},
+ {
+ "package": "HAP",
+ "repositoryURL": "https://github.com/Bouke/HAP.git",
+ "state": {
+ "branch": "master",
+ "revision": "4104c73e94f9a9657ed5bcb660fdd7e02641cca4",
+ "version": null
+ }
+ },
+ {
+ "package": "HKDF",
+ "repositoryURL": "https://github.com/Bouke/HKDF.git",
+ "state": {
+ "branch": null,
+ "revision": "9b9e521b49e5b529c33f4abe35a00e144af51e00",
+ "version": "3.1.0"
+ }
+ },
+ {
+ "package": "Kitura",
+ "repositoryURL": "https://github.com/IBM-Swift/Kitura.git",
+ "state": {
+ "branch": null,
+ "revision": "0777165ed5b966ad2e62f9f9101cfa8b283e7684",
+ "version": "2.8.1"
+ }
+ },
+ {
+ "package": "Kitura-net",
+ "repositoryURL": "https://github.com/IBM-Swift/Kitura-net.git",
+ "state": {
+ "branch": null,
+ "revision": "00985729329b73f3e20e40ec43071e4c294dfea3",
+ "version": "2.4.0"
+ }
+ },
+ {
+ "package": "Kitura-TemplateEngine",
+ "repositoryURL": "https://github.com/IBM-Swift/Kitura-TemplateEngine.git",
+ "state": {
+ "branch": null,
+ "revision": "d62d74bca48c6fb76f9fc1f48eeb2d7af415e80b",
+ "version": "2.0.1"
+ }
+ },
+ {
+ "package": "KituraContracts",
+ "repositoryURL": "https://github.com/IBM-Swift/KituraContracts.git",
+ "state": {
+ "branch": null,
+ "revision": "a30e2fb79e926672776a05ec6b919c239870a221",
+ "version": "1.2.1"
+ }
+ },
+ {
+ "package": "LoggerAPI",
+ "repositoryURL": "https://github.com/IBM-Swift/LoggerAPI.git",
+ "state": {
+ "branch": null,
+ "revision": "3357dd9526cdf9436fa63bb792b669e6efdc43da",
+ "version": "1.9.0"
+ }
+ },
+ {
+ "package": "NetService",
+ "repositoryURL": "https://github.com/Bouke/NetService.git",
+ "state": {
+ "branch": null,
+ "revision": "ade5b7bac667f91da62dfbbdb918aa748ce06bc0",
+ "version": "0.7.0"
+ }
+ },
+ {
+ "package": "Regex",
+ "repositoryURL": "https://github.com/crossroadlabs/Regex.git",
+ "state": {
+ "branch": null,
+ "revision": "166728756082a9cac6e4aed3ebbce8e41cb3a945",
+ "version": "1.2.0"
+ }
+ },
+ {
+ "package": "SRP",
+ "repositoryURL": "https://github.com/Bouke/SRP.git",
+ "state": {
+ "branch": null,
+ "revision": "9f3d77de5dbaac52d30b1d51d75e853fabb8fa8b",
+ "version": "3.1.0"
+ }
+ },
+ {
+ "package": "swift-log",
+ "repositoryURL": "https://github.com/apple/swift-log.git",
+ "state": {
+ "branch": null,
+ "revision": "e8aabbe95db22e064ad42f1a4a9f8982664c70ed",
+ "version": "1.1.1"
+ }
+ },
+ {
+ "package": "swift-nio",
+ "repositoryURL": "https://github.com/apple/swift-nio.git",
+ "state": {
+ "branch": null,
+ "revision": "ba7970fe396e8198b84c6c1b44b38a1d4e2eb6bd",
+ "version": "1.14.1"
+ }
+ },
+ {
+ "package": "swift-nio-zlib-support",
+ "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git",
+ "state": {
+ "branch": null,
+ "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd",
+ "version": "1.0.0"
+ }
+ },
{
"package": "SwiftyGPIO",
"repositoryURL": "https://github.com/uraimo/SwiftyGPIO.git",
@@ -63,6 +243,15 @@
"revision": "8c41d0fd48f6fc00c429aad025c114a941aad764",
"version": null
}
+ },
+ {
+ "package": "TypeDecoder",
+ "repositoryURL": "https://github.com/IBM-Swift/TypeDecoder.git",
+ "state": {
+ "branch": null,
+ "revision": "a5978582981f7151594bdaf5fe0d3cd9b1d2d0c0",
+ "version": "1.3.4"
+ }
}
]
},
diff --git a/Package.swift b/Package.swift
index b1beedd6..18bed1a7 100644
--- a/Package.swift
+++ b/Package.swift
@@ -45,6 +45,22 @@ let package = Package(
.package(
url: "https://github.com/PureSwift/BluetoothDarwin.git",
.branch("master")
+ ),
+ .package(
+ url: "https://github.com/IBM-Swift/Kitura.git",
+ from: "2.8.1"
+ ),
+ .package(
+ url: "https://github.com/Bouke/HAP.git",
+ .branch("master")
+ ),
+ .package(
+ url: "https://github.com/Bouke/NetService.git",
+ from: "0.7.0"
+ ),
+ .package(
+ url: "https://github.com/PureSwift/Bonjour.git",
+ .branch("master")
)
],
targets: [
@@ -54,7 +70,9 @@ let package = Package(
nativeBluetooth,
nativeGATT,
"CoreLockGATTServer",
- "SwiftyGPIO"
+ "SwiftyGPIO",
+ "HAP",
+ "CoreLockWebServer"
]
),
.target(
@@ -62,13 +80,21 @@ let package = Package(
dependencies: [
nativeGATT,
"TLVCoding",
- "CryptoSwift"
+ "CryptoSwift",
+ "Bonjour"
]
),
.target(
name: "CoreLockGATTServer",
dependencies: ["CoreLock"]
),
+ .target(
+ name: "CoreLockWebServer",
+ dependencies: [
+ "CoreLock",
+ "Kitura"
+ ]
+ ),
.testTarget(
name: "CoreLockTests",
dependencies: ["CoreLock"]
@@ -79,3 +105,7 @@ let package = Package(
)
]
)
+
+#if os(Linux)
+package.targets.first(where: { $0.name == "CoreLockWebServer" })?.dependencies.append("NetService")
+#endif
diff --git a/Sources/CoreLock/Authentication.swift b/Sources/CoreLock/Authentication.swift
index a030cb67..9cd76826 100644
--- a/Sources/CoreLock/Authentication.swift
+++ b/Sources/CoreLock/Authentication.swift
@@ -21,7 +21,6 @@ public struct Authentication: Equatable, Codable {
}
public func isAuthenticated(with key: KeyData) -> Bool {
-
return signedData.isAuthenticated(with: key, message: message)
}
}
diff --git a/Sources/CoreLock/CreateNewKeyRequest.swift b/Sources/CoreLock/CreateNewKeyRequest.swift
new file mode 100644
index 00000000..cf7ba36e
--- /dev/null
+++ b/Sources/CoreLock/CreateNewKeyRequest.swift
@@ -0,0 +1,91 @@
+//
+// CreateNewKeyRequest.swift
+// CoreLock
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+// Copyright © 2019 ColemanCDA. All rights reserved.
+//
+
+import Foundation
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+public struct CreateNewKeyNetServiceRequest: Equatable {
+
+ /// Lock server
+ public let server: URL
+
+ /// Authorization header
+ public let authorization: LockNetService.Authorization
+
+ /// Encrypted request
+ public let encryptedData: LockNetService.EncryptedData
+}
+
+// MARK: - URL Request
+
+public extension CreateNewKeyNetServiceRequest {
+
+ func urlRequest(encoder: JSONEncoder = JSONEncoder()) -> URLRequest {
+
+ // http://localhost:8080/keys
+ let url = server.appendingPathComponent("key")
+ var urlRequest = URLRequest(url: url)
+ urlRequest.addValue(authorization.header, forHTTPHeaderField: LockNetService.Authorization.headerField)
+ urlRequest.httpMethod = "POST"
+ urlRequest.httpBody = try! encoder.encode(encryptedData)
+ return urlRequest
+ }
+}
+
+// MARK: - Encryption
+
+public extension CreateNewKeyNetServiceRequest {
+
+ init(server: URL,
+ encrypt value: CreateNewKeyRequest,
+ with key: KeyCredentials,
+ encoder: JSONEncoder = JSONEncoder()) throws {
+
+ self.server = server
+ self.authorization = LockNetService.Authorization(key: key)
+ let data = try encoder.encode(value)
+ self.encryptedData = try .init(encrypt: data, with: key.secret)
+ }
+
+ static func decrypt(_ encryptedData: LockNetService.EncryptedData,
+ with key: KeyData,
+ decoder: JSONDecoder = JSONDecoder()) throws -> CreateNewKeyRequest {
+
+ let jsonData = try encryptedData.decrypt(with: key)
+ return try decoder.decode(CreateNewKeyRequest.self, from: jsonData)
+ }
+}
+
+// MARK: - Client
+
+public extension LockNetService.Client {
+
+ /// Create new key.
+ func createKey(_ newKey: CreateNewKeyRequest,
+ for server: LockNetService,
+ with key: KeyCredentials,
+ timeout: TimeInterval = LockNetService.defaultTimeout) throws {
+
+ log?("Create \(newKey.permission.type) key \"\(newKey.name)\" \(newKey.identifier) for \(server.url.absoluteString)")
+
+ let request = try CreateNewKeyNetServiceRequest(
+ server: server.url,
+ encrypt: newKey,
+ with: key,
+ encoder: jsonEncoder
+ ).urlRequest(encoder: jsonEncoder)
+
+ let (httpResponse, _) = try urlSession.synchronousDataTask(with: request)
+
+ guard httpResponse.statusCode == 201
+ else { throw LockNetService.Error.statusCode(httpResponse.statusCode) }
+ }
+}
diff --git a/Sources/CoreLock/DeleteKeyRequest.swift b/Sources/CoreLock/DeleteKeyRequest.swift
new file mode 100644
index 00000000..8f60aff0
--- /dev/null
+++ b/Sources/CoreLock/DeleteKeyRequest.swift
@@ -0,0 +1,73 @@
+//
+// DeleteKeyRequest.swift
+// CoreLock
+//
+// Created by Alsey Coleman Miller on 10/19/19.
+// Copyright © 2019 ColemanCDA. All rights reserved.
+//
+
+import Foundation
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+/// Lock Software Update HTTP Request
+public struct DeleteKeyRequest: Equatable {
+
+ /// Lock server
+ public let server: URL
+
+ /// Authorization header
+ public let authorization: LockNetService.Authorization
+
+ /// Key to delete
+ public let key: UUID
+
+ /// Type of Key to delete
+ public let type: KeyType
+}
+
+// MARK: - URL Request
+
+public extension DeleteKeyRequest {
+
+ func urlRequest() -> URLRequest {
+
+ // http://localhost:8080/key/A6E3EC9B-FD6E-4A50-B459-51CFDA2A21DD
+ let url = server
+ .appendingPathComponent(type.stringValue)
+ .appendingPathComponent(key.uuidString)
+ var urlRequest = URLRequest(url: url)
+ urlRequest.addValue(authorization.header, forHTTPHeaderField: LockNetService.Authorization.headerField)
+ urlRequest.httpMethod = "DELETE"
+ return urlRequest
+ }
+}
+
+// MARK: - Client
+
+public extension LockNetService.Client {
+
+ /// Remove the specified key.
+ func removeKey(_ identifier: UUID,
+ type: KeyType = .key,
+ for server: LockNetService,
+ with key: KeyCredentials,
+ timeout: TimeInterval = LockNetService.defaultTimeout) throws {
+
+ log?("Remove \(type) \(identifier) for \(server.url.absoluteString)")
+
+ let request = DeleteKeyRequest(
+ server: server.url,
+ authorization: .init(key: key),
+ key: identifier,
+ type: type
+ ).urlRequest()
+
+ let (httpResponse, _) = try urlSession.synchronousDataTask(with: request)
+
+ guard httpResponse.statusCode == 200
+ else { throw LockNetService.Error.statusCode(httpResponse.statusCode) }
+ }
+}
diff --git a/Sources/CoreLock/DeviceManager.swift b/Sources/CoreLock/DeviceManager.swift
index e913893b..2675e07c 100644
--- a/Sources/CoreLock/DeviceManager.swift
+++ b/Sources/CoreLock/DeviceManager.swift
@@ -19,7 +19,6 @@ public final class LockManager {
// MARK: - Initialization
public init(central: Central) {
-
self.central = central
}
@@ -149,7 +148,7 @@ public final class LockManager {
with key: KeyCredentials,
timeout: TimeInterval = .gattDefaultTimeout) throws {
- log?("Create \(newKey.permission.type) key \"\(newKey.name)\" \(newKey.identifier)")
+ log?("Create \(newKey.permission.type) key \"\(newKey.name)\" \(newKey.identifier) for \(peripheral)")
let timeout = Timeout(timeout: timeout)
@@ -172,7 +171,7 @@ public final class LockManager {
with key: KeyCredentials,
timeout: TimeInterval = .gattDefaultTimeout) throws {
- log?("Confirm key \(key.identifier)")
+ log?("Confirm key \(key.identifier) for \(peripheral)")
let timeout = Timeout(timeout: timeout)
@@ -196,7 +195,7 @@ public final class LockManager {
with key: KeyCredentials,
timeout: TimeInterval = .gattDefaultTimeout) throws {
- log?("Remove \(type) \(identifier)")
+ log?("Remove \(type) \(identifier) for \(peripheral)")
let timeout = Timeout(timeout: timeout)
@@ -311,7 +310,7 @@ public final class LockManager {
}
/// Retreive a list of events on device.
- public func listEvents(fetchRequest: ListEventsCharacteristic.FetchRequest? = nil,
+ public func listEvents(fetchRequest: LockEvent.FetchRequest? = nil,
for peripheral: Peripheral,
with key: KeyCredentials,
timeout: TimeInterval = .gattDefaultTimeout) throws -> EventsList {
@@ -326,7 +325,7 @@ public final class LockManager {
}
/// Retreive a list of events on device.
- public func listEvents(fetchRequest: ListEventsCharacteristic.FetchRequest? = nil,
+ public func listEvents(fetchRequest: LockEvent.FetchRequest? = nil,
for peripheral: Peripheral,
with key: KeyCredentials,
timeout: TimeInterval = .gattDefaultTimeout,
diff --git a/Sources/CoreLock/EncryptedData.swift b/Sources/CoreLock/EncryptedData.swift
index 3575a911..770586c9 100644
--- a/Sources/CoreLock/EncryptedData.swift
+++ b/Sources/CoreLock/EncryptedData.swift
@@ -40,7 +40,6 @@ public extension EncryptedData {
// attempt to decrypt
do { return try CoreLock.decrypt(key: key.data, iv: initializationVector, data: encryptedData) }
-
catch { throw AuthenticationError.decryptionError(error) }
}
}
diff --git a/Sources/CoreLock/Event.swift b/Sources/CoreLock/Event.swift
index 14c78840..3c44c190 100644
--- a/Sources/CoreLock/Event.swift
+++ b/Sources/CoreLock/Event.swift
@@ -275,3 +275,4 @@ public extension LockEvent {
}
}
}
+
diff --git a/Sources/CoreLock/EventStore.swift b/Sources/CoreLock/EventStore.swift
new file mode 100644
index 00000000..7ddf9e40
--- /dev/null
+++ b/Sources/CoreLock/EventStore.swift
@@ -0,0 +1,119 @@
+//
+// EventStore.swift
+// CoreLock
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+// Copyright © 2019 ColemanCDA. All rights reserved.
+//
+
+import Foundation
+
+public protocol LockEventStore: class {
+
+ func fetch(_ fetchRequest: LockEvent.FetchRequest) throws -> [LockEvent]
+
+ func save(_ event: LockEvent) throws
+}
+
+// MARK: - Supporting Types
+
+public final class InMemoryLockEvents: LockEventStore {
+
+ public init() { }
+
+ public private(set) var events = [LockEvent]()
+
+ public func fetch(_ fetchRequest: LockEvent.FetchRequest) throws -> [LockEvent] {
+ return events.fetch(fetchRequest)
+ }
+
+ public func save(_ event: LockEvent) throws {
+ events.append(event)
+ }
+}
+
+// MARK: - Fetch Request
+
+public extension LockEvent {
+
+ struct FetchRequest: Codable, Equatable {
+
+ /// The fetch offset of the fetch request.
+ public var offset: UInt8
+
+ /// The fetch limit of the fetch request.
+ public var limit: UInt8?
+
+ /// The predicate of the fetch request.
+ public var predicate: Predicate?
+
+ public init(offset: UInt8 = 0,
+ limit: UInt8? = nil,
+ predicate: Predicate? = nil) {
+
+ self.offset = offset
+ self.limit = limit
+ self.predicate = predicate
+ }
+ }
+
+ struct Predicate: Codable, Equatable {
+
+ public var keys: [UUID]?
+
+ public var start: Date?
+
+ public var end: Date?
+
+ public init(keys: [UUID]?,
+ start: Date? = nil,
+ end: Date? = nil) {
+
+ self.keys = keys
+ self.start = start
+ self.end = end
+ }
+ }
+}
+
+public extension LockEvent.Predicate {
+
+ static var empty: LockEvent.Predicate { return .init(keys: nil, start: nil, end: nil) }
+}
+
+public extension Collection where Self.Element == LockEvent {
+
+ func fetch(_ fetchRequest: LockEvent.FetchRequest) -> [LockEvent] {
+
+ guard isEmpty == false else { return [] }
+ let limit = Int(fetchRequest.limit ?? .max)
+ let offset = Int(fetchRequest.offset)
+ let predicate = fetchRequest.predicate ?? .empty
+ return sorted(by: { $0.date > $1.date })
+ .lazy
+ .suffix(from: offset)
+ .lazy
+ .prefix(limit)
+ .filter(predicate)
+ }
+
+ func filter(_ predicate: LockEvent.Predicate) -> [LockEvent] {
+
+ // don't filter
+ guard predicate != .empty else { return Array(self) }
+ var value = Array(self)
+ // filter by keys
+ if let keys = predicate.keys,
+ keys.isEmpty == false {
+ value.removeAll(where: { keys.contains($0.key) == false })
+ }
+ // filter by date
+ if let startDate = predicate.start {
+ value.removeAll(where: { $0.date < startDate })
+ }
+ if let endDate = predicate.end {
+ value.removeAll(where: { $0.date > endDate })
+ }
+ return value
+ }
+}
diff --git a/Sources/CoreLock/EventsRequest.swift b/Sources/CoreLock/EventsRequest.swift
new file mode 100644
index 00000000..c93de29d
--- /dev/null
+++ b/Sources/CoreLock/EventsRequest.swift
@@ -0,0 +1,149 @@
+//
+// EventsRequest.swift
+// CoreLock
+//
+// Created by Alsey Coleman Miller on 10/20/19.
+// Copyright © 2019 ColemanCDA. All rights reserved.
+//
+
+import Foundation
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+public struct EventsNetServiceRequest: Equatable {
+
+ /// Lock server
+ public let server: URL
+
+ /// Authorization header
+ public let authorization: LockNetService.Authorization
+
+ /// Encrypted request
+ public let fetchRequest: LockEvent.FetchRequest?
+}
+
+// MARK: - URL Request
+
+public extension EventsNetServiceRequest {
+
+ func urlRequest() -> URLRequest {
+
+ // http://localhost:8080/event
+ guard var urlComponents = URLComponents(url: server.appendingPathComponent("event"), resolvingAgainstBaseURL: false)
+ else { fatalError() }
+ urlComponents.queryItems = fetchRequest?.queryItems
+ guard let url = urlComponents.url
+ else { fatalError() }
+ var urlRequest = URLRequest(url: url)
+ urlRequest.addValue(authorization.header, forHTTPHeaderField: LockNetService.Authorization.headerField)
+ urlRequest.httpMethod = "GET"
+ return urlRequest
+ }
+}
+
+public extension LockEvent.FetchRequest {
+
+ init?(queryItems: [URLQueryItem]) {
+ guard let offset = queryItems.first(.offset).flatMap(UInt8.init)
+ else { return nil }
+ self.offset = offset
+ self.limit = queryItems.first(.limit).flatMap(UInt8.init)
+ var predicate = LockEvent.Predicate.empty
+ predicate.start = queryItems.firstDate(.start)
+ predicate.end = queryItems.firstDate(.end)
+ predicate.keys = queryItems
+ .compactMap(.key)
+ .compactMap { UUID(uuidString: $0) }
+ self.predicate = predicate != .empty ? predicate : nil
+ }
+
+ var queryItems: [URLQueryItem] {
+ var queryItems = [URLQueryItem]()
+ queryItems.reserveCapacity(3)
+ queryItems.append(.init(name: .offset, value: offset.description))
+ limit.flatMap { queryItems.append(.init(name: .limit, value: $0.description)) }
+ if let predicate = predicate {
+ predicate.keys.flatMap {
+ queryItems += $0.map { .init(name: .key, value: $0.uuidString) }
+ }
+ predicate.start.flatMap { queryItems.append(.init(name: .start, value: $0)) }
+ predicate.end.flatMap { queryItems.append(.init(name: .end, value: $0)) }
+ }
+ return queryItems
+ }
+}
+
+internal extension EventsNetServiceRequest {
+
+ enum QueryItem: String {
+ case offset
+ case limit
+ case key
+ case start
+ case end
+ }
+}
+
+internal extension URLQueryItem {
+
+ init(name: EventsNetServiceRequest.QueryItem, value: String? = nil) {
+ self.init(name: name.rawValue, value: value)
+ }
+
+ init(name: EventsNetServiceRequest.QueryItem, value date: Date) {
+ let value = Int(date.timeIntervalSince1970).description
+ self.init(name: name, value: value)
+ }
+}
+
+internal extension Sequence where Self.Element == URLQueryItem {
+
+ func first(_ name: EventsNetServiceRequest.QueryItem) -> String? {
+ return first(where: { $0.name == name.rawValue })?.value
+ }
+
+ func compactMap(_ name: EventsNetServiceRequest.QueryItem) -> [String] {
+ return compactMap { $0.name == name.rawValue ? $0.value : nil }
+ }
+
+ func firstDate(_ name: EventsNetServiceRequest.QueryItem) -> Date? {
+ guard let value = first(name),
+ let timeInterval = TimeInterval(value)
+ else { return nil }
+ return Date(timeIntervalSince1970: timeInterval)
+ }
+}
+
+// MARK: - Client
+
+public extension LockNetService.Client {
+
+ /// Retreive a list of events on device.
+ func listEvents(fetchRequest: LockEvent.FetchRequest? = nil,
+ for server: LockNetService,
+ with key: KeyCredentials,
+ timeout: TimeInterval = LockNetService.defaultTimeout) throws -> EventsList {
+
+ log?("List events for \(server.url.absoluteString)")
+
+ let request = EventsNetServiceRequest(
+ server: server.url,
+ authorization: LockNetService.Authorization(key: key),
+ fetchRequest: fetchRequest
+ )
+
+ let (httpResponse, data) = try urlSession.synchronousDataTask(with: request.urlRequest())
+
+ guard httpResponse.statusCode == 200
+ else { throw LockNetService.Error.statusCode(httpResponse.statusCode) }
+
+ guard let jsonData = data,
+ let response = try? jsonDecoder.decode(EventsResponse.self, from: jsonData)
+ else { throw LockNetService.Error.invalidResponse }
+
+ let keys = try response.decrypt(with: key.secret, decoder: jsonDecoder)
+ return keys
+ }
+}
diff --git a/Sources/CoreLock/EventsResponse.swift b/Sources/CoreLock/EventsResponse.swift
new file mode 100644
index 00000000..3213698c
--- /dev/null
+++ b/Sources/CoreLock/EventsResponse.swift
@@ -0,0 +1,47 @@
+//
+// EventsResponse.swift
+// CoreLock
+//
+// Created by Alsey Coleman Miller on 10/21/19.
+// Copyright © 2019 ColemanCDA. All rights reserved.
+//
+
+import Foundation
+
+public struct EventsResponse: Equatable {
+
+ public let encryptedData: LockNetService.EncryptedData
+}
+
+// MARK: - Codable
+
+extension EventsResponse: Codable {
+
+ public init(from decoder: Decoder) throws {
+ self.encryptedData = try LockNetService.EncryptedData(from: decoder)
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ try encryptedData.encode(to: encoder)
+ }
+}
+
+// MARK: - Encryption
+
+public extension EventsResponse {
+
+ init(encrypt value: EventsList,
+ with key: KeyData,
+ encoder: JSONEncoder = JSONEncoder()) throws {
+
+ let data = try encoder.encode(value)
+ self.encryptedData = try .init(encrypt: data, with: key)
+ }
+
+ func decrypt(with key: KeyData,
+ decoder: JSONDecoder = JSONDecoder()) throws -> EventsList {
+
+ let data = try encryptedData.decrypt(with: key)
+ return try decoder.decode(EventsList.self, from: data)
+ }
+}
diff --git a/Sources/CoreLock/KeysRequest.swift b/Sources/CoreLock/KeysRequest.swift
new file mode 100644
index 00000000..61237d80
--- /dev/null
+++ b/Sources/CoreLock/KeysRequest.swift
@@ -0,0 +1,70 @@
+//
+// KeysRequest.swift
+// CoreLock
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+// Copyright © 2019 ColemanCDA. All rights reserved.
+//
+
+import Foundation
+import CryptoSwift
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+public struct KeysNetServiceRequest: Equatable {
+
+ /// Lock server
+ public let server: URL
+
+ /// Authorization header
+ public let authorization: LockNetService.Authorization
+}
+
+// MARK: - URL Request
+
+public extension KeysNetServiceRequest {
+
+ func urlRequest() -> URLRequest {
+
+ // http://localhost:8080/keys
+ let url = server.appendingPathComponent("key")
+ var urlRequest = URLRequest(url: url)
+ urlRequest.addValue(authorization.header, forHTTPHeaderField: LockNetService.Authorization.headerField)
+ return urlRequest
+ }
+}
+
+// MARK: - Client
+
+public extension LockNetService.Client {
+
+ /// Retreive a list of all keys on device.
+ func listKeys(for server: LockNetService,
+ with key: KeyCredentials,
+ timeout: TimeInterval = LockNetService.defaultTimeout) throws -> KeysList {
+
+ log?("List keys for \(server.url.absoluteString)")
+
+ let request = KeysNetServiceRequest(
+ server: server.url,
+ authorization: LockNetService.Authorization(
+ key: key.identifier,
+ authentication: Authentication(key: key.secret)
+ )
+ )
+
+ let (httpResponse, data) = try urlSession.synchronousDataTask(with: request.urlRequest())
+
+ guard httpResponse.statusCode == 200
+ else { throw LockNetService.Error.statusCode(httpResponse.statusCode) }
+
+ guard let jsonData = data,
+ let response = try? jsonDecoder.decode(KeysResponse.self, from: jsonData)
+ else { throw LockNetService.Error.invalidResponse }
+
+ let keys = try response.decrypt(with: key.secret, decoder: jsonDecoder)
+ return keys
+ }
+}
diff --git a/Sources/CoreLock/KeysResponse.swift b/Sources/CoreLock/KeysResponse.swift
new file mode 100644
index 00000000..915fa539
--- /dev/null
+++ b/Sources/CoreLock/KeysResponse.swift
@@ -0,0 +1,47 @@
+//
+// KeysResponse.swift
+// CoreLock
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+// Copyright © 2019 ColemanCDA. All rights reserved.
+//
+
+import Foundation
+
+public struct KeysResponse: Equatable {
+
+ public let encryptedData: LockNetService.EncryptedData
+}
+
+// MARK: - Codable
+
+extension KeysResponse: Codable {
+
+ public init(from decoder: Decoder) throws {
+ self.encryptedData = try LockNetService.EncryptedData(from: decoder)
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ try encryptedData.encode(to: encoder)
+ }
+}
+
+// MARK: - Encryption
+
+public extension KeysResponse {
+
+ init(encrypt value: KeysList,
+ with key: KeyData,
+ encoder: JSONEncoder = JSONEncoder()) throws {
+
+ let data = try encoder.encode(value)
+ self.encryptedData = try .init(encrypt: data, with: key)
+ }
+
+ func decrypt(with key: KeyData,
+ decoder: JSONDecoder = JSONDecoder()) throws -> KeysList {
+
+ let data = try encryptedData.decrypt(with: key)
+ return try decoder.decode(KeysList.self, from: data)
+ }
+}
diff --git a/Sources/CoreLock/ListEventsCharacteristic.swift b/Sources/CoreLock/ListEventsCharacteristic.swift
index b548e571..148b5e8a 100644
--- a/Sources/CoreLock/ListEventsCharacteristic.swift
+++ b/Sources/CoreLock/ListEventsCharacteristic.swift
@@ -24,95 +24,14 @@ public struct ListEventsCharacteristic: TLVCharacteristic, Codable, Equatable {
public let authentication: Authentication
/// Fetch limit for events to view.
- public let fetchRequest: FetchRequest?
+ public let fetchRequest: LockEvent.FetchRequest?
public init(identifier: UUID,
authentication: Authentication,
- fetchRequest: FetchRequest? = nil) {
+ fetchRequest: LockEvent.FetchRequest? = nil) {
self.identifier = identifier
self.authentication = authentication
self.fetchRequest = fetchRequest
}
}
-
-public extension ListEventsCharacteristic {
-
- struct FetchRequest: Codable, Equatable {
-
- /// The fetch offset of the fetch request.
- public var offset: UInt8
-
- /// The fetch limit of the fetch request.
- public var limit: UInt8?
-
- /// The predicate of the fetch request.
- public var predicate: Predicate?
-
- public init(offset: UInt8 = 0,
- limit: UInt8? = nil,
- predicate: Predicate? = nil) {
-
- self.offset = offset
- self.limit = limit
- self.predicate = predicate
- }
- }
-
- struct Predicate: Codable, Equatable {
-
- public var keys: [UUID]?
-
- public var start: Date?
-
- public var end: Date?
-
- public static var empty: Predicate { return .init(keys: nil, start: nil, end: nil) }
-
- public init(keys: [UUID]?,
- start: Date? = nil,
- end: Date? = nil) {
-
- self.keys = keys
- self.start = start
- self.end = end
- }
- }
-}
-
-public extension Collection where Self.Element == LockEvent {
-
- func fetch(_ fetchRequest: ListEventsCharacteristic.FetchRequest) -> [LockEvent] {
-
- guard isEmpty == false else { return [] }
- let limit = Int(fetchRequest.limit ?? .max)
- let offset = Int(fetchRequest.offset)
- let predicate = fetchRequest.predicate ?? .empty
- return sorted(by: { $0.date > $1.date })
- .lazy
- .suffix(from: offset)
- .lazy
- .prefix(limit)
- .filter(predicate)
- }
-
- func filter(_ predicate: ListEventsCharacteristic.Predicate) -> [LockEvent] {
-
- // don't filter
- guard predicate != .empty else { return Array(self) }
- var value = Array(self)
- // filter by keys
- if let keys = predicate.keys,
- keys.isEmpty == false {
- value.removeAll(where: { keys.contains($0.key) == false })
- }
- // filter by date
- if let startDate = predicate.start {
- value.removeAll(where: { $0.date < startDate })
- }
- if let endDate = predicate.end {
- value.removeAll(where: { $0.date > endDate })
- }
- return value
- }
-}
diff --git a/Sources/CoreLock/LockAuthorizationStore.swift b/Sources/CoreLock/LockAuthorizationStore.swift
new file mode 100644
index 00000000..29aeedfb
--- /dev/null
+++ b/Sources/CoreLock/LockAuthorizationStore.swift
@@ -0,0 +1,113 @@
+//
+// LockAuthorizationStore.swift
+//
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+//
+
+import Foundation
+
+/// Lock Authorization Store
+public protocol LockAuthorizationStore: class {
+
+ var isEmpty: Bool { get }
+
+ func add(_ key: Key, secret: KeyData) throws
+
+ func key(for identifier: UUID) throws -> (key: Key, secret: KeyData)?
+
+ func add(_ key: NewKey, secret: KeyData) throws
+
+ func newKey(for identifier: UUID) throws -> (newKey: NewKey, secret: KeyData)?
+
+ func removeKey(_ identifier: UUID) throws
+
+ func removeNewKey(_ identifier: UUID) throws
+
+ func removeAll() throws
+
+ var list: KeysList { get }
+}
+
+// MARK: - Supporting Types
+
+public final class InMemoryLockAuthorization: LockAuthorizationStore {
+
+ public init() { }
+
+ private var keys = [KeyEntry]()
+
+ private var newKeys = [NewKeyEntry]()
+
+ public var isEmpty: Bool {
+
+ return keys.isEmpty && newKeys.isEmpty
+ }
+
+ public func add(_ key: Key, secret: KeyData) throws {
+
+ keys.append(KeyEntry(key: key, secret: secret))
+ }
+
+ public func key(for identifier: UUID) throws -> (key: Key, secret: KeyData)? {
+
+ guard let keyEntry = keys.first(where: { $0.key.identifier == identifier })
+ else { return nil }
+
+ return (keyEntry.key, keyEntry.secret)
+ }
+
+ public func add(_ key: NewKey, secret: KeyData) throws {
+
+ newKeys.append(NewKeyEntry(newKey: key, secret: secret))
+ }
+
+ public func newKey(for identifier: UUID) throws -> (newKey: NewKey, secret: KeyData)? {
+
+ guard let keyEntry = newKeys.first(where: { $0.newKey.identifier == identifier })
+ else { return nil }
+
+ return (keyEntry.newKey, keyEntry.secret)
+ }
+
+ public func removeKey(_ identifier: UUID) throws {
+
+ keys.removeAll(where: { $0.key.identifier == identifier })
+ }
+
+ public func removeNewKey(_ identifier: UUID) throws {
+
+ newKeys.removeAll(where: { $0.newKey.identifier == identifier })
+ }
+
+ public func removeAll() throws {
+
+ keys.removeAll()
+ newKeys.removeAll()
+ }
+
+ public var list: KeysList {
+
+ return KeysList(
+ keys: keys.map { $0.key },
+ newKeys: newKeys.map { $0.newKey }
+ )
+ }
+}
+
+private extension InMemoryLockAuthorization {
+
+ struct KeyEntry {
+
+ let key: Key
+
+ let secret: KeyData
+ }
+
+ struct NewKeyEntry {
+
+ let newKey: NewKey
+
+ let secret: KeyData
+ }
+}
diff --git a/Sources/CoreLock/LockConfigurationStore.swift b/Sources/CoreLock/LockConfigurationStore.swift
new file mode 100644
index 00000000..87ba52e0
--- /dev/null
+++ b/Sources/CoreLock/LockConfigurationStore.swift
@@ -0,0 +1,31 @@
+//
+// LockConfigurationStore.swift
+//
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+//
+
+import Foundation
+
+/// Lock Configuration Storage
+public protocol LockConfigurationStore: class {
+
+ var configuration: LockConfiguration { get }
+
+ func update(_ configuration: LockConfiguration) throws
+}
+
+// MARK: - Supporting Types
+
+public final class InMemoryLockConfigurationStore: LockConfigurationStore {
+
+ public private(set) var configuration: LockConfiguration
+
+ public init(configuration: LockConfiguration = LockConfiguration()) {
+ self.configuration = configuration
+ }
+
+ public func update(_ configuration: LockConfiguration) throws {
+ self.configuration = configuration
+ }
+}
diff --git a/Sources/CoreLock/LockHardware.swift b/Sources/CoreLock/LockHardware.swift
index 2b452ab8..b253b40b 100644
--- a/Sources/CoreLock/LockHardware.swift
+++ b/Sources/CoreLock/LockHardware.swift
@@ -34,8 +34,7 @@ public extension LockHardware {
/// Empty / Null Lock Hardware information.
static var empty: LockHardware {
-
- return LockHardware(model: "", hardwareRevision: "", serialNumber: "")
+ return .init(model: "", hardwareRevision: "", serialNumber: "")
}
}
diff --git a/Sources/CoreLock/LockInformationRequest.swift b/Sources/CoreLock/LockInformationRequest.swift
new file mode 100644
index 00000000..f529ab22
--- /dev/null
+++ b/Sources/CoreLock/LockInformationRequest.swift
@@ -0,0 +1,53 @@
+//
+// LockInformationRequest.swift
+// CoreLock
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+// Copyright © 2019 ColemanCDA. All rights reserved.
+//
+
+import Foundation
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+/// Lock Information Web Request
+public struct LockInformationNetServiceRequest: Equatable {
+
+ /// Lock server
+ public let server: URL
+}
+
+public extension LockInformationNetServiceRequest {
+
+ func urlRequest() -> URLRequest {
+
+ // http://localhost:8080/info
+ let url = server.appendingPathComponent("info")
+ return URLRequest(url: url)
+ }
+}
+
+public extension LockNetService.Client {
+
+ /// Read the lock's information characteristic.
+ func readInformation(for server: LockNetService,
+ timeout: TimeInterval = LockNetService.defaultTimeout) throws -> LockNetService.LockInformation {
+
+ log?("Read information for \(server.url.absoluteString)")
+
+ let request = LockInformationNetServiceRequest(server: server.url).urlRequest()
+
+ let (httpResponse, data) = try urlSession.synchronousDataTask(with: request)
+
+ guard httpResponse.statusCode == 200
+ else { throw LockNetService.Error.statusCode(httpResponse.statusCode) }
+
+ guard let jsonData = data,
+ let response = try? jsonDecoder.decode(LockNetService.LockInformation.self, from: jsonData)
+ else { throw LockNetService.Error.invalidResponse }
+
+ return response
+ }
+}
diff --git a/Sources/CoreLock/LockInformationResponse.swift b/Sources/CoreLock/LockInformationResponse.swift
new file mode 100644
index 00000000..275d0e9b
--- /dev/null
+++ b/Sources/CoreLock/LockInformationResponse.swift
@@ -0,0 +1,43 @@
+//
+// LockInformationResponse.swift
+// CoreLock
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+// Copyright © 2019 ColemanCDA. All rights reserved.
+//
+
+import Foundation
+
+public extension LockNetService {
+
+ struct LockInformation: Equatable, Codable {
+
+ /// Lock identifier
+ public let identifier: UUID
+
+ /// Firmware build number
+ public let buildVersion: LockBuildVersion
+
+ /// Firmware version
+ public let version: LockVersion
+
+ /// Device state
+ public var status: LockStatus
+
+ /// Supported lock actions
+ public let unlockActions: Set
+
+ public init(identifier: UUID,
+ buildVersion: LockBuildVersion = .current,
+ version: LockVersion = .current,
+ status: LockStatus,
+ unlockActions: Set = [.default]) {
+
+ self.identifier = identifier
+ self.buildVersion = buildVersion
+ self.version = version
+ self.status = status
+ self.unlockActions = unlockActions
+ }
+ }
+}
diff --git a/Sources/CoreLock/LockModel.swift b/Sources/CoreLock/LockModel.swift
index 0cc6742d..50a8f6c3 100644
--- a/Sources/CoreLock/LockModel.swift
+++ b/Sources/CoreLock/LockModel.swift
@@ -14,7 +14,6 @@ public struct LockModel: RawRepresentable, Equatable, Hashable {
public let rawValue: String
public init(rawValue: String) {
-
self.rawValue = rawValue
}
}
@@ -24,7 +23,6 @@ public struct LockModel: RawRepresentable, Equatable, Hashable {
extension LockModel: CustomStringConvertible {
public var description: String {
-
return rawValue
}
}
@@ -34,7 +32,6 @@ extension LockModel: CustomStringConvertible {
extension LockModel: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
-
self.init(rawValue: value)
}
}
@@ -53,15 +50,14 @@ public extension LockModel {
// MARK: - Darwin
#if os(macOS)
+
+public extension LockModel {
- public extension LockModel {
-
- static var currentMac: LockModel {
-
- return LockModel(rawValue: UIDevice.current.model)
- }
+ static var currentMac: LockModel {
+ return LockModel(rawValue: UIDevice.current.model)
}
-
+}
+
#endif
// MARK: - Codable
diff --git a/Sources/CoreLock/LockNetService.swift b/Sources/CoreLock/LockNetService.swift
new file mode 100644
index 00000000..c1270f38
--- /dev/null
+++ b/Sources/CoreLock/LockNetService.swift
@@ -0,0 +1,223 @@
+//
+// LockNetService.swift
+//
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+//
+
+import Foundation
+import Bonjour
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+public struct LockNetService: Equatable, Hashable {
+
+ public let identifier: UUID
+
+ public let url: URL
+}
+
+internal extension LockNetService {
+
+ init(identifier: UUID, address: NetServiceAddress) {
+
+ guard let url = URL(string: "http://" + address.description)
+ else { fatalError("Could not create URL from \(address)") }
+
+ self.identifier = identifier
+ self.url = url
+ }
+}
+
+public extension LockNetService {
+
+ final class Client {
+
+ // MARK: - Properties
+
+ /// The log message handler.
+ public var log: ((String) -> ())?
+
+ /// Bonjour Client
+ public let bonjour: NetServiceClient
+
+ /// URL Session
+ public let urlSession: URLSession
+
+ /// Whether the client is discovering net services.
+ private(set) var isDiscovering = false
+
+ internal lazy var jsonDecoder = JSONDecoder()
+
+ internal lazy var jsonEncoder = JSONEncoder()
+
+ // MARK: - Initialization
+
+ public init(bonjour: NetServiceClient,
+ urlSession: URLSession = .shared) {
+
+ self.bonjour = bonjour
+ self.urlSession = urlSession
+ }
+
+ // MARK: - Methods
+
+ public func discover(duration: TimeInterval = 5.0,
+ timeout: TimeInterval = 10.0) throws -> Set {
+
+ log?("Scanning for \(String(format: "%.2f", duration))s")
+
+ isDiscovering = true
+ defer { isDiscovering = false }
+
+ var foundServices = [UUID: Bonjour.Service](minimumCapacity: 1)
+
+ let end = Date() + duration
+ try bonjour.discoverServices(of: .lock, in: .local, shouldContinue: {
+ Date() < end
+ }, foundService: { (service) in
+ guard service.type == .lock,
+ let identifier = UUID(uuidString: service.name)
+ else { return }
+ foundServices[identifier] = service
+ })
+
+ var locks = Set()
+ locks.reserveCapacity(foundServices.count)
+
+ for (identifier, service) in foundServices {
+
+ guard let addresses = try? bonjour.resolve(service, timeout: timeout),
+ let address = addresses.filter({
+ switch $0.address {
+ case .ipv4: return true
+ case .ipv6: return false
+ }
+ }).first
+ else { continue }
+
+ let lock = LockNetService(
+ identifier: identifier,
+ address: address
+ )
+
+ locks.insert(lock)
+ }
+
+ log?("Found \(locks.count) devices")
+
+ return locks
+ }
+ }
+}
+
+// MARK: - Extensions
+
+public extension NetServiceType {
+
+ static let lock = NetServiceType(rawValue: LockNetService.serviceType)
+}
+
+public extension LockNetService {
+
+ static let serviceType = "_lock._tcp."
+
+ static var defaultTimeout: TimeInterval = 30.0
+}
+
+// MARK: - Supporting Types
+
+public extension LockNetService {
+
+ enum Error: Swift.Error {
+
+ case invalidURL
+ case statusCode(Int)
+ case invalidResponse
+ }
+}
+
+public extension LockNetService {
+
+ /// Lock authorization for Web API
+ struct Authorization: Equatable, Codable {
+
+ /// Identifier of key making request.
+ public let key: UUID
+
+ /// HMAC of key and nonce, and HMAC message
+ public let authentication: Authentication
+ }
+}
+
+public extension LockNetService.Authorization {
+
+ init(key: KeyCredentials) {
+
+ self.init(key: key.identifier, authentication: Authentication(key: key.secret))
+ }
+}
+
+public extension LockNetService.Authorization {
+
+ private static let jsonDecoder = JSONDecoder()
+
+ private static let jsonEncoder = JSONEncoder()
+
+ static let headerField: String = "Authorization"
+
+ init?(header: String) {
+
+ guard let data = Data(base64Encoded: header),
+ let authorization = try? type(of: self).jsonDecoder.decode(LockNetService.Authorization.self, from: data)
+ else { return nil }
+
+ self = authorization
+ }
+
+ var header: String {
+ let data = try! type(of: self).jsonEncoder.encode(self)
+ let base64 = data.base64EncodedString()
+ return base64
+ }
+}
+
+public extension LockNetService {
+
+ struct EncryptedData: Equatable, Codable {
+
+ internal enum CodingKeys: String, CodingKey {
+
+ case initializationVector = "iv"
+ case encryptedData = "data"
+ }
+
+ /// Crypto IV
+ public let initializationVector: InitializationVector
+
+ /// Encrypted data
+ public let encryptedData: Data
+ }
+}
+
+extension LockNetService.EncryptedData {
+
+ init(encrypt data: Data, with key: KeyData) throws {
+
+ do {
+ let (encryptedData, iv) = try CoreLock.encrypt(key: key.data, data: data)
+ self.initializationVector = iv
+ self.encryptedData = encryptedData
+ }
+ catch { throw AuthenticationError.encryptionError(error) }
+ }
+
+ func decrypt(with key: KeyData) throws -> Data {
+
+ // attempt to decrypt
+ do { return try CoreLock.decrypt(key: key.data, iv: initializationVector, data: encryptedData) }
+ catch { throw AuthenticationError.decryptionError(error) }
+ }
+}
diff --git a/Sources/CoreLock/URLSession.swift b/Sources/CoreLock/URLSession.swift
new file mode 100644
index 00000000..33ac6ce8
--- /dev/null
+++ b/Sources/CoreLock/URLSession.swift
@@ -0,0 +1,47 @@
+//
+// URLSession.swift
+// CoreLock
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+// Copyright © 2019 ColemanCDA. All rights reserved.
+//
+
+import Foundation
+import Dispatch
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+internal extension URLSession {
+
+ func synchronousDataTask(with request: URLRequest) throws -> (HTTPURLResponse, Data?) {
+
+ var data: Data?
+ var response: URLResponse?
+ var error: Error?
+
+ let semaphore = DispatchSemaphore(value: 0)
+
+ let dataTask = self.dataTask(with: request) {
+ data = $0
+ response = $1
+ error = $2
+
+ semaphore.signal()
+ }
+
+ dataTask.resume()
+
+ _ = semaphore.wait(timeout: .distantFuture)
+
+ if let error = error {
+ throw error
+ }
+
+ guard let urlResponse = response as? HTTPURLResponse
+ else { fatalError("Invalid response: \(response?.description ?? "nil")") }
+
+ return (urlResponse, data)
+ }
+}
diff --git a/Sources/CoreLock/UpdateRequest.swift b/Sources/CoreLock/UpdateRequest.swift
new file mode 100644
index 00000000..8c3a4707
--- /dev/null
+++ b/Sources/CoreLock/UpdateRequest.swift
@@ -0,0 +1,60 @@
+//
+// UpdateRequest.swift
+// CoreLock
+//
+// Created by Alsey Coleman Miller on 10/18/19.
+//
+
+import Foundation
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+/// Lock Software Update HTTP Request
+public struct UpdateNetServiceRequest: Equatable {
+
+ /// Lock server
+ public let server: URL
+
+ /// Authorization header
+ public let authorization: LockNetService.Authorization
+}
+
+// MARK: - URL Request
+
+public extension UpdateNetServiceRequest {
+
+ func urlRequest() -> URLRequest {
+
+ // http://localhost:8080/update
+ let url = server.appendingPathComponent("update")
+ var urlRequest = URLRequest(url: url)
+ urlRequest.addValue(authorization.header, forHTTPHeaderField: LockNetService.Authorization.headerField)
+ urlRequest.httpMethod = "POST"
+ return urlRequest
+ }
+}
+
+// MARK: - Client
+
+public extension LockNetService.Client {
+
+ /// Create new key.
+ func update(for server: LockNetService,
+ with key: KeyCredentials,
+ timeout: TimeInterval = LockNetService.defaultTimeout) throws {
+
+ log?("Update software for \(server.url.absoluteString)")
+
+ let request = UpdateNetServiceRequest(
+ server: server.url,
+ authorization: .init(key: key)
+ ).urlRequest()
+
+ let (httpResponse, _) = try urlSession.synchronousDataTask(with: request)
+
+ guard httpResponse.statusCode == 200
+ else { throw LockNetService.Error.statusCode(httpResponse.statusCode) }
+ }
+}
diff --git a/Sources/CoreLock/Version.swift b/Sources/CoreLock/Version.swift
index dab15823..8da964f7 100644
--- a/Sources/CoreLock/Version.swift
+++ b/Sources/CoreLock/Version.swift
@@ -27,7 +27,7 @@ public struct LockVersion: Equatable, Hashable {
public extension LockVersion {
- static var current: LockVersion { return LockVersion(major: 0, minor: 0, patch: 1) }
+ static var current: LockVersion { return LockVersion(major: 1, minor: 0, patch: 1) }
}
// MARK: - RawRepresentable
diff --git a/Sources/CoreLockGATTServer/LockController.swift b/Sources/CoreLockGATTServer/LockGATTController.swift
similarity index 86%
rename from Sources/CoreLockGATTServer/LockController.swift
rename to Sources/CoreLockGATTServer/LockGATTController.swift
index 50385270..eda2794e 100644
--- a/Sources/CoreLockGATTServer/LockController.swift
+++ b/Sources/CoreLockGATTServer/LockGATTController.swift
@@ -17,7 +17,7 @@ import GATT
import CoreLock
/// Smart Lock GATT Server controller.
-public final class LockController {
+public final class LockGATTController {
// MARK: - Properties
@@ -25,11 +25,10 @@ public final class LockController {
public let deviceInformationController: GATTDeviceInformationServiceController
- public let lockServiceController: LockServiceController
+ public let lockServiceController: LockGATTServiceController
// Lock hardware model
public var hardware: LockHardware = .empty {
-
didSet {
self.lockServiceController.hardware = hardware
self.deviceInformationController.hardware = hardware
@@ -44,7 +43,7 @@ public final class LockController {
// load services
self.deviceInformationController = try GATTDeviceInformationServiceController(peripheral: peripheral)
- self.lockServiceController = try LockServiceController(peripheral: peripheral)
+ self.lockServiceController = try LockGATTServiceController(peripheral: peripheral)
// set callbacks
self.peripheral.willRead = { [unowned self] in self.willRead($0) }
@@ -57,15 +56,10 @@ public final class LockController {
private func willRead(_ request: GATTReadRequest) -> ATT.Error? {
if lockServiceController.supportsCharacteristic(request.uuid) {
-
return lockServiceController.willRead(request)
-
} else if deviceInformationController.supportsCharacteristic(request.uuid) {
-
return deviceInformationController.willRead(request)
-
} else {
-
return nil
}
}
@@ -73,15 +67,10 @@ public final class LockController {
private func willWrite(_ request: GATTWriteRequest) -> ATT.Error? {
if lockServiceController.supportsCharacteristic(request.uuid) {
-
return lockServiceController.willWrite(request)
-
} else if deviceInformationController.supportsCharacteristic(request.uuid) {
-
return deviceInformationController.willWrite(request)
-
} else {
-
return nil
}
}
@@ -89,11 +78,8 @@ public final class LockController {
private func didWrite(_ confirmation: GATTWriteConfirmation) {
if lockServiceController.supportsCharacteristic(confirmation.uuid) {
-
lockServiceController.didWrite(confirmation)
-
} else if deviceInformationController.supportsCharacteristic(confirmation.uuid) {
-
deviceInformationController.didWrite(confirmation)
}
}
@@ -102,7 +88,6 @@ public final class LockController {
private extension GATTServiceController {
func supportsCharacteristic(_ characteristicUUID: BluetoothUUID) -> Bool {
-
return characteristics.contains(characteristicUUID)
}
}
diff --git a/Sources/CoreLockGATTServer/LockServiceController.swift b/Sources/CoreLockGATTServer/LockServiceController.swift
index 90cbf7a9..2455bf3e 100644
--- a/Sources/CoreLockGATTServer/LockServiceController.swift
+++ b/Sources/CoreLockGATTServer/LockServiceController.swift
@@ -10,7 +10,7 @@ import Bluetooth
import GATT
import CoreLock
-public final class LockServiceController : GATTServiceController {
+public final class LockGATTServiceController : GATTServiceController {
public static var service: BluetoothUUID { return Service.uuid }
@@ -21,7 +21,7 @@ public final class LockServiceController : GATT
// MARK: - Properties
public let peripheral: Peripheral
-
+
public var hardware: LockHardware = .empty {
didSet { updateInformation() }
}
@@ -29,13 +29,13 @@ public final class LockServiceController : GATT
public var configurationStore: LockConfigurationStore = InMemoryLockConfigurationStore() {
didSet { updateInformation() }
}
-
- public var setupSecret: KeyData = KeyData()
-
+
public var authorization: LockAuthorizationStore = InMemoryLockAuthorization() {
didSet { updateInformation() }
}
+ public var setupSecret: KeyData = KeyData()
+
public var unlockDelegate: UnlockDelegate = UnlockSimulator()
public var events: LockEventStore = InMemoryLockEvents()
@@ -155,7 +155,13 @@ public final class LockServiceController : GATT
public func willRead(_ request: GATTReadRequest) -> ATT.Error? {
- return nil
+ switch request.handle {
+ case informationHandle:
+ print("Requested lock information")
+ return nil
+ default:
+ return nil
+ }
}
public func willWrite(_ request: GATTWriteRequest) -> ATT.Error? {
@@ -600,62 +606,12 @@ public final class LockServiceController : GATT
}
}
-/// Lock Configuration Storage
-public protocol LockConfigurationStore {
-
- var configuration: LockConfiguration { get }
-
- func update(_ configuration: LockConfiguration) throws
-}
-
-/// Lock Authorization Store
-public protocol LockAuthorizationStore {
-
- var isEmpty: Bool { get }
-
- func add(_ key: Key, secret: KeyData) throws
-
- func key(for identifier: UUID) throws -> (key: Key, secret: KeyData)?
-
- func add(_ key: NewKey, secret: KeyData) throws
-
- func newKey(for identifier: UUID) throws -> (newKey: NewKey, secret: KeyData)?
-
- func removeKey(_ identifier: UUID) throws
-
- func removeNewKey(_ identifier: UUID) throws
-
- func removeAll() throws
-
- var list: KeysList { get }
-}
-
-public protocol LockEventStore {
-
- func fetch(_ fetchRequest: ListEventsCharacteristic.FetchRequest) throws -> [LockEvent]
-
- func save(_ event: LockEvent) throws
-}
-
/// Lock unlock manager
public protocol UnlockDelegate {
func unlock(_ action: UnlockAction) throws
}
-public final class InMemoryLockConfigurationStore: LockConfigurationStore {
-
- public private(set) var configuration: LockConfiguration
-
- public init(configuration: LockConfiguration = LockConfiguration()) {
- self.configuration = configuration
- }
-
- public func update(_ configuration: LockConfiguration) throws {
- self.configuration = configuration
- }
-}
-
public struct UnlockSimulator: UnlockDelegate {
public func unlock(_ action: UnlockAction) throws {
@@ -663,97 +619,3 @@ public struct UnlockSimulator: UnlockDelegate {
print("Simulate unlock with action \(action)")
}
}
-
-public final class InMemoryLockEvents: LockEventStore {
-
- public private(set) var events = [LockEvent]()
-
- public func fetch(_ fetchRequest: ListEventsCharacteristic.FetchRequest) throws -> [LockEvent] {
- return events.fetch(fetchRequest)
- }
-
- public func save(_ event: LockEvent) throws {
- events.append(event)
- }
-}
-
-public final class InMemoryLockAuthorization: LockAuthorizationStore {
-
- public init() { }
-
- private var keys = [KeyEntry]()
-
- private var newKeys = [NewKeyEntry]()
-
- public var isEmpty: Bool {
-
- return keys.isEmpty && newKeys.isEmpty
- }
-
- public func add(_ key: Key, secret: KeyData) throws {
-
- keys.append(KeyEntry(key: key, secret: secret))
- }
-
- public func key(for identifier: UUID) throws -> (key: Key, secret: KeyData)? {
-
- guard let keyEntry = keys.first(where: { $0.key.identifier == identifier })
- else { return nil }
-
- return (keyEntry.key, keyEntry.secret)
- }
-
- public func add(_ key: NewKey, secret: KeyData) throws {
-
- newKeys.append(NewKeyEntry(newKey: key, secret: secret))
- }
-
- public func newKey(for identifier: UUID) throws -> (newKey: NewKey, secret: KeyData)? {
-
- guard let keyEntry = newKeys.first(where: { $0.newKey.identifier == identifier })
- else { return nil }
-
- return (keyEntry.newKey, keyEntry.secret)
- }
-
- public func removeKey(_ identifier: UUID) throws {
-
- keys.removeAll(where: { $0.key.identifier == identifier })
- }
-
- public func removeNewKey(_ identifier: UUID) throws {
-
- newKeys.removeAll(where: { $0.newKey.identifier == identifier })
- }
-
- public func removeAll() throws {
-
- keys.removeAll()
- newKeys.removeAll()
- }
-
- public var list: KeysList {
-
- return KeysList(
- keys: keys.map { $0.key },
- newKeys: newKeys.map { $0.newKey }
- )
- }
-}
-
-private extension InMemoryLockAuthorization {
-
- struct KeyEntry {
-
- let key: Key
-
- let secret: KeyData
- }
-
- struct NewKeyEntry {
-
- let newKey: NewKey
-
- let secret: KeyData
- }
-}
diff --git a/Sources/CoreLockWebServer/Credentials.swift b/Sources/CoreLockWebServer/Credentials.swift
new file mode 100644
index 00000000..d6407b8f
--- /dev/null
+++ b/Sources/CoreLockWebServer/Credentials.swift
@@ -0,0 +1,58 @@
+//
+// Credentials.swift
+//
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+//
+
+import Foundation
+import CoreLock
+import Kitura
+
+internal extension LockNetService.Authorization {
+
+ init?(request: RouterRequest) {
+
+ guard let authorizationHeader = request.headers[LockNetService.Authorization.headerField],
+ let authorization = LockNetService.Authorization(header: authorizationHeader) else {
+ return nil
+ }
+
+ self = authorization
+ }
+}
+
+internal extension LockWebServer {
+
+ func authenticate(request: RouterRequest) throws -> (Key, KeyData)? {
+
+ // authenticate
+ guard let authorization = LockNetService.Authorization(request: request) else {
+ log?("\(request.urlURL.path) Missing authentication")
+ return nil
+ }
+
+ // validate key
+ guard let (key, secret) = try self.authorization.key(for: authorization.key) else {
+ log?("\(request.urlURL.path) Invalid key \(authorization.key)")
+ return nil
+ }
+
+ // validate HMAC
+ guard authorization.authentication.isAuthenticated(with: secret) else {
+ log?("\(request.urlURL.path) Invalid HMAC")
+ return nil
+ }
+
+ // guard against replay attacks
+ let timestamp = authorization.authentication.message.date
+ let now = Date()
+ guard timestamp <= now + authorizationTimeout, // cannot be used later for replay attacks
+ timestamp > now - authorizationTimeout else { // only valid for 5 seconds
+ log?("\(request.urlURL.path) Authentication expired \(timestamp) < \(now)")
+ return nil
+ }
+
+ return (key, secret)
+ }
+}
diff --git a/Sources/CoreLockWebServer/Events.swift b/Sources/CoreLockWebServer/Events.swift
new file mode 100644
index 00000000..4f8c6445
--- /dev/null
+++ b/Sources/CoreLockWebServer/Events.swift
@@ -0,0 +1,68 @@
+//
+// Events.swift
+// CoreLockWebServer
+//
+// Created by Alsey Coleman Miller on 10/22/19.
+//
+
+import Foundation
+import CoreLock
+import Kitura
+
+internal extension LockWebServer {
+
+ func addEventsRoute() {
+
+ router.get("/event") { [unowned self] (request, response, next) in
+ do {
+ if let statusCode = try self.getEvents(request: request, response: response) {
+ _ = response.send(status: statusCode)
+ }
+ }
+ catch {
+ self.log?("\(request.urlURL.path) Internal server error. \(error.localizedDescription)")
+ dump(error)
+ _ = response.send(status: .internalServerError)
+ }
+ try response.end()
+ }
+ }
+
+ private func getEvents(request: RouterRequest, response: RouterResponse) throws -> HTTPStatusCode? {
+
+ // authenticate
+ guard let (key, secret) = try authenticate(request: request) else {
+ return .unauthorized
+ }
+
+ log?("Key \(key.identifier) \(key.name) requested events list")
+
+ var fetchRequest = LockEvent.FetchRequest()
+
+ if let urlComponents = URLComponents(url: request.urlURL, resolvingAgainstBaseURL: true),
+ let queryItems = urlComponents.queryItems,
+ let queryFetchRequest = LockEvent.FetchRequest(queryItems: queryItems) {
+ var fetchDescription = ""
+ dump(fetchRequest, to: &fetchDescription)
+ log?(fetchDescription)
+ fetchRequest = queryFetchRequest
+ }
+
+ // enforce permission, non-administrators can only view their own events.
+ if key.permission.isAdministrator == false {
+ var predicate = fetchRequest.predicate ?? .empty
+ predicate.keys = [key.identifier]
+ fetchRequest.predicate = predicate
+ }
+
+ // execute fetch
+ let list = try events.fetch(fetchRequest)
+
+ // encrypt
+ let eventsResponse = try EventsResponse(encrypt: list, with: secret, encoder: jsonEncoder)
+
+ // respond
+ response.send(eventsResponse)
+ return nil
+ }
+}
diff --git a/Sources/CoreLockWebServer/Information.swift b/Sources/CoreLockWebServer/Information.swift
new file mode 100644
index 00000000..6a290f90
--- /dev/null
+++ b/Sources/CoreLockWebServer/Information.swift
@@ -0,0 +1,36 @@
+//
+// File.swift
+//
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+//
+
+import Foundation
+import CoreLock
+import Kitura
+import KituraContracts
+
+internal extension LockWebServer {
+
+ func addLockInformationRoute() {
+
+ router.get("/info", handler: getLockInformation)
+ }
+
+ private func getLockInformation(completion: (LockNetService.LockInformation?, RequestError?) -> ()) {
+
+ log?("Requested lock information")
+
+ let status: LockStatus = authorization.isEmpty ? .setup : .unlock
+
+ let identifier = configurationStore.configuration.identifier
+
+ let information = LockNetService.LockInformation(identifier: identifier,
+ buildVersion: .current,
+ version: .current,
+ status: status,
+ unlockActions: [UnlockAction.default])
+
+ completion(information, nil)
+ }
+}
diff --git a/Sources/CoreLockWebServer/Permissions.swift b/Sources/CoreLockWebServer/Permissions.swift
new file mode 100644
index 00000000..e00df4f7
--- /dev/null
+++ b/Sources/CoreLockWebServer/Permissions.swift
@@ -0,0 +1,177 @@
+//
+// Permissions.swift
+//
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+//
+
+import Foundation
+import CoreLock
+import Kitura
+
+internal extension LockWebServer {
+
+ func addPermissionsRoute() {
+
+ router.post("/key") { [unowned self] (request, response, next) in
+ do {
+ let statusCode = try self.createKey(request: request, response: response)
+ _ = response.send(status: statusCode)
+ }
+ catch {
+ self.log?("\(request.urlURL.path) Internal server error. \(error.localizedDescription)")
+ dump(error)
+ _ = response.send(status: .internalServerError)
+ }
+ try response.end()
+ }
+
+ router.get("/key") { [unowned self] (request, response, next) in
+ do {
+ if let statusCode = try self.getKeys(request: request, response: response) {
+ _ = response.send(status: statusCode)
+ }
+ }
+ catch {
+ self.log?("\(request.urlURL.path) Internal server error. \(error.localizedDescription)")
+ dump(error)
+ _ = response.send(status: .internalServerError)
+ }
+ try response.end()
+ }
+
+ router.delete("/key/:id") { [unowned self] (request, response, next) in
+ guard let identifierString = request.parameters["id"],
+ let identifier = UUID(uuidString: identifierString) else {
+ _ = response.send(status: .notFound)
+ try response.end()
+ return
+ }
+ do {
+ let statusCode = try self.deleteKey(identifier, type: .key, request: request, response: response)
+ _ = response.send(status: statusCode)
+ }
+ catch {
+ self.log?("\(request.urlURL.path) Internal server error. \(error.localizedDescription)")
+ dump(error)
+ _ = response.send(status: .internalServerError)
+ }
+ try response.end()
+ }
+
+ router.delete("/newKey/:id") { [unowned self] (request, response, next) in
+ guard let identifierString = request.parameters["id"],
+ let identifier = UUID(uuidString: identifierString) else {
+ _ = response.send(status: .notFound)
+ try response.end()
+ return
+ }
+ do {
+ let statusCode = try self.deleteKey(identifier, type: .newKey, request: request, response: response)
+ _ = response.send(status: statusCode)
+ }
+ catch {
+ self.log?("\(request.urlURL.path) Internal server error. \(error.localizedDescription)")
+ dump(error)
+ _ = response.send(status: .internalServerError)
+ }
+ try response.end()
+ }
+ }
+
+ private func getKeys(request: RouterRequest, response: RouterResponse) throws -> HTTPStatusCode? {
+
+ // authenticate
+ guard let (key, secret) = try authenticate(request: request) else {
+ return .unauthorized
+ }
+
+ // enforce permission
+ guard key.permission.isAdministrator else {
+ log?("Only lock owner and admins can view list of keys")
+ return .forbidden
+ }
+
+ log?("Key \(key.identifier) \(key.name) requested keys list")
+
+ // get list
+ let list = authorization.list
+
+ // encrypt
+ let keysResponse = try KeysResponse(encrypt: list, with: secret, encoder: jsonEncoder)
+
+ // respond
+ response.send(keysResponse)
+ return nil
+ }
+
+ private func createKey(request: RouterRequest, response: RouterResponse) throws -> HTTPStatusCode {
+
+ // authenticate
+ guard let (key, secret) = try authenticate(request: request) else {
+ return .unauthorized
+ }
+
+ // enforce permission
+ guard key.permission.isAdministrator else {
+ log?("Only lock owner and admins can view list of keys")
+ return .forbidden
+ }
+
+ // parse body
+ var data = Data()
+ _ = try request.read(into: &data)
+ let encryptedData = try jsonDecoder.decode(LockNetService.EncryptedData.self, from: data)
+ let newKeyRequest = try CreateNewKeyNetServiceRequest.decrypt(
+ encryptedData,
+ with: secret,
+ decoder: jsonDecoder
+ )
+ let newKey = NewKey(request: newKeyRequest)
+
+ log?("Create \(newKey.permission.type) key \"\(newKey.name)\" \(newKey.identifier)")
+
+ try self.authorization.add(newKey, secret: newKeyRequest.secret)
+
+ try events.save(.createNewKey(.init(key: key.identifier, newKey: newKey.identifier)))
+
+ lockChanged?()
+
+ return .created
+ }
+
+ private func deleteKey(_ identifier: UUID, type: KeyType, request: RouterRequest, response: RouterResponse) throws -> HTTPStatusCode {
+
+ // authenticate
+ guard let (key, _) = try authenticate(request: request) else {
+ return .unauthorized
+ }
+
+ // enforce permission
+ guard key.permission.isAdministrator else {
+ log?("Only lock owner and admins can remove keys")
+ return .forbidden
+ }
+
+ switch type {
+ case .key:
+ guard let (removeKey, _) = try authorization.key(for: identifier)
+ else { log?("Key \(identifier) does not exist"); return .notFound }
+ assert(removeKey.identifier == identifier)
+ try authorization.removeKey(removeKey.identifier)
+ case .newKey:
+ guard let (removeKey, _) = try authorization.newKey(for: identifier)
+ else { log?("New Key \(identifier) does not exist"); return .notFound }
+ assert(removeKey.identifier == identifier)
+ try authorization.removeNewKey(removeKey.identifier)
+ }
+
+ log?("Key \(key.identifier) \(key.name) removed \(type) \(identifier)")
+
+ try events.save(.removeKey(.init(key: key.identifier, removedKey: identifier, type: type)))
+
+ lockChanged?()
+
+ return .OK
+ }
+}
diff --git a/Sources/CoreLockWebServer/Server.swift b/Sources/CoreLockWebServer/Server.swift
new file mode 100644
index 00000000..63ac31f1
--- /dev/null
+++ b/Sources/CoreLockWebServer/Server.swift
@@ -0,0 +1,82 @@
+//
+// Server.swift
+//
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+//
+
+import Foundation
+import Dispatch
+import CoreLock
+import Kitura
+import KituraNet
+
+#if os(Linux)
+import NetService
+#endif
+
+/// Lock Web Server
+public final class LockWebServer {
+
+ // MARK: - Properties
+
+ public var log: ((String) -> ())?
+
+ public var port: Int = 8080
+
+ public var hardware: LockHardware = .empty
+
+ public var configurationStore: LockConfigurationStore = InMemoryLockConfigurationStore()
+
+ public var authorization: LockAuthorizationStore = InMemoryLockAuthorization()
+
+ public var authorizationTimeout: TimeInterval = 10.0
+
+ public var events: LockEventStore = InMemoryLockEvents()
+
+ public var lockChanged: (() -> ())?
+
+ public var update: (() -> ())?
+
+ internal lazy var jsonEncoder = JSONEncoder()
+
+ internal lazy var jsonDecoder = JSONDecoder()
+
+ internal let router = Router()
+
+ private var httpServer: HTTPServer?
+
+ private var netService: NetService?
+
+ // MARK: - Initialization
+
+ public init() {
+ setupRouter()
+ }
+
+ // MARK: - Methods
+
+ private func setupRouter() {
+
+ addLockInformationRoute()
+ addPermissionsRoute()
+ addEventsRoute()
+ addUpdateRoute()
+ }
+
+ public func run() {
+
+ // Bonjour
+ netService = NetService(domain: "local.",
+ type: LockNetService.serviceType,
+ name: configurationStore.configuration.identifier.uuidString,
+ port: Int32(port))
+
+ netService?.publish(options: [])
+
+ // Kiture
+ httpServer = Kitura.addHTTPServer(onPort: port, with: router)
+ log?("Started HTTP Server on port \(port)")
+ Kitura.run()
+ }
+}
diff --git a/Sources/CoreLockWebServer/Update.swift b/Sources/CoreLockWebServer/Update.swift
new file mode 100644
index 00000000..41c3e481
--- /dev/null
+++ b/Sources/CoreLockWebServer/Update.swift
@@ -0,0 +1,49 @@
+//
+// Update.swift
+// CoreLockWebServer
+//
+// Created by Alsey Coleman Miller on 10/19/19.
+//
+
+import Foundation
+import CoreLock
+import Kitura
+
+internal extension LockWebServer {
+
+ func addUpdateRoute() {
+
+ router.post("/update") { [unowned self] (request, response, next) in
+ do {
+ let statusCode = try self.update(request: request, response: response)
+ _ = response.send(status: statusCode)
+ }
+ catch {
+ self.log?("\(request.urlURL.path) Internal server error. \(error.localizedDescription)")
+ dump(error)
+ _ = response.send(status: .internalServerError)
+ }
+ try response.end()
+ }
+ }
+
+ private func update(request: RouterRequest, response: RouterResponse) throws -> HTTPStatusCode {
+
+ // authenticate
+ guard let (key, _) = try authenticate(request: request) else {
+ return .unauthorized
+ }
+
+ // enforce permission
+ guard key.permission.isAdministrator else {
+ log?("Only lock owner and admins can update")
+ return .forbidden
+ }
+
+ log?("Key \(key.identifier) \(key.name) requested software update")
+
+ update?()
+
+ return .OK
+ }
+}
diff --git a/Sources/lockd/LockEventsFile.swift b/Sources/lockd/LockEventsFile.swift
index 68b4380d..45c422f8 100644
--- a/Sources/lockd/LockEventsFile.swift
+++ b/Sources/lockd/LockEventsFile.swift
@@ -9,8 +9,8 @@ import Foundation
import CoreLock
import CoreLockGATTServer
-/// Stores the lock evets in a JSON file.
-public struct LockEventsFile: LockEventStore {
+/// Stores the lock events in a JSON file.
+public final class LockEventsFile: LockEventStore {
typealias Events = [LockEvent]
@@ -29,7 +29,7 @@ public struct LockEventsFile: LockEventStore {
// MARK: - Methods
- public func fetch(_ fetchRequest: ListEventsCharacteristic.FetchRequest) throws -> [LockEvent] {
+ public func fetch(_ fetchRequest: LockEvent.FetchRequest) throws -> [LockEvent] {
return try load { $0.fetch(fetchRequest) }
}
diff --git a/Sources/lockd/main.swift b/Sources/lockd/main.swift
index f43bc02b..c8890e5c 100644
--- a/Sources/lockd/main.swift
+++ b/Sources/lockd/main.swift
@@ -22,14 +22,17 @@ import Bluetooth
import GATT
import CoreLock
import CoreLockGATTServer
+import CoreLockWebServer
#if os(Linux)
typealias LinuxPeripheral = GATTPeripheral
-var controller: LockController?
+var controller: LockGATTController?
#elseif os(macOS)
-var controller: LockController?
+var controller: LockGATTController?
#endif
+let webServer = LockWebServer()
+
var gpio: LockGPIOController?
var advertiseTimer: Timer?
let backgroundQueue = DispatchQueue(label: "com.colemancda.lockd")
@@ -78,36 +81,54 @@ func run() throws {
while peripheral.state != .poweredOn { sleep(1) }
#endif
+ // load files
let configurationStore = try LockConfigurationFile(
url: URL(fileURLWithPath: "/opt/colemancda/lockd/config.json")
)
+ let authorization = try AuthorizationStoreFile(
+ url: URL(fileURLWithPath: "/opt/colemancda/lockd/data.json")
+ )
+ let events = LockEventsFile(
+ url: URL(fileURLWithPath: "/opt/colemancda/lockd/events.json")
+ )
+ let setupSecret = try LockSetupSecretFile(
+ createdAt: URL(fileURLWithPath: "/opt/colemancda/lockd/sharedSecret")
+ )
let lockIdentifier = configurationStore.configuration.identifier
print("🔒 Lock \(lockIdentifier)")
- // Intialize Smart Connect BLE Controller
- controller = try LockController(peripheral: peripheral)
-
- // load files
+ // configure Smart Connect BLE Controller
+ controller = try LockGATTController(peripheral: peripheral)
controller?.lockServiceController.configurationStore = configurationStore
- controller?.lockServiceController.authorization = try AuthorizationStoreFile(
- url: URL(fileURLWithPath: "/opt/colemancda/lockd/data.json")
- )
- controller?.lockServiceController.setupSecret = try LockSetupSecretFile(
- createdAt: URL(fileURLWithPath: "/opt/colemancda/lockd/sharedSecret")
- ).sharedSecret
- controller?.lockServiceController.events = LockEventsFile(
- url: URL(fileURLWithPath: "/opt/colemancda/lockd/events.json")
- )
+ controller?.lockServiceController.authorization = authorization
+ controller?.lockServiceController.events = events
+ controller?.lockServiceController.setupSecret = setupSecret.sharedSecret
+
+ // configure web server
+ webServer.authorization = authorization
+ webServer.configurationStore = configurationStore
+ webServer.events = events
+ webServer.log = { print("Web Server:", $0) }
+ webServer.update = {
+ DispatchQueue.global(qos: .userInitiated).async {
+ #if os(Linux)
+ system("/opt/colemancda/lockd/update.sh")
+ #else
+ print("Simulate software update")
+ #endif
+ }
+ }
- // setup controller
+ // load hardware configuration
if let hardware = try? JSONDecoder().decode(LockHardware.self, from: URL(fileURLWithPath: "/opt/colemancda/lockd/hardware.json")) {
print("Running on hardware:")
dump(hardware)
controller?.hardware = hardware
+ webServer.hardware = hardware
// load GPIO
if let gpioController = hardware.gpioController() {
@@ -130,7 +151,7 @@ func run() throws {
try hostController.writeLocalName("Lock")
// change advertisment for notifications
- controller?.lockServiceController.lockChanged = {
+ func lockChanged() {
backgroundQueue.asyncAfter(deadline: .now() + 2) {
do {
try hostController.setNotificationAdvertisement(rssi: 30) // FIXME: RSSI
@@ -144,6 +165,9 @@ func run() throws {
}
}
+ controller?.lockServiceController.lockChanged = lockChanged
+ webServer.lockChanged = lockChanged
+
// make sure the device is always discoverable
if #available(macOS 10.12, *) {
advertiseTimer = .scheduledTimer(withTimeInterval: 30, repeats: true) { _ in
@@ -158,6 +182,11 @@ func run() throws {
}
}
+ // start web server
+ DispatchQueue.global(qos: .userInitiated).async {
+ webServer.run()
+ }
+
// run main loop
RunLoop.main.run()
}
diff --git a/Xcode/CoreLock.xcodeproj/project.pbxproj b/Xcode/CoreLock.xcodeproj/project.pbxproj
index 983ef46c..778a8194 100644
--- a/Xcode/CoreLock.xcodeproj/project.pbxproj
+++ b/Xcode/CoreLock.xcodeproj/project.pbxproj
@@ -12,10 +12,26 @@
6E0A32D722D8FCFB002EF9DE /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0A32D422D8FCFB002EF9DE /* URL.swift */; };
6E0A32D822D8FCFB002EF9DE /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0A32D422D8FCFB002EF9DE /* URL.swift */; };
6E0A32DA22D91E1D002EF9DE /* URLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0A32D922D91E1D002EF9DE /* URLTests.swift */; };
+ 6E23152023576F7E00C363EC /* LockNetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E23151F23576F7E00C363EC /* LockNetService.swift */; };
+ 6E23152123576F7E00C363EC /* LockNetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E23151F23576F7E00C363EC /* LockNetService.swift */; };
+ 6E23152223576F7E00C363EC /* LockNetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E23151F23576F7E00C363EC /* LockNetService.swift */; };
+ 6E23152323576F7E00C363EC /* LockNetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E23151F23576F7E00C363EC /* LockNetService.swift */; };
+ 6E4729EB235AE7B4007CBC07 /* UpdateRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729EA235AE7B3007CBC07 /* UpdateRequest.swift */; };
+ 6E4729EC235AE7B4007CBC07 /* UpdateRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729EA235AE7B3007CBC07 /* UpdateRequest.swift */; };
+ 6E4729ED235AE7B4007CBC07 /* UpdateRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729EA235AE7B3007CBC07 /* UpdateRequest.swift */; };
+ 6E4729EE235AE7B4007CBC07 /* UpdateRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729EA235AE7B3007CBC07 /* UpdateRequest.swift */; };
+ 6E4729FE235B768E007CBC07 /* DeleteKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729FD235B768E007CBC07 /* DeleteKeyRequest.swift */; };
+ 6E4729FF235B768F007CBC07 /* DeleteKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729FD235B768E007CBC07 /* DeleteKeyRequest.swift */; };
+ 6E472A00235B768F007CBC07 /* DeleteKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729FD235B768E007CBC07 /* DeleteKeyRequest.swift */; };
+ 6E472A01235B768F007CBC07 /* DeleteKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729FD235B768E007CBC07 /* DeleteKeyRequest.swift */; };
6E621244234721FF007D49BF /* Bluetooth in Frameworks */ = {isa = PBXBuildFile; productRef = 6E621243234721FF007D49BF /* Bluetooth */; };
6E62124823472217007D49BF /* Bluetooth in Frameworks */ = {isa = PBXBuildFile; productRef = 6E62124723472217007D49BF /* Bluetooth */; };
6E621266234723ED007D49BF /* Bluetooth in Frameworks */ = {isa = PBXBuildFile; productRef = 6E621265234723ED007D49BF /* Bluetooth */; };
6E62126A23472400007D49BF /* Bluetooth in Frameworks */ = {isa = PBXBuildFile; productRef = 6E62126923472400007D49BF /* Bluetooth */; };
+ 6E87864B2357ACAB008624C1 /* KeysRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E87864A2357ACAB008624C1 /* KeysRequest.swift */; };
+ 6E87864C2357ACAB008624C1 /* KeysRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E87864A2357ACAB008624C1 /* KeysRequest.swift */; };
+ 6E87864D2357ACAB008624C1 /* KeysRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E87864A2357ACAB008624C1 /* KeysRequest.swift */; };
+ 6E87864E2357ACAB008624C1 /* KeysRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E87864A2357ACAB008624C1 /* KeysRequest.swift */; };
6E93D354231C624300119F65 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D351231C623B00119F65 /* Notification.swift */; };
6E93D355231C624300119F65 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D351231C623B00119F65 /* Notification.swift */; };
6E93D356231C624300119F65 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D351231C623B00119F65 /* Notification.swift */; };
@@ -40,6 +56,30 @@
6EACDA9A231B353A000CF82A /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EACDA98231B3539000CF82A /* Event.swift */; };
6EACDA9B231B353A000CF82A /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EACDA98231B3539000CF82A /* Event.swift */; };
6EACDA9C231B353A000CF82A /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EACDA98231B3539000CF82A /* Event.swift */; };
+ 6EBA70F6235790CB0005BEB7 /* Bonjour in Frameworks */ = {isa = PBXBuildFile; productRef = 6EBA70F5235790CB0005BEB7 /* Bonjour */; };
+ 6EBA70F8235790D90005BEB7 /* Bonjour in Frameworks */ = {isa = PBXBuildFile; productRef = 6EBA70F7235790D90005BEB7 /* Bonjour */; };
+ 6EBA70FA235790E10005BEB7 /* Bonjour in Frameworks */ = {isa = PBXBuildFile; productRef = 6EBA70F9235790E10005BEB7 /* Bonjour */; };
+ 6EBA70FC235790E70005BEB7 /* Bonjour in Frameworks */ = {isa = PBXBuildFile; productRef = 6EBA70FB235790E70005BEB7 /* Bonjour */; };
+ 6EBA70FE2357A7040005BEB7 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA70FD2357A7040005BEB7 /* URLSession.swift */; };
+ 6EBA70FF2357A7040005BEB7 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA70FD2357A7040005BEB7 /* URLSession.swift */; };
+ 6EBA71002357A7040005BEB7 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA70FD2357A7040005BEB7 /* URLSession.swift */; };
+ 6EBA71012357A7040005BEB7 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA70FD2357A7040005BEB7 /* URLSession.swift */; };
+ 6EBA71032357A7B40005BEB7 /* LockInformationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71022357A7B40005BEB7 /* LockInformationRequest.swift */; };
+ 6EBA71042357A7B40005BEB7 /* LockInformationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71022357A7B40005BEB7 /* LockInformationRequest.swift */; };
+ 6EBA71052357A7B40005BEB7 /* LockInformationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71022357A7B40005BEB7 /* LockInformationRequest.swift */; };
+ 6EBA71062357A7B40005BEB7 /* LockInformationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71022357A7B40005BEB7 /* LockInformationRequest.swift */; };
+ 6EBA71082357A9870005BEB7 /* LockInformationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71072357A9870005BEB7 /* LockInformationResponse.swift */; };
+ 6EBA71092357A9870005BEB7 /* LockInformationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71072357A9870005BEB7 /* LockInformationResponse.swift */; };
+ 6EBA710A2357A9870005BEB7 /* LockInformationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71072357A9870005BEB7 /* LockInformationResponse.swift */; };
+ 6EBA710B2357A9870005BEB7 /* LockInformationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71072357A9870005BEB7 /* LockInformationResponse.swift */; };
+ 6EBDB5CB235D5E3200F38CB0 /* EventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CA235D5E3200F38CB0 /* EventsRequest.swift */; };
+ 6EBDB5CC235D5E3200F38CB0 /* EventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CA235D5E3200F38CB0 /* EventsRequest.swift */; };
+ 6EBDB5CD235D5E3200F38CB0 /* EventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CA235D5E3200F38CB0 /* EventsRequest.swift */; };
+ 6EBDB5CE235D5E3200F38CB0 /* EventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CA235D5E3200F38CB0 /* EventsRequest.swift */; };
+ 6EBDB5D0235D7FF900F38CB0 /* EventsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CF235D7FF900F38CB0 /* EventsResponse.swift */; };
+ 6EBDB5D1235D7FF900F38CB0 /* EventsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CF235D7FF900F38CB0 /* EventsResponse.swift */; };
+ 6EBDB5D2235D7FF900F38CB0 /* EventsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CF235D7FF900F38CB0 /* EventsResponse.swift */; };
+ 6EBDB5D3235D7FF900F38CB0 /* EventsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CF235D7FF900F38CB0 /* EventsResponse.swift */; };
6ED81FE1231662E200B69520 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FE0231662E200B69520 /* CryptoSwift */; };
6ED81FE3231662E200B69520 /* DarwinGATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FE2231662E200B69520 /* DarwinGATT */; };
6ED81FE5231662E200B69520 /* GATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FE4231662E200B69520 /* GATT */; };
@@ -52,6 +92,22 @@
6ED81FF3231662FB00B69520 /* DarwinGATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FF2231662FB00B69520 /* DarwinGATT */; };
6ED81FF5231662FB00B69520 /* GATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FF4231662FB00B69520 /* GATT */; };
6ED81FF7231662FB00B69520 /* TLVCoding in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FF6231662FB00B69520 /* TLVCoding */; };
+ 6EE1D6E12357C639004DD856 /* KeysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E02357C639004DD856 /* KeysResponse.swift */; };
+ 6EE1D6E22357C639004DD856 /* KeysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E02357C639004DD856 /* KeysResponse.swift */; };
+ 6EE1D6E32357C639004DD856 /* KeysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E02357C639004DD856 /* KeysResponse.swift */; };
+ 6EE1D6E42357C639004DD856 /* KeysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E02357C639004DD856 /* KeysResponse.swift */; };
+ 6EE1D6EA2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E92357F1E9004DD856 /* CreateNewKeyRequest.swift */; };
+ 6EE1D6EB2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E92357F1E9004DD856 /* CreateNewKeyRequest.swift */; };
+ 6EE1D6EC2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E92357F1E9004DD856 /* CreateNewKeyRequest.swift */; };
+ 6EE1D6ED2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E92357F1E9004DD856 /* CreateNewKeyRequest.swift */; };
+ 6EE1D6EF2357FAF3004DD856 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6EE2357FAF2004DD856 /* URLSession.swift */; };
+ 6EE1D6F02357FAF3004DD856 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6EE2357FAF2004DD856 /* URLSession.swift */; };
+ 6EE1D6F12357FAF3004DD856 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6EE2357FAF2004DD856 /* URLSession.swift */; };
+ 6EE1D6F22357FAF3004DD856 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6EE2357FAF2004DD856 /* URLSession.swift */; };
+ 6EE1D79F235801BF004DD856 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D79E235801BF004DD856 /* EventStore.swift */; };
+ 6EE1D7A0235801BF004DD856 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D79E235801BF004DD856 /* EventStore.swift */; };
+ 6EE1D7A1235801BF004DD856 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D79E235801BF004DD856 /* EventStore.swift */; };
+ 6EE1D7A2235801BF004DD856 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D79E235801BF004DD856 /* EventStore.swift */; };
6EF1C65522C9AC9F005E9818 /* SetupCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C62E22C9AC9D005E9818 /* SetupCharacteristic.swift */; };
6EF1C65622C9AC9F005E9818 /* SetupCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C62E22C9AC9D005E9818 /* SetupCharacteristic.swift */; };
6EF1C65722C9AC9F005E9818 /* SetupCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C62E22C9AC9D005E9818 /* SetupCharacteristic.swift */; };
@@ -228,15 +284,28 @@
52D6DA0F1BF000BD002C0205 /* CoreLock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreLock.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6E0A32D422D8FCFB002EF9DE /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; };
6E0A32D922D91E1D002EF9DE /* URLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTests.swift; sourceTree = ""; };
+ 6E23151F23576F7E00C363EC /* LockNetService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockNetService.swift; sourceTree = ""; };
+ 6E4729EA235AE7B3007CBC07 /* UpdateRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateRequest.swift; sourceTree = ""; };
+ 6E4729FD235B768E007CBC07 /* DeleteKeyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteKeyRequest.swift; sourceTree = ""; };
6E60FF532121041400787DAA /* CryptoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoTests.swift; sourceTree = ""; };
6E60FF542121041400787DAA /* GATTProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GATTProfileTests.swift; sourceTree = ""; };
6E60FF552121041400787DAA /* LinuxMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinuxMain.swift; sourceTree = ""; };
6E60FF572121041400787DAA /* ServerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTests.swift; sourceTree = ""; };
+ 6E87864A2357ACAB008624C1 /* KeysRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysRequest.swift; sourceTree = ""; };
6E93D351231C623B00119F65 /* Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; };
6E93D352231C623E00119F65 /* ListEventsCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListEventsCharacteristic.swift; sourceTree = ""; };
6E93D353231C624000119F65 /* EventsCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventsCharacteristic.swift; sourceTree = ""; };
6E9794C323301FC400B5C5C9 /* UUID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUID.swift; sourceTree = ""; };
6EACDA98231B3539000CF82A /* Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; };
+ 6EBA70FD2357A7040005BEB7 /* URLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; };
+ 6EBA71022357A7B40005BEB7 /* LockInformationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockInformationRequest.swift; sourceTree = ""; };
+ 6EBA71072357A9870005BEB7 /* LockInformationResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockInformationResponse.swift; sourceTree = ""; };
+ 6EBDB5CA235D5E3200F38CB0 /* EventsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsRequest.swift; sourceTree = ""; };
+ 6EBDB5CF235D7FF900F38CB0 /* EventsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsResponse.swift; sourceTree = ""; };
+ 6EE1D6E02357C639004DD856 /* KeysResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysResponse.swift; sourceTree = ""; };
+ 6EE1D6E92357F1E9004DD856 /* CreateNewKeyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateNewKeyRequest.swift; sourceTree = ""; };
+ 6EE1D6EE2357FAF2004DD856 /* URLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; };
+ 6EE1D79E235801BF004DD856 /* EventStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventStore.swift; sourceTree = ""; };
6EF1C62E22C9AC9D005E9818 /* SetupCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetupCharacteristic.swift; sourceTree = ""; };
6EF1C62F22C9AC9D005E9818 /* LockConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockConfiguration.swift; sourceTree = ""; };
6EF1C63022C9AC9D005E9818 /* EncryptedData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptedData.swift; sourceTree = ""; };
@@ -286,6 +355,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 6EBA70F6235790CB0005BEB7 /* Bonjour in Frameworks */,
6E621244234721FF007D49BF /* Bluetooth in Frameworks */,
6E9C95ED231149A5007C18FE /* GATT in Frameworks */,
6E9C95EA23114962007C18FE /* CryptoSwift in Frameworks */,
@@ -298,6 +368,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 6EBA70FA235790E10005BEB7 /* Bonjour in Frameworks */,
6E621266234723ED007D49BF /* Bluetooth in Frameworks */,
6ED81FED231662F100B69520 /* GATT in Frameworks */,
6ED81FE9231662F100B69520 /* CryptoSwift in Frameworks */,
@@ -310,6 +381,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 6EBA70FC235790E70005BEB7 /* Bonjour in Frameworks */,
6E62126A23472400007D49BF /* Bluetooth in Frameworks */,
6ED81FF5231662FB00B69520 /* GATT in Frameworks */,
6ED81FF1231662FB00B69520 /* CryptoSwift in Frameworks */,
@@ -322,6 +394,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 6EBA70F8235790D90005BEB7 /* Bonjour in Frameworks */,
6E62124823472217007D49BF /* Bluetooth in Frameworks */,
6ED81FE5231662E200B69520 /* GATT in Frameworks */,
6ED81FE1231662E200B69520 /* CryptoSwift in Frameworks */,
@@ -427,6 +500,7 @@
6EF1C64022C9AC9E005E9818 /* Crypto.swift */,
6EF1C65122C9AC9F005E9818 /* DateComponents.swift */,
6EF1C64B22C9AC9E005E9818 /* DeviceManager.swift */,
+ 6EBA70FD2357A7040005BEB7 /* URLSession.swift */,
6EF1C63022C9AC9D005E9818 /* EncryptedData.swift */,
6EF1C64622C9AC9E005E9818 /* GATTError.swift */,
6EF1C64322C9AC9E005E9818 /* GATTProfile.swift */,
@@ -437,11 +511,19 @@
6EF1C63322C9AC9D005E9818 /* ListKeysCharacteristic.swift */,
6E93D353231C624000119F65 /* EventsCharacteristic.swift */,
6E93D352231C623E00119F65 /* ListEventsCharacteristic.swift */,
+ 6EBDB5CA235D5E3200F38CB0 /* EventsRequest.swift */,
+ 6EBDB5CF235D7FF900F38CB0 /* EventsResponse.swift */,
6E93D351231C623B00119F65 /* Notification.swift */,
6EF1C62F22C9AC9D005E9818 /* LockConfiguration.swift */,
6EF1C65222C9AC9F005E9818 /* LockHardware.swift */,
6EF1C64822C9AC9E005E9818 /* LockInformationCharacteristic.swift */,
6EF1C64222C9AC9E005E9818 /* LockModel.swift */,
+ 6E23151F23576F7E00C363EC /* LockNetService.swift */,
+ 6EBA71022357A7B40005BEB7 /* LockInformationRequest.swift */,
+ 6EBA71072357A9870005BEB7 /* LockInformationResponse.swift */,
+ 6E87864A2357ACAB008624C1 /* KeysRequest.swift */,
+ 6EE1D6E02357C639004DD856 /* KeysResponse.swift */,
+ 6EE1D6E92357F1E9004DD856 /* CreateNewKeyRequest.swift */,
6EF1C64422C9AC9E005E9818 /* LockState.swift */,
6EF1C63422C9AC9D005E9818 /* NewKey.swift */,
6EF1C64E22C9AC9F005E9818 /* Permission.swift */,
@@ -456,9 +538,13 @@
6EF1C65322C9AC9F005E9818 /* UnlockAction.swift */,
6EF1C64722C9AC9E005E9818 /* UnlockCharacteristic.swift */,
6EF1C64922C9AC9E005E9818 /* Version.swift */,
- 6E0A32D422D8FCFB002EF9DE /* URL.swift */,
6EACDA98231B3539000CF82A /* Event.swift */,
+ 6EE1D79E235801BF004DD856 /* EventStore.swift */,
+ 6E0A32D422D8FCFB002EF9DE /* URL.swift */,
+ 6EE1D6EE2357FAF2004DD856 /* URLSession.swift */,
6E9794C323301FC400B5C5C9 /* UUID.swift */,
+ 6E4729EA235AE7B3007CBC07 /* UpdateRequest.swift */,
+ 6E4729FD235B768E007CBC07 /* DeleteKeyRequest.swift */,
);
path = CoreLock;
sourceTree = "";
@@ -540,6 +626,7 @@
6E9C95EE231149A5007C18FE /* DarwinGATT */,
6E9C95F1231149F5007C18FE /* TLVCoding */,
6E621243234721FF007D49BF /* Bluetooth */,
+ 6EBA70F5235790CB0005BEB7 /* Bonjour */,
);
productName = SmartLock;
productReference = 52D6D97C1BEFF229002C0205 /* CoreLock.framework */;
@@ -565,6 +652,7 @@
6ED81FEC231662F100B69520 /* GATT */,
6ED81FEE231662F100B69520 /* TLVCoding */,
6E621265234723ED007D49BF /* Bluetooth */,
+ 6EBA70F9235790E10005BEB7 /* Bonjour */,
);
productName = "SmartLock-watchOS";
productReference = 52D6D9E21BEFFF6E002C0205 /* CoreLock.framework */;
@@ -590,6 +678,7 @@
6ED81FF4231662FB00B69520 /* GATT */,
6ED81FF6231662FB00B69520 /* TLVCoding */,
6E62126923472400007D49BF /* Bluetooth */,
+ 6EBA70FB235790E70005BEB7 /* Bonjour */,
);
productName = "SmartLock-tvOS";
productReference = 52D6D9F01BEFFFBE002C0205 /* CoreLock.framework */;
@@ -615,6 +704,7 @@
6ED81FE4231662E200B69520 /* GATT */,
6ED81FE6231662E200B69520 /* TLVCoding */,
6E62124723472217007D49BF /* Bluetooth */,
+ 6EBA70F7235790D90005BEB7 /* Bonjour */,
);
productName = "SmartLock-macOS";
productReference = 52D6DA0F1BF000BD002C0205 /* CoreLock.framework */;
@@ -684,6 +774,7 @@
6E9C95EB231149A5007C18FE /* XCRemoteSwiftPackageReference "GATT" */,
6E9C95F0231149F5007C18FE /* XCRemoteSwiftPackageReference "TLVCoding" */,
6E621242234721FF007D49BF /* XCRemoteSwiftPackageReference "Bluetooth" */,
+ 6EBA70F4235790CB0005BEB7 /* XCRemoteSwiftPackageReference "Bonjour" */,
);
productRefGroup = 52D6D97D1BEFF229002C0205 /* Products */;
projectDirPath = "";
@@ -749,28 +840,40 @@
6EF1C6CD22C9AC9F005E9818 /* TLV.swift in Sources */,
6EF1C6D922C9AC9F005E9818 /* Integer.swift in Sources */,
6E93D358231C624300119F65 /* ListEventsCharacteristic.swift in Sources */,
+ 6E23152023576F7E00C363EC /* LockNetService.swift in Sources */,
6EF1C69922C9AC9F005E9818 /* RemoveKeyCharacteristic.swift in Sources */,
6EF1C6A522C9AC9F005E9818 /* LockModel.swift in Sources */,
+ 6E87864B2357ACAB008624C1 /* KeysRequest.swift in Sources */,
6EF1C69122C9AC9F005E9818 /* ConfirmNewKeyCharacteristic.swift in Sources */,
+ 6EBA70FE2357A7040005BEB7 /* URLSession.swift in Sources */,
+ 6E4729FE235B768E007CBC07 /* DeleteKeyRequest.swift in Sources */,
6EF1C68522C9AC9F005E9818 /* GitCommits.swift in Sources */,
6EF1C6DD22C9AC9F005E9818 /* SmartLockProfile.swift in Sources */,
+ 6EBA71032357A7B40005BEB7 /* LockInformationRequest.swift in Sources */,
6EF1C69D22C9AC9F005E9818 /* Crypto.swift in Sources */,
6EF1C6E522C9AC9F005E9818 /* LockHardware.swift in Sources */,
+ 6EE1D6EF2357FAF3004DD856 /* URLSession.swift in Sources */,
6EF1C6B522C9AC9F005E9818 /* GATTError.swift in Sources */,
6E0A32D522D8FCFB002EF9DE /* URL.swift in Sources */,
6EF1C6A922C9AC9F005E9818 /* GATTProfile.swift in Sources */,
6EF1C6BD22C9AC9F005E9818 /* LockInformationCharacteristic.swift in Sources */,
6EF1C66122C9AC9F005E9818 /* SecureData.swift in Sources */,
+ 6E4729EB235AE7B4007CBC07 /* UpdateRequest.swift in Sources */,
6EF1C67122C9AC9F005E9818 /* Advertisement.swift in Sources */,
6EF1C6C922C9AC9F005E9818 /* DeviceManager.swift in Sources */,
6EF1C6D122C9AC9F005E9818 /* Authentication.swift in Sources */,
6EF1C66D22C9AC9F005E9818 /* NewKey.swift in Sources */,
+ 6EE1D6EA2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */,
6EF1C67922C9AC9F005E9818 /* KeysCharacteristic.swift in Sources */,
+ 6EBA71082357A9870005BEB7 /* LockInformationResponse.swift in Sources */,
6EF1C69522C9AC9F005E9818 /* Key.swift in Sources */,
6EF1C68922C9AC9F005E9818 /* POSIXTime.swift in Sources */,
6EF1C68122C9AC9F005E9818 /* Central.swift in Sources */,
6EF1C6C522C9AC9F005E9818 /* Status.swift in Sources */,
6EF1C6C122C9AC9F005E9818 /* Version.swift in Sources */,
+ 6EE1D6E12357C639004DD856 /* KeysResponse.swift in Sources */,
+ 6EE1D79F235801BF004DD856 /* EventStore.swift in Sources */,
+ 6EBDB5D0235D7FF900F38CB0 /* EventsResponse.swift in Sources */,
6EACDA99231B353A000CF82A /* Event.swift in Sources */,
6E93D35C231C624300119F65 /* EventsCharacteristic.swift in Sources */,
6EF1C65D22C9AC9F005E9818 /* EncryptedData.swift in Sources */,
@@ -783,6 +886,7 @@
6EF1C66522C9AC9F005E9818 /* AuthenticationError.swift in Sources */,
6EF1C66922C9AC9F005E9818 /* ListKeysCharacteristic.swift in Sources */,
6E93D354231C624300119F65 /* Notification.swift in Sources */,
+ 6EBDB5CB235D5E3200F38CB0 /* EventsRequest.swift in Sources */,
6EF1C6B922C9AC9F005E9818 /* UnlockCharacteristic.swift in Sources */,
6EF1C65922C9AC9F005E9818 /* LockConfiguration.swift in Sources */,
6EF1C6D522C9AC9F005E9818 /* Permission.swift in Sources */,
@@ -801,28 +905,40 @@
6EF1C6CF22C9AC9F005E9818 /* TLV.swift in Sources */,
6EF1C6DB22C9AC9F005E9818 /* Integer.swift in Sources */,
6E93D35A231C624300119F65 /* ListEventsCharacteristic.swift in Sources */,
+ 6E23152223576F7E00C363EC /* LockNetService.swift in Sources */,
6EF1C69B22C9AC9F005E9818 /* RemoveKeyCharacteristic.swift in Sources */,
6EF1C6A722C9AC9F005E9818 /* LockModel.swift in Sources */,
+ 6E87864D2357ACAB008624C1 /* KeysRequest.swift in Sources */,
6EF1C69322C9AC9F005E9818 /* ConfirmNewKeyCharacteristic.swift in Sources */,
+ 6EBA71002357A7040005BEB7 /* URLSession.swift in Sources */,
+ 6E472A00235B768F007CBC07 /* DeleteKeyRequest.swift in Sources */,
6EF1C68722C9AC9F005E9818 /* GitCommits.swift in Sources */,
6EF1C6DF22C9AC9F005E9818 /* SmartLockProfile.swift in Sources */,
+ 6EBA71052357A7B40005BEB7 /* LockInformationRequest.swift in Sources */,
6EF1C69F22C9AC9F005E9818 /* Crypto.swift in Sources */,
6EF1C6E722C9AC9F005E9818 /* LockHardware.swift in Sources */,
+ 6EE1D6F12357FAF3004DD856 /* URLSession.swift in Sources */,
6EF1C6B722C9AC9F005E9818 /* GATTError.swift in Sources */,
6E0A32D722D8FCFB002EF9DE /* URL.swift in Sources */,
6EF1C6AB22C9AC9F005E9818 /* GATTProfile.swift in Sources */,
6EF1C6BF22C9AC9F005E9818 /* LockInformationCharacteristic.swift in Sources */,
6EF1C66322C9AC9F005E9818 /* SecureData.swift in Sources */,
+ 6E4729ED235AE7B4007CBC07 /* UpdateRequest.swift in Sources */,
6EF1C67322C9AC9F005E9818 /* Advertisement.swift in Sources */,
6EF1C6CB22C9AC9F005E9818 /* DeviceManager.swift in Sources */,
6EF1C6D322C9AC9F005E9818 /* Authentication.swift in Sources */,
6EF1C66F22C9AC9F005E9818 /* NewKey.swift in Sources */,
+ 6EE1D6EC2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */,
6EF1C67B22C9AC9F005E9818 /* KeysCharacteristic.swift in Sources */,
+ 6EBA710A2357A9870005BEB7 /* LockInformationResponse.swift in Sources */,
6EF1C69722C9AC9F005E9818 /* Key.swift in Sources */,
6EF1C68B22C9AC9F005E9818 /* POSIXTime.swift in Sources */,
6EF1C68322C9AC9F005E9818 /* Central.swift in Sources */,
6EF1C6C722C9AC9F005E9818 /* Status.swift in Sources */,
6EF1C6C322C9AC9F005E9818 /* Version.swift in Sources */,
+ 6EE1D6E32357C639004DD856 /* KeysResponse.swift in Sources */,
+ 6EE1D7A1235801BF004DD856 /* EventStore.swift in Sources */,
+ 6EBDB5D2235D7FF900F38CB0 /* EventsResponse.swift in Sources */,
6EACDA9B231B353A000CF82A /* Event.swift in Sources */,
6E93D35E231C624300119F65 /* EventsCharacteristic.swift in Sources */,
6EF1C65F22C9AC9F005E9818 /* EncryptedData.swift in Sources */,
@@ -835,6 +951,7 @@
6EF1C66722C9AC9F005E9818 /* AuthenticationError.swift in Sources */,
6EF1C66B22C9AC9F005E9818 /* ListKeysCharacteristic.swift in Sources */,
6E93D356231C624300119F65 /* Notification.swift in Sources */,
+ 6EBDB5CD235D5E3200F38CB0 /* EventsRequest.swift in Sources */,
6EF1C6BB22C9AC9F005E9818 /* UnlockCharacteristic.swift in Sources */,
6EF1C65B22C9AC9F005E9818 /* LockConfiguration.swift in Sources */,
6EF1C6D722C9AC9F005E9818 /* Permission.swift in Sources */,
@@ -853,28 +970,40 @@
6EF1C6D022C9AC9F005E9818 /* TLV.swift in Sources */,
6EF1C6DC22C9AC9F005E9818 /* Integer.swift in Sources */,
6E93D35B231C624300119F65 /* ListEventsCharacteristic.swift in Sources */,
+ 6E23152323576F7E00C363EC /* LockNetService.swift in Sources */,
6EF1C69C22C9AC9F005E9818 /* RemoveKeyCharacteristic.swift in Sources */,
6EF1C6A822C9AC9F005E9818 /* LockModel.swift in Sources */,
+ 6E87864E2357ACAB008624C1 /* KeysRequest.swift in Sources */,
6EF1C69422C9AC9F005E9818 /* ConfirmNewKeyCharacteristic.swift in Sources */,
+ 6EBA71012357A7040005BEB7 /* URLSession.swift in Sources */,
+ 6E472A01235B768F007CBC07 /* DeleteKeyRequest.swift in Sources */,
6EF1C68822C9AC9F005E9818 /* GitCommits.swift in Sources */,
6EF1C6E022C9AC9F005E9818 /* SmartLockProfile.swift in Sources */,
+ 6EBA71062357A7B40005BEB7 /* LockInformationRequest.swift in Sources */,
6EF1C6A022C9AC9F005E9818 /* Crypto.swift in Sources */,
6EF1C6E822C9AC9F005E9818 /* LockHardware.swift in Sources */,
+ 6EE1D6F22357FAF3004DD856 /* URLSession.swift in Sources */,
6EF1C6B822C9AC9F005E9818 /* GATTError.swift in Sources */,
6E0A32D822D8FCFB002EF9DE /* URL.swift in Sources */,
6EF1C6AC22C9AC9F005E9818 /* GATTProfile.swift in Sources */,
6EF1C6C022C9AC9F005E9818 /* LockInformationCharacteristic.swift in Sources */,
6EF1C66422C9AC9F005E9818 /* SecureData.swift in Sources */,
+ 6E4729EE235AE7B4007CBC07 /* UpdateRequest.swift in Sources */,
6EF1C67422C9AC9F005E9818 /* Advertisement.swift in Sources */,
6EF1C6CC22C9AC9F005E9818 /* DeviceManager.swift in Sources */,
6EF1C6D422C9AC9F005E9818 /* Authentication.swift in Sources */,
6EF1C67022C9AC9F005E9818 /* NewKey.swift in Sources */,
+ 6EE1D6ED2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */,
6EF1C67C22C9AC9F005E9818 /* KeysCharacteristic.swift in Sources */,
+ 6EBA710B2357A9870005BEB7 /* LockInformationResponse.swift in Sources */,
6EF1C69822C9AC9F005E9818 /* Key.swift in Sources */,
6EF1C68C22C9AC9F005E9818 /* POSIXTime.swift in Sources */,
6EF1C68422C9AC9F005E9818 /* Central.swift in Sources */,
6EF1C6C822C9AC9F005E9818 /* Status.swift in Sources */,
6EF1C6C422C9AC9F005E9818 /* Version.swift in Sources */,
+ 6EE1D6E42357C639004DD856 /* KeysResponse.swift in Sources */,
+ 6EE1D7A2235801BF004DD856 /* EventStore.swift in Sources */,
+ 6EBDB5D3235D7FF900F38CB0 /* EventsResponse.swift in Sources */,
6EACDA9C231B353A000CF82A /* Event.swift in Sources */,
6E93D35F231C624300119F65 /* EventsCharacteristic.swift in Sources */,
6EF1C66022C9AC9F005E9818 /* EncryptedData.swift in Sources */,
@@ -887,6 +1016,7 @@
6EF1C66822C9AC9F005E9818 /* AuthenticationError.swift in Sources */,
6EF1C66C22C9AC9F005E9818 /* ListKeysCharacteristic.swift in Sources */,
6E93D357231C624300119F65 /* Notification.swift in Sources */,
+ 6EBDB5CE235D5E3200F38CB0 /* EventsRequest.swift in Sources */,
6EF1C6BC22C9AC9F005E9818 /* UnlockCharacteristic.swift in Sources */,
6EF1C65C22C9AC9F005E9818 /* LockConfiguration.swift in Sources */,
6EF1C6D822C9AC9F005E9818 /* Permission.swift in Sources */,
@@ -905,28 +1035,40 @@
6EF1C6CE22C9AC9F005E9818 /* TLV.swift in Sources */,
6EF1C6DA22C9AC9F005E9818 /* Integer.swift in Sources */,
6E93D359231C624300119F65 /* ListEventsCharacteristic.swift in Sources */,
+ 6E23152123576F7E00C363EC /* LockNetService.swift in Sources */,
6EF1C69A22C9AC9F005E9818 /* RemoveKeyCharacteristic.swift in Sources */,
6EF1C6A622C9AC9F005E9818 /* LockModel.swift in Sources */,
+ 6E87864C2357ACAB008624C1 /* KeysRequest.swift in Sources */,
6EF1C69222C9AC9F005E9818 /* ConfirmNewKeyCharacteristic.swift in Sources */,
+ 6EBA70FF2357A7040005BEB7 /* URLSession.swift in Sources */,
+ 6E4729FF235B768F007CBC07 /* DeleteKeyRequest.swift in Sources */,
6EF1C68622C9AC9F005E9818 /* GitCommits.swift in Sources */,
6EF1C6DE22C9AC9F005E9818 /* SmartLockProfile.swift in Sources */,
+ 6EBA71042357A7B40005BEB7 /* LockInformationRequest.swift in Sources */,
6EF1C69E22C9AC9F005E9818 /* Crypto.swift in Sources */,
6EF1C6E622C9AC9F005E9818 /* LockHardware.swift in Sources */,
+ 6EE1D6F02357FAF3004DD856 /* URLSession.swift in Sources */,
6EF1C6B622C9AC9F005E9818 /* GATTError.swift in Sources */,
6E0A32D622D8FCFB002EF9DE /* URL.swift in Sources */,
6EF1C6AA22C9AC9F005E9818 /* GATTProfile.swift in Sources */,
6EF1C6BE22C9AC9F005E9818 /* LockInformationCharacteristic.swift in Sources */,
6EF1C66222C9AC9F005E9818 /* SecureData.swift in Sources */,
+ 6E4729EC235AE7B4007CBC07 /* UpdateRequest.swift in Sources */,
6EF1C67222C9AC9F005E9818 /* Advertisement.swift in Sources */,
6EF1C6CA22C9AC9F005E9818 /* DeviceManager.swift in Sources */,
6EF1C6D222C9AC9F005E9818 /* Authentication.swift in Sources */,
6EF1C66E22C9AC9F005E9818 /* NewKey.swift in Sources */,
+ 6EE1D6EB2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */,
6EF1C67A22C9AC9F005E9818 /* KeysCharacteristic.swift in Sources */,
+ 6EBA71092357A9870005BEB7 /* LockInformationResponse.swift in Sources */,
6EF1C69622C9AC9F005E9818 /* Key.swift in Sources */,
6EF1C68A22C9AC9F005E9818 /* POSIXTime.swift in Sources */,
6EF1C68222C9AC9F005E9818 /* Central.swift in Sources */,
6EF1C6C622C9AC9F005E9818 /* Status.swift in Sources */,
6EF1C6C222C9AC9F005E9818 /* Version.swift in Sources */,
+ 6EE1D6E22357C639004DD856 /* KeysResponse.swift in Sources */,
+ 6EE1D7A0235801BF004DD856 /* EventStore.swift in Sources */,
+ 6EBDB5D1235D7FF900F38CB0 /* EventsResponse.swift in Sources */,
6EACDA9A231B353A000CF82A /* Event.swift in Sources */,
6E93D35D231C624300119F65 /* EventsCharacteristic.swift in Sources */,
6EF1C65E22C9AC9F005E9818 /* EncryptedData.swift in Sources */,
@@ -939,6 +1081,7 @@
6EF1C66622C9AC9F005E9818 /* AuthenticationError.swift in Sources */,
6EF1C66A22C9AC9F005E9818 /* ListKeysCharacteristic.swift in Sources */,
6E93D355231C624300119F65 /* Notification.swift in Sources */,
+ 6EBDB5CC235D5E3200F38CB0 /* EventsRequest.swift in Sources */,
6EF1C6BA22C9AC9F005E9818 /* UnlockCharacteristic.swift in Sources */,
6EF1C65A22C9AC9F005E9818 /* LockConfiguration.swift in Sources */,
6EF1C6D622C9AC9F005E9818 /* Permission.swift in Sources */,
@@ -1407,6 +1550,14 @@
kind = branch;
};
};
+ 6EBA70F4235790CB0005BEB7 /* XCRemoteSwiftPackageReference "Bonjour" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "git@github.com:PureSwift/Bonjour.git";
+ requirement = {
+ branch = master;
+ kind = branch;
+ };
+ };
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@@ -1450,6 +1601,26 @@
package = 6E9C95F0231149F5007C18FE /* XCRemoteSwiftPackageReference "TLVCoding" */;
productName = TLVCoding;
};
+ 6EBA70F5235790CB0005BEB7 /* Bonjour */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 6EBA70F4235790CB0005BEB7 /* XCRemoteSwiftPackageReference "Bonjour" */;
+ productName = Bonjour;
+ };
+ 6EBA70F7235790D90005BEB7 /* Bonjour */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 6EBA70F4235790CB0005BEB7 /* XCRemoteSwiftPackageReference "Bonjour" */;
+ productName = Bonjour;
+ };
+ 6EBA70F9235790E10005BEB7 /* Bonjour */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 6EBA70F4235790CB0005BEB7 /* XCRemoteSwiftPackageReference "Bonjour" */;
+ productName = Bonjour;
+ };
+ 6EBA70FB235790E70005BEB7 /* Bonjour */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 6EBA70F4235790CB0005BEB7 /* XCRemoteSwiftPackageReference "Bonjour" */;
+ productName = Bonjour;
+ };
6ED81FE0231662E200B69520 /* CryptoSwift */ = {
isa = XCSwiftPackageProductDependency;
package = 6E9C95E823114962007C18FE /* XCRemoteSwiftPackageReference "CryptoSwift" */;
diff --git a/iOS/LockKit/Base.lproj/Intents.intentdefinition b/iOS/LockKit/Base.lproj/Intents.intentdefinition
index 22882e47..f2beda58 100644
--- a/iOS/LockKit/Base.lproj/Intents.intentdefinition
+++ b/iOS/LockKit/Base.lproj/Intents.intentdefinition
@@ -9,11 +9,11 @@
INIntentDefinitionNamespace
9Yc029
INIntentDefinitionSystemVersion
- 18G87
+ 19A602
INIntentDefinitionToolsBuildVersion
- 11M392q
+ 11A1027
INIntentDefinitionToolsVersion
- 11.0
+ 11.1
INIntents
@@ -38,7 +38,7 @@
INIntentParameterCombinationSupportsBackgroundExecution
INIntentParameterCombinationTitle
- Unlock `${lock}`
+ Unlock ${lock}
INIntentParameterCombinationTitleID
O5ezH4
INIntentParameterCombinationUpdatesLinked
@@ -53,10 +53,14 @@
INIntentParameterCombinationIsPrimary
+ INIntentParameterCombinationSubtitle
+ Unlock the specified lock.
+ INIntentParameterCombinationSubtitleID
+ bJj0q0
INIntentParameterCombinationSupportsBackgroundExecution
INIntentParameterCombinationTitle
- Unlock `${lock}`
+ Unlock ${lock}
INIntentParameterCombinationTitleID
dAVo2n
@@ -94,7 +98,7 @@
INIntentParameterPromptDialogCustom
INIntentParameterPromptDialogFormatString
- You have ${count} locks matching ‘${lock}’.
+ You have ${count} locks matching ${lock}.
INIntentParameterPromptDialogFormatStringID
FwuKZ6
INIntentParameterPromptDialogType
@@ -112,7 +116,7 @@
INIntentParameterPromptDialogCustom
INIntentParameterPromptDialogFormatString
- Just to confirm, you want to unlock ‘${lock}’?
+ Just to confirm, you want to unlock ${lock}?
INIntentParameterPromptDialogFormatStringID
FhxgkI
INIntentParameterPromptDialogType
@@ -135,7 +139,7 @@
INIntentParameterUnsupportedReasonCustom
INIntentParameterUnsupportedReasonFormatString
- `${displayName}` is not in range. Please move closer to ${displayName}.
+ ${displayName} is not in range. Please move closer to ${displayName}.
INIntentParameterUnsupportedReasonFormatStringID
bvi2l8
@@ -145,7 +149,7 @@
INIntentParameterUnsupportedReasonCustom
INIntentParameterUnsupportedReasonFormatString
- You do not have a key for `${displayName}`.
+ You do not have a key for ${displayName}.
INIntentParameterUnsupportedReasonFormatStringID
vARXDT
@@ -155,7 +159,7 @@
INIntentParameterUnsupportedReasonCustom
INIntentParameterUnsupportedReasonFormatString
- You can only unlock `${displayName}` during the specified schedule.
+ You can only unlock ${displayName} during the specified schedule.
INIntentParameterUnsupportedReasonFormatStringID
wkobUh
@@ -168,7 +172,7 @@
INIntentResponseCodeConciseFormatString
- Unlocked ‘${lock}‘.
+ Unlocked ${lock}.
INIntentResponseCodeConciseFormatStringID
claxan
INIntentResponseCodeFormatString
diff --git a/iOS/LockKit/Controller/ContactsViewController.swift b/iOS/LockKit/Controller/ContactsViewController.swift
index dae5457a..d2cad165 100644
--- a/iOS/LockKit/Controller/ContactsViewController.swift
+++ b/iOS/LockKit/Controller/ContactsViewController.swift
@@ -171,9 +171,8 @@ public final class ContactsViewController: TableViewController {
let image: UIImage
if let contactImage = managedObject.image.flatMap({ UIImage(data: $0) }) {
image = contactImage
- } else if #available(iOS 13, *),
- let systemImage = UIImage(systemName: "person.crop.circle.fill") {
- image = systemImage
+ } else if #available(iOS 13, *) {
+ image = UIImage(systemSymbol: .personCropCircleFill)
} else {
image = UIImage(permission: .admin)
}
diff --git a/iOS/LockKit/Controller/Extensions/ContextMenu.swift b/iOS/LockKit/Controller/Extensions/ContextMenu.swift
index e7acfbf5..950e17af 100644
--- a/iOS/LockKit/Controller/Extensions/ContextMenu.swift
+++ b/iOS/LockKit/Controller/Extensions/ContextMenu.swift
@@ -8,6 +8,8 @@
import Foundation
import UIKit
+import IntentsUI
+import SFSafeSymbols
@available(iOSApplicationExtension 13.0, *)
public extension UIViewController {
@@ -24,22 +26,12 @@ public extension UIViewController {
children: []
)
}
-
- let rename = UIAction(title: R.string.contextMenu.itemRename(), image: UIImage(systemName: "square.and.pencil")) { [weak self] (action) in
- let alert = RenameActivity.viewController(for: lock) { _ in }
- self?.present(alert, animated: true, completion: nil)
- }
-
- let delete = UIAction(title: R.string.contextMenu.itemDelete(), image: UIImage(systemName: "trash"), attributes: .destructive) { [weak self] (action) in
- let alert = DeleteLockActivity.viewController(for: lock) { _ in }
- self?.present(alert, animated: true, completion: nil)
- }
-
+
var actions = [UIAction]()
if cache.key.permission.isAdministrator {
- let share = UIAction(title: R.string.contextMenu.itemShareKey(), image: UIImage(systemName: "square.and.arrow.up")) { [weak self] (action) in
+ let share = UIAction(title: R.string.contextMenu.itemShareKey(), image: UIImage(systemSymbol: .squareAndArrowUp)) { [weak self] (action) in
let viewController = NewKeySelectPermissionViewController.fromStoryboard(with: lock)
viewController.completion = {
guard let (invitation, sender) = $0 else {
@@ -54,7 +46,7 @@ public extension UIViewController {
actions.append(share)
- let manageKeys = UIAction(title: R.string.contextMenu.itemManage(), image: UIImage(systemName: "list.bullet")) { [weak self] (action) in
+ let manageKeys = UIAction(title: R.string.contextMenu.itemManage(), image: UIImage(systemSymbol: .listBullet)) { [weak self] (action) in
let viewController = LockPermissionsViewController.fromStoryboard(
with: lock,
completion: { self?.dismiss(animated: true, completion: nil) }
@@ -66,7 +58,44 @@ public extension UIViewController {
actions.append(manageKeys)
}
+ #if canImport(IntentsUI) && !targetEnvironment(macCatalyst)
+ if let delegate = self as? INUIAddVoiceShortcutViewControllerDelegate {
+
+ let siri = UIAction(title: R.string.contextMenu.itemSiriShortcut(), image: UIImage(systemSymbol: .micFill)) { [weak self] (action) in
+ let siriViewController = INUIAddVoiceShortcutViewController(
+ unlock: lock,
+ cache: cache,
+ delegate: delegate
+ )
+ self?.present(siriViewController, animated: true, completion: nil)
+ }
+
+ actions.append(siri)
+ }
+ #endif
+
+ let rename = UIAction(title: R.string.contextMenu.itemRename(), image: UIImage(systemSymbol: .squareAndPencil)) { [weak self] (action) in
+ let alert = RenameActivity.viewController(for: lock) { _ in }
+ self?.present(alert, animated: true, completion: nil)
+ }
+
actions.append(rename)
+
+ if cache.key.permission.isAdministrator,
+ let viewController = self as? (UIViewController & ActivityIndicatorViewController) {
+
+ let update = UIAction(title: R.string.activity.updateActivityTitle(), image: UIImage(systemSymbol: .squareAndArrowDown)) { (action) in
+ viewController.update(lock: lock)
+ }
+
+ actions.append(update)
+ }
+
+ let delete = UIAction(title: R.string.contextMenu.itemDelete(), image: UIImage(systemSymbol: .trash), attributes: .destructive) { [weak self] (action) in
+ let alert = DeleteLockActivity.viewController(for: lock) { _ in }
+ self?.present(alert, animated: true, completion: nil)
+ }
+
actions.append(delete)
return UIMenu(
diff --git a/iOS/LockKit/Controller/Extensions/Update.swift b/iOS/LockKit/Controller/Extensions/Update.swift
new file mode 100644
index 00000000..877b6ecc
--- /dev/null
+++ b/iOS/LockKit/Controller/Extensions/Update.swift
@@ -0,0 +1,46 @@
+//
+// Update.swift
+// LockKit
+//
+// Created by Alsey Coleman Miller on 10/20/19.
+// Copyright © 2019 ColemanCDA. All rights reserved.
+//
+
+import Foundation
+import UIKit
+import CoreLock
+
+public extension ActivityIndicatorViewController where Self: UIViewController {
+
+ func update(lock identifier: UUID) {
+
+ guard let key = Store.shared.credentials(for: identifier)
+ else { assertionFailure(); return }
+
+ let alert = UIAlertController(title: R.string.activity.updateActivityAlertTitle(),
+ message: R.string.activity.updateActivityAlertMessage(),
+ preferredStyle: .alert)
+
+ alert.addAction(UIAlertAction(title: R.string.activity.updateActivityAlertCancel(), style: .cancel, handler: { _ in
+
+ alert.dismiss(animated: true) { }
+ }))
+
+ alert.addAction(UIAlertAction(title: R.string.activity.updateActivityAlertUpdate(), style: .`default`, handler: { [unowned self] _ in
+
+ alert.dismiss(animated: true) { }
+
+ self.performActivity(queue: .app, {
+
+ let client = Store.shared.netServiceClient
+
+ guard let netService = try client.discover(duration: 2.0, timeout: 10.0).first(where: { $0.identifier == identifier })
+ else { throw LockError.notInRange(lock: identifier) }
+
+ try client.update(for: netService, with: key, timeout: 30.0)
+ })
+ }))
+
+ self.present(alert, animated: true, completion: nil)
+ }
+}
diff --git a/iOS/LockKit/Controller/LockEventsViewController.swift b/iOS/LockKit/Controller/LockEventsViewController.swift
index b0c2ecbb..22e9b272 100644
--- a/iOS/LockKit/Controller/LockEventsViewController.swift
+++ b/iOS/LockKit/Controller/LockEventsViewController.swift
@@ -140,8 +140,8 @@ public final class LockEventsViewController: TableViewController {
private func reloadData() {
- typealias FetchRequest = ListEventsCharacteristic.FetchRequest
- typealias Predicate = ListEventsCharacteristic.Predicate
+ typealias FetchRequest = LockEvent.FetchRequest
+ typealias Predicate = LockEvent.Predicate
// load keys if neccesary
if needsKeys.isEmpty == false {
@@ -167,42 +167,46 @@ public final class LockEventsViewController: TableViewController {
let locks = self.locks
let context = Store.shared.backgroundContext
- if Store.shared.lockManager.central.state == .poweredOn {
- performActivity(queue: .bluetooth, { [weak self] in
- for lock in locks {
- guard let device = try Store.shared.device(for: lock, scanDuration: 1.0) else {
- if self?.lock == nil {
- continue
- } else {
- throw CentralError.unknownPeripheral
- }
- }
- let lastEventDate = try context.performErrorBlockAndWait {
- try context.find(identifier: lock, type: LockManagedObject.self)
- .flatMap { try $0.lastEvent(in: context)?.date }
- }
- let fetchRequest = FetchRequest(
- offset: 0,
- limit: nil,
- predicate: Predicate(
- keys: nil,
- start: lastEventDate,
- end: nil
- )
+ performActivity(queue: .app, { [weak self] in
+ guard let self = self else { return }
+ let expectsLock = self.lock != nil
+ for lock in locks {
+
+ // fetch request
+ let lastEventDate = try context.performErrorBlockAndWait {
+ try context.find(identifier: lock, type: LockManagedObject.self)
+ .flatMap { try $0.lastEvent(in: context)?.date }
+ }
+ let fetchRequest = FetchRequest(
+ offset: 0,
+ limit: nil,
+ predicate: Predicate(
+ keys: nil,
+ start: lastEventDate,
+ end: nil
)
- do { try Store.shared.listEvents(device, fetchRequest: fetchRequest) }
- catch {
- if self?.lock == nil {
- continue
- } else {
- throw error
- }
+ )
+ // first try via Bonjour
+ if let netService = try Store.shared.netServiceClient.discover(duration: 1.0, timeout: 10.0).first(where: { $0.identifier == lock }) {
+
+ try Store.shared.listEvents(netService, fetchRequest: fetchRequest)
+
+ } else if Store.shared.lockManager.central.state == .poweredOn,
+ let device = try DispatchQueue.bluetooth.sync(execute: { try Store.shared.device(for: lock, scanDuration: 2.0) }) {
+
+ try DispatchQueue.bluetooth.sync {
+ _ = try Store.shared.listEvents(device, fetchRequest: fetchRequest)
}
+
+ } else if expectsLock {
+ throw LockError.notInRange(lock: lock)
+ } else {
+ continue
}
- }, completion: { (viewController, _) in
- viewController.needsKeys.removeAll()
- })
- }
+ }
+ }, completion: { (viewController, _) in
+ viewController.needsKeys.removeAll()
+ })
}
private subscript (indexPath: IndexPath) -> EventManagedObject {
diff --git a/iOS/LockKit/Controller/LockPermissionsViewController.swift b/iOS/LockKit/Controller/LockPermissionsViewController.swift
index 2a24538e..c16d7c5d 100644
--- a/iOS/LockKit/Controller/LockPermissionsViewController.swift
+++ b/iOS/LockKit/Controller/LockPermissionsViewController.swift
@@ -77,13 +77,47 @@ public final class LockPermissionsViewController: UITableViewController {
guard let lockIdentifier = self.lockIdentifier
else { assertionFailure(); return }
-
- performActivity(queue: .bluetooth, {
- guard let peripheral = try Store.shared.device(for: lockIdentifier, scanDuration: 1.0)
- else { throw LockError.notInRange(lock: lockIdentifier) }
- try Store.shared.listKeys(peripheral, notification: { (list, isComplete) in
- mainQueue { [weak self] in self?.list = list }
- })
+
+ performActivity(queue: .app, {
+
+ // get lock key
+ guard let lockCache = Store.shared[lock: lockIdentifier]
+ else { throw LockError.noKey(lock: lockIdentifier) }
+
+ guard lockCache.key.permission.isAdministrator
+ else { throw LockError.notAdmin(lock: lockIdentifier) }
+
+ guard let keyData = Store.shared[key: lockCache.key.identifier] else {
+ assertionFailure("Missing from Keychain")
+ throw LockError.noKey(lock: lockIdentifier)
+ }
+
+ let key = KeyCredentials(
+ identifier: lockCache.key.identifier,
+ secret: keyData
+ )
+
+ // attempt to load via Bonjour
+ let servers = (try? Store.shared.netServiceClient.discover(duration: 1.0, timeout: 3.0)) ?? []
+ if let netService = servers.first(where: { $0.identifier == lockIdentifier }) {
+ let list = try Store.shared.netServiceClient.listKeys(
+ for: netService,
+ with: key,
+ timeout: 30.0
+ )
+ mainQueue { [weak self] in
+ self?.list = list
+ }
+ } else {
+ // attempt to load via Bluetooth
+ try DispatchQueue.bluetooth.sync {
+ guard let peripheral = try Store.shared.device(for: lockIdentifier, scanDuration: 1.0)
+ else { throw LockError.notInRange(lock: lockIdentifier) }
+ try Store.shared.listKeys(peripheral, notification: { (list, isComplete) in
+ mainQueue { [weak self] in self?.list = list }
+ })
+ }
+ }
})
}
@@ -216,33 +250,42 @@ public final class LockPermissionsViewController: UITableViewController {
alert.dismiss(animated: true, completion: nil)
}))
- alert.addAction(UIAlertAction(title: R.string.localizable.alertDelete(), style: .destructive, handler: { (UIAlertAction) in
+ alert.addAction(UIAlertAction(title: R.string.localizable.alertDelete(), style: .destructive, handler: { [weak self] (UIAlertAction) in
alert.dismiss(animated: true) { }
- self.showActivity()
-
- DispatchQueue.bluetooth.async { [weak self] in
+ self?.performActivity(queue: .app, {
- do {
- guard let peripheral = Store.shared[peripheral: lockIdentifier]
- else { return }
+ // first try via BLE
+ if Store.shared.lockManager.central.state == .poweredOn,
+ let device = try DispatchQueue.bluetooth.sync(execute: { try Store.shared.device(for: lockIdentifier, scanDuration: 2.0) }) {
- try LockManager.shared.removeKey(keyEntry.identifier, type: keyEntry.type, for: peripheral, with: key)
-
- }
- catch {
- mainQueue {
- self?.showErrorAlert(error.localizedDescription)
- self?.hideActivity(animated: false)
+ try DispatchQueue.bluetooth.sync {
+ try Store.shared.lockManager.removeKey(
+ keyEntry.identifier,
+ type: keyEntry.type,
+ for: device.scanData.peripheral,
+ with: key
+ )
}
- return
- }
- mainQueue {
- self?.list.remove(keyEntry.identifier, type: keyEntry.type)
- self?.hideActivity(animated: true)
+
+ } else if let netService = try Store.shared.netServiceClient.discover(duration: 1.0, timeout: 10.0).first(where: { $0.identifier == lockIdentifier }) {
+
+ // try via Bonjour
+ try Store.shared.netServiceClient.removeKey(
+ keyEntry.identifier,
+ type: keyEntry.type,
+ for: netService,
+ with: key,
+ timeout: 30.0
+ )
+
+ } else {
+ throw LockError.notInRange(lock: lockIdentifier)
}
- }
+ }, completion: { (viewController, _) in
+ viewController.list.remove(keyEntry.identifier, type: keyEntry.type)
+ })
}))
self.present(alert, animated: true, completion: nil)
diff --git a/iOS/LockKit/Controller/LockViewController.swift b/iOS/LockKit/Controller/LockViewController.swift
index 5f93867c..bfb9cb17 100644
--- a/iOS/LockKit/Controller/LockViewController.swift
+++ b/iOS/LockKit/Controller/LockViewController.swift
@@ -106,17 +106,16 @@ public final class LockViewController: UITableViewController {
let activities = [
NewKeyActivity(),
ManageKeysActivity(),
- HomeKitEnableActivity(),
RenameActivity(),
UpdateActivity(),
DeleteLockActivity { [unowned self] in
self.navigationController?.popViewController(animated: true)
},
- AddVoiceShortcutActivity()
+ AddSiriShortcutActivity()
]
let lockItem = LockActivityItem(identifier: lockIdentifier)
- let activityItems = [lockItem, lockItem.text, lockItem.image] as [Any]
+ let activityItems = [lockItem] as [Any]
let activityViewController = UIActivityViewController(
activityItems: activityItems,
applicationActivities: activities
diff --git a/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift b/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift
index 7b48da21..38695f06 100644
--- a/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift
+++ b/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift
@@ -47,7 +47,7 @@ public extension NewKeyViewController {
self.showActivity()
// add new key to lock
- DispatchQueue.bluetooth.async { [weak self] in
+ DispatchQueue.app.async { [weak self] in
guard let self = self else { return }
@@ -59,27 +59,55 @@ public extension NewKeyViewController {
let newKeySharedSecret = KeyData()
+ // file for sharing
let newKeyInvitation = NewKey.Invitation(
lock: lockIdentifier,
key: newKey,
secret: newKeySharedSecret
)
+ // for BLE / HTTP request
+ let newKeyRequest = CreateNewKeyRequest(key: newKey, secret: newKeySharedSecret)
+
do {
- guard let peripheral = try Store.shared.device(for: lockIdentifier, scanDuration: 2.0) else {
+ // first try via BLE
+ if Store.shared.lockManager.central.state == .poweredOn,
+ let peripheral = try DispatchQueue.bluetooth.sync(execute: { try Store.shared.device(for: lockIdentifier, scanDuration: 2.0) }) {
+
+ try DispatchQueue.bluetooth.sync {
+ try Store.shared.lockManager.createKey(
+ newKeyRequest,
+ for: peripheral.scanData.peripheral,
+ with: parentKey,
+ timeout: 30.0
+ )
+ }
+
+ } else if let netService = try Store.shared.netServiceClient.discover(duration: 1.0, timeout: 10.0).first(where: { $0.identifier == lockIdentifier }) {
+
+ // try via Bonjour
+ try Store.shared.netServiceClient.createKey(
+ newKeyRequest,
+ for: netService,
+ with: parentKey,
+ timeout: 30.0
+ )
+
+ } else {
+ // not in range
mainQueue {
self.hideActivity(animated: false)
self.newKeyError(R.string.error.notInRange())
}
return
}
- try LockManager.shared.createKey(
- .init(key: newKey, secret: newKeySharedSecret),
- for: peripheral.scanData.peripheral,
- with: parentKey)
+
}
catch {
mainQueue {
+ #if DEBUG
+ dump(error)
+ #endif
self.hideActivity(animated: false)
self.newKeyError(error.localizedDescription)
}
@@ -123,6 +151,7 @@ public extension NewKeyViewController {
private func newKeyError(_ error: String) {
+ log("⚠️ Unable to share key. \(error)")
self.showErrorAlert(error, okHandler: { self.dismiss(animated: true, completion: nil) }, retryHandler: nil)
}
}
diff --git a/iOS/LockKit/Localized Strings/en.lproj/ContextMenu.strings b/iOS/LockKit/Localized Strings/en.lproj/ContextMenu.strings
index 4eb8cd69..5c791e63 100644
--- a/iOS/LockKit/Localized Strings/en.lproj/ContextMenu.strings
+++ b/iOS/LockKit/Localized Strings/en.lproj/ContextMenu.strings
@@ -10,3 +10,4 @@
"Item.Delete" = "Delete";
"Item.ShareKey" = "Share Key";
"Item.Manage" = "Manage";
+"Item.SiriShortcut" = "Add to Siri";
diff --git a/iOS/LockKit/Localized Strings/en.lproj/Error.strings b/iOS/LockKit/Localized Strings/en.lproj/Error.strings
index 2d348d1e..24706a3a 100644
--- a/iOS/LockKit/Localized Strings/en.lproj/Error.strings
+++ b/iOS/LockKit/Localized Strings/en.lproj/Error.strings
@@ -8,6 +8,7 @@
"NotInRange" = "Lock not in range.";
"NoKey" = "You don't have a key for this lock.";
+"NotAdmin" = "You don't have administrator permissions for this lock.";
"InvalidNewKeyFile" = "Invalid key file.";
"ExistingKey" = "You already have a key for this lock.";
"NewKeyExpired" = "Key expired.";
diff --git a/iOS/LockKit/Localized Strings/es.lproj/ContextMenu.strings b/iOS/LockKit/Localized Strings/es.lproj/ContextMenu.strings
index 4c6c66c8..571b0a92 100644
--- a/iOS/LockKit/Localized Strings/es.lproj/ContextMenu.strings
+++ b/iOS/LockKit/Localized Strings/es.lproj/ContextMenu.strings
@@ -10,3 +10,4 @@
"Item.Delete" = "Eliminar";
"Item.ShareKey" = "Compartir Llave";
"Item.Manage" = "Administrar";
+"Item.SiriShortcut" = "Agregar a Siri";
diff --git a/iOS/LockKit/Localized Strings/es.lproj/Error.strings b/iOS/LockKit/Localized Strings/es.lproj/Error.strings
index 3c79a77c..306c8c1d 100644
--- a/iOS/LockKit/Localized Strings/es.lproj/Error.strings
+++ b/iOS/LockKit/Localized Strings/es.lproj/Error.strings
@@ -8,6 +8,7 @@
"NotInRange" = "La cerradura no se encuentra cerca";
"NoKey" = "No tiene una llave para esta cerradura.";
+"NotAdmin" = "No tiene permisos de administrador para esta cerradura.";
"InvalidNewKeyFile" = "Archivo invalido.";
"ExistingKey" = "Usted ya cuenta con una llave para la cerradura.";
"NewKeyExpired" = "La llave pendiente expiro.";
diff --git a/iOS/LockKit/Model/Activity.swift b/iOS/LockKit/Model/Activity.swift
index f7376227..d4df133d 100644
--- a/iOS/LockKit/Model/Activity.swift
+++ b/iOS/LockKit/Model/Activity.swift
@@ -187,8 +187,7 @@ public final class NewKeyActivity: UIActivity {
public override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
guard let lockItem = activityItems.compactMap({ $0 as? LockActivityItem }).first,
- let lockCache = Store.shared[lock: lockItem.identifier],
- Store.shared[peripheral: lockItem.identifier] != nil // Lock must be reachable
+ let lockCache = Store.shared[lock: lockItem.identifier]
else { return false }
// only owner and admin can share keys
@@ -239,8 +238,7 @@ public final class ManageKeysActivity: UIActivity {
public override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
guard let lockItem = activityItems.compactMap({ $0 as? LockActivityItem }).first,
- let lockCache = Store.shared[lock: lockItem.identifier],
- Store.shared[peripheral: lockItem.identifier] != nil // Lock must be reachable
+ let lockCache = Store.shared[lock: lockItem.identifier]
else { return false }
return lockCache.key.permission.isAdministrator
@@ -388,92 +386,6 @@ public final class RenameActivity: UIActivity {
}
}
-/// Activity for enabling HomeKit.
-public final class HomeKitEnableActivity: UIActivity {
-
- public override class var activityCategory: UIActivity.Category { return .action }
-
- private var item: LockActivityItem!
-
- public override var activityType: UIActivity.ActivityType? {
- return LockActivity.homeKitEnable.activityType
- }
-
- public override var activityTitle: String? {
- return R.string.activity.homeKitEnableActivityTitle()
- }
-
- public override var activityImage: UIImage? {
- return R.image.activityHomeKit()
- }
-
- public override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
-
- guard let lockItem = activityItems.compactMap({ $0 as? LockActivityItem }).first,
- let lockCache = Store.shared[lock: lockItem.identifier],
- Store.shared[peripheral: lockItem.identifier] != nil // Lock must be reachable
- else { return false }
-
- guard lockCache.key.permission == .owner
- else { return false }
-
- #if DEBUG
- return true
- #else
- return false
- #endif
- }
-
- public override func prepare(withActivityItems activityItems: [Any]) {
-
- self.item = activityItems.compactMap({ $0 as? LockActivityItem }).first
- }
-
- public override var activityViewController: UIViewController? {
-
- let lockItem = self.item!
-
- let alert = UIAlertController(title: R.string.activity.homeKitEnableActivityAlertTitle(),
- message: R.string.activity.homeKitEnableActivityAlertMessage(),
- preferredStyle: .alert)
-
- func enableHomeKit(_ enable: Bool = true) {
-
- guard let lockItem = self.item,
- let lockCache = Store.shared[lock: lockItem.identifier],
- let keyData = Store.shared[key: lockCache.key.identifier],
- let peripheral = Store.shared[peripheral: lockItem.identifier] // Lock must be reachable
- else { alert.dismiss(animated: true) { self.activityDidFinish(false) }; return }
-
- DispatchQueue.bluetooth.async {
-
- //do { try LockManager.shared.enableHomeKit(lockItem.identifier, key: (lockCache.keyIdentifier, keyData), enable: enable) }
-
- //catch { mainQueue { alert.showErrorAlert("\(error)"); self.activityDidFinish(false) }; return }
-
- mainQueue { alert.dismiss(animated: true) { self.activityDidFinish(true) } }
- }
- }
-
- alert.addAction(UIAlertAction(title:R.string.activity.homeKitEnableActivityAlertCancel(), style: .cancel, handler: { (UIAlertAction) in
-
- alert.dismiss(animated: true) { self.activityDidFinish(false) }
- }))
-
- alert.addAction(UIAlertAction(title: R.string.activity.homeKitEnableActivityAlertYes(), style: .`default`, handler: { (UIAlertAction) in
-
- enableHomeKit()
- }))
-
- alert.addAction(UIAlertAction(title: R.string.activity.homeKitEnableActivityAlertNo(), style: .`default`, handler: { (UIAlertAction) in
-
- enableHomeKit(false)
- }))
-
- return alert
- }
-}
-
public final class UpdateActivity: UIActivity {
public override class var activityCategory: UIActivity.Category { return .action }
@@ -481,7 +393,7 @@ public final class UpdateActivity: UIActivity {
private var item: LockActivityItem!
public override var activityType: UIActivity.ActivityType? {
- return LockActivity.homeKitEnable.activityType
+ return LockActivity.update.activityType
}
public override var activityTitle: String? {
@@ -495,11 +407,10 @@ public final class UpdateActivity: UIActivity {
public override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
guard let lockItem = activityItems.compactMap({ $0 as? LockActivityItem }).first,
- let lockCache = Store.shared[lock: lockItem.identifier],
- Store.shared[peripheral: lockItem.identifier] != nil // Lock must be reachable
+ let lockCache = Store.shared[lock: lockItem.identifier]
else { return false }
- guard lockCache.key.permission == .owner
+ guard lockCache.key.permission.isAdministrator
else { return false }
return true
@@ -512,7 +423,7 @@ public final class UpdateActivity: UIActivity {
public override var activityViewController: UIViewController? {
- //let lockItem = self.item!
+ let lockItem = self.item!
let alert = UIAlertController(title: R.string.activity.updateActivityAlertTitle(),
message: R.string.activity.updateActivityAlertMessage(),
@@ -524,8 +435,8 @@ public final class UpdateActivity: UIActivity {
}))
alert.addAction(UIAlertAction(title: R.string.activity.updateActivityAlertUpdate(), style: .`default`, handler: { (UIAlertAction) in
- /*
- let progressHUD = JGProgressHUD(style: .dark)!
+
+ let progressHUD = JGProgressHUD(style: .dark)
func showProgressHUD() {
@@ -542,19 +453,40 @@ public final class UpdateActivity: UIActivity {
}
// fetch cache
- guard let (lockCache, keyData) = Store.shared[lockItem.identifier]
+ guard let lockCache = Store.shared[lock: lockItem.identifier],
+ let keyData = Store.shared[key: lockCache.key.identifier]
else { alert.dismiss(animated: true) { self.activityDidFinish(false) }; return }
+ let key = KeyCredentials(identifier: lockCache.key.identifier, secret: keyData)
+
showProgressHUD()
- async {
+ DispatchQueue.app.async {
+
+ let client = Store.shared.netServiceClient
- do { try LockManager.shared.update(lockItem.identifier, key: (lockCache.keyIdentifier, keyData)) }
+ do {
+ guard let netService = try client.discover(duration: 1.0, timeout: 10.0).first(where: { $0.identifier == lockItem.identifier })
+ else { throw LockError.notInRange(lock: lockItem.identifier) }
+
+ try client.update(for: netService, with: key, timeout: 30.0)
+ }
- catch { mainQueue { dismissProgressHUD(false); alert.showErrorAlert("\(error)"); self.activityDidFinish(false) }; return }
+ catch {
+ mainQueue {
+ dismissProgressHUD(false)
+ alert.showErrorAlert("\(error)")
+ self.activityDidFinish(false)
+ }
+ return
+ }
- mainQueue { dismissProgressHUD(); alert.dismiss(animated: true) { self.activityDidFinish(true) } }
- }*/
+ mainQueue {
+ dismissProgressHUD()
+ self.activityDidFinish(true)
+ alert.dismiss(animated: true) { }
+ }
+ }
}))
return alert
@@ -629,11 +561,11 @@ public final class ShareKeyCloudKitActivity: UIActivity {
#if canImport(IntentsUI)
import IntentsUI
-public final class AddVoiceShortcutActivity: UIActivity {
+public final class AddSiriShortcutActivity: UIActivity {
public override class var activityCategory: UIActivity.Category { return .action }
- private var item: LockActivityItem!
+ private var item: LockActivityItem?
public override var activityType: UIActivity.ActivityType? {
return LockActivity.addVoiceShortcut.activityType
@@ -676,17 +608,16 @@ public final class AddVoiceShortcutActivity: UIActivity {
else { return nil }
guard let lockItem = self.item
- else { fatalError() }
+ else { assertionFailure(); return nil }
guard let lockCache = Store.shared[lock: lockItem.identifier]
else { assertionFailure("Invalid lock"); return nil }
- let intent = UnlockIntent(identifier: lockItem.identifier, cache: lockCache)
- let shortcut = INShortcut.intent(intent)
- let viewController = INUIAddVoiceShortcutViewController(shortcut: shortcut)
- viewController.modalPresentationStyle = .formSheet
- viewController.delegate = self
- return viewController
+ return INUIAddVoiceShortcutViewController(
+ unlock: lockItem.identifier,
+ cache: lockCache,
+ delegate: self
+ )
#endif
}
}
@@ -694,7 +625,7 @@ public final class AddVoiceShortcutActivity: UIActivity {
// MARK: - INUIAddVoiceShortcutViewControllerDelegate
@available(iOS 12, *)
-extension AddVoiceShortcutActivity: INUIAddVoiceShortcutViewControllerDelegate {
+extension AddSiriShortcutActivity: INUIAddVoiceShortcutViewControllerDelegate {
public func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?) {
diff --git a/iOS/LockKit/Model/Error.swift b/iOS/LockKit/Model/Error.swift
index 38168158..493ee713 100644
--- a/iOS/LockKit/Model/Error.swift
+++ b/iOS/LockKit/Model/Error.swift
@@ -17,6 +17,9 @@ public enum LockError: Error {
/// No key for the specified lock.
case noKey(lock: UUID)
+ /// Must be an administrator for the specified lock.
+ case notAdmin(lock: UUID)
+
/// Invalid QR code.
case invalidQRCode
@@ -57,6 +60,9 @@ extension LockError: CustomNSError {
case let .noKey(lock: lock):
userInfo[NSLocalizedDescriptionKey] = R.string.error.noKey()
userInfo[UserInfoKey.lock.rawValue] = lock as NSUUID
+ case let .notAdmin(lock: lock):
+ userInfo[NSLocalizedDescriptionKey] = R.string.error.notAdmin()
+ userInfo[UserInfoKey.lock.rawValue] = lock as NSUUID
case .invalidQRCode:
userInfo[NSLocalizedDescriptionKey] = R.string.error.invalidQRCode()
case .invalidNewKeyFile:
diff --git a/iOS/LockKit/Model/Intent.swift b/iOS/LockKit/Model/Intent.swift
index a4f95f53..3be2bb69 100644
--- a/iOS/LockKit/Model/Intent.swift
+++ b/iOS/LockKit/Model/Intent.swift
@@ -33,12 +33,6 @@ public extension UnlockIntent {
self.__setImage(INImage(uiImage: UIImage(permission: cache.key.permission)), forParameterNamed: #keyPath(lock))
#endif
}
-
- static var any: UnlockIntent {
- let intent = UnlockIntent()
- intent.suggestedInvocationPhrase = "Unlock"
- return intent
- }
}
@available(iOS 12, iOSApplicationExtension 12.0, watchOS 5.0, *)
@@ -48,3 +42,22 @@ public extension IntentLock {
self.init(identifier: identifier.uuidString, display: name, pronunciationHint: name)
}
}
+
+#if canImport(IntentsUI) && !targetEnvironment(macCatalyst)
+import IntentsUI
+
+@available(iOSApplicationExtension 12.0, *)
+public extension INUIAddVoiceShortcutViewController {
+
+ convenience init(unlock lock: UUID,
+ cache: LockCache,
+ delegate: INUIAddVoiceShortcutViewControllerDelegate) {
+
+ let intent = UnlockIntent(identifier: lock, cache: cache)
+ self.init(shortcut: .intent(intent))
+ self.modalPresentationStyle = .formSheet
+ self.delegate = delegate
+ }
+}
+
+#endif
diff --git a/iOS/LockKit/Model/NetService.swift b/iOS/LockKit/Model/NetService.swift
new file mode 100644
index 00000000..307e44bd
--- /dev/null
+++ b/iOS/LockKit/Model/NetService.swift
@@ -0,0 +1,28 @@
+//
+// NetService.swift
+// LockKit
+//
+// Created by Alsey Coleman Miller on 10/16/19.
+// Copyright © 2019 ColemanCDA. All rights reserved.
+//
+
+import Foundation
+import CoreLock
+import Bonjour
+
+public typealias LockNetServiceClient = CoreLock.LockNetService.Client
+
+public extension LockNetServiceClient {
+
+ static var shared: LockNetServiceClient {
+ return LockNetServiceCache.client
+ }
+}
+
+private struct LockNetServiceCache {
+
+ static let client = LockNetServiceClient(
+ bonjour: NetServiceClient(),
+ urlSession: .shared
+ )
+}
diff --git a/iOS/LockKit/Model/Store.swift b/iOS/LockKit/Model/Store.swift
index a24f8c9a..7838ea3f 100644
--- a/iOS/LockKit/Model/Store.swift
+++ b/iOS/LockKit/Model/Store.swift
@@ -142,6 +142,8 @@ public final class Store {
public lazy var beaconController: BeaconController = .shared
public lazy var spotlight: SpotlightController = .shared
+
+ public lazy var netServiceClient: LockNetServiceClient = .shared
#endif
// BLE cache
@@ -184,14 +186,18 @@ public final class Store {
}
set {
-
+ let key = identifier.uuidString
do {
guard let data = newValue?.data else {
- try keychain.remove(identifier.uuidString)
+ try keychain.remove(key)
return
}
- try keychain.set(data, key: identifier.uuidString)
- } catch {
+ if try keychain.contains(key) {
+ try keychain.remove(key)
+ }
+ try keychain.set(data, key: key)
+ }
+ catch {
#if DEBUG
print(error)
#endif
@@ -218,6 +224,14 @@ public final class Store {
return true
}
+ /// Get credentials from Keychain to authorize requests.
+ public func credentials(for lock: UUID) -> KeyCredentials? {
+ guard let cache = self[lock: lock],
+ let keyData = self[key: cache.key.identifier]
+ else { return nil }
+ return .init(identifier: cache.key.identifier, secret: keyData)
+ }
+
/// Forceably load cache.
public func loadCache() {
@@ -319,8 +333,8 @@ public final class Store {
log("📶 Lock notification")
guard preferences.monitorBluetoothNotifications
else { return } // ignore notification
- typealias FetchRequest = ListEventsCharacteristic.FetchRequest
- typealias Predicate = ListEventsCharacteristic.Predicate
+ typealias FetchRequest = LockEvent.FetchRequest
+ typealias Predicate = LockEvent.Predicate
let context = Store.shared.backgroundContext
DispatchQueue.bluetooth.async {
// scan for all locks
@@ -556,7 +570,7 @@ public extension Store {
@discardableResult
func listEvents(_ lock: LockPeripheral,
- fetchRequest: ListEventsCharacteristic.FetchRequest? = nil,
+ fetchRequest: LockEvent.FetchRequest? = nil,
notification: @escaping ((EventsList, Bool) -> ()) = { _,_ in }) throws -> Bool {
// get lock key
@@ -604,6 +618,59 @@ public extension Store {
}
}
+// MARK: - Bonjour Requests
+
+#if os(iOS)
+
+public extension Store {
+
+ @discardableResult
+ func listEvents(_ lock: LockNetService,
+ fetchRequest: LockEvent.FetchRequest? = nil) throws -> Bool {
+
+ // get lock key
+ guard let lockCache = self[lock: lock.identifier],
+ let keyData = self[key: lockCache.key.identifier]
+ else { return false }
+
+ let key = KeyCredentials(
+ identifier: lockCache.key.identifier,
+ secret: keyData
+ )
+
+ let events = try netServiceClient.listEvents(
+ fetchRequest: fetchRequest,
+ for: lock,
+ with: key,
+ timeout: 30
+ )
+
+ backgroundContext.commit { (context) in
+ try context.insert(events, for: lock.identifier)
+ }
+
+ #if os(iOS)
+ if preferences.isCloudBackupEnabled {
+ DispatchQueue.cloud.async { [weak self] in
+ // upload to iCloud
+ do {
+ for event in events {
+ let value = LockEvent.Cloud(event: event, for: lock.identifier)
+ try self?.cloud.upload(value)
+ }
+ } catch {
+ log("⚠️ Could not upload latest events to iCloud: \(error.localizedDescription)")
+ }
+ }
+ }
+ #endif
+
+ return true
+ }
+}
+
+#endif
+
// MARK: - CloudKit Operations
#if os(iOS)
diff --git a/iOS/LockKit/View/LockTableViewCell.swift b/iOS/LockKit/View/LockTableViewCell.swift
index d7e79e97..3a603265 100644
--- a/iOS/LockKit/View/LockTableViewCell.swift
+++ b/iOS/LockKit/View/LockTableViewCell.swift
@@ -13,9 +13,6 @@ public final class LockTableViewCell: UITableViewCell {
// MARK: - IB Outlets
- @available(*, deprecated, message: "Use `permissionView` instead")
- @IBOutlet public private(set) weak var lockImageView: UIImageView!
-
@IBOutlet public private(set) weak var permissionView: PermissionIconView!
@IBOutlet public private(set) weak var lockTitleLabel: UILabel!
diff --git a/iOS/LockKit/View/LockTableViewCell.xib b/iOS/LockKit/View/LockTableViewCell.xib
index 0ceca063..2dab5e1e 100644
--- a/iOS/LockKit/View/LockTableViewCell.xib
+++ b/iOS/LockKit/View/LockTableViewCell.xib
@@ -1,78 +1,96 @@
-
+
-
-
+
-
-
+
+
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
-
+
diff --git a/iOS/LockKit/en.lproj/Intents.strings b/iOS/LockKit/en.lproj/Intents.strings
index 381556fc..5a369316 100644
--- a/iOS/LockKit/en.lproj/Intents.strings
+++ b/iOS/LockKit/en.lproj/Intents.strings
@@ -20,11 +20,11 @@
"Z5kgSL" = "Lock";
-"bvi2l8" = "`${displayName}` is not in range. Please move closer to ${displayName}.";
+"bvi2l8" = "${displayName} is not in range. Please move closer to ${displayName}.";
-"claxan" = "Unlocked ‘${lock}‘.";
+"claxan" = "Unlocked ${lock}.";
-"dAVo2n" = "Unlock `${lock}`";
+"dAVo2n" = "Unlock ${lock}";
"fjnXtW" = "Unlock";
@@ -32,7 +32,7 @@
"tnOjCJ" = "Lock";
-"vARXDT" = "You do not have a key for `${displayName}`.";
+"vARXDT" = "You do not have a key for ${displayName}.";
-"wkobUh" = "You can only unlock `${displayName}` during the specified schedule.";
+"wkobUh" = "You can only unlock ${displayName} during the specified schedule.";
diff --git a/iOS/LockKit/es.lproj/Intents.strings b/iOS/LockKit/es.lproj/Intents.strings
index 1abc46a3..c34b5eb1 100644
--- a/iOS/LockKit/es.lproj/Intents.strings
+++ b/iOS/LockKit/es.lproj/Intents.strings
@@ -20,11 +20,11 @@
"Z5kgSL" = "Lock";
-"bvi2l8" = "`${displayName}` no esta en rango. Por favor acérquese a ${displayName}.";
+"bvi2l8" = "${displayName} no esta en rango. Por favor acérquese a ${displayName}.";
-"claxan" = "Abrío ‘${lock}‘.";
+"claxan" = "Abrío ${lock}.";
-"dAVo2n" = "Abre `${lock}`";
+"dAVo2n" = "Abre ${lock}";
"fjnXtW" = "Abre";
@@ -32,7 +32,7 @@
"tnOjCJ" = "Cerradura";
-"vARXDT" = "Usted no tiene una llave para `${displayName}`.";
+"vARXDT" = "Usted no tiene una llave para ${displayName}.";
-"wkobUh" = "Usted solo puede abrir `${displayName}` durante el horario especificado.";
+"wkobUh" = "Usted solo puede abrir ${displayName} durante el horario especificado.";
diff --git a/iOS/LockSwiftUI/PermissionScheduleView.swift b/iOS/LockSwiftUI/PermissionScheduleView.swift
index ff1bfa60..4045ae40 100644
--- a/iOS/LockSwiftUI/PermissionScheduleView.swift
+++ b/iOS/LockSwiftUI/PermissionScheduleView.swift
@@ -9,6 +9,7 @@
import Foundation
import SwiftUI
import CoreLock
+import SFSafeSymbols
/// Permission Schedule View
@available(iOS 13, *)
@@ -65,7 +66,7 @@ public struct PermissionScheduleView: View {
}
private var checkmark: some View {
- return Image(systemName: "checkmark")
+ return Image(systemSymbol: .checkmark)
.foregroundColor(Color.orange)
}
diff --git a/iOS/Message/Info.plist b/iOS/Message/Info.plist
index 2488dc4e..8c2d7cc0 100644
--- a/iOS/Message/Info.plist
+++ b/iOS/Message/Info.plist
@@ -5,7 +5,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- Lock
+ Cerradura
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
diff --git a/iOS/SmartLock.xcodeproj/project.pbxproj b/iOS/SmartLock.xcodeproj/project.pbxproj
index 441f3d57..daad1a28 100644
--- a/iOS/SmartLock.xcodeproj/project.pbxproj
+++ b/iOS/SmartLock.xcodeproj/project.pbxproj
@@ -228,6 +228,7 @@
6E83BB4A233D906C00D6BA04 /* LockTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6EAADFB521229CBF0074CEDF /* LockTableViewCell.xib */; };
6E8772CA231259DE003B9469 /* JGProgressHUD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E8772C5231258CD003B9469 /* JGProgressHUD.framework */; platformFilter = ios; };
6E8772CB231259DE003B9469 /* JGProgressHUD.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E8772C5231258CD003B9469 /* JGProgressHUD.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 6E8786FB2357B199008624C1 /* NetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8786FA2357B199008624C1 /* NetService.swift */; };
6E94EF75232F4A790028B745 /* CloudKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E94EF74232F4A790028B745 /* CloudKit.swift */; };
6E9740412318C13D00634631 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6ED81FB32316618100B69520 /* Assets.xcassets */; };
6E9740422318C14800634631 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6ED81FC72316618400B69520 /* Assets.xcassets */; };
@@ -321,6 +322,8 @@
6ECD35C1233B4CB000E49357 /* NewKeyDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECD35C0233B4CB000E49357 /* NewKeyDocument.swift */; };
6ECD39A52336D369003E4AB4 /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECD39A42336D369003E4AB4 /* ActivityIndicator.swift */; };
6ECD6AEA23320D2C0007E7DA /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECD6AE923320D2C0007E7DA /* ContextMenu.swift */; };
+ 6ED44E44235C4406002E8F54 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED44E43235C4406002E8F54 /* SFSafeSymbols */; };
+ 6ED44E46235C49AC002E8F54 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED44E45235C49AC002E8F54 /* Update.swift */; };
6ED81F972316292900B69520 /* BackgroundTasks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED81F962316292900B69520 /* BackgroundTasks.swift */; };
6ED8207D231674E700B69520 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E994AD8230E56DD0076DC1B /* IntentHandler.swift */; };
6ED82092231676C100B69520 /* CoreLock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E60005A2121164B00787DAA /* CoreLock.framework */; };
@@ -835,6 +838,7 @@
6E83BAF72323843500FBC12E /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = ""; };
6E83BB4E233D906C00D6BA04 /* LockKitSwiftUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LockKitSwiftUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6E8772BB231258CC003B9469 /* JGProgressHUD.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = JGProgressHUD.xcodeproj; path = ../Carthage/Checkouts/JGProgressHUD/JGProgressHUD.xcodeproj; sourceTree = ""; };
+ 6E8786FA2357B199008624C1 /* NetService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetService.swift; sourceTree = ""; };
6E94EF74232F4A790028B745 /* CloudKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKit.swift; sourceTree = ""; };
6E9794D323315EA000B5C5C9 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS6.0.sdk/System/Library/Frameworks/CloudKit.framework; sourceTree = DEVELOPER_DIR; };
6E994A79230DE1700076DC1B /* CoreSpotlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSpotlight.swift; sourceTree = ""; };
@@ -906,6 +910,7 @@
6ECD35C0233B4CB000E49357 /* NewKeyDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyDocument.swift; sourceTree = ""; };
6ECD39A42336D369003E4AB4 /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; };
6ECD6AE923320D2C0007E7DA /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; };
+ 6ED44E45235C49AC002E8F54 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = ""; };
6ED81F962316292900B69520 /* BackgroundTasks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTasks.swift; sourceTree = ""; };
6ED81FB12316617E00B69520 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = ""; };
6ED81FB32316618100B69520 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
@@ -999,6 +1004,7 @@
6E64B7EA231236E5002BC0A2 /* Rswift in Frameworks */,
6E64B7F4231247EF002BC0A2 /* QRCodeReader in Frameworks */,
6E64B7F1231240CC002BC0A2 /* JGProgressHUD in Frameworks */,
+ 6ED44E44235C4406002E8F54 /* SFSafeSymbols in Frameworks */,
6E5DF8B32319E3A50070787B /* OpenCombine in Frameworks */,
6EE5A10B2329FCA00000235C /* CloudKitCodable in Frameworks */,
6E64B7ED23123846002BC0A2 /* KeychainAccess in Frameworks */,
@@ -1286,6 +1292,7 @@
6E994A79230DE1700076DC1B /* CoreSpotlight.swift */,
6E73745E2325EA1600EC85B2 /* Event.swift */,
6E60004A212113DF00787DAA /* DeviceManager.swift */,
+ 6E8786FA2357B199008624C1 /* NetService.swift */,
6E806B97230F520400C6FF78 /* FileManager.swift */,
6E994AFC230E657A0076DC1B /* Intent.swift */,
6E806B93230F4F4700C6FF78 /* Keychain.swift */,
@@ -1324,6 +1331,7 @@
6E6000C321211CD400787DAA /* ErrorAlert.swift */,
6E6000C621211CD400787DAA /* PresentPopover.swift */,
6E6000C521211CD400787DAA /* UIAlertAction.swift */,
+ 6ED44E45235C49AC002E8F54 /* Update.swift */,
6ECD6AE923320D2C0007E7DA /* ContextMenu.swift */,
);
path = Extensions;
@@ -1774,6 +1782,7 @@
6E64B7F3231247EF002BC0A2 /* QRCodeReader */,
6E5DF8B22319E3A50070787B /* OpenCombine */,
6EE5A10A2329FCA00000235C /* CloudKitCodable */,
+ 6ED44E43235C4406002E8F54 /* SFSafeSymbols */,
);
productName = LockKit;
productReference = 6E994A84230DFB6C0076DC1B /* LockKit.framework */;
@@ -2151,6 +2160,7 @@
6E64B7F2231247EF002BC0A2 /* XCRemoteSwiftPackageReference "QRCodeReader.swift" */,
6E5DF8B12319E3A50070787B /* XCRemoteSwiftPackageReference "OpenCombine" */,
6EE5A1092329FCA00000235C /* XCRemoteSwiftPackageReference "CloudKitCodable" */,
+ 6ED44E3C235C43EE002E8F54 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
);
productRefGroup = 6E6000112121119400787DAA /* Products */;
projectDirPath = "";
@@ -2838,12 +2848,14 @@
6E994AFD230E657A0076DC1B /* Intent.swift in Sources */,
6E994AC5230DFDC60076DC1B /* NewKeyRecieveViewController.swift in Sources */,
6E50B0F4232243A2001014A8 /* UnlockEventManagedObject.swift in Sources */,
+ 6E8786FB2357B199008624C1 /* NetService.swift in Sources */,
6E08FB8B2316F9F600177026 /* WatchMessage.swift in Sources */,
6ECD39A52336D369003E4AB4 /* ActivityIndicator.swift in Sources */,
6E806BAB230F67D900C6FF78 /* LogViewController.swift in Sources */,
6E994AC1230DFDC60076DC1B /* LockViewController.swift in Sources */,
6E994ABB230DFDB60076DC1B /* AdaptiveNavigation.swift in Sources */,
6E806B94230F4F4700C6FF78 /* Keychain.swift in Sources */,
+ 6ED44E46235C49AC002E8F54 /* Update.swift in Sources */,
6ECD6AEA23320D2C0007E7DA /* ContextMenu.swift in Sources */,
6E6DB101232D7E8100481C36 /* CloudLock.swift in Sources */,
6E50B0EA2322424B001014A8 /* ScheduleManagedObject.swift in Sources */,
@@ -4767,6 +4779,14 @@
kind = branch;
};
};
+ 6ED44E3C235C43EE002E8F54 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/piknotech/SFSafeSymbols.git";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 1.0.0;
+ };
+ };
6EE5A1092329FCA00000235C /* XCRemoteSwiftPackageReference "CloudKitCodable" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/colemancda/CloudKitCodable.git";
@@ -4838,6 +4858,11 @@
package = 6E83BADB233D906C00D6BA04 /* XCRemoteSwiftPackageReference "CloudKitCodable" */;
productName = CloudKitCodable;
};
+ 6ED44E43235C4406002E8F54 /* SFSafeSymbols */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 6ED44E3C235C43EE002E8F54 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
+ productName = SFSafeSymbols;
+ };
6ED820992316777A00B69520 /* KeychainAccess */ = {
isa = XCSwiftPackageProductDependency;
package = 6E64B7EB23123846002BC0A2 /* XCRemoteSwiftPackageReference "KeychainAccess" */;
diff --git a/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 33f0ed97..a6794efe 100644
--- a/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -10,6 +10,15 @@
"version": null
}
},
+ {
+ "package": "Bonjour",
+ "repositoryURL": "git@github.com:PureSwift/Bonjour.git",
+ "state": {
+ "branch": "master",
+ "revision": "b3d28cc68b7fac11811848613633c90c8dd2e058",
+ "version": null
+ }
+ },
{
"package": "CloudKitCodable",
"repositoryURL": "https://github.com/colemancda/CloudKitCodable.git",
@@ -24,7 +33,7 @@
"repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift",
"state": {
"branch": "master",
- "revision": "4ccfe9034a07af4b153657ba1cdac702ffcf8f2f",
+ "revision": "7c94c0bc9d222f9c88d9f6ed8f71926f372d30a8",
"version": null
}
},
@@ -91,6 +100,15 @@
"version": null
}
},
+ {
+ "package": "SFSafeSymbols",
+ "repositoryURL": "https://github.com/piknotech/SFSafeSymbols.git",
+ "state": {
+ "branch": null,
+ "revision": "d63bbbd0e70182362d33f108549968b69d0b859a",
+ "version": "1.0.0"
+ }
+ },
{
"package": "TLVCoding",
"repositoryURL": "https://github.com/PureSwift/TLVCoding.git",
diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Intent.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Intent.xcscheme
index af512881..2b953844 100644
--- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Intent.xcscheme
+++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Intent.xcscheme
@@ -30,7 +30,7 @@
@@ -62,7 +62,7 @@
@@ -80,7 +80,7 @@
diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/IntentUI.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/IntentUI.xcscheme
index 20ffa69e..930e1378 100644
--- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/IntentUI.xcscheme
+++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/IntentUI.xcscheme
@@ -30,7 +30,7 @@
@@ -62,7 +62,7 @@
@@ -80,7 +80,7 @@
diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Message.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Message.xcscheme
index 7c13e63a..f707e339 100644
--- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Message.xcscheme
+++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Message.xcscheme
@@ -30,7 +30,7 @@
@@ -62,7 +62,7 @@
@@ -80,7 +80,7 @@
diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme
index 3e6734a0..33dda053 100644
--- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme
+++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme
@@ -15,7 +15,7 @@
@@ -31,7 +31,7 @@
@@ -74,7 +74,7 @@
@@ -91,7 +91,7 @@
diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today macOS.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today macOS.xcscheme
index e526beff..eb8849b7 100644
--- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today macOS.xcscheme
+++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today macOS.xcscheme
@@ -46,7 +46,7 @@
diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today.xcscheme
index fd58b642..4f4e5453 100644
--- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today.xcscheme
+++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today.xcscheme
@@ -30,7 +30,7 @@
@@ -62,7 +62,7 @@
@@ -80,7 +80,7 @@
diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Complication).xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Complication).xcscheme
index 3070f68c..ea9834d5 100644
--- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Complication).xcscheme
+++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Complication).xcscheme
@@ -29,7 +29,7 @@
diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Notification).xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Notification).xcscheme
index 49ce905c..edfa09f1 100644
--- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Notification).xcscheme
+++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Notification).xcscheme
@@ -29,7 +29,7 @@
diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch.xcscheme
index ffe21246..c321ffcf 100644
--- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch.xcscheme
+++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch.xcscheme
@@ -29,7 +29,7 @@
diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/WatchIntent.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/WatchIntent.xcscheme
index 2e87fb02..bb186056 100644
--- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/WatchIntent.xcscheme
+++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/WatchIntent.xcscheme
@@ -43,7 +43,7 @@
diff --git a/iOS/SmartLock/AppDelegate.swift b/iOS/SmartLock/AppDelegate.swift
index 1cab6127..e59e2e1a 100644
--- a/iOS/SmartLock/AppDelegate.swift
+++ b/iOS/SmartLock/AppDelegate.swift
@@ -76,6 +76,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// setup logging
LockManager.shared.log = { log("🔒 LockManager: " + $0) }
+ LockNetServiceClient.shared.log = { log("🌐 NetService: " + $0) }
BeaconController.shared.log = { log("📶 \(BeaconController.self): " + $0) }
SpotlightController.shared.log = { log("🔦 \(SpotlightController.self): " + $0) }
WatchController.shared.log = { log("⌚️ \(WatchController.self): " + $0) }
diff --git a/iOS/SmartLock/Controller/KeysViewController.swift b/iOS/SmartLock/Controller/KeysViewController.swift
index bd0ef621..bdeb50f9 100644
--- a/iOS/SmartLock/Controller/KeysViewController.swift
+++ b/iOS/SmartLock/Controller/KeysViewController.swift
@@ -8,6 +8,7 @@
import Foundation
import UIKit
+import IntentsUI
import Bluetooth
import DarwinGATT
import GATT
@@ -16,6 +17,7 @@ import LockKit
import JGProgressHUD
import OpenCombine
import CloudKit
+import SFSafeSymbols
final class KeysViewController: UITableViewController {
@@ -368,7 +370,7 @@ final class KeysViewController: UITableViewController {
case let .key(identifier, _):
return self?.menu(forLock: identifier)
case .newKey:
- let delete = UIAction(title: R.string.keysViewController.delete(), image: UIImage(systemName: "trash"), attributes: .destructive) { [weak self] (action) in
+ let delete = UIAction(title: R.string.keysViewController.delete(), image: UIImage(systemSymbol: .trash), attributes: .destructive) { [weak self] (action) in
self?.delete(item)
}
return UIMenu(
@@ -387,6 +389,21 @@ final class KeysViewController: UITableViewController {
extension KeysViewController: TableViewActivityIndicatorViewController { }
+// MARK: - INUIAddVoiceShortcutViewControllerDelegate
+
+@available(iOS 12, *)
+extension KeysViewController: INUIAddVoiceShortcutViewControllerDelegate {
+
+ public func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?) {
+
+ controller.dismiss(animated: true, completion: nil)
+ }
+
+ public func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController) {
+ controller.dismiss(animated: true, completion: nil)
+ }
+}
+
// MARK: - UIDocumentPickerDelegate
extension KeysViewController: UIDocumentPickerDelegate {
diff --git a/iOS/SmartLock/Info.plist b/iOS/SmartLock/Info.plist
index 19270abb..928f10ff 100644
--- a/iOS/SmartLock/Info.plist
+++ b/iOS/SmartLock/Info.plist
@@ -58,6 +58,11 @@
public.app-category.utilities
LSRequiresIPhoneOS
+ NSAppTransportSecurity
+
+ NSAllowsLocalNetworking
+
+
NSBluetoothAlwaysUsageDescription
Bluetooth is used to scan for nearby locks in the background.
NSBluetoothPeripheralUsageDescription
diff --git a/iOS/SmartLock/Localizable Strings/es.lproj/InfoPlist.strings b/iOS/SmartLock/Localizable Strings/es.lproj/InfoPlist.strings
index fde39bc6..2ca3b890 100644
--- a/iOS/SmartLock/Localizable Strings/es.lproj/InfoPlist.strings
+++ b/iOS/SmartLock/Localizable Strings/es.lproj/InfoPlist.strings
@@ -6,7 +6,7 @@
Copyright © 2019 ColemanCDA. All rights reserved.
*/
-"CFBundleDisplayName" = "Lock";
+"CFBundleDisplayName" = "Cerradura";
"NSBluetoothAlwaysUsageDescription" = "Bluetooth es usado para escanear Cerrarudas cercanas en segundo plano.";
"NSBluetoothPeripheralUsageDescription" = "Se necesita Bluetooth para conectarse a su Cerradura Inteligente.";
"NSCameraUsageDescription" = "Se necesita la camara para instalar la Cerradura Inteligente.";
diff --git a/iOS/Today macOS/Info.plist b/iOS/Today macOS/Info.plist
index c7f3f358..10e4e1ba 100644
--- a/iOS/Today macOS/Info.plist
+++ b/iOS/Today macOS/Info.plist
@@ -5,7 +5,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- Lock
+ Cerradura
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
diff --git a/iOS/Today macOS/en.lproj/InfoPlist.strings b/iOS/Today macOS/en.lproj/InfoPlist.strings
index dd798eda..df7e9f5a 100644
--- a/iOS/Today macOS/en.lproj/InfoPlist.strings
+++ b/iOS/Today macOS/en.lproj/InfoPlist.strings
@@ -1,3 +1,3 @@
/* Display name and description for this extension. */
-"CFBundleDisplayName" = "Lock";
+"CFBundleDisplayName" = "Cerradura";
"com.apple.notificationcenter.widget.description" = "Widget for controlling your locks.";
diff --git a/iOS/Today/Base.lproj/MainInterface.storyboard b/iOS/Today/Base.lproj/MainInterface.storyboard
index 82595eb9..18a9bc72 100644
--- a/iOS/Today/Base.lproj/MainInterface.storyboard
+++ b/iOS/Today/Base.lproj/MainInterface.storyboard
@@ -1,48 +1,33 @@
-
+
-
-
+
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
-
+
diff --git a/iOS/Today/Info.plist b/iOS/Today/Info.plist
index 89bcbe0d..1debbec9 100644
--- a/iOS/Today/Info.plist
+++ b/iOS/Today/Info.plist
@@ -5,7 +5,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- Lock
+ Cerradura
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
diff --git a/iOS/Today/TodayViewController.swift b/iOS/Today/TodayViewController.swift
index e1457f6c..abc8bed8 100644
--- a/iOS/Today/TodayViewController.swift
+++ b/iOS/Today/TodayViewController.swift
@@ -16,20 +16,19 @@ import DarwinGATT
import CoreLock
import LockKit
import OpenCombine
+import Combine
-final class TodayViewController: UIViewController, NCWidgetProviding {
-
- // MARK: - IB Outlets
-
- @IBOutlet private(set) weak var tableView: UITableView!
+final class TodayViewController: UITableViewController {
// MARK: - Properties
- private(set) var items: [Item] = [.noNearbyLocks]
+ private(set) var items: [Item] = [.loading] {
+ didSet { tableView.reloadData() }
+ }
- private var peripheralsObserver: AnyCancellable?
- private var informationObserver: AnyCancellable?
- private var locksObserver: AnyCancellable?
+ private(set) var isScanning = true {
+ didSet { configureView() }
+ }
@available(iOS 10.0, *)
private lazy var selectionFeedbackGenerator: UISelectionFeedbackGenerator = {
@@ -38,6 +37,13 @@ final class TodayViewController: UIViewController, NCWidgetProviding {
return feedbackGenerator
}()
+ private var peripheralsObserver: OpenCombine.AnyCancellable?
+ private var informationObserver: OpenCombine.AnyCancellable?
+ private var locksObserver: OpenCombine.AnyCancellable?
+ @available(iOS 13.0, *)
+ private lazy var updateTableViewSubject = Combine.PassthroughSubject()
+ private var updateTableViewObserver: AnyObject? // Combine.AnyCancellable
+
// MARK: - Loading
override func viewDidLoad() {
@@ -55,35 +61,32 @@ final class TodayViewController: UIViewController, NCWidgetProviding {
tableView.register(LockTableViewCell.self)
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 60
+ tableView.tableFooterView = UIView()
// Set Logging
LockManager.shared.log = { log("🔒 LockManager: " + $0) }
BeaconController.shared.log = { log("📶 \(BeaconController.self): " + $0) }
- // scan beacons
- BeaconController.shared.scanBeacons()
-
- // Observe changes
+ // observe model changes
peripheralsObserver = Store.shared.peripherals.sink { [weak self] _ in
- mainQueue { self?.configureView() }
+ self?.locksChanged()
}
informationObserver = Store.shared.lockInformation.sink { [weak self] _ in
- mainQueue { self?.configureView() }
+ self?.locksChanged()
}
locksObserver = Store.shared.locks.sink { [weak self] _ in
- mainQueue { self?.configureView() }
+ self?.locksChanged()
+ }
+
+ if #available(iOS 13.0, *) {
+ updateTableViewObserver = updateTableViewSubject
+ .delay(for: 1.0, scheduler: DispatchQueue.main)
+ .sink(receiveValue: { [weak self] in self?.configureView() })
}
// update UI
configureView()
- // scan for locks
- if Store.shared.lockInformation.value.isEmpty {
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
- self?.scan()
- }
- }
-
if #available(iOS 10.0, *) {
selectionFeedbackGenerator.prepare()
}
@@ -97,39 +100,32 @@ final class TodayViewController: UIViewController, NCWidgetProviding {
}
}
- // MARK: - NCWidgetProviding
-
- func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
-
- log("☀️ Update Widget Data")
-
- // load updated lock information
- Store.shared.loadCache()
-
- if #available(iOS 10.0, *) {
- selectionFeedbackGenerator.prepare()
+ override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
+ super.viewWillTransition(to: size, with: coordinator)
+
+ let updatedVisibleCellCount = cellsToDisplay
+ let currentVisibleCellCount = self.tableView.visibleCells.count
+ let cellCountDifference = updatedVisibleCellCount - currentVisibleCellCount
+
+ // If the number of visible cells has changed, animate them in/out along with the resize animation.
+ if cellCountDifference != 0 {
+ coordinator.animate(alongsideTransition: { [unowned self] (UIViewControllerTransitionCoordinatorContext) in
+ self.tableView.performBatchUpdates({ [unowned self] in
+ // Build an array of IndexPath objects representing the rows to be inserted or deleted.
+ let range = (1...abs(cellCountDifference))
+ let indexPaths = range.map { IndexPath(row: $0, section: 0) }
+
+ // Animate the insertion or deletion of the rows.
+ if cellCountDifference > 0 {
+ self.tableView.insertRows(at: indexPaths, with: .fade)
+ } else {
+ self.tableView.deleteRows(at: indexPaths, with: .fade)
+ }
+ }, completion: nil)
+ }, completion: nil)
}
-
- // Perform any setup necessary in order to update the view.
-
- // If an error is encountered, use NCUpdateResult.Failed
- // If there's no update required, use NCUpdateResult.NoData
- // If there's an update, use NCUpdateResult.NewData
-
- scan { completionHandler($0 ? .newData : .failed) }
- }
-
- @available(iOSApplicationExtension 10.0, *)
- func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
-
- log("☀️ Widget Display Mode changed \(activeDisplayMode.debugDescription) \(maxSize)")
-
-
- }
-
- func widgetMarginInsets(forProposedMarginInsets defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets {
- return .zero
}
+
// MARK: - Methods
@@ -139,11 +135,16 @@ final class TodayViewController: UIViewController, NCWidgetProviding {
get { return items[indexPath.row] }
}
+ private func locksChanged() {
+ if #available(iOS 13.0, *) {
+ updateTableViewSubject.send()
+ } else {
+ mainQueue { [weak self] in self?.configureView() }
+ }
+ }
+
private func configureView() {
- // load updated lock information
- Store.shared.loadCache()
-
let locks = Store.shared.peripherals.value.values
.lazy
.sorted { $0.scanData.rssi < $1.scanData.rssi }
@@ -155,29 +156,26 @@ final class TodayViewController: UIViewController, NCWidgetProviding {
.flatMap { (identifier: information.identifier, cache: $0) }
}
- let oldItems = self.items
if locks.isEmpty {
- items = [.noNearbyLocks]
+ items = [isScanning ? .loading : .noNearbyLocks]
} else {
items = locks.map { .lock($0.identifier, $0.cache) }
}
- // reload table view
- if oldItems != items {
- tableView.reloadData()
- }
+ // Show expanded view for multiple devices
+ extensionContext?.widgetLargestAvailableDisplayMode = items.count > 1 ? .expanded : .compact
}
private func scan(_ completion: ((Bool) -> ())? = nil) {
- // load updated lock information
- Store.shared.loadCache()
+ self.isScanning = true
// scan beacons
BeaconController.shared.scanBeacons()
// scan for devices
DispatchQueue.bluetooth.async {
+ defer { mainQueue { self.isScanning = false } }
do { try Store.shared.scan(duration: 1.0) }
catch {
log("⚠️ Could not scan: \(error.localizedDescription)")
@@ -193,6 +191,16 @@ final class TodayViewController: UIViewController, NCWidgetProviding {
let item = self[indexPath]
switch item {
+ case .loading:
+ cell.lockTitleLabel.text = "Loading..."
+ cell.lockDetailLabel.text = nil
+ cell.activityIndicatorView.isHidden = false
+ if cell.activityIndicatorView.isAnimating == false {
+ cell.activityIndicatorView.startAnimating()
+ }
+ cell.permissionView.isHidden = true
+ cell.selectionStyle = .none
+ cell.accessoryType = .none
case .noNearbyLocks:
cell.lockTitleLabel.text = "No Nearby Locks"
cell.lockDetailLabel.text = nil
@@ -212,6 +220,14 @@ final class TodayViewController: UIViewController, NCWidgetProviding {
}
}
+ private var cellsToDisplay: Int {
+ if extensionContext?.widgetActiveDisplayMode == .compact {
+ return 1
+ } else {
+ return items.count
+ }
+ }
+
private func select(_ item: Item) {
if #available(iOSApplicationExtension 10.0, *) {
@@ -219,6 +235,8 @@ final class TodayViewController: UIViewController, NCWidgetProviding {
}
switch item {
+ case .loading:
+ break
case .noNearbyLocks:
scan()
case let .lock(identifier, cache):
@@ -239,17 +257,17 @@ final class TodayViewController: UIViewController, NCWidgetProviding {
// MARK: - UITableViewDataSource
-extension TodayViewController: UITableViewDataSource {
+extension TodayViewController {
- func numberOfSections(in tableView: UITableView) -> Int {
+ override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
- func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- return items.count
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return min(cellsToDisplay, items.count)
}
- func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(LockTableViewCell.self, for: indexPath)
else { fatalError("Could not dequeue resusable cell \(LockTableViewCell.self)") }
@@ -260,16 +278,16 @@ extension TodayViewController: UITableViewDataSource {
// MARK: - UITableViewDelegate
-extension TodayViewController: UITableViewDelegate {
+extension TodayViewController {
- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
defer { tableView.deselectRow(at: indexPath, animated: true) }
let item = self[indexPath]
select(item)
}
- func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
+ override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
let item = self[indexPath]
guard case let .lock(identifier, _) = item
@@ -277,6 +295,63 @@ extension TodayViewController: UITableViewDelegate {
let url = LockURL.unlock(lock: identifier)
self.extensionContext?.open(url.rawValue, completionHandler: nil)
}
+
+ override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+
+ let activeDisplayMode = extensionContext?.widgetActiveDisplayMode ?? .compact
+ switch activeDisplayMode {
+ case .compact:
+ return LockTableViewCell.todayCellHeight
+ case .expanded:
+ return LockTableViewCell.standardCellHeight
+ @unknown default:
+ assertionFailure("Unexpected value \(activeDisplayMode.rawValue) for activeDisplayMode.")
+ return LockTableViewCell.todayCellHeight
+ }
+ }
+}
+
+// MARK: - NCWidgetProviding
+
+extension TodayViewController: NCWidgetProviding {
+
+ func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
+
+ log("☀️ Update Widget Data")
+
+ // load updated lock information
+ Store.shared.loadCache()
+
+ if #available(iOS 10.0, *) {
+ selectionFeedbackGenerator.prepare()
+ }
+
+ // Perform any setup necessary in order to update the view.
+
+ // If an error is encountered, use NCUpdateResult.Failed
+ // If there's no update required, use NCUpdateResult.NoData
+ // If there's an update, use NCUpdateResult.NewData
+
+ scan { completionHandler($0 ? .newData : .failed) }
+ }
+
+ @available(iOSApplicationExtension 10.0, *)
+ func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
+
+ log("☀️ Widget Display Mode changed \(activeDisplayMode.debugDescription) \(maxSize)")
+
+ switch activeDisplayMode {
+ case .compact:
+ // The compact view is a fixed size.
+ preferredContentSize = maxSize
+ case .expanded:
+ // Dynamically calculate the height of the cells for the extended height.
+ let height = CGFloat(items.count) * LockTableViewCell.standardCellHeight
+ preferredContentSize = CGSize(width: maxSize.width, height: min(height, maxSize.height))
+ @unknown default:
+ assertionFailure("Unexpected value \(activeDisplayMode.rawValue) for activeDisplayMode.")
+ }
+ }
}
// MARK: - Supporting Types
@@ -285,11 +360,21 @@ extension TodayViewController {
enum Item: Equatable {
+ case loading
case noNearbyLocks
case lock(UUID, LockCache)
}
}
+// MARK: - Extensions
+
+internal extension LockTableViewCell {
+
+ // Heights for the two styles of cell display.
+ static let todayCellHeight: CGFloat = 110
+ static let standardCellHeight: CGFloat = 75
+}
+
@available(iOSApplicationExtension 10.0, *)
extension NCWidgetDisplayMode {
diff --git a/iOS/Watch Extension/Controller/InterfaceController.swift b/iOS/Watch Extension/Controller/InterfaceController.swift
index 31165e29..b624611b 100644
--- a/iOS/Watch Extension/Controller/InterfaceController.swift
+++ b/iOS/Watch Extension/Controller/InterfaceController.swift
@@ -159,7 +159,6 @@ final class InterfaceController: WKInterfaceController {
log("Selected lock \(item.identifier)")
- donateUnlockIntent(for: item.identifier)
unlock(lock: item.identifier, peripheral: item.peripheral)
}
diff --git a/iOS/Watch Extension/Controller/Unlock.swift b/iOS/Watch Extension/Controller/Unlock.swift
index 49243e2c..cb87d3db 100644
--- a/iOS/Watch Extension/Controller/Unlock.swift
+++ b/iOS/Watch Extension/Controller/Unlock.swift
@@ -33,27 +33,3 @@ public extension ActivityInterface where Self: WKInterfaceController {
}
}
-public extension WKInterfaceController {
-
- /// Donate Siri Shortcut to unlock the specified lock.
- func donateUnlockIntent(for lock: UUID) {
-
- guard let lockCache = Store.shared[lock: lock] else {
- assertionFailure("Invalid lock \(lock)")
- return
- }
-
- if #available(watchOS 5.0, *) {
- let intent = UnlockIntent(identifier: lock, cache: lockCache)
- let interaction = INInteraction(intent: intent, response: nil)
- interaction.donate { error in
- if let error = error {
- log("⚠️ Donating intent failed with error \(error.localizedDescription)")
- #if DEBUG
- print(error)
- #endif
- }
- }
- }
- }
-}
diff --git a/iOS/Watch/Info.plist b/iOS/Watch/Info.plist
index 602edf49..4d5f25e1 100644
--- a/iOS/Watch/Info.plist
+++ b/iOS/Watch/Info.plist
@@ -5,7 +5,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- Lock
+ Cerradura
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier