88 lines
3.1 KiB
Swift
88 lines
3.1 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import TermindDomain
|
|
|
|
public nonisolated struct SecurityKeyAssertion: Hashable, Sendable {
|
|
public var credentialID: Data
|
|
public var authenticatorData: Data
|
|
public var clientDataJSON: Data
|
|
public var signature: Data
|
|
|
|
public init(credentialID: Data, authenticatorData: Data, clientDataJSON: Data, signature: Data) {
|
|
self.credentialID = credentialID
|
|
self.authenticatorData = authenticatorData
|
|
self.clientDataJSON = clientDataJSON
|
|
self.signature = signature
|
|
}
|
|
|
|
public var userPresent: Bool {
|
|
authenticatorData.count >= 37 && authenticatorData[authenticatorData.startIndex + 32] & 1 != 0
|
|
}
|
|
public var userVerified: Bool {
|
|
authenticatorData.count >= 37 && authenticatorData[authenticatorData.startIndex + 32] & 4 != 0
|
|
}
|
|
}
|
|
|
|
public nonisolated struct SecurityKeySigningRequest: Identifiable, Hashable, Sendable {
|
|
public let id = UUID()
|
|
public var hostID: UUID
|
|
public var hostname: String
|
|
public var keyName: String
|
|
public var credential: SecurityKeyCredential
|
|
public var challenge: Data
|
|
public var allowsUserInteraction: Bool
|
|
|
|
public init(
|
|
hostID: UUID, hostname: String, credential: SecurityKeyCredential, challenge: Data,
|
|
allowsUserInteraction: Bool = true, keyName: String = "Security Key"
|
|
) {
|
|
self.hostID = hostID
|
|
self.hostname = hostname
|
|
self.keyName = keyName
|
|
self.credential = credential
|
|
self.challenge = challenge
|
|
self.allowsUserInteraction = allowsUserInteraction
|
|
}
|
|
}
|
|
|
|
public typealias SecurityKeySigningHandler = @Sendable (SecurityKeySigningRequest) async throws -> SecurityKeyAssertion
|
|
|
|
@MainActor
|
|
@Observable
|
|
public final class SecurityKeySigningCoordinator {
|
|
public private(set) var pending: SecurityKeySigningRequest?
|
|
private var continuation: CheckedContinuation<SecurityKeyAssertion, any Error>?
|
|
|
|
public init() {}
|
|
|
|
public func sign(_ request: SecurityKeySigningRequest) async throws -> SecurityKeyAssertion {
|
|
// Each connection serializes authentication; reject overlapping auxiliary requests.
|
|
guard request.allowsUserInteraction, pending == nil else { throw SSHInteractionRequired() }
|
|
return try await withTaskCancellationHandler {
|
|
try Task.checkCancellation()
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
self.continuation = continuation
|
|
pending = request
|
|
}
|
|
} onCancel: {
|
|
Task { @MainActor [weak self] in
|
|
self?.respond(request.id, result: .failure(CancellationError()))
|
|
}
|
|
}
|
|
}
|
|
|
|
public func respond(_ id: UUID, result: Result<SecurityKeyAssertion, any Error>) {
|
|
guard pending?.id == id else { return }
|
|
let continuation = continuation
|
|
self.continuation = nil
|
|
pending = nil
|
|
continuation?.resume(with: result)
|
|
}
|
|
|
|
public func cancel() {
|
|
if let pending { respond(pending.id, result: .failure(CancellationError())) }
|
|
}
|
|
}
|