Add authentication for remote
This commit is contained in:
parent
e6fc0308ed
commit
aa0646ba87
@ -1 +0,0 @@
|
|||||||
access token
|
|
2
Resources/keys
Normal file
2
Resources/keys
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
access token
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000000
|
59
Sources/App/API/Data+Extensions.swift
Normal file
59
Sources/App/API/Data+Extensions.swift
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Data {
|
||||||
|
|
||||||
|
public var hexEncoded: String {
|
||||||
|
return map { String(format: "%02hhx", $0) }.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert 0 ... 9, a ... f, A ...F to their decimal value,
|
||||||
|
// return nil for all other input characters
|
||||||
|
private func decodeNibble(_ u: UInt16) -> UInt8? {
|
||||||
|
switch(u) {
|
||||||
|
case 0x30 ... 0x39:
|
||||||
|
return UInt8(u - 0x30)
|
||||||
|
case 0x41 ... 0x46:
|
||||||
|
return UInt8(u - 0x41 + 10)
|
||||||
|
case 0x61 ... 0x66:
|
||||||
|
return UInt8(u - 0x61 + 10)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init?(fromHexEncodedString string: String) {
|
||||||
|
let utf16 = string.utf16
|
||||||
|
self.init(capacity: utf16.count/2)
|
||||||
|
|
||||||
|
var i = utf16.startIndex
|
||||||
|
guard utf16.count % 2 == 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
while i != utf16.endIndex {
|
||||||
|
guard let hi = decodeNibble(utf16[i]),
|
||||||
|
let lo = decodeNibble(utf16[utf16.index(i, offsetBy: 1, limitedBy: utf16.endIndex)!]) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var value = hi << 4 + lo
|
||||||
|
self.append(&value, count: 1)
|
||||||
|
i = utf16.index(i, offsetBy: 2, limitedBy: utf16.endIndex)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Data {
|
||||||
|
|
||||||
|
|
||||||
|
func convert<T>(into value: T) -> T {
|
||||||
|
withUnsafeBytes {
|
||||||
|
$0.baseAddress!.load(as: T.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init<T>(from value: T) {
|
||||||
|
var target = value
|
||||||
|
self = Swift.withUnsafeBytes(of: &target) {
|
||||||
|
Data($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,11 @@ struct DeviceResponse {
|
|||||||
.init(event: .deviceNotConnected)
|
.init(event: .deviceNotConnected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shorthand property for a connected event.
|
||||||
|
static var deviceConnected: DeviceResponse {
|
||||||
|
.init(event: .deviceConnected)
|
||||||
|
}
|
||||||
|
|
||||||
/// Shorthand property for an unexpected socket event.
|
/// Shorthand property for an unexpected socket event.
|
||||||
static var unexpectedSocketEvent: DeviceResponse {
|
static var unexpectedSocketEvent: DeviceResponse {
|
||||||
.init(event: .unexpectedSocketEvent)
|
.init(event: .unexpectedSocketEvent)
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,36 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import NIOCore
|
import NIOCore
|
||||||
|
|
||||||
|
#if canImport(CryptoKit)
|
||||||
|
import CryptoKit
|
||||||
|
#else
|
||||||
|
import Crypto
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
An authenticated message to or from the device.
|
An authenticated message to or from the device.
|
||||||
*/
|
*/
|
||||||
struct Message: Equatable, Hashable {
|
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 {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The message content without authentication.
|
The message content without authentication.
|
||||||
*/
|
*/
|
||||||
@ -30,13 +55,13 @@ struct Message: Equatable, Hashable {
|
|||||||
/**
|
/**
|
||||||
Decode message content from data.
|
Decode message content from data.
|
||||||
|
|
||||||
The data consists of two `UInt32` encoded in big endian format (MSB at index 0)
|
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.
|
- Warning: The sequence must contain at least 8 bytes, or the function will crash.
|
||||||
- Parameter data: The sequence containing the bytes.
|
- Parameter data: The sequence containing the bytes.
|
||||||
*/
|
*/
|
||||||
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
|
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
|
||||||
self.time = UInt32(data: data.prefix(MemoryLayout<UInt32>.size))
|
self.time = UInt32(data: Data(data.prefix(MemoryLayout<UInt32>.size)))
|
||||||
self.id = UInt32(data: data.dropFirst(MemoryLayout<UInt32>.size))
|
self.id = UInt32(data: Data(data.dropFirst(MemoryLayout<UInt32>.size)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The byte length of an encoded message content
|
/// The byte length of an encoded message content
|
||||||
@ -48,27 +73,15 @@ struct Message: Equatable, Hashable {
|
|||||||
var encoded: Data {
|
var encoded: Data {
|
||||||
time.encoded + id.encoded
|
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
|
extension Message {
|
||||||
let content: Content
|
|
||||||
|
|
||||||
/**
|
/// The length of a message in bytes
|
||||||
Create an authenticated message
|
static var length: Int {
|
||||||
- Parameter mac: The message authentication code
|
SHA256.byteCount + Content.length
|
||||||
- Parameter content: The message content
|
|
||||||
*/
|
|
||||||
init(mac: Data, content: Content) {
|
|
||||||
self.mac = mac
|
|
||||||
self.content = content
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,35 +101,51 @@ struct Message: Equatable, Hashable {
|
|||||||
mac + content.encoded
|
mac + content.encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The message encoded to bytes
|
|
||||||
var bytes: [UInt8] {
|
var bytes: [UInt8] {
|
||||||
Array(mac) + content.bytes
|
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 UInt32 {
|
extension Message.Content {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Create a value from a big-endian data representation (MSB first)
|
Calculate an authentication code for the message content.
|
||||||
- Note: The data must contain exactly four bytes.
|
- Parameter key: The key to use to sign the content.
|
||||||
*/
|
- Returns: The new message signed with the key.
|
||||||
init<T: Sequence>(data: T) where T.Element == UInt8 {
|
*/
|
||||||
self = data
|
func authenticate(using key: SymmetricKey) -> Message {
|
||||||
.reversed()
|
let mac = HMAC<SHA256>.authenticationCode(for: encoded, using: key)
|
||||||
.enumerated()
|
return .init(mac: Data(mac.map { $0 }), content: self)
|
||||||
.map { UInt32($0.element) << ($0.offset * 8) }
|
}
|
||||||
.reduce(0, +)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The value encoded to a big-endian representation
|
/**
|
||||||
var encoded: Data {
|
Calculate an authentication code for the message content and convert everything to data.
|
||||||
.init(bytes)
|
- Parameter key: The key to use to sign the content.
|
||||||
}
|
- Returns: The new message signed with the key, serialized to bytes.
|
||||||
|
*/
|
||||||
/// The value encoded to a big-endian byte array
|
func authenticateAndSerialize(using key: SymmetricKey) -> Data {
|
||||||
var bytes: [UInt8] {
|
let encoded = self.encoded
|
||||||
(0..<4).reversed().map {
|
let mac = HMAC<SHA256>.authenticationCode(for: encoded, using: key)
|
||||||
UInt8((self >> ($0*8)) & 0xFF)
|
return Data(mac.map { $0 }) + encoded
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,9 @@ enum MessageResult: UInt8 {
|
|||||||
|
|
||||||
/// Another message is being processed by the device
|
/// Another message is being processed by the device
|
||||||
case operationInProgress = 14
|
case operationInProgress = 14
|
||||||
|
|
||||||
|
/// The device is connected
|
||||||
|
case deviceConnected = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MessageResult: CustomStringConvertible {
|
extension MessageResult: CustomStringConvertible {
|
||||||
@ -66,6 +69,8 @@ extension MessageResult: CustomStringConvertible {
|
|||||||
return "The device did not respond"
|
return "The device did not respond"
|
||||||
case .operationInProgress:
|
case .operationInProgress:
|
||||||
return "Another operation is in progress"
|
return "Another operation is in progress"
|
||||||
|
case .deviceConnected:
|
||||||
|
return "The device is connected"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
46
Sources/App/API/ServerMessage.swift
Normal file
46
Sources/App/API/ServerMessage.swift
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import Foundation
|
||||||
|
import NIOCore
|
||||||
|
|
||||||
|
#if canImport(CryptoKit)
|
||||||
|
import CryptoKit
|
||||||
|
#else
|
||||||
|
import Crypto
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct ServerMessage {
|
||||||
|
|
||||||
|
static let authTokenSize = SHA256.byteCount
|
||||||
|
|
||||||
|
static let length = authTokenSize + Message.length
|
||||||
|
|
||||||
|
let authToken: Data
|
||||||
|
|
||||||
|
let message: Message
|
||||||
|
|
||||||
|
/**
|
||||||
|
Decode a message from a byte buffer.
|
||||||
|
The buffer must contain at least `ServerMessage.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: ServerMessage.length) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.authToken = Data(data.prefix(ServerMessage.authTokenSize))
|
||||||
|
self.message = Message(decodeFrom: Data(data.dropFirst(ServerMessage.length)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoded: Data {
|
||||||
|
authToken + message.encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
static func token(from buffer: ByteBuffer) -> Data? {
|
||||||
|
guard buffer.readableBytes == authTokenSize else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let bytes = buffer.getBytes(at: 0, length: authTokenSize) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Data(bytes)
|
||||||
|
}
|
||||||
|
}
|
18
Sources/App/API/UInt32+Extensions.swift
Normal file
18
Sources/App/API/UInt32+Extensions.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension UInt32 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
Create a value from a little-endian data representation (MSB first)
|
||||||
|
- Note: The data must contain exactly four bytes.
|
||||||
|
*/
|
||||||
|
init(data: Data) {
|
||||||
|
let value = data.convert(into: UInt32.zero)
|
||||||
|
self = CFSwapInt32LittleToHost(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value encoded to a little-endian representation
|
||||||
|
var encoded: Data {
|
||||||
|
Data(from: CFSwapInt32HostToLittle(self))
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,9 @@ final class DeviceManager {
|
|||||||
/// The authentication token of the device for the socket connection
|
/// The authentication token of the device for the socket connection
|
||||||
private let deviceKey: String
|
private let deviceKey: String
|
||||||
|
|
||||||
|
/// The authentication token of the remote
|
||||||
|
private let remoteKey: Data
|
||||||
|
|
||||||
/// Indicate that the socket is fully initialized with an authorized device
|
/// Indicate that the socket is fully initialized with an authorized device
|
||||||
var deviceIsAuthenticated = false
|
var deviceIsAuthenticated = false
|
||||||
|
|
||||||
@ -23,8 +26,9 @@ 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<DeviceResponse>?
|
||||||
|
|
||||||
init(deviceKey: String) {
|
init(deviceKey: String, remoteKey: Data) {
|
||||||
self.deviceKey = deviceKey
|
self.deviceKey = deviceKey
|
||||||
|
self.remoteKey = remoteKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: API
|
// MARK: API
|
||||||
@ -64,6 +68,11 @@ final class DeviceManager {
|
|||||||
deviceIsAuthenticated = true
|
deviceIsAuthenticated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func authenticateRemote(_ token: Data) -> Bool {
|
||||||
|
let hash = SHA256.hash(data: token)
|
||||||
|
return hash == remoteKey
|
||||||
|
}
|
||||||
|
|
||||||
func processDeviceResponse(_ data: ByteBuffer) {
|
func processDeviceResponse(_ data: ByteBuffer) {
|
||||||
guard let promise = requestInProgress else {
|
guard let promise = requestInProgress else {
|
||||||
return
|
return
|
||||||
|
@ -2,15 +2,32 @@ import Vapor
|
|||||||
|
|
||||||
var deviceManager: DeviceManager!
|
var deviceManager: DeviceManager!
|
||||||
|
|
||||||
|
enum ServerError: Error {
|
||||||
|
case invalidAuthenticationFileContent
|
||||||
|
case invalidRemoteAuthenticationToken
|
||||||
|
}
|
||||||
|
|
||||||
// configures your application
|
// configures your application
|
||||||
public func configure(_ app: Application) throws {
|
public func configure(_ app: Application) throws {
|
||||||
app.http.server.configuration.port = Config.port
|
app.http.server.configuration.port = Config.port
|
||||||
|
|
||||||
let storageFolder = URL(fileURLWithPath: app.directory.resourcesDirectory)
|
let storageFolder = URL(fileURLWithPath: app.directory.resourcesDirectory)
|
||||||
let keyFile = storageFolder.appendingPathComponent(Config.keyFileName)
|
let keyFile = storageFolder.appendingPathComponent(Config.keyFileName)
|
||||||
let deviceKey = try String(contentsOf: keyFile)
|
let authContent = try String(contentsOf: keyFile)
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
deviceManager = DeviceManager(deviceKey: deviceKey)
|
.components(separatedBy: "\n")
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
guard authContent.count == 2 else {
|
||||||
|
throw ServerError.invalidAuthenticationFileContent
|
||||||
|
}
|
||||||
|
let deviceKey = authContent[0]
|
||||||
|
guard let remoteKey = Data(fromHexEncodedString: authContent[1]) else {
|
||||||
|
throw ServerError.invalidRemoteAuthenticationToken
|
||||||
|
}
|
||||||
|
guard remoteKey.count == SHA256.byteCount else {
|
||||||
|
throw ServerError.invalidRemoteAuthenticationToken
|
||||||
|
}
|
||||||
|
deviceManager = DeviceManager(deviceKey: deviceKey, remoteKey: remoteKey)
|
||||||
try routes(app)
|
try routes(app)
|
||||||
|
|
||||||
// Gracefully shut down by closing potentially open socket
|
// Gracefully shut down by closing potentially open socket
|
||||||
|
@ -15,25 +15,51 @@ private func messageTransmission(_ req: Request) -> EventLoopFuture<DeviceRespon
|
|||||||
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 message = Message(decodeFrom: body) else {
|
guard let message = ServerMessage(decodeFrom: body) else {
|
||||||
return req.eventLoop.makeSucceededFuture(.invalidMessageData)
|
return req.eventLoop.makeSucceededFuture(.invalidMessageData)
|
||||||
}
|
}
|
||||||
return deviceManager.sendMessageToDevice(message, on: req.eventLoop)
|
guard deviceManager.authenticateRemote(message.authToken) else {
|
||||||
|
return req.eventLoop.makeSucceededFuture(.invalidMessageData)
|
||||||
|
}
|
||||||
|
return deviceManager.sendMessageToDevice(message.message, on: req.eventLoop)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deviceStatus(_ req: Request) -> EventLoopFuture<DeviceResponse> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
guard deviceManager.authenticateRemote(authToken) else {
|
||||||
|
return req.eventLoop.makeSucceededFuture(.invalidMessageData)
|
||||||
|
}
|
||||||
|
guard deviceManager.deviceIsConnected else {
|
||||||
|
return req.eventLoop.makeSucceededFuture(.deviceNotConnected)
|
||||||
|
}
|
||||||
|
return req.eventLoop.makeSucceededFuture(.deviceConnected)
|
||||||
}
|
}
|
||||||
|
|
||||||
func routes(_ app: Application) throws {
|
func routes(_ app: Application) throws {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Get the connection status of the device.
|
Get the connection status of the device.
|
||||||
|
|
||||||
|
The request expects the authentication token of the remote in the body data of the POST request.
|
||||||
|
|
||||||
The response is a string of either "1" (connected) or "0" (disconnected)
|
The request returns one byte of data, which is the raw value of a `MessageResult`.
|
||||||
|
Possible results are `noBodyData`, `invalidMessageData`, `deviceNotConnected`, `deviceConnected`.
|
||||||
*/
|
*/
|
||||||
app.get(RouteAPI.getDeviceStatus.path) { req -> String in
|
app.post(RouteAPI.getDeviceStatus.path) { req in
|
||||||
deviceManager.deviceStatus
|
deviceStatus(req).map {
|
||||||
|
Response(status: .ok, body: .init(data: $0.encoded))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Post a message to the device for unlocking.
|
Post a message to the device for unlocking.
|
||||||
|
|
||||||
|
The expects a `ServerMessage` in the body data of the POST request, containing the valid remote authentication token and the message to send to the device.
|
||||||
|
|
||||||
The request returns one or `Message.length+1` bytes of data, where the first byte is the raw value of a `MessageResult`,
|
The request returns one or `Message.length+1` bytes of data, where the first byte is the raw value of a `MessageResult`,
|
||||||
and the optional following bytes contain the response message of the device. This request does not complete until either the device responds or the request times out. The timeout is specified by `KeyManagement.deviceTimeout`.
|
and the optional following bytes contain the response message of the device. This request does not complete until either the device responds or the request times out. The timeout is specified by `KeyManagement.deviceTimeout`.
|
||||||
|
@ -12,7 +12,7 @@ final class AppTests: XCTestCase {
|
|||||||
|
|
||||||
func testEncodingContent() {
|
func testEncodingContent() {
|
||||||
let input = Message.Content(time: 1234567890, id: 23456789)
|
let input = Message.Content(time: 1234567890, id: 23456789)
|
||||||
let data = input.bytes
|
let data = Array(input.encoded)
|
||||||
let output = Message.Content(decodeFrom: data)
|
let output = Message.Content(decodeFrom: data)
|
||||||
XCTAssertEqual(input, output)
|
XCTAssertEqual(input, output)
|
||||||
let data2 = [42, 42] + data
|
let data2 = [42, 42] + data
|
||||||
|
Loading…
Reference in New Issue
Block a user