Extract shared code

This commit is contained in:
Christoph Hagen
2022-04-13 14:55:22 +02:00
parent a4221c47f7
commit 4723c98502
10 changed files with 186 additions and 100 deletions

View File

@ -0,0 +1,85 @@
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)
}
/// 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?(_ buffer: ByteBuffer) {
guard let byte = buffer.getBytes(at: 0, length: 1) else {
print("No bytes received from device")
return nil
}
guard let event = MessageResult(rawValue: byte[0]) else {
print("Unknown response \(byte[0]) received from device")
return nil
}
self.event = event
guard let data = buffer.getSlice(at: 1, length: Message.length) else {
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

@ -0,0 +1,38 @@
import Foundation
#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif
extension Message {
static var length: Int {
SHA256.byteCount + Content.length
}
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)))
}
func isValid(using key: SymmetricKey) -> Bool {
HMAC<SHA256>.isValidAuthenticationCode(mac, authenticating: content.encoded, using: key)
}
}
extension Message.Content {
func authenticate(using key: SymmetricKey) -> Message {
let mac = HMAC<SHA256>.authenticationCode(for: encoded, using: key)
return .init(mac: Data(mac.map { $0 }), content: self)
}
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
}
}

122
Sesame/API/Message.swift Normal file
View File

@ -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<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
self.time = UInt32(data: data.prefix(MemoryLayout<UInt32>.size))
self.id = UInt32(data: data.dropFirst(MemoryLayout<UInt32>.size))
}
/// The byte length of an encoded message content
static var length: Int {
MemoryLayout<UInt32>.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<T: Sequence>(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)
}
}
}

View File

@ -0,0 +1,71 @@
import Foundation
/**
A result from sending a key to the device.
*/
enum MessageResult: UInt8 {
/// Text content was received, although binary data was expected
case textReceived = 1
/// 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 transmitted message could not be authenticated using the key
case messageAuthenticationFailed = 4
/// The message time was not within the acceptable bounds
case messageTimeMismatch = 5
/// A later key has been used, invalidating this key (to prevent replay attacks after blocked communication)
case messageCounterInvalid = 6
/// The key was accepted by the device, and the door will be opened
case messageAccepted = 7
/// The request did not contain body data with the key
case noBodyData = 10
/// The device is not connected
case deviceNotConnected = 12
/// The device did not respond within the timeout
case deviceTimedOut = 13
/// Another message is being processed by the device
case operationInProgress = 14
}
extension MessageResult: CustomStringConvertible {
var description: String {
switch self {
case .textReceived:
return "The device received unexpected text"
case .unexpectedSocketEvent:
return "Unexpected socket event for the device"
case .invalidMessageData:
return "Invalid message data"
case .messageAuthenticationFailed:
return "Message authentication failed"
case .messageTimeMismatch:
return "Message time invalid"
case .messageCounterInvalid:
return "Message counter invalid"
case .messageAccepted:
return "Message accepted"
case .noBodyData:
return "No body data included in the request"
case .deviceNotConnected:
return "Device not connected"
case .deviceTimedOut:
return "The device did not respond"
case .operationInProgress:
return "Another operation is in progress"
}
}
}

16
Sesame/API/RouteAPI.swift Normal file
View File

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