Delete history, allow retry
This commit is contained in:
93
Sesame/Common/Client.swift
Normal file
93
Sesame/Common/Client.swift
Normal file
@ -0,0 +1,93 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
final class Client {
|
||||
|
||||
// TODO: Use or delete
|
||||
private let delegate = NeverCacheDelegate()
|
||||
|
||||
init() {}
|
||||
|
||||
func deviceStatus(authToken: Data, server: String) async -> ClientState {
|
||||
await send(path: .getDeviceStatus, server: server, data: authToken).state
|
||||
}
|
||||
|
||||
func sendMessageOverLocalNetwork(_ message: Message, server: String) async -> (state: ClientState, response: Message?) {
|
||||
let data = message.encoded.hexEncoded
|
||||
guard let url = URL(string: server + "message?m=\(data)") else {
|
||||
return (.internalError("Invalid server url"), nil)
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
return await requestAndDecode(request)
|
||||
}
|
||||
|
||||
func send(_ message: Message, server: String, authToken: Data) async -> (state: ClientState, response: Message?) {
|
||||
let serverMessage = ServerMessage(authToken: authToken, message: message)
|
||||
return await send(path: .postMessage, server: server, data: serverMessage.encoded)
|
||||
}
|
||||
|
||||
private func send(path: RouteAPI, server: String, data: Data) async -> (state: ClientState, response: Message?) {
|
||||
guard let url = URL(string: server) else {
|
||||
return (.internalError("Invalid server url"), nil)
|
||||
}
|
||||
let fullUrl = url.appendingPathComponent(path.rawValue)
|
||||
return await send(to: fullUrl, data: data)
|
||||
}
|
||||
|
||||
private func send(to url: URL, data: Data) async -> (state: ClientState, response: Message?) {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpBody = data
|
||||
request.httpMethod = "POST"
|
||||
request.timeoutInterval = 10
|
||||
return await requestAndDecode(request)
|
||||
}
|
||||
|
||||
private func requestAndDecode(_ request: URLRequest) async -> (state: ClientState, response: Message?) {
|
||||
guard let data = await fulfill(request) else {
|
||||
return (.deviceNotAvailable(.serverNotReached), nil)
|
||||
}
|
||||
guard let byte = data.first else {
|
||||
return (.internalError("Empty response"), nil)
|
||||
}
|
||||
guard let status = MessageResult(rawValue: byte) else {
|
||||
return (.internalError("Invalid message response: \(byte)"), nil)
|
||||
}
|
||||
let result = ClientState(keyResult: status)
|
||||
guard data.count == Message.length + 1 else {
|
||||
if data.count != 1 {
|
||||
print("Device response with only \(data.count) bytes")
|
||||
}
|
||||
return (result, nil)
|
||||
}
|
||||
let messageData = Array(data.advanced(by: 1))
|
||||
let message = Message(decodeFrom: messageData)
|
||||
return (result, message)
|
||||
}
|
||||
|
||||
private func fulfill(_ request: URLRequest) async -> Data? {
|
||||
do {
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
guard let code = (response as? HTTPURLResponse)?.statusCode else {
|
||||
print("No response from server")
|
||||
return nil
|
||||
}
|
||||
guard code == 200 else {
|
||||
print("Invalid server response \(code)")
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
} catch {
|
||||
print("Request failed: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NeverCacheDelegate: NSObject, NSURLConnectionDataDelegate {
|
||||
|
||||
func connection(_ connection: NSURLConnection, willCacheResponse cachedResponse: CachedURLResponse) -> CachedURLResponse? {
|
||||
return nil
|
||||
}
|
||||
}
|
371
Sesame/Common/ClientState.swift
Normal file
371
Sesame/Common/ClientState.swift
Normal file
@ -0,0 +1,371 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
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 {
|
||||
case invalidDeviceId
|
||||
case invalidCounter
|
||||
case invalidTime
|
||||
case invalidAuthentication
|
||||
case timeout
|
||||
case missingKey
|
||||
}
|
||||
|
||||
extension RejectionCause: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .invalidDeviceId:
|
||||
return "Invalid device ID"
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ClientState {
|
||||
|
||||
/// 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
|
||||
|
||||
/// The remote device is not connected (no socket opened)
|
||||
case deviceNotAvailable(ConnectionError)
|
||||
|
||||
/// The device is connected and ready to receive a message
|
||||
case ready
|
||||
|
||||
/// The message is being transmitted and a response is awaited
|
||||
case waitingForResponse
|
||||
|
||||
/// The transmitted message was rejected (multiple possible reasons)
|
||||
case messageRejected(RejectionCause)
|
||||
|
||||
case responseRejected(RejectionCause)
|
||||
|
||||
/// The device responded that the opening action was started
|
||||
case openSesame
|
||||
|
||||
case internalError(String)
|
||||
|
||||
var canSendKey: Bool {
|
||||
switch self {
|
||||
case .ready, .openSesame, .messageRejected:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
init(keyResult: MessageResult) {
|
||||
switch keyResult {
|
||||
case .messageAuthenticationFailed:
|
||||
self = .messageRejected(.invalidAuthentication)
|
||||
case .messageTimeMismatch:
|
||||
self = .messageRejected(.invalidTime)
|
||||
case .messageCounterInvalid:
|
||||
self = .messageRejected(.invalidCounter)
|
||||
case .deviceTimedOut:
|
||||
self = .messageRejected(.timeout)
|
||||
case .messageAccepted:
|
||||
self = .openSesame
|
||||
case .messageDeviceInvalid:
|
||||
self = .messageRejected(.invalidDeviceId)
|
||||
case .noBodyData, .invalidMessageSize, .textReceived, .unexpectedSocketEvent, .invalidUrlParameter, .invalidResponseAuthentication:
|
||||
print("Unexpected internal error: \(keyResult)")
|
||||
self = .internalError(keyResult.description)
|
||||
case .deviceNotConnected:
|
||||
self = .deviceNotAvailable(.deviceDisconnected)
|
||||
case .operationInProgress:
|
||||
self = .waitingForResponse
|
||||
case .deviceConnected:
|
||||
self = .ready
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
var requiresDescription: Bool {
|
||||
switch self {
|
||||
case .deviceNotAvailable, .messageRejected, .internalError, .responseRejected:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .noKeyAvailable:
|
||||
return Color(red: 50/255, green: 50/255, blue: 50/255)
|
||||
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)
|
||||
case .internalError:
|
||||
return Color(red: 100/255, green: 0/255, blue: 0/255)
|
||||
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)
|
||||
case .openSesame:
|
||||
return Color(red: 65/255, green: 110/255, blue: 60/255)
|
||||
}
|
||||
}
|
||||
|
||||
var allowsAction: Bool {
|
||||
switch self {
|
||||
case .noKeyAvailable, .waitingForResponse:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ClientState: Equatable {
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
case .invalidDeviceId:
|
||||
return 20
|
||||
}
|
||||
case .openSesame:
|
||||
return 17
|
||||
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("")
|
||||
case 19:
|
||||
self = .messageRejected(.invalidDeviceId)
|
||||
case 20:
|
||||
self = .responseRejected(.invalidDeviceId)
|
||||
default:
|
||||
self = .internalError("Unknown code \(code)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
10
Sesame/Common/ConnectionStrategy.swift
Normal file
10
Sesame/Common/ConnectionStrategy.swift
Normal file
@ -0,0 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
enum ConnectionStrategy: String, CaseIterable, Identifiable {
|
||||
case local = "Local"
|
||||
case localFirst = "Local first"
|
||||
case remote = "Remote"
|
||||
case remoteFirst = "Remote first"
|
||||
|
||||
var id: Self { self }
|
||||
}
|
143
Sesame/Common/HistoryManager.swift
Normal file
143
Sesame/Common/HistoryManager.swift
Normal file
@ -0,0 +1,143 @@
|
||||
import Foundation
|
||||
import CBORCoding
|
||||
|
||||
class HistoryManagerBase: ObservableObject {
|
||||
|
||||
@Published
|
||||
var entries: [HistoryItem] = []
|
||||
}
|
||||
|
||||
protocol HistoryManagerProtocol: HistoryManagerBase {
|
||||
|
||||
var entries: [HistoryItem] { get }
|
||||
|
||||
func save(item: HistoryItem) throws
|
||||
|
||||
func delete(item: HistoryItem) -> Bool
|
||||
}
|
||||
|
||||
final class HistoryManager: HistoryManagerBase, HistoryManagerProtocol {
|
||||
|
||||
private let encoder = CBOREncoder(dateEncodingStrategy: .secondsSince1970)
|
||||
|
||||
private var fm: FileManager {
|
||||
.default
|
||||
}
|
||||
|
||||
static var documentDirectory: URL {
|
||||
try! FileManager.default.url(
|
||||
for: .documentDirectory,
|
||||
in: .userDomainMask,
|
||||
appropriateFor: nil, create: true)
|
||||
}
|
||||
|
||||
private let fileUrl: URL
|
||||
|
||||
override init() {
|
||||
self.fileUrl = HistoryManager.documentDirectory.appendingPathComponent("history2.bin")
|
||||
super.init()
|
||||
Task {
|
||||
print("Loading history...")
|
||||
let all = loadEntries()
|
||||
DispatchQueue.main.async {
|
||||
self.entries = all
|
||||
print("History loaded (\(self.entries.count) entries)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadEntries() -> [HistoryItem] {
|
||||
guard fm.fileExists(atPath: fileUrl.path) else {
|
||||
print("No history data found")
|
||||
return []
|
||||
}
|
||||
let content: Data
|
||||
do {
|
||||
content = try Data(contentsOf: fileUrl)
|
||||
} catch {
|
||||
print("Failed to read history data: \(error)")
|
||||
return []
|
||||
}
|
||||
let decoder = CBORDecoder()
|
||||
var index = 0
|
||||
var entries = [HistoryItem]()
|
||||
while index < content.count {
|
||||
let length = Int(content[index])
|
||||
index += 1
|
||||
if index + length > content.count {
|
||||
print("Missing bytes in history file: needed \(length), has only \(content.count - index)")
|
||||
return entries
|
||||
}
|
||||
let entryData = content[index..<index+length]
|
||||
index += length
|
||||
do {
|
||||
let entry: HistoryItem = try decoder.decode(from: entryData)
|
||||
entries.append(entry)
|
||||
} catch {
|
||||
print("Failed to decode history (index: \(index), length \(length)): \(error)")
|
||||
return entries
|
||||
}
|
||||
}
|
||||
return entries.sorted().reversed()
|
||||
}
|
||||
|
||||
func save(item: HistoryItem) throws {
|
||||
let data = try convertForStorage(item)
|
||||
guard fm.fileExists(atPath: fileUrl.path) else {
|
||||
try data.write(to: fileUrl)
|
||||
print("First history item written (\(data[0]))")
|
||||
return
|
||||
}
|
||||
let handle = try FileHandle(forWritingTo: fileUrl)
|
||||
try handle.seekToEnd()
|
||||
try handle.write(contentsOf: data)
|
||||
try handle.close()
|
||||
print("History item written (\(data[0]))")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func delete(item: HistoryItem) -> Bool {
|
||||
let newItems = entries
|
||||
.filter { $0 != item }
|
||||
|
||||
let data: FlattenSequence<[Data]>
|
||||
do {
|
||||
data = try newItems
|
||||
.map(convertForStorage)
|
||||
.joined()
|
||||
} catch {
|
||||
print("Failed to encode items: \(error)")
|
||||
return false
|
||||
}
|
||||
do {
|
||||
try Data(data).write(to: fileUrl)
|
||||
} catch {
|
||||
print("Failed to save items: \(error)")
|
||||
return false
|
||||
}
|
||||
entries = newItems
|
||||
return true
|
||||
}
|
||||
|
||||
private func convertForStorage(_ item: HistoryItem) throws -> Data {
|
||||
let entryData = try encoder.encode(item)
|
||||
return Data([UInt8(entryData.count)]) + entryData
|
||||
}
|
||||
}
|
||||
|
||||
final class HistoryManagerMock: HistoryManagerBase, HistoryManagerProtocol {
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
self.entries = [.mock]
|
||||
}
|
||||
|
||||
func save(item: HistoryItem) throws {
|
||||
entries.append(item)
|
||||
}
|
||||
|
||||
func delete(item: HistoryItem) -> Bool {
|
||||
entries = entries.filter { $0 != item }
|
||||
return true
|
||||
}
|
||||
}
|
193
Sesame/Common/KeyManagement.swift
Normal file
193
Sesame/Common/KeyManagement.swift
Normal file
@ -0,0 +1,193 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import SwiftUI
|
||||
|
||||
struct KeySet {
|
||||
|
||||
let remote: SymmetricKey
|
||||
|
||||
let device: SymmetricKey
|
||||
|
||||
let server: Data
|
||||
}
|
||||
|
||||
extension KeyManagement {
|
||||
|
||||
enum KeyType: String, Identifiable, CaseIterable {
|
||||
|
||||
case deviceKey = "sesame-device"
|
||||
case remoteKey = "sesame-remote"
|
||||
case authToken = "sesame-remote-auth"
|
||||
|
||||
var id: String {
|
||||
rawValue
|
||||
}
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .deviceKey:
|
||||
return "Unlock Key"
|
||||
case .remoteKey:
|
||||
return "Response Key"
|
||||
case .authToken:
|
||||
return "Server Token"
|
||||
}
|
||||
}
|
||||
|
||||
var keyLength: SymmetricKeySize {
|
||||
.bits256
|
||||
}
|
||||
|
||||
var usesHashing: Bool {
|
||||
switch self {
|
||||
case .authToken:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyManagement.KeyType: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
displayName
|
||||
}
|
||||
}
|
||||
|
||||
private struct KeyChain {
|
||||
|
||||
private let keyType = kSecAttrKeyTypeEC
|
||||
|
||||
private let keyClass = kSecAttrKeyClassSymmetric
|
||||
|
||||
private let domain: String
|
||||
|
||||
init(domain: String) {
|
||||
self.domain = domain
|
||||
}
|
||||
|
||||
private func baseQuery(for type: KeyManagement.KeyType) -> [String : Any] {
|
||||
[kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccount as String: type.rawValue,
|
||||
kSecAttrServer as String: domain]
|
||||
}
|
||||
|
||||
func save(_ type: KeyManagement.KeyType, _ key: SymmetricKey) {
|
||||
var query = baseQuery(for: type)
|
||||
query[kSecValueData as String] = key.data
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
guard status == errSecSuccess else {
|
||||
print("Failed to store \(type): \(status)")
|
||||
return
|
||||
}
|
||||
print("\(type) saved to keychain")
|
||||
}
|
||||
|
||||
func load(_ type: KeyManagement.KeyType) -> SymmetricKey? {
|
||||
var query = baseQuery(for: type)
|
||||
query[kSecReturnData as String] = kCFBooleanTrue
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
guard status != -25300 else {
|
||||
return nil
|
||||
}
|
||||
guard status == errSecSuccess else {
|
||||
print("Failed to get \(type): \(status)")
|
||||
return nil
|
||||
}
|
||||
let key = item as! CFData
|
||||
return SymmetricKey(data: key as Data)
|
||||
}
|
||||
|
||||
func delete(_ type: KeyManagement.KeyType) {
|
||||
let status = SecItemDelete(baseQuery(for: type) as CFDictionary)
|
||||
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||
print("Failed to remove \(type): \(status)")
|
||||
return
|
||||
}
|
||||
print("\(type) removed from keychain")
|
||||
}
|
||||
|
||||
func has(_ type: KeyManagement.KeyType) -> Bool {
|
||||
load(type) != nil
|
||||
}
|
||||
}
|
||||
|
||||
final class KeyManagement: ObservableObject {
|
||||
|
||||
private let keyChain: KeyChain
|
||||
|
||||
@Published
|
||||
private(set) var hasRemoteKey = false
|
||||
|
||||
@Published
|
||||
private(set) var hasDeviceKey = false
|
||||
|
||||
@Published
|
||||
private(set) var hasAuthToken = false
|
||||
|
||||
var hasAllKeys: Bool {
|
||||
hasRemoteKey && hasDeviceKey && hasAuthToken
|
||||
}
|
||||
|
||||
init() {
|
||||
self.keyChain = KeyChain(domain: "christophhagen.de")
|
||||
updateKeyStates()
|
||||
}
|
||||
|
||||
func has(_ type: KeyType) -> Bool {
|
||||
switch type {
|
||||
case .deviceKey:
|
||||
return hasDeviceKey
|
||||
case .remoteKey:
|
||||
return hasRemoteKey
|
||||
case .authToken:
|
||||
return hasAuthToken
|
||||
}
|
||||
}
|
||||
|
||||
func getAllKeys() -> KeySet? {
|
||||
guard let remoteKey = get(.remoteKey),
|
||||
let token = get(.authToken)?.data,
|
||||
let deviceKey = get(.deviceKey) else {
|
||||
return nil
|
||||
}
|
||||
return .init(remote: remoteKey, device: deviceKey, server: token)
|
||||
}
|
||||
|
||||
func get(_ type: KeyType) -> SymmetricKey? {
|
||||
keyChain.load(type)
|
||||
}
|
||||
|
||||
func delete(_ type: KeyType) {
|
||||
keyChain.delete(type)
|
||||
updateKeyStates()
|
||||
}
|
||||
|
||||
func generate(_ type: KeyType) {
|
||||
let key = SymmetricKey(size: type.keyLength)
|
||||
save(type, key: key)
|
||||
}
|
||||
|
||||
func save(_ type: KeyType, data: Data) {
|
||||
let key = SymmetricKey(data: data)
|
||||
save(type, key: key)
|
||||
}
|
||||
|
||||
private func save(_ type: KeyType, key: SymmetricKey) {
|
||||
if keyChain.has(type) {
|
||||
keyChain.delete(type)
|
||||
}
|
||||
keyChain.save(type, key)
|
||||
updateKeyStates()
|
||||
}
|
||||
|
||||
private func updateKeyStates() {
|
||||
self.hasRemoteKey = keyChain.has(.remoteKey)
|
||||
self.hasDeviceKey = keyChain.has(.deviceKey)
|
||||
self.hasAuthToken = keyChain.has(.authToken)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user