Change to new message format
This commit is contained in:
parent
9ab32cc758
commit
f9039c7b3a
@ -1,10 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
enum PublicAPI: String {
|
||||
case getDeviceResponse = "response"
|
||||
case getDeviceStatus = "status"
|
||||
case clearKeyRequest = "clear"
|
||||
case postKey = "key"
|
||||
case postKeyIdParameter = "id"
|
||||
case postMessage = "message"
|
||||
case socket = "listen"
|
||||
}
|
||||
|
@ -2,22 +2,18 @@ import Foundation
|
||||
import WebSocketKit
|
||||
import Vapor
|
||||
|
||||
final class KeyManagement {
|
||||
|
||||
/// The security parameter for the keys (in bits)
|
||||
private static let keySecurity = 128
|
||||
|
||||
/// The size of the individual keys in bytes
|
||||
static let keySize = keySecurity / 8
|
||||
final class DeviceManager {
|
||||
|
||||
/// The seconds to wait for a response from the device
|
||||
static let deviceTimeout: Int64 = 20
|
||||
|
||||
/// The connection to the device
|
||||
private var connection: WebSocket?
|
||||
|
||||
|
||||
/// The authentication token of the device for the socket connection
|
||||
private let deviceKey: String
|
||||
|
||||
|
||||
/// Indicate that the socket is fully initialized with an authorized device
|
||||
var deviceIsAuthenticated = false
|
||||
|
||||
/// Indicator for device availability
|
||||
@ -25,8 +21,8 @@ final class KeyManagement {
|
||||
!(connection?.isClosed ?? true) && deviceIsAuthenticated
|
||||
}
|
||||
|
||||
/// The id of the key which was sent to the device
|
||||
private var keyInTransit: (id: UInt16, promise: EventLoopPromise<KeyResult>)?
|
||||
/// A promise to finish the request once the device responds or times out
|
||||
private var requestInProgress: EventLoopPromise<DeviceResponse>?
|
||||
|
||||
init(deviceKey: String) {
|
||||
self.deviceKey = deviceKey
|
||||
@ -38,26 +34,24 @@ final class KeyManagement {
|
||||
deviceIsConnected ? "1" : "0"
|
||||
}
|
||||
|
||||
func sendKeyToDevice(_ key: Data, keyId: UInt16, on eventLoop: EventLoop) -> EventLoopFuture<KeyResult> {
|
||||
guard key.count == KeyManagement.keySize else {
|
||||
return eventLoop.makeSucceededFuture(.invalidPayloadSize)
|
||||
}
|
||||
func sendMessageToDevice(_ message: Message, on eventLoop: EventLoop) -> EventLoopFuture<DeviceResponse> {
|
||||
guard let socket = connection, !socket.isClosed else {
|
||||
connection = nil
|
||||
return eventLoop.makeSucceededFuture(.deviceNotConnected)
|
||||
}
|
||||
let keyIdData = [UInt8(keyId >> 8), UInt8(keyId & 0xFF)]
|
||||
let promise = eventLoop.makePromise(of: KeyResult.self)
|
||||
keyInTransit = (keyId, promise)
|
||||
socket.send(keyIdData + key, promise: nil)
|
||||
guard requestInProgress == nil else {
|
||||
return eventLoop.makeSucceededFuture(.operationInProgress)
|
||||
}
|
||||
requestInProgress = eventLoop.makePromise(of: DeviceResponse.self)
|
||||
socket.send(message.bytes, promise: nil)
|
||||
eventLoop.scheduleTask(in: .seconds(Self.deviceTimeout)) { [weak self] in
|
||||
guard let (storedKeyId, promise) = self?.keyInTransit, storedKeyId == keyId else {
|
||||
guard let promise = self?.requestInProgress else {
|
||||
return
|
||||
}
|
||||
self?.keyInTransit = nil
|
||||
self?.requestInProgress = nil
|
||||
promise.succeed(.deviceTimedOut)
|
||||
}
|
||||
return promise.futureResult
|
||||
return requestInProgress!.futureResult
|
||||
}
|
||||
|
||||
func authenticateDevice(psk: String) {
|
||||
@ -72,27 +66,12 @@ final class KeyManagement {
|
||||
}
|
||||
|
||||
func processDeviceResponse(_ data: ByteBuffer) {
|
||||
guard let (_, promise) = keyInTransit else {
|
||||
print("No key in transit for response from device \(data)")
|
||||
guard let promise = requestInProgress else {
|
||||
print("No message in transit for response from device \(data)")
|
||||
return
|
||||
}
|
||||
defer { keyInTransit = nil }
|
||||
guard data.readableBytes == 1 else {
|
||||
print("Unexpected number of bytes received from device")
|
||||
promise.succeed(.unexpectedSocketEvent)
|
||||
return
|
||||
}
|
||||
guard let rawValue = data.getBytes(at: 0, length: 1)?.first else {
|
||||
print("Unreadable data received from device")
|
||||
promise.succeed(.unexpectedSocketEvent)
|
||||
return
|
||||
}
|
||||
guard let response = KeyResult(rawValue: rawValue) else {
|
||||
print("Unknown response \(rawValue) received from device")
|
||||
promise.succeed(.unexpectedSocketEvent)
|
||||
return
|
||||
}
|
||||
promise.succeed(response)
|
||||
defer { requestInProgress = nil }
|
||||
promise.succeed(DeviceResponse(data) ?? .unexpectedSocketEvent)
|
||||
}
|
||||
|
||||
func didCloseDeviceSocket() {
|
69
Sources/App/DeviceResponse.swift
Normal file
69
Sources/App/DeviceResponse.swift
Normal file
@ -0,0 +1,69 @@
|
||||
import Foundation
|
||||
import NIOCore
|
||||
|
||||
|
||||
struct DeviceResponse {
|
||||
|
||||
static var deviceTimedOut: DeviceResponse {
|
||||
.init(event: .deviceTimedOut)
|
||||
}
|
||||
|
||||
static var deviceNotConnected: DeviceResponse {
|
||||
.init(event: .deviceNotConnected)
|
||||
}
|
||||
|
||||
static var unexpectedSocketEvent: DeviceResponse {
|
||||
.init(event: .unexpectedSocketEvent)
|
||||
}
|
||||
|
||||
static var invalidMessageData: DeviceResponse {
|
||||
.init(event: .invalidMessageData)
|
||||
}
|
||||
|
||||
static var noBodyData: DeviceResponse {
|
||||
.init(event: .noBodyData)
|
||||
}
|
||||
|
||||
static var corruptkeyData: DeviceResponse {
|
||||
.init(event: .corruptkeyData)
|
||||
}
|
||||
|
||||
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?
|
||||
|
||||
init?(_ buffer: ByteBuffer) {
|
||||
guard let byte = buffer.getData(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)
|
||||
}
|
||||
|
||||
init(event: MessageResult) {
|
||||
self.event = event
|
||||
self.response = nil
|
||||
}
|
||||
|
||||
var encoded: Data {
|
||||
guard let message = response else {
|
||||
return Data([event.rawValue])
|
||||
}
|
||||
return Data([event.rawValue]) + message.encoded
|
||||
}
|
||||
}
|
107
Sources/App/Message.swift
Normal file
107
Sources/App/Message.swift
Normal file
@ -0,0 +1,107 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import NIOCore
|
||||
import Vapor
|
||||
|
||||
struct Message: Equatable, Hashable {
|
||||
|
||||
static var length: Int {
|
||||
SHA256Digest.byteCount + Content.length
|
||||
}
|
||||
|
||||
struct Content: Equatable, Hashable {
|
||||
|
||||
let time: UInt32
|
||||
|
||||
let id: UInt32
|
||||
|
||||
init(time: UInt32, id: UInt32) {
|
||||
self.time = time
|
||||
self.id = id
|
||||
}
|
||||
|
||||
init(decodeFrom data: Data) {
|
||||
self.time = UInt32(data: data[data.startIndex..<data.startIndex+4])
|
||||
self.id = UInt32(data: data[data.startIndex+4..<data.startIndex+8])
|
||||
}
|
||||
|
||||
static var length: Int {
|
||||
MemoryLayout<UInt32>.size * 2
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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.getData(at: 0, length: Message.length) else {
|
||||
return nil
|
||||
}
|
||||
self.init(decodeFrom: data)
|
||||
}
|
||||
|
||||
private init(decodeFrom data: Data) {
|
||||
let count = SHA256Digest.byteCount
|
||||
self.mac = data[data.startIndex..<data.startIndex+count]
|
||||
self.content = .init(decodeFrom: data.advanced(by: count))
|
||||
}
|
||||
|
||||
func isValid(using key: SymmetricKey) -> Bool {
|
||||
HMAC<SHA256>.isValidAuthenticationCode(mac, authenticating: content.encoded, using: key)
|
||||
}
|
||||
|
||||
var encoded: Data {
|
||||
mac + content.encoded
|
||||
}
|
||||
|
||||
var bytes: [UInt8] {
|
||||
Array(mac) + content.bytes
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt32 {
|
||||
|
||||
init(data: Data) {
|
||||
self = data
|
||||
.reversed()
|
||||
.enumerated()
|
||||
.map { UInt32($0.element) << ($0.offset * 8) }
|
||||
.reduce(0, +)
|
||||
|
||||
}
|
||||
|
||||
var encoded: Data {
|
||||
.init(bytes)
|
||||
}
|
||||
|
||||
var bytes: [UInt8] {
|
||||
(0..<4).reversed().map {
|
||||
UInt8((self >> ($0*8)) & 0xFF)
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ import Foundation
|
||||
/**
|
||||
A result from sending a key to the device.
|
||||
*/
|
||||
enum KeyResult: UInt8 {
|
||||
enum MessageResult: UInt8 {
|
||||
|
||||
/// Text content was received, although binary data was expected
|
||||
case textReceived = 1
|
||||
@ -11,24 +11,22 @@ enum KeyResult: UInt8 {
|
||||
/// A socket event on the device was unexpected (not binary data)
|
||||
case unexpectedSocketEvent = 2
|
||||
|
||||
/// The size of the payload (key id + key data, or just key) was invalid
|
||||
case invalidPayloadSize = 3
|
||||
/// 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 index of the key was out of bounds
|
||||
case invalidKeyIndex = 4
|
||||
|
||||
/// The transmitted key data did not match the expected key
|
||||
case invalidKey = 5
|
||||
|
||||
/// The key has been previously used and is no longer valid
|
||||
case keyAlreadyUsed = 6
|
||||
/// 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 keyWasSkipped = 7
|
||||
case messageCounterInvalid = 6
|
||||
|
||||
/// The key was accepted by the device, and the door will be opened
|
||||
case keyAccepted = 8
|
||||
|
||||
case messageAccepted = 7
|
||||
|
||||
|
||||
/// The device produced an unknown error
|
||||
case unknownDeviceError = 9
|
||||
|
||||
@ -43,38 +41,41 @@ enum KeyResult: UInt8 {
|
||||
|
||||
/// The device did not respond within the timeout
|
||||
case deviceTimedOut = 13
|
||||
|
||||
/// Another message is being processed by the device
|
||||
case operationInProgress = 14
|
||||
}
|
||||
|
||||
extension KeyResult: CustomStringConvertible {
|
||||
extension MessageResult: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .invalidKeyIndex:
|
||||
return "Invalid key id (too large)"
|
||||
case .noBodyData:
|
||||
return "No body data included in the request"
|
||||
case .invalidPayloadSize:
|
||||
return "Invalid key size"
|
||||
case .corruptkeyData:
|
||||
return "Key data corrupted"
|
||||
case .deviceNotConnected:
|
||||
return "Device not connected"
|
||||
case .textReceived:
|
||||
return "The device received unexpected text"
|
||||
case .unexpectedSocketEvent:
|
||||
return "Unexpected socket event for the device"
|
||||
case .invalidKey:
|
||||
return "The transmitted key was not correct"
|
||||
case .keyAlreadyUsed:
|
||||
return "The transmitted key was already used"
|
||||
case .keyWasSkipped:
|
||||
return "A newer key was already used"
|
||||
case .keyAccepted:
|
||||
return "Key successfully sent"
|
||||
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 .unknownDeviceError:
|
||||
return "The device experienced an unknown error"
|
||||
case .noBodyData:
|
||||
return "No body data included in the request"
|
||||
case .corruptkeyData:
|
||||
return "Key data corrupted"
|
||||
case .deviceNotConnected:
|
||||
return "Device not connected"
|
||||
case .deviceTimedOut:
|
||||
return "The device did not respond"
|
||||
case .operationInProgress:
|
||||
return "Another operation is in progress"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +1,22 @@
|
||||
import Vapor
|
||||
|
||||
var keyManager: KeyManagement!
|
||||
var deviceManager: DeviceManager!
|
||||
|
||||
// configures your application
|
||||
public func configure(_ app: Application) throws {
|
||||
app.http.server.configuration.port = 6003
|
||||
|
||||
let storageFolder = URL(fileURLWithPath: app.directory.resourcesDirectory)
|
||||
let keyFile = storageFolder.appendingPathComponent("device.key")
|
||||
let keyFile = storageFolder.appendingPathComponent("deviceKey")
|
||||
let deviceKey = try String(contentsOf: keyFile)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
keyManager = KeyManagement(deviceKey: deviceKey)
|
||||
deviceManager = DeviceManager(deviceKey: deviceKey)
|
||||
try routes(app)
|
||||
|
||||
// Gracefully shut down by closing potentially open socket
|
||||
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + .seconds(5)) {
|
||||
_ = app.server.onShutdown.always { _ in
|
||||
keyManager.removeDeviceConnection()
|
||||
deviceManager.removeDeviceConnection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,20 +11,14 @@ extension PublicAPI {
|
||||
}
|
||||
}
|
||||
|
||||
private func keyTransmission(_ req: Request) -> EventLoopFuture<KeyResult> {
|
||||
guard let keyId = req.parameters.get(PublicAPI.postKeyIdParameter.rawValue, as: UInt16.self) else {
|
||||
return req.eventLoop.makeSucceededFuture(.invalidKeyIndex)
|
||||
}
|
||||
private func messageTransmission(_ req: Request) -> EventLoopFuture<DeviceResponse> {
|
||||
guard let body = req.body.data else {
|
||||
return req.eventLoop.makeSucceededFuture(.noBodyData)
|
||||
}
|
||||
guard body.readableBytes == KeyManagement.keySize else {
|
||||
return req.eventLoop.makeSucceededFuture(.invalidPayloadSize)
|
||||
guard let message = Message(decodeFrom: body) else {
|
||||
return req.eventLoop.makeSucceededFuture(.invalidMessageData)
|
||||
}
|
||||
guard let key = body.getData(at: 0, length: KeyManagement.keySize) else {
|
||||
return req.eventLoop.makeSucceededFuture(.corruptkeyData)
|
||||
}
|
||||
return keyManager.sendKeyToDevice(key, keyId: keyId, on: req.eventLoop)
|
||||
return deviceManager.sendMessageToDevice(message, on: req.eventLoop)
|
||||
}
|
||||
|
||||
func routes(_ app: Application) throws {
|
||||
@ -35,20 +29,19 @@ func routes(_ app: Application) throws {
|
||||
The response is a string of either "1" (connected) or "0" (disconnected)
|
||||
*/
|
||||
app.get(PublicAPI.getDeviceStatus.path) { req -> String in
|
||||
keyManager.deviceStatus
|
||||
deviceManager.deviceStatus
|
||||
}
|
||||
|
||||
/**
|
||||
Post a key to the device for unlocking.
|
||||
|
||||
The corresponding integer key id for the key data must be contained in the url path.
|
||||
|
||||
The request returns a string containing a `rawValue` of a `KeyPostResponse`
|
||||
A success of this method does not yet signal successful unlocking.
|
||||
The client should request the status by inquiring the device response.
|
||||
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`.
|
||||
*/
|
||||
app.post(PublicAPI.postKey.path, PublicAPI.postKeyIdParameter.pathParameter) { req in
|
||||
keyTransmission(req).map { String($0.rawValue) }
|
||||
app.post(PublicAPI.postMessage.path) { req in
|
||||
messageTransmission(req).map {
|
||||
Response(status: .ok, body: .init(data: $0.encoded))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,15 +51,15 @@ func routes(_ app: Application) throws {
|
||||
*/
|
||||
app.webSocket(PublicAPI.socket.path) { req, socket in
|
||||
socket.onBinary { _, data in
|
||||
keyManager.processDeviceResponse(data)
|
||||
deviceManager.processDeviceResponse(data)
|
||||
}
|
||||
socket.onText { _, text in
|
||||
keyManager.authenticateDevice(psk: text)
|
||||
deviceManager.authenticateDevice(psk: text)
|
||||
}
|
||||
|
||||
_ = socket.onClose.always { _ in
|
||||
keyManager.didCloseDeviceSocket()
|
||||
deviceManager.didCloseDeviceSocket()
|
||||
}
|
||||
keyManager.createNewDeviceConnection(socket)
|
||||
deviceManager.createNewDeviceConnection(socket)
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,44 @@
|
||||
import XCTVapor
|
||||
|
||||
final class AppTests: XCTestCase {
|
||||
func testHelloWorld() throws {
|
||||
let app = Application(.testing)
|
||||
defer { app.shutdown() }
|
||||
try configure(app)
|
||||
|
||||
try app.test(.GET, "hello", afterResponse: { res in
|
||||
XCTAssertEqual(res.status, .ok)
|
||||
XCTAssertEqual(res.body.string, "Hello, world!")
|
||||
})
|
||||
func testEncodingUInt32() {
|
||||
let input: UInt32 = 123
|
||||
let data = input.encoded
|
||||
let output = UInt32(data: data)
|
||||
XCTAssertEqual(input, output)
|
||||
}
|
||||
|
||||
func testEncodingContent() {
|
||||
let input = Message.Content(time: 1234567890, id: 23456789)
|
||||
let data = input.encoded
|
||||
let output = Message.Content(decodeFrom: data)
|
||||
XCTAssertEqual(input, output)
|
||||
let data2 = Data([42, 42]) + data
|
||||
let output2 = Message.Content(decodeFrom: Data(data2[2...]))
|
||||
XCTAssertEqual(input, output2)
|
||||
}
|
||||
|
||||
func testEncodingMessage() {
|
||||
let input = Message(mac: Data(repeating: 42, count: 32),
|
||||
content: Message.Content(time: 1234567890, id: 23456789))
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user