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)
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<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
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
static var length: Int {
MemoryLayout<UInt32>.size * 2
MemoryLayout<UInt32>.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
}
}

View File

@ -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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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