Treat messages as data

This commit is contained in:
Christoph Hagen 2023-08-09 16:26:07 +02:00
parent 5d4adf8b15
commit 107b609aea
8 changed files with 53 additions and 345 deletions

View File

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

View File

@ -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<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.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 + 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<T: Sequence>(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<SHA256>.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<SHA256>.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<SHA256>.authenticationCode(for: encoded, using: key)
return Data(mac.map { $0 }) + encoded
}
}

View File

@ -11,8 +11,8 @@ enum MessageResult: UInt8 {
/// A socket event on the device was unexpected (not binary data) /// A socket event on the device was unexpected (not binary data)
case unexpectedSocketEvent = 2 case unexpectedSocketEvent = 2
/// The size of the payload (i.e. message) was invalid, or the data could not be read /// The size of the payload (i.e. message) was invalid
case invalidMessageData = 3 case invalidMessageSize = 3
/// The transmitted message could not be authenticated using the key /// The transmitted message could not be authenticated using the key
case messageAuthenticationFailed = 4 case messageAuthenticationFailed = 4
@ -44,6 +44,10 @@ enum MessageResult: UInt8 {
/// The device is connected /// The device is connected
case deviceConnected = 15 case deviceConnected = 15
case invalidUrlParameter = 20
case invalidResponseAuthentication = 21
} }
extension MessageResult: CustomStringConvertible { extension MessageResult: CustomStringConvertible {
@ -54,7 +58,7 @@ extension MessageResult: CustomStringConvertible {
return "The device received unexpected text" return "The device received unexpected text"
case .unexpectedSocketEvent: case .unexpectedSocketEvent:
return "Unexpected socket event for the device" return "Unexpected socket event for the device"
case .invalidMessageData: case .invalidMessageSize:
return "Invalid message data" return "Invalid message data"
case .messageAuthenticationFailed: case .messageAuthenticationFailed:
return "Message authentication failed" return "Message authentication failed"
@ -76,6 +80,17 @@ extension MessageResult: CustomStringConvertible {
return "Another operation is in progress" return "Another operation is in progress"
case .deviceConnected: case .deviceConnected:
return "The device is connected" 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])
}
}

View File

@ -11,11 +11,11 @@ struct ServerMessage {
static let authTokenSize = SHA256.byteCount static let authTokenSize = SHA256.byteCount
static let length = authTokenSize + Message.length static let maxLength = authTokenSize + 200
let authToken: Data let authToken: Data
let message: Message let message: Data
/** /**
Decode a message from a byte buffer. Decode a message from a byte buffer.
@ -23,15 +23,16 @@ struct ServerMessage {
- Parameter buffer: The buffer containing the bytes. - Parameter buffer: The buffer containing the bytes.
*/ */
init?(decodeFrom buffer: ByteBuffer) { 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 return nil
} }
self.authToken = Data(data.prefix(ServerMessage.authTokenSize)) self.authToken = Data(data.prefix(ServerMessage.authTokenSize))
self.message = Message(decodeFrom: Data(data.dropFirst(ServerMessage.authTokenSize))) self.message = Data(data.dropFirst(ServerMessage.authTokenSize))
}
var encoded: Data {
authToken + message.encoded
} }
static func token(from buffer: ByteBuffer) -> Data? { static func token(from buffer: ByteBuffer) -> Data? {

View File

@ -31,7 +31,7 @@ final class DeviceManager {
} }
/// A promise to finish the request once the device responds or times out /// A promise to finish the request once the device responds or times out
private var requestInProgress: EventLoopPromise<DeviceResponse>? private var requestInProgress: EventLoopPromise<Data>?
init(deviceKey: Data, remoteKey: Data, deviceTimeout: Int64) async { init(deviceKey: Data, remoteKey: Data, deviceTimeout: Int64) async {
self.deviceKey = deviceKey self.deviceKey = deviceKey
@ -66,24 +66,25 @@ final class DeviceManager {
deviceIsConnected ? "1" : "0" deviceIsConnected ? "1" : "0"
} }
func sendMessageToDevice(_ message: Message, on eventLoop: EventLoop) -> EventLoopFuture<DeviceResponse> { func sendMessageToDevice(_ message: Data, on eventLoop: EventLoop) -> EventLoopFuture<Data> {
guard let socket = connection, !socket.isClosed else { guard let socket = connection, !socket.isClosed else {
connection = nil connection = nil
return eventLoop.makeSucceededFuture(.deviceNotConnected) return eventLoop.makeSucceededFuture(MessageResult.deviceNotConnected.encoded)
} }
guard requestInProgress == nil else { 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 self.requestInProgress = result
socket.send(message.bytes, promise: nil) socket.send(Array(message), promise: nil)
updateMessageCountMetric() updateMessageCountMetric()
eventLoop.scheduleTask(in: .seconds(deviceTimeout)) { [weak self] in eventLoop.scheduleTask(in: .seconds(deviceTimeout)) { [weak self] in
guard let promise = self?.requestInProgress else { guard let promise = self?.requestInProgress else {
return return
} }
self?.requestInProgress = nil self?.requestInProgress = nil
promise.succeed(.deviceTimedOut) log("Timed out waiting for device response")
promise.succeed(MessageResult.deviceTimedOut.encoded)
} }
return result.futureResult return result.futureResult
} }
@ -116,9 +117,8 @@ final class DeviceManager {
return return
} }
defer { requestInProgress = nil } defer { requestInProgress = nil }
let response = DeviceResponse(data, request: RouteAPI.socket.rawValue) ?? .unexpectedSocketEvent
log("Device response received") log("Device response received")
promise.succeed(response) promise.succeed(data)
} }
func didCloseDeviceSocket() { func didCloseDeviceSocket() {

View File

@ -8,6 +8,13 @@ enum ServerError: Error {
case invalidAuthenticationToken case invalidAuthenticationToken
} }
private let dateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateStyle = .short
df.timeStyle = .short
return df
}()
// configures your application // configures your application
public func configure(_ app: Application) async throws { public func configure(_ app: Application) async throws {
let storageFolder = URL(fileURLWithPath: app.directory.resourcesDirectory) let storageFolder = URL(fileURLWithPath: app.directory.resourcesDirectory)
@ -50,6 +57,7 @@ public func configure(_ app: Application) async throws {
} }
try await status.update(.nominal) try await status.update(.nominal)
print("[\(dateFormatter.string(from: Date()))] Server started")
} }
private func loadKeys(at url: URL) throws -> (deviceKey: Data, remoteKey: Data) { private func loadKeys(at url: URL) throws -> (deviceKey: Data, remoteKey: Data) {

View File

@ -11,28 +11,28 @@ extension RouteAPI {
} }
} }
private func messageTransmission(_ req: Request) -> EventLoopFuture<DeviceResponse> { private func messageTransmission(_ req: Request) -> EventLoopFuture<Data> {
guard let body = req.body.data else { 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 { 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 { 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) return deviceManager.sendMessageToDevice(message.message, on: req.eventLoop)
} }
private func deviceStatus(_ req: Request) -> EventLoopFuture<DeviceResponse> { private func deviceStatus(_ req: Request) -> EventLoopFuture<MessageResult> {
guard let body = req.body.data else { guard let body = req.body.data else {
return req.eventLoop.makeSucceededFuture(.noBodyData) return req.eventLoop.makeSucceededFuture(.noBodyData)
} }
guard let authToken = ServerMessage.token(from: body) else { guard let authToken = ServerMessage.token(from: body) else {
return req.eventLoop.makeSucceededFuture(.invalidMessageData) return req.eventLoop.makeSucceededFuture(.invalidMessageSize)
} }
guard deviceManager.authenticateRemote(authToken) else { guard deviceManager.authenticateRemote(authToken) else {
return req.eventLoop.makeSucceededFuture(.invalidMessageData) return req.eventLoop.makeSucceededFuture(.messageAuthenticationFailed)
} }
guard deviceManager.deviceIsConnected else { guard deviceManager.deviceIsConnected else {
return req.eventLoop.makeSucceededFuture(.deviceNotConnected) 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 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`. 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 app.post(RouteAPI.getDeviceStatus.path) { req in
deviceStatus(req).map { deviceStatus(req).map {
@ -66,7 +66,7 @@ func routes(_ app: Application) throws {
*/ */
app.post(RouteAPI.postMessage.path) { req in app.post(RouteAPI.postMessage.path) { req in
messageTransmission(req).map { messageTransmission(req).map {
Response(status: .ok, body: .init(data: $0.encoded)) Response(status: .ok, body: .init(data: $0))
} }
} }

View File

@ -10,44 +10,4 @@ final class AppTests: XCTestCase {
XCTAssertEqual(input, output) 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?
}
} }