import Foundation /** The message content without authentication. */ struct Message: Equatable, Hashable { /// The type of message being sent. let messageType: MessageType /** * The random nonce created by the remote * * This nonce is a random number created by the remote, different for each unlock request. * It is set for all message types. */ let clientChallenge: UInt32 /** * A random number to sign by the remote * * This nonce is set by the server after receiving an initial message. * It is set for the message types `challenge`, `request`, and `response`. */ let serverChallenge: UInt32 /** * The response status for the previous message. * * It is set only for messages from the server, e.g. the `challenge` and `response` message types. * Must be set to `MessageAccepted` for other messages. */ let result: MessageResult init(messageType: MessageType, clientChallenge: UInt32, serverChallenge: UInt32, result: MessageResult) { self.messageType = messageType self.clientChallenge = clientChallenge self.serverChallenge = serverChallenge self.result = result } /** 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: Data) throws { guard data.count == Message.size else { print("Invalid message size \(data.count)") throw MessageResult.invalidMessageSizeFromDevice } guard let messageType = MessageType(rawValue: data.first!) else { print("Invalid message type \(data.first!)") throw MessageResult.invalidMessageTypeFromDevice } self.messageType = messageType self.clientChallenge = UInt32(data: data.dropFirst().prefix(UInt32.byteSize)) self.serverChallenge = UInt32(data: data.dropFirst(UInt32.byteSize+1).prefix(UInt32.byteSize)) guard let result = MessageResult(rawValue: data.last!) else { print("Invalid message result \(data.last!)") throw MessageResult.unknownMessageResultFromDevice } self.result = result } /// The message content encoded to data var encoded: Data { messageType.encoded + clientChallenge.encoded + serverChallenge.encoded + result.encoded } } extension Message: Codable { enum CodingKeys: Int, CodingKey { case messageType = 1 case clientChallenge = 2 case serverChallenge = 3 case result = 4 } } extension Message { init(error: MessageResult, type: MessageType) { self.init(messageType: type, clientChallenge: 0, serverChallenge: 0, result: error) } static func initial() -> Message { .init( messageType: .initial, clientChallenge: .random(), serverChallenge: 0, result: .messageAccepted) } func with(result: MessageResult) -> Message { .init( messageType: messageType.responseType, clientChallenge: clientChallenge, serverChallenge: serverChallenge, result: result) } /** Create the message to respond to this challenge */ func requestMessage() -> Message { .init( messageType: .request, clientChallenge: clientChallenge, serverChallenge: serverChallenge, result: .messageAccepted) } } extension Message: CustomStringConvertible { var description: String { "\(messageType)(\(clientChallenge)->\(serverChallenge), \(result))" } }