Use device ID to distinguish remotes

This commit is contained in:
Christoph Hagen 2023-08-07 15:47:40 +02:00
parent 8a17eef19b
commit 9b14f442b0
8 changed files with 185 additions and 12 deletions

View File

@ -49,15 +49,18 @@ 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,26 +72,28 @@ 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])
} }
} }
} }
extension Message.Content: Codable { 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
} }
} }

View File

@ -25,6 +25,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
@ -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:

View File

@ -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)")
} }

View File

@ -17,6 +17,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

View File

@ -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)
} }

View File

@ -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)
} }

View File

@ -10,6 +10,12 @@ 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
@ -17,6 +23,19 @@ struct SettingsView: View {
@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))
} }

View File

@ -8,6 +8,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
@ -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))
} }
} }