Sesame-iOS/Sesame/ClientState.swift

372 lines
11 KiB
Swift
Raw Normal View History

2022-01-29 18:59:42 +01:00
import Foundation
import SwiftUI
2023-08-09 16:27:15 +02:00
import SFSafeSymbols
2022-01-29 18:59:42 +01:00
2022-04-09 17:43:33 +02:00
enum ConnectionError {
case serverNotReached
case deviceDisconnected
}
extension ConnectionError: CustomStringConvertible {
var description: String {
switch self {
case .serverNotReached:
return "Server unavailable"
case .deviceDisconnected:
return "Device disconnected"
}
}
}
enum RejectionCause {
2023-08-07 15:47:40 +02:00
case invalidDeviceId
2022-04-09 17:43:33 +02:00
case invalidCounter
case invalidTime
case invalidAuthentication
case timeout
case missingKey
2022-04-09 17:43:33 +02:00
}
extension RejectionCause: CustomStringConvertible {
var description: String {
switch self {
2023-08-07 15:47:40 +02:00
case .invalidDeviceId:
return "Invalid device ID"
2022-04-09 17:43:33 +02:00
case .invalidCounter:
return "Invalid counter"
case .invalidTime:
return "Invalid time"
case .invalidAuthentication:
return "Invalid authentication"
case .timeout:
return "Device not responding"
case .missingKey:
return "No key to verify message"
2022-04-09 17:43:33 +02:00
}
}
}
2022-01-29 18:59:42 +01:00
enum ClientState {
2022-04-09 17:43:33 +02:00
/// There is no key stored locally on the client. A new key must be generated before use.
case noKeyAvailable
/// The device status is being requested
case requestingStatus
2022-01-29 18:59:42 +01:00
/// The remote device is not connected (no socket opened)
2022-04-09 17:43:33 +02:00
case deviceNotAvailable(ConnectionError)
/// The device is connected and ready to receive a message
case ready
2022-01-29 18:59:42 +01:00
2022-04-09 17:43:33 +02:00
/// The message is being transmitted and a response is awaited
2022-01-29 18:59:42 +01:00
case waitingForResponse
2022-04-09 17:43:33 +02:00
/// The transmitted message was rejected (multiple possible reasons)
case messageRejected(RejectionCause)
2022-01-29 18:59:42 +01:00
case responseRejected(RejectionCause)
2022-01-29 18:59:42 +01:00
/// The device responded that the opening action was started
case openSesame
2022-04-09 17:43:33 +02:00
case internalError(String)
2022-01-29 18:59:42 +01:00
var canSendKey: Bool {
switch self {
2022-04-09 17:43:33 +02:00
case .ready, .openSesame, .messageRejected:
2022-01-29 18:59:42 +01:00
return true
default:
return false
}
}
2022-04-09 17:43:33 +02:00
init(keyResult: MessageResult) {
2022-01-29 18:59:42 +01:00
switch keyResult {
2022-04-09 17:43:33 +02:00
case .messageAuthenticationFailed:
self = .messageRejected(.invalidAuthentication)
case .messageTimeMismatch:
self = .messageRejected(.invalidTime)
case .messageCounterInvalid:
self = .messageRejected(.invalidCounter)
case .deviceTimedOut:
self = .messageRejected(.timeout)
case .messageAccepted:
2022-01-29 18:59:42 +01:00
self = .openSesame
2023-08-07 15:47:40 +02:00
case .messageDeviceInvalid:
self = .messageRejected(.invalidDeviceId)
2023-08-09 16:27:15 +02:00
case .noBodyData, .invalidMessageSize, .textReceived, .unexpectedSocketEvent, .invalidUrlParameter, .invalidResponseAuthentication:
print("Unexpected internal error: \(keyResult)")
2022-04-09 17:43:33 +02:00
self = .internalError(keyResult.description)
case .deviceNotConnected:
self = .deviceNotAvailable(.deviceDisconnected)
case .operationInProgress:
self = .waitingForResponse
case .deviceConnected:
self = .ready
2022-01-29 18:59:42 +01:00
}
}
2022-04-09 17:43:33 +02:00
2022-04-13 14:55:47 +02:00
var actionText: String {
switch self {
case .noKeyAvailable:
return "No key"
case .requestingStatus:
return "Checking..."
case .deviceNotAvailable(let connectionError):
switch connectionError {
case .serverNotReached:
return "Server not found"
case .deviceDisconnected:
return "Device disconnected"
}
case .ready:
return "Unlock"
case .waitingForResponse:
return "Unlocking..."
case .messageRejected(let rejectionCause):
switch rejectionCause {
case .invalidDeviceId:
return "Invalid device ID"
case .invalidCounter:
return "Invalid counter"
case .invalidTime:
return "Invalid timestamp"
case .invalidAuthentication:
return "Invalid signature"
case .timeout:
return "Device not responding"
case .missingKey:
return "Device key missing"
}
case .responseRejected(let rejectionCause):
switch rejectionCause {
case .invalidDeviceId:
return "Invalid device id (response)"
case .invalidCounter:
return "Invalid counter (response)"
case .invalidTime:
return "Invalid time (response)"
case .invalidAuthentication:
return "Invalid signature (response)"
case .timeout:
return "Timed out (response)"
case .missingKey:
return "Missing key (response)"
}
case .openSesame:
return "Unlocked"
case .internalError(let string):
return string
}
2022-01-29 18:59:42 +01:00
}
2022-04-13 14:55:47 +02:00
var requiresDescription: Bool {
switch self {
case .deviceNotAvailable, .messageRejected, .internalError, .responseRejected:
2022-04-13 14:55:47 +02:00
return true
default:
return false
}
}
2022-01-29 18:59:42 +01:00
2022-04-13 14:55:47 +02:00
var color: Color {
2022-01-29 18:59:42 +01:00
switch self {
2022-04-13 14:55:47 +02:00
case .noKeyAvailable:
return Color(red: 50/255, green: 50/255, blue: 50/255)
2022-04-13 14:55:47 +02:00
case .deviceNotAvailable:
return Color(red: 150/255, green: 90/255, blue: 90/255)
case .messageRejected, .responseRejected:
return Color(red: 160/255, green: 30/255, blue: 30/255)
2022-04-13 14:55:47 +02:00
case .internalError:
return Color(red: 100/255, green: 0/255, blue: 0/255)
2022-04-13 14:55:47 +02:00
case .ready:
return Color(red: 115/255, green: 140/255, blue: 90/255)
case .requestingStatus, .waitingForResponse:
return Color(red: 160/255, green: 170/255, blue: 110/255)
2022-04-13 14:55:47 +02:00
case .openSesame:
return Color(red: 65/255, green: 110/255, blue: 60/255)
2022-01-29 18:59:42 +01:00
}
}
2022-04-13 14:55:47 +02:00
var allowsAction: Bool {
2022-01-29 18:59:42 +01:00
switch self {
case .noKeyAvailable, .waitingForResponse:
2022-01-29 18:59:42 +01:00
return false
default:
return true
}
}
2022-04-09 17:43:33 +02:00
}
extension ClientState: Equatable {
2022-01-29 18:59:42 +01:00
2022-04-09 17:43:33 +02:00
}
extension ClientState: CustomStringConvertible {
var description: String {
switch self {
case .noKeyAvailable:
return "No key set."
case .requestingStatus:
return "Checking device status"
case .deviceNotAvailable(let status):
return status.description
case .ready:
return "Ready"
case .waitingForResponse:
return "Unlocking..."
case .messageRejected(let cause):
return cause.description
case .openSesame:
return "Unlocked"
case .internalError(let e):
return "Error: \(e)"
case .responseRejected(let cause):
switch cause {
case .invalidAuthentication:
return "Device message not authenticated"
default:
return cause.description
}
}
}
}
// MARK: Coding
extension ClientState {
var encoded: Data {
Data([code])
}
var code: UInt8 {
switch self {
case .noKeyAvailable:
return 1
case .requestingStatus:
return 2
case .deviceNotAvailable(let connectionError):
switch connectionError {
case .serverNotReached:
return 3
case .deviceDisconnected:
return 4
}
case .ready:
return 5
case .waitingForResponse:
return 6
case .messageRejected(let rejectionCause):
switch rejectionCause {
2023-08-07 15:47:40 +02:00
case .invalidDeviceId:
return 19
case .invalidCounter:
return 7
case .invalidTime:
return 8
case .invalidAuthentication:
return 9
case .timeout:
return 10
case .missingKey:
return 11
}
case .responseRejected(let rejectionCause):
switch rejectionCause {
case .invalidCounter:
return 12
case .invalidTime:
return 13
case .invalidAuthentication:
return 14
case .timeout:
return 15
case .missingKey:
return 16
2023-08-07 15:47:40 +02:00
case .invalidDeviceId:
return 20
}
case .openSesame:
return 17
2023-08-09 16:27:15 +02:00
case .internalError:
return 18
}
}
init(code: UInt8) {
switch code {
case 1:
self = .noKeyAvailable
case 2:
self = .requestingStatus
case 3:
self = .deviceNotAvailable(.serverNotReached)
case 4:
self = .deviceNotAvailable(.deviceDisconnected)
case 5:
self = .ready
case 6:
self = .waitingForResponse
case 7:
self = .messageRejected(.invalidCounter)
case 8:
self = .messageRejected(.invalidTime)
case 9:
self = .messageRejected(.invalidAuthentication)
case 10:
self = .messageRejected(.timeout)
case 11:
self = .messageRejected(.missingKey)
case 12:
self = .responseRejected(.invalidCounter)
case 13:
self = .responseRejected(.invalidTime)
case 14:
self = .responseRejected(.invalidAuthentication)
case 15:
self = .responseRejected(.timeout)
case 16:
self = .responseRejected(.missingKey)
case 17:
self = .openSesame
case 18:
self = .internalError("")
2023-08-07 15:47:40 +02:00
case 19:
self = .messageRejected(.invalidDeviceId)
case 20:
self = .responseRejected(.invalidDeviceId)
default:
self = .internalError("Unknown code \(code)")
2022-04-09 17:43:33 +02:00
}
}
2022-01-29 18:59:42 +01:00
}
2023-08-09 16:27:15 +02:00
extension ClientState {
@available(iOS 16, *)
var symbol: SFSymbol {
switch self {
case .deviceNotAvailable:
return .wifiExclamationmark
case .internalError:
return .applewatchSlash
case .noKeyAvailable:
return .lockTrianglebadgeExclamationmark
case .openSesame:
return .lockOpen
case .messageRejected:
return .nosign
case .responseRejected:
return .exclamationmarkTriangle
case .requestingStatus, .ready, .waitingForResponse:
return .wifiExclamationmark
}
}
}