From 107b609aea633ee568472cde51a0464e627c31a3 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Wed, 9 Aug 2023 16:26:07 +0200 Subject: [PATCH] Treat messages as data --- Sources/App/API/DeviceResponse.swift | 97 --------------- Sources/App/API/Message.swift | 179 --------------------------- Sources/App/API/MessageResult.swift | 21 +++- Sources/App/API/ServerMessage.swift | 17 +-- Sources/App/DeviceManager.swift | 18 +-- Sources/App/configure.swift | 8 ++ Sources/App/routes.swift | 18 +-- Tests/AppTests/AppTests.swift | 40 ------ 8 files changed, 53 insertions(+), 345 deletions(-) delete mode 100644 Sources/App/API/DeviceResponse.swift delete mode 100644 Sources/App/API/Message.swift diff --git a/Sources/App/API/DeviceResponse.swift b/Sources/App/API/DeviceResponse.swift deleted file mode 100644 index 77ccf06..0000000 --- a/Sources/App/API/DeviceResponse.swift +++ /dev/null @@ -1,97 +0,0 @@ -import Foundation -import NIOCore - -/** - Encapsulates a response from a device. - */ -struct DeviceResponse { - - /// Shorthand property for a timeout event. - static var deviceTimedOut: DeviceResponse { - .init(event: .deviceTimedOut) - } - - /// Shorthand property for a disconnected event. - static var deviceNotConnected: DeviceResponse { - .init(event: .deviceNotConnected) - } - - /// Shorthand property for a connected event. - static var deviceConnected: DeviceResponse { - .init(event: .deviceConnected) - } - - /// Shorthand property for an unexpected socket event. - static var unexpectedSocketEvent: DeviceResponse { - .init(event: .unexpectedSocketEvent) - } - - /// Shorthand property for an invalid message. - static var invalidMessageData: DeviceResponse { - .init(event: .invalidMessageData) - } - - /// Shorthand property for missing body data. - static var noBodyData: DeviceResponse { - .init(event: .noBodyData) - } - - /// Shorthand property for a busy connection - static var operationInProgress: DeviceResponse { - .init(event: .operationInProgress) - } - - /// The response to a key from the server - let event: MessageResult - - /// The index of the next key to use - let response: Message? - - /** - Decode a message from a buffer. - - The buffer must contain `Message.length+1` bytes. The first byte denotes the event type, - the remaining bytes contain the message. - - Parameter buffer: The buffer where the message bytes are stored - */ - init?(_ data: Data, request: String) { - guard let byte = data.first else { - log("\(request): No bytes received from device") - return nil - } - guard let event = MessageResult(rawValue: byte) else { - log("\(request): Unknown response \(byte) received from device") - return nil - } - self.event = event - let messageData = data.dropFirst() - guard !messageData.isEmpty else { - // TODO: Check if event should have response message - self.response = nil - return - } - guard messageData.count == Message.length else { - log("\(request): Insufficient message data received from device (expected \(Message.length), got \(messageData.count))") - self.response = nil - return - } - self.response = Message(decodeFrom: data) - } - - /** - Create a response from an event without a message from the device. - - Parameter event: The response from the device. - */ - init(event: MessageResult) { - self.event = event - self.response = nil - } - - /// Get the reponse encoded in bytes. - var encoded: Data { - guard let message = response else { - return Data([event.rawValue]) - } - return Data([event.rawValue]) + message.encoded - } -} diff --git a/Sources/App/API/Message.swift b/Sources/App/API/Message.swift deleted file mode 100644 index 90d2b2d..0000000 --- a/Sources/App/API/Message.swift +++ /dev/null @@ -1,179 +0,0 @@ -import Foundation -import NIOCore - -#if canImport(CryptoKit) -import CryptoKit -#else -import Crypto -#endif - -/** - An authenticated message to or from the device. - */ -struct Message: Equatable, Hashable { - - /// The message authentication code for the message (32 bytes) - let mac: Data - - /// The message content - let content: Content - - /** - Create an authenticated message - - Parameter mac: The message authentication code - - Parameter content: The message content - */ - init(mac: Data, content: Content) { - self.mac = mac - self.content = content - } -} - -extension Message: Codable { - - enum CodingKeys: Int, CodingKey { - case mac = 1 - case content = 2 - } -} - -extension Message { - - /** - The message content without authentication. - */ - struct Content: Equatable, Hashable { - - /// The time of message creation, in UNIX time (seconds since 1970) - let time: UInt32 - - /// 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, device: UInt8) { - self.time = time - self.id = id - self.deviceId = device - } - - /** - Decode message content from data. - - The data consists of two `UInt32` encoded in little endian format - - Warning: The sequence must contain at least 8 bytes, or the function will crash. - - Parameter data: The sequence containing the bytes. - */ - init(decodeFrom data: T) where T.Element == UInt8 { - self.time = UInt32(data: Data(data.prefix(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 + 1 - } - - /// The message content encoded to data - var encoded: Data { - time.encoded + id.encoded + Data([deviceId ?? 0]) - } - } -} - -extension Message.Content: Codable { - - enum CodingKeys: Int, CodingKey { - case time = 1 - case id = 2 - case deviceId = 3 - } -} - -extension Message { - - /// The length of a message in bytes - static var length: Int { - SHA256.byteCount + Content.length - } - - /** - Decode a message from a byte buffer. - The buffer must contain at least `Message.length` bytes, or it will return `nil`. - - Parameter buffer: The buffer containing the bytes. - */ - init?(decodeFrom buffer: ByteBuffer) { - guard let data = buffer.getBytes(at: 0, length: Message.length) else { - return nil - } - self.init(decodeFrom: data) - } - - init?(decodeFrom data: Data, index: inout Int) { - guard index + Message.length <= data.count else { - return nil - } - self.init(decodeFrom: data.advanced(by: index)) - index += Message.length - } - - /// The message encoded to data - var encoded: Data { - mac + content.encoded - } - - var bytes: [UInt8] { - Array(encoded) - } - - /** - Create a message from received bytes. - - Parameter data: The sequence of bytes - - Note: The sequence must contain at least `Message.length` bytes, or the function will crash. - */ - init(decodeFrom data: T) where T.Element == UInt8 { - let count = SHA256.byteCount - self.mac = Data(data.prefix(count)) - self.content = .init(decodeFrom: Array(data.dropFirst(count))) - } - - /** - Check if the message contains a valid authentication code - - Parameter key: The key used to sign the message. - - Returns: `true`, if the message is valid. - */ - func isValid(using key: SymmetricKey) -> Bool { - HMAC.isValidAuthenticationCode(mac, authenticating: content.encoded, using: key) - } -} - -extension Message.Content { - - /** - Calculate an authentication code for the message content. - - Parameter key: The key to use to sign the content. - - Returns: The new message signed with the key. - */ - func authenticate(using key: SymmetricKey) -> Message { - let mac = HMAC.authenticationCode(for: encoded, using: key) - return .init(mac: Data(mac.map { $0 }), content: self) - } - - /** - Calculate an authentication code for the message content and convert everything to data. - - Parameter key: The key to use to sign the content. - - Returns: The new message signed with the key, serialized to bytes. - */ - func authenticateAndSerialize(using key: SymmetricKey) -> Data { - let encoded = self.encoded - let mac = HMAC.authenticationCode(for: encoded, using: key) - return Data(mac.map { $0 }) + encoded - } -} diff --git a/Sources/App/API/MessageResult.swift b/Sources/App/API/MessageResult.swift index ccfbf71..1038dfa 100644 --- a/Sources/App/API/MessageResult.swift +++ b/Sources/App/API/MessageResult.swift @@ -11,8 +11,8 @@ enum MessageResult: UInt8 { /// A socket event on the device was unexpected (not binary data) case unexpectedSocketEvent = 2 - /// The size of the payload (i.e. message) was invalid, or the data could not be read - case invalidMessageData = 3 + /// The size of the payload (i.e. message) was invalid + case invalidMessageSize = 3 /// The transmitted message could not be authenticated using the key case messageAuthenticationFailed = 4 @@ -44,6 +44,10 @@ enum MessageResult: UInt8 { /// The device is connected case deviceConnected = 15 + + case invalidUrlParameter = 20 + + case invalidResponseAuthentication = 21 } extension MessageResult: CustomStringConvertible { @@ -54,7 +58,7 @@ extension MessageResult: CustomStringConvertible { return "The device received unexpected text" case .unexpectedSocketEvent: return "Unexpected socket event for the device" - case .invalidMessageData: + case .invalidMessageSize: return "Invalid message data" case .messageAuthenticationFailed: return "Message authentication failed" @@ -76,6 +80,17 @@ extension MessageResult: CustomStringConvertible { return "Another operation is in progress" case .deviceConnected: return "The device is connected" + case .invalidUrlParameter: + return "The url parameter could not be found" + case .invalidResponseAuthentication: + return "The response could not be authenticated" } } } + +extension MessageResult { + + var encoded: Data { + Data([rawValue]) + } +} diff --git a/Sources/App/API/ServerMessage.swift b/Sources/App/API/ServerMessage.swift index ce84f55..7dfa873 100644 --- a/Sources/App/API/ServerMessage.swift +++ b/Sources/App/API/ServerMessage.swift @@ -11,11 +11,11 @@ struct ServerMessage { static let authTokenSize = SHA256.byteCount - static let length = authTokenSize + Message.length + static let maxLength = authTokenSize + 200 let authToken: Data - let message: Message + let message: Data /** Decode a message from a byte buffer. @@ -23,15 +23,16 @@ struct ServerMessage { - Parameter buffer: The buffer containing the bytes. */ init?(decodeFrom buffer: ByteBuffer) { - guard let data = buffer.getBytes(at: 0, length: ServerMessage.length) else { + guard buffer.readableBytes < ServerMessage.maxLength else { + log("Received invalid message with \(buffer.readableBytes) bytes") + return nil + } + guard let data = buffer.getBytes(at: 0, length: buffer.readableBytes) else { + log("Failed to read bytes of received message") return nil } self.authToken = Data(data.prefix(ServerMessage.authTokenSize)) - self.message = Message(decodeFrom: Data(data.dropFirst(ServerMessage.authTokenSize))) - } - - var encoded: Data { - authToken + message.encoded + self.message = Data(data.dropFirst(ServerMessage.authTokenSize)) } static func token(from buffer: ByteBuffer) -> Data? { diff --git a/Sources/App/DeviceManager.swift b/Sources/App/DeviceManager.swift index 0a7255a..963a891 100644 --- a/Sources/App/DeviceManager.swift +++ b/Sources/App/DeviceManager.swift @@ -31,7 +31,7 @@ final class DeviceManager { } /// A promise to finish the request once the device responds or times out - private var requestInProgress: EventLoopPromise? + private var requestInProgress: EventLoopPromise? init(deviceKey: Data, remoteKey: Data, deviceTimeout: Int64) async { self.deviceKey = deviceKey @@ -66,24 +66,25 @@ final class DeviceManager { deviceIsConnected ? "1" : "0" } - func sendMessageToDevice(_ message: Message, on eventLoop: EventLoop) -> EventLoopFuture { + func sendMessageToDevice(_ message: Data, on eventLoop: EventLoop) -> EventLoopFuture { guard let socket = connection, !socket.isClosed else { connection = nil - return eventLoop.makeSucceededFuture(.deviceNotConnected) + return eventLoop.makeSucceededFuture(MessageResult.deviceNotConnected.encoded) } guard requestInProgress == nil else { - return eventLoop.makeSucceededFuture(.operationInProgress) + return eventLoop.makeSucceededFuture(MessageResult.operationInProgress.encoded) } - let result = eventLoop.makePromise(of: DeviceResponse.self) + let result = eventLoop.makePromise(of: Data.self) self.requestInProgress = result - socket.send(message.bytes, promise: nil) + socket.send(Array(message), promise: nil) updateMessageCountMetric() eventLoop.scheduleTask(in: .seconds(deviceTimeout)) { [weak self] in guard let promise = self?.requestInProgress else { return } self?.requestInProgress = nil - promise.succeed(.deviceTimedOut) + log("Timed out waiting for device response") + promise.succeed(MessageResult.deviceTimedOut.encoded) } return result.futureResult } @@ -116,9 +117,8 @@ final class DeviceManager { return } defer { requestInProgress = nil } - let response = DeviceResponse(data, request: RouteAPI.socket.rawValue) ?? .unexpectedSocketEvent log("Device response received") - promise.succeed(response) + promise.succeed(data) } func didCloseDeviceSocket() { diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 3e6bb98..194b45b 100755 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -8,6 +8,13 @@ enum ServerError: Error { case invalidAuthenticationToken } +private let dateFormatter: DateFormatter = { + let df = DateFormatter() + df.dateStyle = .short + df.timeStyle = .short + return df +}() + // configures your application public func configure(_ app: Application) async throws { let storageFolder = URL(fileURLWithPath: app.directory.resourcesDirectory) @@ -50,6 +57,7 @@ public func configure(_ app: Application) async throws { } try await status.update(.nominal) + print("[\(dateFormatter.string(from: Date()))] Server started") } private func loadKeys(at url: URL) throws -> (deviceKey: Data, remoteKey: Data) { diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 0c5531c..f61f062 100755 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -11,28 +11,28 @@ extension RouteAPI { } } -private func messageTransmission(_ req: Request) -> EventLoopFuture { +private func messageTransmission(_ req: Request) -> EventLoopFuture { guard let body = req.body.data else { - return req.eventLoop.makeSucceededFuture(.noBodyData) + return req.eventLoop.makeSucceededFuture(MessageResult.noBodyData.encoded) } guard let message = ServerMessage(decodeFrom: body) else { - return req.eventLoop.makeSucceededFuture(.invalidMessageData) + return req.eventLoop.makeSucceededFuture(MessageResult.invalidMessageSize.encoded) } guard deviceManager.authenticateRemote(message.authToken) else { - return req.eventLoop.makeSucceededFuture(.invalidMessageData) + return req.eventLoop.makeSucceededFuture(MessageResult.messageAuthenticationFailed.encoded) } return deviceManager.sendMessageToDevice(message.message, on: req.eventLoop) } -private func deviceStatus(_ req: Request) -> EventLoopFuture { +private func deviceStatus(_ req: Request) -> EventLoopFuture { guard let body = req.body.data else { return req.eventLoop.makeSucceededFuture(.noBodyData) } guard let authToken = ServerMessage.token(from: body) else { - return req.eventLoop.makeSucceededFuture(.invalidMessageData) + return req.eventLoop.makeSucceededFuture(.invalidMessageSize) } guard deviceManager.authenticateRemote(authToken) else { - return req.eventLoop.makeSucceededFuture(.invalidMessageData) + return req.eventLoop.makeSucceededFuture(.messageAuthenticationFailed) } guard deviceManager.deviceIsConnected else { return req.eventLoop.makeSucceededFuture(.deviceNotConnected) @@ -48,7 +48,7 @@ func routes(_ app: Application) throws { The request expects the authentication token of the remote in the body data of the POST request. The request returns one byte of data, which is the raw value of a `MessageResult`. - Possible results are `noBodyData`, `invalidMessageData`, `deviceNotConnected`, `deviceConnected`. + Possible results are `noBodyData`, `invalidMessageSize`, `deviceNotConnected`, `deviceConnected`. */ app.post(RouteAPI.getDeviceStatus.path) { req in deviceStatus(req).map { @@ -66,7 +66,7 @@ func routes(_ app: Application) throws { */ app.post(RouteAPI.postMessage.path) { req in messageTransmission(req).map { - Response(status: .ok, body: .init(data: $0.encoded)) + Response(status: .ok, body: .init(data: $0)) } } diff --git a/Tests/AppTests/AppTests.swift b/Tests/AppTests/AppTests.swift index 7d12bd8..913872e 100644 --- a/Tests/AppTests/AppTests.swift +++ b/Tests/AppTests/AppTests.swift @@ -10,44 +10,4 @@ final class AppTests: XCTestCase { XCTAssertEqual(input, output) } - func testEncodingContent() { - let input = Message.Content(time: 1234567890, id: 23456789, device: 0) - let data = Array(input.encoded) - let output = Message.Content(decodeFrom: data) - XCTAssertEqual(input, output) - let data2 = [42, 42] + data - let output2 = Message.Content(decodeFrom: data2[2...]) - XCTAssertEqual(input, output2) - } - - func testEncodingMessage() { - let input = Message(mac: Data(repeating: 42, count: 32), - content: Message.Content(time: 1234567890, id: 23456789, device: 0)) - let data = input.encoded - let buffer = ByteBuffer(data: data) - let output = Message(decodeFrom: buffer) - XCTAssertEqual(input, output) - } - - func testSigning() throws { - let key = SymmetricKey(size: .bits256) - let content = Message.Content(time: 1234567890, id: 23456789, device: 0) - let input = content.authenticate(using: key) - XCTAssertTrue(input.isValid(using: key)) - - let data = content.authenticateAndSerialize(using: key) - let decoded = Message(decodeFrom: ByteBuffer(data: data)) - XCTAssertNotNil(decoded) - XCTAssertTrue(decoded!.isValid(using: key)) - XCTAssertEqual(decoded!, input) - XCTAssertEqual(content, input.content) - } - - func testMessageTransmission() async throws { - let app = Application(.testing) - defer { app.shutdown() } - try await configure(app) - - // How to open a socket via request? - } }