diff --git a/Sesame.xcodeproj/project.pbxproj b/Sesame.xcodeproj/project.pbxproj index 703d792..cfa6890 100644 --- a/Sesame.xcodeproj/project.pbxproj +++ b/Sesame.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ 884A45BB279F48C300D6E650 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 884A45BA279F48C300D6E650 /* Assets.xcassets */; }; 884A45BE279F48C300D6E650 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 884A45BD279F48C300D6E650 /* Preview Assets.xcassets */; }; 884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */; }; - 884A45C727A429EF00D6E650 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C627A429EF00D6E650 /* ShareSheet.swift */; }; 884A45C927A43D7900D6E650 /* ClientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C827A43D7900D6E650 /* ClientState.swift */; }; 884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */; }; 884A45CD27A465F500D6E650 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; }; @@ -22,6 +21,7 @@ E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = E24EE77627FF95C00011CFD2 /* NIOCore */; }; E24EE77927FF95E00011CFD2 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; }; E24EE77B280058240011CFD2 /* Message+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77A280058240011CFD2 /* Message+Extensions.swift */; }; + E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -31,7 +31,6 @@ 884A45BA279F48C300D6E650 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 884A45BD279F48C300D6E650 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagement.swift; sourceTree = ""; }; - 884A45C627A429EF00D6E650 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = ""; }; 884A45C827A43D7900D6E650 /* ClientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientState.swift; sourceTree = ""; }; 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SymmetricKey+Extensions.swift"; sourceTree = ""; }; 884A45CC27A465F500D6E650 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; @@ -40,6 +39,7 @@ E24EE77327FF95920011CFD2 /* DeviceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceResponse.swift; sourceTree = ""; }; E24EE77827FF95E00011CFD2 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; E24EE77A280058240011CFD2 /* Message+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+Extensions.swift"; sourceTree = ""; }; + E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteAPI.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -73,20 +73,16 @@ 884A45B5279F48C100D6E650 /* Sesame */ = { isa = PBXGroup; children = ( + E2C5C1D92806FE4A00769EF6 /* API */, 884A45B6279F48C100D6E650 /* SesameApp.swift */, - E24EE77827FF95E00011CFD2 /* Message.swift */, - E24EE77A280058240011CFD2 /* Message+Extensions.swift */, 884A45B8279F48C100D6E650 /* ContentView.swift */, 884A45CC27A465F500D6E650 /* Client.swift */, - 884A45CE27A5402D00D6E650 /* MessageResult.swift */, 884A45C827A43D7900D6E650 /* ClientState.swift */, - 884A45C627A429EF00D6E650 /* ShareSheet.swift */, 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */, - E24EE77327FF95920011CFD2 /* DeviceResponse.swift */, 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */, + E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */, 884A45BA279F48C300D6E650 /* Assets.xcassets */, 884A45BC279F48C300D6E650 /* Preview Content */, - E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */, ); path = Sesame; sourceTree = ""; @@ -99,6 +95,18 @@ path = "Preview Content"; sourceTree = ""; }; + E2C5C1D92806FE4A00769EF6 /* API */ = { + isa = PBXGroup; + children = ( + E24EE77327FF95920011CFD2 /* DeviceResponse.swift */, + E24EE77827FF95E00011CFD2 /* Message.swift */, + 884A45CE27A5402D00D6E650 /* MessageResult.swift */, + E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */, + E24EE77A280058240011CFD2 /* Message+Extensions.swift */, + ); + path = API; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -177,6 +185,7 @@ files = ( 884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */, 884A45B9279F48C100D6E650 /* ContentView.swift in Sources */, + E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */, 884A45CD27A465F500D6E650 /* Client.swift in Sources */, E24EE77B280058240011CFD2 /* Message+Extensions.swift in Sources */, E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */, @@ -186,7 +195,6 @@ 884A45C927A43D7900D6E650 /* ClientState.swift in Sources */, 884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */, 884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */, - 884A45C727A429EF00D6E650 /* ShareSheet.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sesame.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate b/Sesame.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate index 11df39e..e737ffa 100644 Binary files a/Sesame.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate and b/Sesame.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Sesame.xcodeproj/xcuserdata/ch.xcuserdatad/xcschemes/xcschememanagement.plist b/Sesame.xcodeproj/xcuserdata/ch.xcuserdatad/xcschemes/xcschememanagement.plist index 14b438d..db32b40 100644 --- a/Sesame.xcodeproj/xcuserdata/ch.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Sesame.xcodeproj/xcuserdata/ch.xcuserdatad/xcschemes/xcschememanagement.plist @@ -70,7 +70,7 @@ Sesame.xcscheme_^#shared#^_ orderHint - 1 + 0 diff --git a/Sesame/DeviceResponse.swift b/Sesame/API/DeviceResponse.swift similarity index 67% rename from Sesame/DeviceResponse.swift rename to Sesame/API/DeviceResponse.swift index 78cda5b..10e3749 100644 --- a/Sesame/DeviceResponse.swift +++ b/Sesame/API/DeviceResponse.swift @@ -1,29 +1,37 @@ 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 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) } @@ -34,6 +42,13 @@ struct DeviceResponse { /// 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?(_ buffer: ByteBuffer) { guard let byte = buffer.getBytes(at: 0, length: 1) else { print("No bytes received from device") @@ -51,11 +66,16 @@ struct DeviceResponse { 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]) diff --git a/Sesame/Message+Extensions.swift b/Sesame/API/Message+Extensions.swift similarity index 84% rename from Sesame/Message+Extensions.swift rename to Sesame/API/Message+Extensions.swift index 1e8f4df..38fb2b9 100644 --- a/Sesame/Message+Extensions.swift +++ b/Sesame/API/Message+Extensions.swift @@ -1,21 +1,19 @@ -// -// Message+Extensions.swift -// Sesame -// -// Created by CH on 08.04.22. -// - import Foundation + +#if canImport(CryptoKit) import CryptoKit +#else +import Crypto +#endif extension Message { static var length: Int { - SHA256Digest.byteCount + Content.length + SHA256.byteCount + Content.length } init(decodeFrom data: T) where T.Element == UInt8 { - let count = SHA256Digest.byteCount + let count = SHA256.byteCount self.mac = Data(data.prefix(count)) self.content = .init(decodeFrom: Array(data.dropFirst(count))) } diff --git a/Sesame/API/Message.swift b/Sesame/API/Message.swift new file mode 100644 index 0000000..60ede14 --- /dev/null +++ b/Sesame/API/Message.swift @@ -0,0 +1,122 @@ +import Foundation +import NIOCore + +/** + An authenticated message to or from the device. + */ +struct Message: Equatable, Hashable { + + /** + 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 + + /** + Create new message content. + - Parameter time: The time of message creation, + - Parameter id: The counter of the message + */ + init(time: UInt32, id: UInt32) { + self.time = time + self.id = id + } + + /** + Decode message content from data. + + The data consists of two `UInt32` encoded in big endian format (MSB at index 0) + - 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.prefix(MemoryLayout.size)) + self.id = UInt32(data: data.dropFirst(MemoryLayout.size)) + } + + /// The byte length of an encoded message content + static var length: Int { + MemoryLayout.size * 2 + } + + /// The message content encoded to data + var encoded: Data { + time.encoded + id.encoded + } + + /// The message content encoded to bytes + var bytes: [UInt8] { + time.bytes + id.bytes + } + } + + /// 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 + } + + /** + 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) + } + + /// The message encoded to data + var encoded: Data { + mac + content.encoded + } + + /// The message encoded to bytes + var bytes: [UInt8] { + Array(mac) + content.bytes + } +} + + extension UInt32 { + + /** + Create a value from a big-endian data representation (MSB first) + - Note: The data must contain exactly four bytes. + */ + init(data: T) where T.Element == UInt8 { + self = data + .reversed() + .enumerated() + .map { UInt32($0.element) << ($0.offset * 8) } + .reduce(0, +) + } + + /// The value encoded to a big-endian representation + var encoded: Data { + .init(bytes) + } + + /// The value encoded to a big-endian byte array + var bytes: [UInt8] { + (0..<4).reversed().map { + UInt8((self >> ($0*8)) & 0xFF) + } + } +} diff --git a/Sesame/MessageResult.swift b/Sesame/API/MessageResult.swift similarity index 100% rename from Sesame/MessageResult.swift rename to Sesame/API/MessageResult.swift diff --git a/Sesame/API/RouteAPI.swift b/Sesame/API/RouteAPI.swift new file mode 100644 index 0000000..22e90f0 --- /dev/null +++ b/Sesame/API/RouteAPI.swift @@ -0,0 +1,16 @@ +import Foundation + +/** + The active urls on the server, for the device and the remote to connect + */ +enum RouteAPI: String { + + /// Check the device status + case getDeviceStatus = "status" + + /// Send a message to the server, to relay to the device + case postMessage = "message" + + /// Open a socket between the device and the server + case socket = "listen" +} diff --git a/Sesame/Client.swift b/Sesame/Client.swift index c1fb623..74106c9 100644 --- a/Sesame/Client.swift +++ b/Sesame/Client.swift @@ -19,7 +19,7 @@ struct Client { } func deviceStatus() async -> ClientState { - let url = server.appendingPathComponent("status") + let url = server.appendingPathComponent(RouteAPI.getDeviceStatus.rawValue) let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData) let response = await integerReponse(to: request) switch response { @@ -42,7 +42,7 @@ struct Client { } func send(_ message: Message) async throws -> (state: ClientState, response: Message?) { - let url = server.appendingPathComponent("message") + let url = server.appendingPathComponent(RouteAPI.postMessage.rawValue) var request = URLRequest(url: url) request.httpBody = message.encoded request.httpMethod = "POST" diff --git a/Sesame/Message.swift b/Sesame/Message.swift deleted file mode 100644 index 415c818..0000000 --- a/Sesame/Message.swift +++ /dev/null @@ -1,78 +0,0 @@ -import Foundation -import NIOCore - -struct Message: Equatable, Hashable { - - struct Content: Equatable, Hashable { - - let time: UInt32 - - let id: UInt32 - - init(time: UInt32, id: UInt32) { - self.time = time - self.id = id - } - - init(decodeFrom data: T) where T.Element == UInt8 { - self.time = UInt32(data: data.prefix(4)) - self.id = UInt32(data: data.dropFirst(4)) - } - - static var length: Int { - MemoryLayout.size * 2 - } - - var encoded: Data { - time.encoded + id.encoded - } - - var bytes: [UInt8] { - time.bytes + id.bytes - } - } - - let mac: Data - - let content: Content - - init(mac: Data, content: Content) { - self.mac = mac - self.content = content - } - - init?(decodeFrom buffer: ByteBuffer) { - guard let data = buffer.getBytes(at: 0, length: Message.length) else { - return nil - } - self.init(decodeFrom: data) - } - - var encoded: Data { - mac + content.encoded - } - - var bytes: [UInt8] { - Array(mac) + content.bytes - } -} - - extension UInt32 { - - init(data: T) where T.Element == UInt8 { - self = data - .enumerated() - .map { UInt32($0.element) << ($0.offset * 8) } - .reduce(0, +) - } - - var encoded: Data { - .init(bytes) - } - - var bytes: [UInt8] { - (0..<4).map { - UInt8((self >> ($0*8)) & 0xFF) - } - } -}