Switch to higher level XPC & enforce signing requirements (#681)

* Revert "Add launch constraints (#678)"

This reverts commit c5a610d786.

* .

* Cleanup.
This commit is contained in:
Max Goedjen
2025-09-08 23:25:40 -07:00
committed by GitHub
parent 5c2d039682
commit 5467474d88
17 changed files with 637 additions and 590 deletions

View File

@@ -47,7 +47,7 @@ import XPCWrappers
/// Manually trigger an update check.
public func checkForUpdates() async throws {
let session = try XPCTypedSession<[Release], Never>(serviceName: "com.maxgoedjen.Secretive.ReleasesDownloader")
let session = try XPCTypedSession<[Release], Never>(serviceName: "com.maxgoedjen.Secretive.SecretiveUpdater")
await evaluate(releases: try await session.send())
session.complete()
}

View File

@@ -0,0 +1,14 @@
import Foundation
@objc protocol _XPCProtocol: Sendable {
func process(_ data: Data, with reply: @Sendable @escaping (Data?, Error?) -> Void)
}
public protocol XPCProtocol<Input, Output>: Sendable {
associatedtype Input: Codable
associatedtype Output: Codable
func process(_ data: Input) async throws -> Output
}

View File

@@ -0,0 +1,70 @@
import Foundation
public final class XPCServiceDelegate: NSObject, NSXPCListenerDelegate {
private let exportedObject: ErasedXPCProtocol
public init<XPCProtocolType: XPCProtocol>(exportedObject: XPCProtocolType) {
self.exportedObject = ErasedXPCProtocol(exportedObject)
}
public func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
newConnection.exportedInterface = NSXPCInterface(with: (any _XPCProtocol).self)
let exportedObject = exportedObject
newConnection.exportedObject = exportedObject
newConnection.setCodeSigningRequirement("anchor apple generic and certificate leaf[subject.OU] = Z72PRUAWF6")
newConnection.resume()
return true
}
}
@objc private final class ErasedXPCProtocol: NSObject, _XPCProtocol {
let _process: @Sendable (Data, @Sendable @escaping (Data?, (any Error)?) -> Void) -> Void
public init<XPCProtocolType: XPCProtocol>(_ exportedObject: XPCProtocolType) {
_process = { data, reply in
Task { [reply] in
do {
let decoded = try JSONDecoder().decode(XPCProtocolType.Input.self, from: data)
let result = try await exportedObject.process(decoded)
let encoded = try JSONEncoder().encode(result)
reply(encoded, nil)
} catch {
if let error = error as? Codable & Error {
reply(nil, NSError(error))
} else {
reply(nil, error)
}
}
}
}
}
func process(_ data: Data, with reply: @Sendable @escaping (Data?, (any Error)?) -> Void) {
_process(data, reply)
}
}
extension NSError {
private enum Constants {
static let domain = "com.maxgoedjen.secretive.xpcwrappers"
static let code = -1
static let dataKey = "underlying"
}
@nonobjc convenience init<ErrorType: Codable & Error>(_ error: ErrorType) {
let encoded = try? JSONEncoder().encode(error)
self.init(domain: Constants.domain, code: Constants.code, userInfo: [Constants.dataKey: encoded as Any])
}
@nonobjc public func underlying<ErrorType: Codable & Error>(as errorType: ErrorType.Type) -> ErrorType? {
guard domain == Constants.domain && code == Constants.code, let data = userInfo[Constants.dataKey] as? Data else { return nil }
return try? JSONDecoder().decode(ErrorType.self, from: data)
}
}

View File

@@ -0,0 +1,54 @@
import Foundation
public struct XPCTypedSession<ResponseType: Codable & Sendable, ErrorType: Error & Codable>: Sendable {
private nonisolated(unsafe) let connection: NSXPCConnection
private var proxy: _XPCProtocol
public init(serviceName: String, warmup: Bool = false) throws {
connection = NSXPCConnection(serviceName: serviceName)
connection.remoteObjectInterface = NSXPCInterface(with: (any _XPCProtocol).self)
connection.setCodeSigningRequirement("anchor apple generic and certificate leaf[subject.OU] = Z72PRUAWF6")
connection.resume()
guard let proxy = connection.remoteObjectProxy as? _XPCProtocol else { fatalError() }
self.proxy = proxy
if warmup {
Task { [self] in
_ = try? await send()
}
}
}
public func send(_ message: some Encodable = Data()) async throws -> ResponseType {
let encoded = try JSONEncoder().encode(message)
return try await withCheckedThrowingContinuation { continuation in
proxy.process(encoded) { data, error in
do {
if let error {
throw error
}
guard let data else {
throw NoDataError()
}
let decoded = try JSONDecoder().decode(ResponseType.self, from: data)
continuation.resume(returning: decoded)
} catch {
if let typed = (error as NSError).underlying(as: ErrorType.self) {
continuation.resume(throwing: typed)
} else {
continuation.resume(throwing: error)
}
}
}
}
}
public func complete() {
connection.invalidate()
}
public struct NoDataError: Error {}
}

View File

@@ -1,49 +0,0 @@
import Foundation
public struct XPCTypedSession<ResponseType: Codable & Sendable, ErrorType: Error & Codable>: Sendable {
private let session: XPCSession
public init(serviceName: String, warmup: Bool = false) throws {
if #available(macOS 26.0, *) {
session = try XPCSession(xpcService: serviceName, requirement: .isFromSameTeam())
} else {
session = try XPCSession(xpcService: serviceName)
}
if warmup {
Task { [self] in
_ = try? await send()
}
}
}
public func send(_ message: some Encodable = Data()) async throws -> ResponseType {
try await withCheckedThrowingContinuation { continuation in
do {
try session.send(message) { result in
switch result {
case .success(let message):
if let result = try? message.decode(as: ResponseType.self) {
continuation.resume(returning: result)
} else if let error = try? message.decode(as: ErrorType.self) {
continuation.resume(throwing: error)
} else {
continuation.resume(throwing: UnknownMessageError())
}
case .failure(let error):
continuation.resume(throwing: error)
}
}
} catch {
continuation.resume(throwing: error)
}
}
}
public func complete() {
session.cancel(reason: "Done")
}
}
public struct UnknownMessageError: Error, Codable {}