Use device ID to distinguish remotes
This commit is contained in:
parent
8a17eef19b
commit
9b14f442b0
@ -50,14 +50,17 @@ extension Message {
|
|||||||
/// The counter of the message (for freshness)
|
/// The counter of the message (for freshness)
|
||||||
let id: UInt32
|
let id: UInt32
|
||||||
|
|
||||||
|
let deviceId: UInt8?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Create new message content.
|
Create new message content.
|
||||||
- Parameter time: The time of message creation,
|
- Parameter time: The time of message creation,
|
||||||
- Parameter id: The counter of the message
|
- Parameter id: The counter of the message
|
||||||
*/
|
*/
|
||||||
init(time: UInt32, id: UInt32) {
|
init(time: UInt32, id: UInt32, device: UInt8) {
|
||||||
self.time = time
|
self.time = time
|
||||||
self.id = id
|
self.id = id
|
||||||
|
self.deviceId = device
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,17 +72,18 @@ extension Message {
|
|||||||
*/
|
*/
|
||||||
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
|
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
|
||||||
self.time = UInt32(data: Data(data.prefix(MemoryLayout<UInt32>.size)))
|
self.time = UInt32(data: Data(data.prefix(MemoryLayout<UInt32>.size)))
|
||||||
self.id = UInt32(data: Data(data.dropFirst(MemoryLayout<UInt32>.size)))
|
self.id = UInt32(data: Data(data.dropLast().suffix(MemoryLayout<UInt32>.size)))
|
||||||
|
self.deviceId = data.suffix(1).last!
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The byte length of an encoded message content
|
/// The byte length of an encoded message content
|
||||||
static var length: Int {
|
static var length: Int {
|
||||||
MemoryLayout<UInt32>.size * 2
|
MemoryLayout<UInt32>.size * 2 + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The message content encoded to data
|
/// The message content encoded to data
|
||||||
var encoded: Data {
|
var encoded: Data {
|
||||||
time.encoded + id.encoded
|
time.encoded + id.encoded + Data([deviceId ?? 0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,6 +93,7 @@ extension Message.Content: Codable {
|
|||||||
enum CodingKeys: Int, CodingKey {
|
enum CodingKeys: Int, CodingKey {
|
||||||
case time = 1
|
case time = 1
|
||||||
case id = 2
|
case id = 2
|
||||||
|
case deviceId = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,9 @@ enum MessageResult: UInt8 {
|
|||||||
/// The key was accepted by the device, and the door will be opened
|
/// The key was accepted by the device, and the door will be opened
|
||||||
case messageAccepted = 7
|
case messageAccepted = 7
|
||||||
|
|
||||||
|
/// The device id is invalid
|
||||||
|
case messageDeviceInvalid = 8
|
||||||
|
|
||||||
|
|
||||||
/// The request did not contain body data with the key
|
/// The request did not contain body data with the key
|
||||||
case noBodyData = 10
|
case noBodyData = 10
|
||||||
@ -61,6 +64,8 @@ extension MessageResult: CustomStringConvertible {
|
|||||||
return "Message counter invalid"
|
return "Message counter invalid"
|
||||||
case .messageAccepted:
|
case .messageAccepted:
|
||||||
return "Message accepted"
|
return "Message accepted"
|
||||||
|
case .messageDeviceInvalid:
|
||||||
|
return "Invalid device ID"
|
||||||
case .noBodyData:
|
case .noBodyData:
|
||||||
return "No body data included in the request"
|
return "No body data included in the request"
|
||||||
case .deviceNotConnected:
|
case .deviceNotConnected:
|
||||||
|
@ -19,6 +19,7 @@ extension ConnectionError: CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum RejectionCause {
|
enum RejectionCause {
|
||||||
|
case invalidDeviceId
|
||||||
case invalidCounter
|
case invalidCounter
|
||||||
case invalidTime
|
case invalidTime
|
||||||
case invalidAuthentication
|
case invalidAuthentication
|
||||||
@ -30,6 +31,8 @@ extension RejectionCause: CustomStringConvertible {
|
|||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .invalidDeviceId:
|
||||||
|
return "Invalid device ID"
|
||||||
case .invalidCounter:
|
case .invalidCounter:
|
||||||
return "Invalid counter"
|
return "Invalid counter"
|
||||||
case .invalidTime:
|
case .invalidTime:
|
||||||
@ -92,6 +95,8 @@ enum ClientState {
|
|||||||
self = .messageRejected(.timeout)
|
self = .messageRejected(.timeout)
|
||||||
case .messageAccepted:
|
case .messageAccepted:
|
||||||
self = .openSesame
|
self = .openSesame
|
||||||
|
case .messageDeviceInvalid:
|
||||||
|
self = .messageRejected(.invalidDeviceId)
|
||||||
case .noBodyData, .invalidMessageData, .textReceived, .unexpectedSocketEvent:
|
case .noBodyData, .invalidMessageData, .textReceived, .unexpectedSocketEvent:
|
||||||
self = .internalError(keyResult.description)
|
self = .internalError(keyResult.description)
|
||||||
case .deviceNotConnected:
|
case .deviceNotConnected:
|
||||||
@ -207,6 +212,8 @@ extension ClientState {
|
|||||||
return 6
|
return 6
|
||||||
case .messageRejected(let rejectionCause):
|
case .messageRejected(let rejectionCause):
|
||||||
switch rejectionCause {
|
switch rejectionCause {
|
||||||
|
case .invalidDeviceId:
|
||||||
|
return 19
|
||||||
case .invalidCounter:
|
case .invalidCounter:
|
||||||
return 7
|
return 7
|
||||||
case .invalidTime:
|
case .invalidTime:
|
||||||
@ -230,6 +237,8 @@ extension ClientState {
|
|||||||
return 15
|
return 15
|
||||||
case .missingKey:
|
case .missingKey:
|
||||||
return 16
|
return 16
|
||||||
|
case .invalidDeviceId:
|
||||||
|
return 20
|
||||||
}
|
}
|
||||||
case .openSesame:
|
case .openSesame:
|
||||||
return 17
|
return 17
|
||||||
@ -276,6 +285,10 @@ extension ClientState {
|
|||||||
self = .openSesame
|
self = .openSesame
|
||||||
case 18:
|
case 18:
|
||||||
self = .internalError("")
|
self = .internalError("")
|
||||||
|
case 19:
|
||||||
|
self = .messageRejected(.invalidDeviceId)
|
||||||
|
case 20:
|
||||||
|
self = .responseRejected(.invalidDeviceId)
|
||||||
default:
|
default:
|
||||||
self = .internalError("Unknown code \(code)")
|
self = .internalError("Unknown code \(code)")
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,9 @@ struct ContentView: View {
|
|||||||
@AppStorage("local")
|
@AppStorage("local")
|
||||||
private var useLocalConnection = false
|
private var useLocalConnection = false
|
||||||
|
|
||||||
|
@AppStorage("deviceID")
|
||||||
|
private var deviceID: Int = 0
|
||||||
|
|
||||||
@State
|
@State
|
||||||
var keyManager = KeyManagement()
|
var keyManager = KeyManagement()
|
||||||
|
|
||||||
@ -133,6 +136,8 @@ struct ContentView: View {
|
|||||||
keyManager: $keyManager,
|
keyManager: $keyManager,
|
||||||
serverAddress: $serverPath,
|
serverAddress: $serverPath,
|
||||||
localAddress: $localAddress,
|
localAddress: $localAddress,
|
||||||
|
deviceID: $deviceID,
|
||||||
|
nextMessageCounter: $nextMessageCounter,
|
||||||
isCompensatingDaylightTime: $isCompensatingDaylightTime,
|
isCompensatingDaylightTime: $isCompensatingDaylightTime,
|
||||||
useLocalConnection: $useLocalConnection)
|
useLocalConnection: $useLocalConnection)
|
||||||
}
|
}
|
||||||
@ -145,7 +150,8 @@ struct ContentView: View {
|
|||||||
|
|
||||||
func mainButtonPressed() {
|
func mainButtonPressed() {
|
||||||
guard let key = keyManager.get(.remoteKey),
|
guard let key = keyManager.get(.remoteKey),
|
||||||
let token = keyManager.get(.authToken)?.data else {
|
let token = keyManager.get(.authToken)?.data,
|
||||||
|
let deviceId = UInt8(exactly: deviceID) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +160,8 @@ struct ContentView: View {
|
|||||||
// Add time to compensate that the device is using daylight savings time
|
// Add time to compensate that the device is using daylight savings time
|
||||||
let content = Message.Content(
|
let content = Message.Content(
|
||||||
time: sentTime.timestamp + compensationTime,
|
time: sentTime.timestamp + compensationTime,
|
||||||
id: count)
|
id: count,
|
||||||
|
device: deviceId)
|
||||||
let message = content.authenticate(using: key)
|
let message = content.authenticate(using: key)
|
||||||
let historyItem = HistoryItem(sent: message.content, date: sentTime, local: useLocalConnection)
|
let historyItem = HistoryItem(sent: message.content, date: sentTime, local: useLocalConnection)
|
||||||
state = .waitingForResponse
|
state = .waitingForResponse
|
||||||
|
@ -123,8 +123,8 @@ extension HistoryItem: Comparable {
|
|||||||
extension HistoryItem {
|
extension HistoryItem {
|
||||||
|
|
||||||
static var mock: HistoryItem {
|
static var mock: HistoryItem {
|
||||||
let content = Message.Content(time: Date.now.timestamp, id: 123)
|
let content = Message.Content(time: Date.now.timestamp, id: 123, device: 0)
|
||||||
let content2 = Message.Content(time: (Date.now + 1).timestamp, id: 124)
|
let content2 = Message.Content(time: (Date.now + 1).timestamp, id: 124, device: 0)
|
||||||
return .init(sent: content, date: .now, local: false)
|
return .init(sent: content, date: .now, local: false)
|
||||||
.didReceive(response: .openSesame, date: .now + 2, message: content2)
|
.didReceive(response: .openSesame, date: .now + 2, message: content2)
|
||||||
}
|
}
|
||||||
|
@ -17,11 +17,11 @@ extension KeyManagement {
|
|||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .deviceKey:
|
case .deviceKey:
|
||||||
return "Device Key"
|
return "Unlock Key"
|
||||||
case .remoteKey:
|
case .remoteKey:
|
||||||
return "Remote Key"
|
return "Response Key"
|
||||||
case .authToken:
|
case .authToken:
|
||||||
return "Authentication Token"
|
return "Server Token"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +148,15 @@ final class KeyManagement: ObservableObject {
|
|||||||
|
|
||||||
func generate(_ type: KeyType) {
|
func generate(_ type: KeyType) {
|
||||||
let key = SymmetricKey(size: type.keyLength)
|
let key = SymmetricKey(size: type.keyLength)
|
||||||
|
save(type, key: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(_ type: KeyType, data: Data) {
|
||||||
|
let key = SymmetricKey(data: data)
|
||||||
|
save(type, key: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func save(_ type: KeyType, key: SymmetricKey) {
|
||||||
if keyChain.has(type) {
|
if keyChain.has(type) {
|
||||||
keyChain.delete(type)
|
keyChain.delete(type)
|
||||||
}
|
}
|
||||||
|
@ -11,12 +11,31 @@ struct SettingsView: View {
|
|||||||
@Binding
|
@Binding
|
||||||
var localAddress: String
|
var localAddress: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var deviceID: Int
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var nextMessageCounter: Int
|
||||||
|
|
||||||
@Binding
|
@Binding
|
||||||
var isCompensatingDaylightTime: Bool
|
var isCompensatingDaylightTime: Bool
|
||||||
|
|
||||||
@Binding
|
@Binding
|
||||||
var useLocalConnection: Bool
|
var useLocalConnection: Bool
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showDeviceIdInput = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var deviceIdText = ""
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showCounterInput = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var counterText = ""
|
||||||
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@ -25,12 +44,14 @@ struct SettingsView: View {
|
|||||||
Text("Server address")
|
Text("Server address")
|
||||||
.bold()
|
.bold()
|
||||||
TextField("Server address", text: $serverAddress)
|
TextField("Server address", text: $serverAddress)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
.padding(.leading, 8)
|
.padding(.leading, 8)
|
||||||
}.padding(.vertical, 8)
|
}.padding(.vertical, 8)
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("Local address")
|
Text("Local address")
|
||||||
.bold()
|
.bold()
|
||||||
TextField("Local address", text: $localAddress)
|
TextField("Local address", text: $localAddress)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
.padding(.leading, 8)
|
.padding(.leading, 8)
|
||||||
}.padding(.vertical, 8)
|
}.padding(.vertical, 8)
|
||||||
Toggle(isOn: $useLocalConnection) {
|
Toggle(isOn: $useLocalConnection) {
|
||||||
@ -39,6 +60,32 @@ struct SettingsView: View {
|
|||||||
Text("Attempt to communicate directly with the device. This is useful if the server is unavailable. Requires a WiFi connection on the same network as the device.")
|
Text("Attempt to communicate directly with the device. This is useful if the server is unavailable. Requires a WiFi connection on the same network as the device.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Device id")
|
||||||
|
.bold()
|
||||||
|
HStack(alignment: .bottom) {
|
||||||
|
Text("\(deviceID)")
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding([.trailing, .bottom])
|
||||||
|
Button("Edit", action: showAlertToChangeDeviceID)
|
||||||
|
.padding([.horizontal, .bottom])
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
}.padding(.vertical, 8)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Message counter")
|
||||||
|
.bold()
|
||||||
|
HStack(alignment: .bottom) {
|
||||||
|
Text("\(nextMessageCounter)")
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding([.trailing, .bottom])
|
||||||
|
Button("Edit", action: showAlertToChangeCounter)
|
||||||
|
.padding([.horizontal, .bottom])
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
}.padding(.vertical, 8)
|
||||||
ForEach(KeyManagement.KeyType.allCases) { keyType in
|
ForEach(KeyManagement.KeyType.allCases) { keyType in
|
||||||
SingleKeyView(
|
SingleKeyView(
|
||||||
keyManager: $keyManager,
|
keyManager: $keyManager,
|
||||||
@ -57,8 +104,54 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Settings")
|
.navigationTitle("Settings")
|
||||||
|
.alert("Update device ID", isPresented: $showDeviceIdInput, actions: {
|
||||||
|
TextField("Device ID", text: $deviceIdText)
|
||||||
|
.keyboardType(.decimalPad)
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.foregroundColor(.black)
|
||||||
|
Button("Save", action: saveDeviceID)
|
||||||
|
Button("Cancel", role: .cancel, action: {})
|
||||||
|
}, message: {
|
||||||
|
Text("Enter the device ID")
|
||||||
|
})
|
||||||
|
.alert("Update message counter", isPresented: $showCounterInput, actions: {
|
||||||
|
TextField("Message counter", text: $counterText)
|
||||||
|
.keyboardType(.decimalPad)
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.foregroundColor(.black)
|
||||||
|
Button("Save", action: saveCounter)
|
||||||
|
Button("Cancel", role: .cancel, action: {})
|
||||||
|
}, message: {
|
||||||
|
Text("Enter the message counter")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func showAlertToChangeDeviceID() {
|
||||||
|
deviceIdText = "\(deviceID)"
|
||||||
|
showDeviceIdInput = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveDeviceID() {
|
||||||
|
guard let id = UInt8(deviceIdText) else {
|
||||||
|
print("Invalid device id '\(deviceIdText)'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.deviceID = Int(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showAlertToChangeCounter() {
|
||||||
|
counterText = "\(nextMessageCounter)"
|
||||||
|
showCounterInput = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveCounter() {
|
||||||
|
guard let id = UInt32(counterText) else {
|
||||||
|
print("Invalid message counter '\(counterText)'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.nextMessageCounter = Int(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SettingsView_Previews: PreviewProvider {
|
struct SettingsView_Previews: PreviewProvider {
|
||||||
@ -67,6 +160,8 @@ struct SettingsView_Previews: PreviewProvider {
|
|||||||
keyManager: .constant(KeyManagement()),
|
keyManager: .constant(KeyManagement()),
|
||||||
serverAddress: .constant("https://example.com"),
|
serverAddress: .constant("https://example.com"),
|
||||||
localAddress: .constant("192.168.178.42"),
|
localAddress: .constant("192.168.178.42"),
|
||||||
|
deviceID: .constant(0),
|
||||||
|
nextMessageCounter: .constant(12345678),
|
||||||
isCompensatingDaylightTime: .constant(true),
|
isCompensatingDaylightTime: .constant(true),
|
||||||
useLocalConnection: .constant(false))
|
useLocalConnection: .constant(false))
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,12 @@ struct SingleKeyView: View {
|
|||||||
@Binding
|
@Binding
|
||||||
var keyManager: KeyManagement
|
var keyManager: KeyManagement
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showEditWindow = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var keyText = ""
|
||||||
|
|
||||||
let type: KeyManagement.KeyType
|
let type: KeyManagement.KeyType
|
||||||
|
|
||||||
private var generateText: String {
|
private var generateText: String {
|
||||||
@ -54,9 +60,41 @@ struct SingleKeyView: View {
|
|||||||
.disabled(!hasKey)
|
.disabled(!hasKey)
|
||||||
.padding([.horizontal, .bottom])
|
.padding([.horizontal, .bottom])
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
|
Button("Edit") {
|
||||||
|
keyText = keyManager.get(type)?.displayString ?? ""
|
||||||
|
print("Set key text to '\(keyText)'")
|
||||||
|
showEditWindow = true
|
||||||
|
}
|
||||||
|
.padding([.horizontal, .bottom])
|
||||||
|
.padding(.top, 4)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.alert("Update key", isPresented: $showEditWindow, actions: {
|
||||||
|
TextField("Key data", text: $keyText)
|
||||||
|
.lineLimit(4)
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.foregroundColor(.black)
|
||||||
|
Button("Save", action: saveKey)
|
||||||
|
Button("Cancel", role: .cancel, action: {})
|
||||||
|
}, message: {
|
||||||
|
Text("Enter the hex encoded key")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveKey() {
|
||||||
|
let cleanText = keyText.replacingOccurrences(of: " ", with: "")
|
||||||
|
guard let keyData = Data(fromHexEncodedString: cleanText) else {
|
||||||
|
print("Invalid key string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let keyLength = type.keyLength.bitCount
|
||||||
|
guard keyData.count * 8 == keyLength else {
|
||||||
|
print("Invalid key length \(keyData.count * 8) bits, expected \(keyLength)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyManager.save(type, data: keyData)
|
||||||
|
print("Key \(type) saved")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,5 +103,6 @@ struct SingleKeyView_Previews: PreviewProvider {
|
|||||||
SingleKeyView(
|
SingleKeyView(
|
||||||
keyManager: .constant(KeyManagement()),
|
keyManager: .constant(KeyManagement()),
|
||||||
type: .deviceKey)
|
type: .deviceKey)
|
||||||
|
.previewLayout(.fixed(width: 350, height: 100))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user