diff --git a/Sesame/API/Message.swift b/Sesame/API/Message.swift index 55f784e..4b690c7 100644 --- a/Sesame/API/Message.swift +++ b/Sesame/API/Message.swift @@ -49,15 +49,18 @@ extension Message { /// The counter of the message (for freshness) let id: UInt32 + + let deviceId: UInt8? /** Create new message content. - Parameter time: The time of message creation, - Parameter id: The counter of the message */ - init(time: UInt32, id: UInt32) { + init(time: UInt32, id: UInt32, device: UInt8) { self.time = time self.id = id + self.deviceId = device } /** @@ -69,26 +72,28 @@ extension Message { */ init(decodeFrom data: T) where T.Element == UInt8 { self.time = UInt32(data: Data(data.prefix(MemoryLayout.size))) - self.id = UInt32(data: Data(data.dropFirst(MemoryLayout.size))) + self.id = UInt32(data: Data(data.dropLast().suffix(MemoryLayout.size))) + self.deviceId = data.suffix(1).last! } /// The byte length of an encoded message content static var length: Int { - MemoryLayout.size * 2 + MemoryLayout.size * 2 + 1 } /// The message content encoded to data var encoded: Data { - time.encoded + id.encoded + time.encoded + id.encoded + Data([deviceId ?? 0]) } } } extension Message.Content: Codable { - + enum CodingKeys: Int, CodingKey { case time = 1 case id = 2 + case deviceId = 3 } } diff --git a/Sesame/API/MessageResult.swift b/Sesame/API/MessageResult.swift index ecf41af..7f58703 100644 --- a/Sesame/API/MessageResult.swift +++ b/Sesame/API/MessageResult.swift @@ -25,6 +25,9 @@ enum MessageResult: UInt8 { /// The key was accepted by the device, and the door will be opened case messageAccepted = 7 + + /// The device id is invalid + case messageDeviceInvalid = 8 /// The request did not contain body data with the key @@ -61,6 +64,8 @@ extension MessageResult: CustomStringConvertible { return "Message counter invalid" case .messageAccepted: return "Message accepted" + case .messageDeviceInvalid: + return "Invalid device ID" case .noBodyData: return "No body data included in the request" case .deviceNotConnected: diff --git a/Sesame/ClientState.swift b/Sesame/ClientState.swift index 3210ea7..b7c90d6 100644 --- a/Sesame/ClientState.swift +++ b/Sesame/ClientState.swift @@ -19,6 +19,7 @@ extension ConnectionError: CustomStringConvertible { } enum RejectionCause { + case invalidDeviceId case invalidCounter case invalidTime case invalidAuthentication @@ -30,6 +31,8 @@ extension RejectionCause: CustomStringConvertible { var description: String { switch self { + case .invalidDeviceId: + return "Invalid device ID" case .invalidCounter: return "Invalid counter" case .invalidTime: @@ -92,6 +95,8 @@ enum ClientState { self = .messageRejected(.timeout) case .messageAccepted: self = .openSesame + case .messageDeviceInvalid: + self = .messageRejected(.invalidDeviceId) case .noBodyData, .invalidMessageData, .textReceived, .unexpectedSocketEvent: self = .internalError(keyResult.description) case .deviceNotConnected: @@ -207,6 +212,8 @@ extension ClientState { return 6 case .messageRejected(let rejectionCause): switch rejectionCause { + case .invalidDeviceId: + return 19 case .invalidCounter: return 7 case .invalidTime: @@ -230,6 +237,8 @@ extension ClientState { return 15 case .missingKey: return 16 + case .invalidDeviceId: + return 20 } case .openSesame: return 17 @@ -276,6 +285,10 @@ extension ClientState { self = .openSesame case 18: self = .internalError("") + case 19: + self = .messageRejected(.invalidDeviceId) + case 20: + self = .responseRejected(.invalidDeviceId) default: self = .internalError("Unknown code \(code)") } diff --git a/Sesame/ContentView.swift b/Sesame/ContentView.swift index 3776ba9..d517cfd 100644 --- a/Sesame/ContentView.swift +++ b/Sesame/ContentView.swift @@ -17,6 +17,9 @@ struct ContentView: View { @AppStorage("local") private var useLocalConnection = false + + @AppStorage("deviceID") + private var deviceID: Int = 0 @State var keyManager = KeyManagement() @@ -133,6 +136,8 @@ struct ContentView: View { keyManager: $keyManager, serverAddress: $serverPath, localAddress: $localAddress, + deviceID: $deviceID, + nextMessageCounter: $nextMessageCounter, isCompensatingDaylightTime: $isCompensatingDaylightTime, useLocalConnection: $useLocalConnection) } @@ -145,7 +150,8 @@ struct ContentView: View { func mainButtonPressed() { 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 } @@ -154,7 +160,8 @@ struct ContentView: View { // Add time to compensate that the device is using daylight savings time let content = Message.Content( time: sentTime.timestamp + compensationTime, - id: count) + id: count, + device: deviceId) let message = content.authenticate(using: key) let historyItem = HistoryItem(sent: message.content, date: sentTime, local: useLocalConnection) state = .waitingForResponse diff --git a/Sesame/HistoryItem.swift b/Sesame/HistoryItem.swift index bfeac93..a975b0b 100644 --- a/Sesame/HistoryItem.swift +++ b/Sesame/HistoryItem.swift @@ -123,8 +123,8 @@ extension HistoryItem: Comparable { extension HistoryItem { static var mock: HistoryItem { - let content = Message.Content(time: Date.now.timestamp, id: 123) - let content2 = Message.Content(time: (Date.now + 1).timestamp, id: 124) + let content = Message.Content(time: Date.now.timestamp, id: 123, device: 0) + let content2 = Message.Content(time: (Date.now + 1).timestamp, id: 124, device: 0) return .init(sent: content, date: .now, local: false) .didReceive(response: .openSesame, date: .now + 2, message: content2) } diff --git a/Sesame/KeyManagement.swift b/Sesame/KeyManagement.swift index 4bd56af..9cbbe80 100644 --- a/Sesame/KeyManagement.swift +++ b/Sesame/KeyManagement.swift @@ -17,11 +17,11 @@ extension KeyManagement { var displayName: String { switch self { case .deviceKey: - return "Device Key" + return "Unlock Key" case .remoteKey: - return "Remote Key" + return "Response Key" case .authToken: - return "Authentication Token" + return "Server Token" } } @@ -148,6 +148,15 @@ final class KeyManagement: ObservableObject { func generate(_ type: KeyType) { 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) { keyChain.delete(type) } diff --git a/Sesame/SettingsView.swift b/Sesame/SettingsView.swift index c7d9820..e216dfc 100644 --- a/Sesame/SettingsView.swift +++ b/Sesame/SettingsView.swift @@ -10,6 +10,12 @@ struct SettingsView: View { @Binding var localAddress: String + + @Binding + var deviceID: Int + + @Binding + var nextMessageCounter: Int @Binding var isCompensatingDaylightTime: Bool @@ -17,6 +23,19 @@ struct SettingsView: View { @Binding 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 { NavigationView { ScrollView { @@ -25,12 +44,14 @@ struct SettingsView: View { Text("Server address") .bold() TextField("Server address", text: $serverAddress) + .foregroundColor(.secondary) .padding(.leading, 8) }.padding(.vertical, 8) VStack(alignment: .leading) { Text("Local address") .bold() TextField("Local address", text: $localAddress) + .foregroundColor(.secondary) .padding(.leading, 8) }.padding(.vertical, 8) 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.") .font(.caption) .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 SingleKeyView( keyManager: $keyManager, @@ -57,8 +104,54 @@ struct SettingsView: View { } } .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 { @@ -67,6 +160,8 @@ struct SettingsView_Previews: PreviewProvider { keyManager: .constant(KeyManagement()), serverAddress: .constant("https://example.com"), localAddress: .constant("192.168.178.42"), + deviceID: .constant(0), + nextMessageCounter: .constant(12345678), isCompensatingDaylightTime: .constant(true), useLocalConnection: .constant(false)) } diff --git a/Sesame/SingleKeyView.swift b/Sesame/SingleKeyView.swift index 06e5413..667c928 100644 --- a/Sesame/SingleKeyView.swift +++ b/Sesame/SingleKeyView.swift @@ -8,6 +8,12 @@ struct SingleKeyView: View { @Binding var keyManager: KeyManagement + + @State + private var showEditWindow = false + + @State + private var keyText = "" let type: KeyManagement.KeyType @@ -54,9 +60,41 @@ struct SingleKeyView: View { .disabled(!hasKey) .padding([.horizontal, .bottom]) .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() } } + .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( keyManager: .constant(KeyManagement()), type: .deviceKey) + .previewLayout(.fixed(width: 350, height: 100)) } }