Update to challenge-response system
This commit is contained in:
17
Sources/App/API/Extensions/Data+Coding.swift
Normal file
17
Sources/App/API/Extensions/Data+Coding.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -40,20 +40,3 @@ extension Data {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,4 +16,7 @@ extension UInt32 {
|
||||
var encoded: Data {
|
||||
Data(from: CFSwapInt32HostToLittle(self))
|
||||
}
|
||||
|
||||
/// The size of a `UInt32` when converted to data
|
||||
static let byteSize = MemoryLayout<UInt32>.size
|
||||
}
|
8
Sources/App/API/Message+Size.swift
Normal file
8
Sources/App/API/Message+Size.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
extension Message {
|
||||
|
||||
/// The byte length of an encoded message content
|
||||
static let size: Int = 2 + 2 * UInt32.byteSize
|
||||
|
||||
}
|
@ -3,98 +3,212 @@ import Foundation
|
||||
/**
|
||||
A result from sending a key to the device.
|
||||
*/
|
||||
enum MessageResult: UInt8, Error {
|
||||
enum MessageResult: UInt8 {
|
||||
|
||||
/// Text content was received, although binary data was expected
|
||||
/// The message was accepted.
|
||||
case messageAccepted = 0
|
||||
|
||||
/// The web socket received text while waiting for binary data.
|
||||
case textReceived = 1
|
||||
|
||||
/// A socket event on the device was unexpected (not binary data)
|
||||
/// An unexpected socket event occured while performing the exchange.
|
||||
case unexpectedSocketEvent = 2
|
||||
|
||||
/// The size of the payload (i.e. message) was invalid
|
||||
case invalidMessageSize = 3
|
||||
/// The received message size is invalid.
|
||||
case invalidMessageSizeFromRemote = 3
|
||||
|
||||
/// The transmitted message could not be authenticated using the key
|
||||
case messageAuthenticationFailed = 4
|
||||
/// The message signature was incorrect.
|
||||
case invalidSignatureByRemote = 4
|
||||
|
||||
/// The message time was not within the acceptable bounds
|
||||
case messageTimeMismatch = 5
|
||||
/// The server challenge of the message did not match previous messages
|
||||
case serverChallengeMismatch = 5
|
||||
|
||||
/// A later key has been used, invalidating this key (to prevent replay attacks after blocked communication)
|
||||
case messageCounterInvalid = 6
|
||||
/// The client challenge of the message did not match previous messages
|
||||
case clientChallengeMismatchFromRemote = 6
|
||||
|
||||
/// The key was accepted by the device, and the door will be opened
|
||||
case messageAccepted = 7
|
||||
/// An unexpected or unsupported message type was received
|
||||
case invalidMessageTypeFromRemote = 7
|
||||
|
||||
/// The device id is invalid
|
||||
case messageDeviceInvalid = 8
|
||||
|
||||
|
||||
/// 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
|
||||
|
||||
/// The device is connected
|
||||
case deviceConnected = 15
|
||||
|
||||
case invalidUrlParameter = 20
|
||||
|
||||
case invalidResponseAuthentication = 21
|
||||
/// A message is already being processed
|
||||
case tooManyRequests = 8
|
||||
|
||||
/// The received message result was not ``messageAccepted``
|
||||
case invalidMessageResultFromRemote = 9
|
||||
|
||||
/// An invalid Url parameter was set sending a message to the device over a local connection
|
||||
case invalidUrlParameter = 10
|
||||
|
||||
/// The request took too long to complete
|
||||
case deviceTimedOut = 20
|
||||
|
||||
case noOrInvalidBodyDataInServerRequest = 21
|
||||
|
||||
/// The device is not connected to the server via web socket
|
||||
case deviceNotConnected = 22
|
||||
case serverNotReached = 23
|
||||
case serverUrlInvalid = 24
|
||||
case invalidDeviceResponseSize = 25
|
||||
case invalidSignatureByDevice = 26
|
||||
case noKeyAvailable = 27
|
||||
case unlocked = 28
|
||||
case unknownMessageResultFromDevice = 29
|
||||
|
||||
/// The device sent a message with an invalid client challenge
|
||||
case clientChallengeMismatchFromDevice = 30
|
||||
|
||||
/// A valid server challenge was received
|
||||
case deviceAvailable = 31
|
||||
|
||||
case invalidMessageTypeFromDevice = 32
|
||||
|
||||
/// The url session request returned an unknown response
|
||||
case unexpectedUrlResponseType = 33
|
||||
|
||||
/// The request to the server returned an unhandled HTTP code
|
||||
case unexpectedServerResponseCode = 34
|
||||
|
||||
/// The server produced an internal error (500)
|
||||
case internalServerError = 35
|
||||
|
||||
/// The Sesame server behind the proxy could not be found (502)
|
||||
case serviceBehindProxyUnavailable = 36
|
||||
|
||||
/// The server url could not be found (404)
|
||||
case pathOnServerNotFound = 37
|
||||
|
||||
/// The header with the authentication token was missing or invalid (not a hex string) from a server request.
|
||||
case missingOrInvalidAuthenticationHeader = 38
|
||||
|
||||
/// The authentication token for the server was invalid
|
||||
case invalidServerAuthentication = 39
|
||||
|
||||
/// The device sent a response of invalid size
|
||||
case invalidMessageSizeFromDevice = 40
|
||||
}
|
||||
|
||||
extension MessageResult: Error {
|
||||
|
||||
case invalidDeviceResponse = 22
|
||||
}
|
||||
|
||||
extension MessageResult: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .messageAccepted:
|
||||
return "Message accepted"
|
||||
case .textReceived:
|
||||
return "The device received unexpected text"
|
||||
case .unexpectedSocketEvent:
|
||||
return "Unexpected socket event for the device"
|
||||
case .invalidMessageSize:
|
||||
return "Invalid message data"
|
||||
case .messageAuthenticationFailed:
|
||||
case .invalidMessageSizeFromRemote:
|
||||
return "Invalid message data from remote"
|
||||
case .invalidSignatureByRemote:
|
||||
return "Message authentication failed"
|
||||
case .messageTimeMismatch:
|
||||
return "Message time invalid"
|
||||
case .messageCounterInvalid:
|
||||
return "Message counter invalid"
|
||||
case .messageAccepted:
|
||||
return "Message accepted"
|
||||
case .messageDeviceInvalid:
|
||||
return "Invalid device ID"
|
||||
case .noBodyData:
|
||||
return "No body data included in the request"
|
||||
case .noOrInvalidBodyDataInServerRequest:
|
||||
return "Invalid body data in server request"
|
||||
case .deviceNotConnected:
|
||||
return "Device not connected"
|
||||
return "Device not connected to server"
|
||||
case .deviceTimedOut:
|
||||
return "The device did not respond"
|
||||
case .operationInProgress:
|
||||
return "Another operation is in progress"
|
||||
case .deviceConnected:
|
||||
return "The device is connected"
|
||||
case .serverChallengeMismatch:
|
||||
return "Server challenge mismatch"
|
||||
case .clientChallengeMismatchFromRemote:
|
||||
return "Wrong client challenge sent"
|
||||
case .invalidMessageTypeFromRemote:
|
||||
return "Message type from remote invalid"
|
||||
case .tooManyRequests:
|
||||
return "Device busy"
|
||||
case .invalidUrlParameter:
|
||||
return "The url parameter could not be found"
|
||||
case .invalidResponseAuthentication:
|
||||
return "The response could not be authenticated"
|
||||
case .invalidDeviceResponse:
|
||||
return "The device responded with invalid data"
|
||||
case .invalidMessageResultFromRemote:
|
||||
return "Invalid message result"
|
||||
case .serverNotReached:
|
||||
return "Server unavailable"
|
||||
case .serverUrlInvalid:
|
||||
return "Invalid server url"
|
||||
case .invalidDeviceResponseSize:
|
||||
return "Invalid Response size"
|
||||
case .invalidSignatureByDevice:
|
||||
return "Invalid device signature"
|
||||
case .noKeyAvailable:
|
||||
return "No key available"
|
||||
case .unlocked:
|
||||
return "Unlocked"
|
||||
case .unknownMessageResultFromDevice:
|
||||
return "Unknown message result"
|
||||
case .deviceAvailable:
|
||||
return "Device available"
|
||||
case .clientChallengeMismatchFromDevice:
|
||||
return "Device sent invalid client challenge"
|
||||
case .invalidMessageTypeFromDevice:
|
||||
return "Message type from device invalid"
|
||||
case .unexpectedUrlResponseType:
|
||||
return "Unexpected URL response"
|
||||
case .unexpectedServerResponseCode:
|
||||
return "Unexpected server response code"
|
||||
case .internalServerError:
|
||||
return "Internal server error"
|
||||
case .serviceBehindProxyUnavailable:
|
||||
return "Service behind proxy not found"
|
||||
case .pathOnServerNotFound:
|
||||
return "Invalid server path"
|
||||
case .missingOrInvalidAuthenticationHeader:
|
||||
return "Invalid server token format"
|
||||
case .invalidServerAuthentication:
|
||||
return "Invalid server token"
|
||||
case .invalidMessageSizeFromDevice:
|
||||
return "Invalid device message size"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MessageResult: Codable {
|
||||
|
||||
}
|
||||
|
||||
extension MessageResult {
|
||||
|
||||
var encoded: Data {
|
||||
Data([rawValue])
|
||||
}
|
||||
}
|
||||
|
||||
extension MessageResult {
|
||||
|
||||
init(httpCode: Int) {
|
||||
switch httpCode {
|
||||
case 200: self = .messageAccepted
|
||||
case 204: self = .noOrInvalidBodyDataInServerRequest
|
||||
case 403: self = .invalidServerAuthentication
|
||||
case 404: self = .pathOnServerNotFound
|
||||
case 408: self = .deviceTimedOut
|
||||
case 412: self = .deviceNotConnected
|
||||
case 413: self = .invalidMessageSizeFromDevice
|
||||
case 422: self = .missingOrInvalidAuthenticationHeader
|
||||
case 429: self = .tooManyRequests
|
||||
case 500: self = .internalServerError
|
||||
case 501: self = .unexpectedServerResponseCode
|
||||
case 502: self = .serviceBehindProxyUnavailable
|
||||
default: self = .unexpectedServerResponseCode
|
||||
}
|
||||
}
|
||||
|
||||
var statusCode: Int {
|
||||
switch self {
|
||||
case .messageAccepted: return 200 // ok
|
||||
case .noOrInvalidBodyDataInServerRequest: return 204 // noContent
|
||||
case .invalidServerAuthentication: return 403 // forbidden
|
||||
case .pathOnServerNotFound: return 404 // notFound
|
||||
case .deviceTimedOut: return 408 // requestTimeout
|
||||
case .invalidMessageSizeFromRemote: return 411 // lengthRequired
|
||||
case .deviceNotConnected: return 412 // preconditionFailed
|
||||
case .invalidMessageSizeFromDevice: return 413 // payloadTooLarge
|
||||
case .missingOrInvalidAuthenticationHeader: return 422 // unprocessableEntity
|
||||
case .tooManyRequests: return 429 // tooManyRequests
|
||||
case .internalServerError: return 500 // internalServerError
|
||||
case .unexpectedServerResponseCode: return 501 // notImplemented
|
||||
case .serviceBehindProxyUnavailable: return 502 // badGateway
|
||||
default: return 501 // == unexpectedServerResponseCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +0,0 @@
|
||||
import Foundation
|
||||
import NIOCore
|
||||
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
struct ServerMessage {
|
||||
|
||||
static let authTokenSize = SHA256.byteCount
|
||||
|
||||
static let maxLength = authTokenSize + 200
|
||||
|
||||
let authToken: Data
|
||||
|
||||
let message: Data
|
||||
|
||||
/**
|
||||
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 buffer.readableBytes < ServerMessage.maxLength else {
|
||||
log("Received invalid message with \(buffer.readableBytes) bytes")
|
||||
return nil
|
||||
}
|
||||
guard let data = buffer.getBytes(at: 0, length: buffer.readableBytes) else {
|
||||
log("Failed to read bytes of received message")
|
||||
return nil
|
||||
}
|
||||
self.authToken = Data(data.prefix(ServerMessage.authTokenSize))
|
||||
self.message = Data(data.dropFirst(ServerMessage.authTokenSize))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
14
Sources/App/API/SesameHeader.swift
Normal file
14
Sources/App/API/SesameHeader.swift
Normal file
@ -0,0 +1,14 @@
|
||||
import Foundation
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
enum SesameHeader {
|
||||
|
||||
static let authenticationHeader = "Authentication"
|
||||
|
||||
static let serverAuthenticationTokenSize = SHA256.byteCount
|
||||
|
||||
}
|
@ -3,10 +3,7 @@ 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"
|
||||
enum SesameRoute: String {
|
||||
|
||||
/// Send a message to the server, to relay to the device
|
||||
case postMessage = "message"
|
15
Sources/App/API/SignedMessage+Size.swift
Normal file
15
Sources/App/API/SignedMessage+Size.swift
Normal file
@ -0,0 +1,15 @@
|
||||
import Foundation
|
||||
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
extension SignedMessage {
|
||||
|
||||
/// The length of a message in bytes
|
||||
static var size: Int {
|
||||
SHA256.byteCount + Message.size
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user