First version

This commit is contained in:
Christoph Hagen
2022-01-24 17:17:06 +01:00
parent 953753d66c
commit 20fa7286ea
7 changed files with 322 additions and 14 deletions

10
Sources/App/API.swift Normal file
View File

@@ -0,0 +1,10 @@
import Foundation
enum PublicAPI: String {
case getDeviceResponse = "response"
case getDeviceStatus = "status"
case clearKeyRequest = "clear"
case postKey = "key"
case postKeyIdParameter = "id"
case socket = "listen"
}

View File

@@ -0,0 +1,124 @@
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
/// The connection to the device
private var connection: WebSocket?
private let deviceKey: String
var deviceIsAuthenticated = false
/// Indicator for device availability
var deviceIsConnected: Bool {
!(connection?.isClosed ?? true) && deviceIsAuthenticated
}
/// The id of the key which was sent to the device
private var keyInTransit: UInt16?
/// The result transmitted by the device for the sent key
var keyResult: KeyResult = .none
init(deviceKey: String) {
self.deviceKey = deviceKey
}
// MARK: API
var deviceResponse: String {
guard let keyId = keyInTransit else {
return "No key"
}
return "\(keyId):\(keyResult.rawValue)"
}
var deviceStatus: String {
deviceIsConnected ? "1" : "0"
}
func clearClientRequest() {
keyInTransit = nil
keyResult = .none
}
func sendKeyToDevice(_ key: Data, keyId: UInt16) -> KeyPostResponse {
guard key.count == KeyManagement.keySize else {
return .invalidKeySize
}
guard let socket = connection, !socket.isClosed else {
connection = nil
return .deviceNotConnected
}
let keyIdData = [UInt8(keyId >> 8), UInt8(keyId & 0xFF)]
keyInTransit = keyId
socket.send(keyIdData + key, promise: nil)
return .success
}
func authenticateDevice(psk: String) {
guard psk == self.deviceKey else {
print("Invalid device key")
_ = connection?.close()
deviceIsAuthenticated = false
return
}
print("Device authenticated")
deviceIsAuthenticated = true
}
func processDeviceResponse(_ data: ByteBuffer) {
guard data.readableBytes == 1 else {
print("Unexpected number of bytes received from device")
keyInTransit = nil
keyResult = .unexpectedSocketEvent
return
}
guard let rawValue = data.getBytes(at: 0, length: 1)?.first else {
print("Unreadable data received from device")
keyInTransit = nil
keyResult = .unexpectedSocketEvent
return
}
guard let response = KeyResult(rawValue: rawValue) else {
print("Unknown response \(rawValue) received from device")
keyInTransit = nil
keyResult = .unexpectedSocketEvent
return
}
guard keyInTransit != nil else {
print("No key in transit for response \(response)")
return
}
keyResult = response
}
func didCloseDeviceSocket() {
deviceIsAuthenticated = false
guard connection != nil else {
return
}
connection = nil
print("Socket closed")
}
func removeDeviceConnection() {
_ = connection?.close()
connection = nil
}
func createNewDeviceConnection(_ socket: WebSocket) {
removeDeviceConnection()
connection = socket
deviceIsAuthenticated = false
print("Socket connected")
}
}

View File

@@ -0,0 +1,64 @@
import Foundation
/**
A result from sending a key to the device.
*/
enum KeyResult: UInt8 {
/// No result from the device, or state not applicable
case none = 0
/// 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 (key id + key data) was invalid
case invalidPayloadSize = 3
/// 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
/// A later key has been used, invalidating this key (to prevent replay attacks after blocked communication)
case keyWasSkipped = 7
/// The key was accepted by the device, and the door will be opened
case keyAccepted = 8
/// The device produced an unknown error
case uknownDeviceError = 9
/// The device is not connected through the socket
case notConnected = 10
}
/**
A response from the server to a key request.
*/
enum KeyPostResponse: Int {
/// The key will be transmitted to the device
case success = 0
/// The key id is out of bounds or otherwise invalid
case invalidKeyId = 1
/// The request did not contain body data with the key
case noBodyData = 2
/// The key contained in the body data has an invalid size
case invalidKeySize = 3
/// The body data could not be read
case corruptkeyData = 4
/// The device is not connected
case deviceNotConnected = 5
}

View File

@@ -1,7 +1,22 @@
import Vapor
var keyManager: KeyManagement!
// configures your application
public func configure(_ app: Application) throws {
app.http.server.configuration.port = 10000
let storageFolder = URL(fileURLWithPath: app.directory.resourcesDirectory)
let keyFile = storageFolder.appendingPathComponent("device.key")
let deviceKey = try String(contentsOf: keyFile)
.trimmingCharacters(in: .whitespacesAndNewlines)
keyManager = KeyManagement(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()
}
}
}

View File

@@ -1,11 +1,74 @@
import Vapor
var connection: WebSocket?
extension PublicAPI {
var path: PathComponent {
.init(stringLiteral: rawValue)
}
var pathParameter: PathComponent {
.parameter(rawValue)
}
}
private func handleKeyPost(_ req: Request) -> KeyPostResponse {
guard let keyId = req.parameters.get(PublicAPI.postKeyIdParameter.rawValue, as: UInt16.self) else {
return .invalidKeyId
}
guard let body = req.body.data else {
return .noBodyData
}
guard body.readableBytes == KeyManagement.keySize else {
return .invalidKeySize
}
guard let key = body.getData(at: 0, length: KeyManagement.keySize) else {
return .corruptkeyData
}
return keyManager.sendKeyToDevice(key, keyId: keyId)
}
func routes(_ app: Application) throws {
app.get { req in
return "It works!"
/**
Get the connection status of the device.
The response is a string of either "1" (connected) or "0" (disconnected)
*/
app.get(PublicAPI.getDeviceStatus.path) { req -> String in
keyManager.deviceStatus
}
/**
Get the response from the device.
The response is a string of an integer `rawValue` of a `KeyResult`
*/
app.get(PublicAPI.getDeviceResponse.path) { req -> String in
keyManager.deviceResponse
}
/**
Post a request to remove the information about the last key transmission.
- The request always succeeds and returns the string "Success"
*/
app.post(PublicAPI.clearKeyRequest.path) { req -> String in
keyManager.clearClientRequest()
return "Success"
}
/**
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.
*/
app.post(PublicAPI.postKey.path, PublicAPI.postKeyIdParameter.pathParameter) { req -> String in
let result = handleKeyPost(req)
return String(result.rawValue)
}
/**
@@ -13,19 +76,17 @@ func routes(_ app: Application) throws {
- Returns: Nothing
- Note: The first (and only) message from the client over the connection must be a valid session token.
*/
app.webSocket("listen") { req, socket in
socket.onBinary { socket, data in
print("\(data)")
app.webSocket(PublicAPI.socket.path) { req, socket in
socket.onBinary { _, data in
keyManager.processDeviceResponse(data)
}
socket.onText { socket, text in
print(text)
socket.onText { _, text in
keyManager.authenticateDevice(psk: text)
}
_ = socket.onClose.always { result in
connection = nil
print("Socket closed")
_ = socket.onClose.always { _ in
keyManager.didCloseDeviceSocket()
}
connection = socket
print("Socket connected")
keyManager.createNewDeviceConnection(socket)
}
}