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

View File

@ -1,3 +1,36 @@
# Sesame-Server
Server code
Server code for the `Sesame`project. The server acts as a relay for messages between the device (ESP32) and the client (iOS App), by providing a DNS entry to which both devices can reliably connect.
The device is permanently connected to the server through a web socket, so that the server can push new requests to the device. The client performs simple POST and GET requests to the server to upload keys and retrieve device responses.
## Operation
The server provides APIs for the device and the iOS client. The API for the device consists of a single entry point to establish a web socket connection for bidirectional communication. The device authenticates with a pre-shared key to prevent other actors from replacing the device.
The client API has multiple routes to inquire about device status and reponses, and to send a key to the device.
### Access
The server is configured to listen on port 10000.
## Security
The system does not rely on any cryptographic algorithms, which are difficult to execute and verify on resource-constrained devices. The system relies instead on pre-shared one-time keys, which are hardcoded into the device when flashing the software. The same codes are stored within the iOS App.
### Protection against attacks
One-time keys are only vulnerable to attacks where they are somehow observed prior to use. Within this system, this can occur on the embedded device, the client App, the programming hardware, and during transmission of the codes over the network.
The device itself is secured against physical attacks by being inside the house which it opens. An attacker with physical access to the device has already succeeded in bypassing the system.
The iOS device is secured by the mechanisms deployed by Apple against unwanted access to app data. These protections are deemed sufficient for this application, and would be difficult to further improve within this project.
The programming hardware is also protected by similar mechanisms, and the codes only reside on this system when initially programming the device, and are afterwards securely deleted.
The one-time codes are vulnerable when being transmitted over the network, including being observed or tampered with on the server. To prevent this, each key is associated with an id, and keys are expected to be used in the correct sequence. Using a key with a higher id invalidates all keys with lower ids. This prevents cases where an attacker blocks the use of a key to use it at a later time.
For the case where an attacker blocks all keys from being used, then all keys must be invalidated manually be uploading new keys to the device.
The device itself may be vulnerable to attacks due to errors in the software stack, including WiFi and other features. The device is isolated from the global network through the routers firewall, and only exposes the web socket connection, which is secured by SSL.

1
Resources/device.key Normal file
View File

@ -0,0 +1 @@
access token

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)
}
}