Compare commits

..
Author SHA1 Message Date
Max Goedjen da56dd6434 WIP 2026-09-06 18:08:16 -07:00
Max Goedjen 00804587de Adding host reader xpc service and connection info 2026-08-19 10:41:50 +02:00
Max Goedjen b71cbf8529 Merge 2026-08-15 19:15:03 +02:00
Max Goedjen b003286745 Fix macOS 27 UI issues (#814)
* Fix janky macOS 27 glass appearance.

* Fix janky macOS 27 glass appearance on multiline view too
2026-07-02 12:49:07 -07:00
Max Goedjen f527e136e3 WIP 2026-03-28 13:22:27 -07:00
Max Goedjen ece3865d9a Merge branch 'main' into sshextensions 2026-03-25 15:07:22 -07:00
Max Goedjen 6b1f5bbb7c WIP 2026-03-12 12:46:45 -07:00
Max Goedjen f848eb659e Messy WIP for agent extensions 2026-03-11 14:57:38 -07:00
39 changed files with 2062 additions and 326 deletions
+1 -1
View File
@@ -139,7 +139,7 @@ var localization: Resource {
var swiftSettings: [PackageDescription.SwiftSetting] { var swiftSettings: [PackageDescription.SwiftSetting] {
[ [
.swiftLanguageMode(.v6), .swiftLanguageMode(.v6),
.treatAllWarnings(as: .error), // .treatAllWarnings(as: .error),
.strictMemorySafety() .strictMemorySafety()
] ]
} }
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -34,13 +34,13 @@ import XPCWrappers
) { ) {
self.osVersion = osVersion self.osVersion = osVersion
self.currentVersion = currentVersion self.currentVersion = currentVersion
Task { _ = Task {
if checkOnLaunch { if checkOnLaunch {
try? await checkForUpdates() try? await checkForUpdates()
} }
while !Task.isCancelled { while !Task.isCancelled {
try? await Task.sleep(for: .seconds(Int(checkFrequency))) try? await Task.sleep(for: .seconds(Int(checkFrequency)))
try? await checkForUpdates() try await checkForUpdates()
} }
} }
} }
@@ -19,7 +19,7 @@ public struct OpenSSHPublicKeyWriter: Sendable {
("nistp" + String(describing: secret.keyType.size)).lengthAndData + ("nistp" + String(describing: secret.keyType.size)).lengthAndData +
secret.publicKey.lengthAndData secret.publicKey.lengthAndData
case .mldsa: case .mldsa:
// https://www.ietf.org/archive/id/draft-sfluhrer-ssh-mldsa-04.txt // https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-05
openSSHIdentifier(for: secret.keyType).lengthAndData + openSSHIdentifier(for: secret.keyType).lengthAndData +
secret.publicKey.lengthAndData secret.publicKey.lengthAndData
case .rsa: case .rsa:
@@ -42,6 +42,18 @@ public final class OpenSSHReader {
return convertEndianness ? T(value.bigEndian) : T(value) return convertEndianness ? T(value.bigEndian) : T(value)
} }
public func readNextByteAsBool() throws(OpenSSHReaderError) -> Bool {
let size = MemoryLayout<Bool>.size
guard remaining.count >= size else { throw .beyondBounds }
let lengthRange = 0..<size
let lengthChunk = remaining[lengthRange]
remaining.removeSubrange(lengthRange)
if remaining.isEmpty {
done = true
}
return unsafe lengthChunk.bytes.unsafeLoad(as: Bool.self)
}
public func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String { public func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String {
try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self) try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self)
} }
@@ -53,5 +65,6 @@ public final class OpenSSHReader {
} }
public enum OpenSSHReaderError: Error, Codable { public enum OpenSSHReaderError: Error, Codable {
case incorrectFormat
case beyondBounds case beyondBounds
} }
@@ -17,7 +17,7 @@ public struct OpenSSHSignatureWriter: Sendable {
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1 // https://datatracker.ietf.org/doc/html/rfc5656#section-3.1
ecdsaSignature(signature, keyType: secret.keyType) ecdsaSignature(signature, keyType: secret.keyType)
case .mldsa: case .mldsa:
// https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-00#name-public-key-algorithms // https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-05
mldsaSignature(signature, keyType: secret.keyType) mldsaSignature(signature, keyType: secret.keyType)
case .rsa: case .rsa:
// https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 // https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
@@ -3,6 +3,8 @@ import OSLog
import SecretKit import SecretKit
import CertificateKit import CertificateKit
import CryptoKit
public protocol SSHAgentInputParserProtocol { public protocol SSHAgentInputParserProtocol {
func parse(data: Data) async throws -> SSHAgent.Request func parse(data: Data) async throws -> SSHAgent.Request
@@ -53,8 +55,8 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol {
return .unlock return .unlock
case SSHAgent.Request.addSmartcardKeyConstrained.protocolID: case SSHAgent.Request.addSmartcardKeyConstrained.protocolID:
return .addSmartcardKeyConstrained return .addSmartcardKeyConstrained
case SSHAgent.Request.protocolExtension.protocolID: case SSHAgent.Request.protocolExtension(.empty).protocolID:
return .protocolExtension return .protocolExtension(try protocolExtension(from: body))
default: default:
return .unknown(rawRequestInt) return .unknown(rawRequestInt)
} }
@@ -64,12 +66,152 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol {
extension SSHAgentInputParser { extension SSHAgentInputParser {
private enum Constants {
static let userAuthMagic: UInt8 = 50 // SSH2_MSG_USERAUTH_REQUEST
static let sshSigMagic = Data("SSHSIG".utf8)
}
func signatureRequestContext(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext { func signatureRequestContext(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext {
let reader = OpenSSHReader(data: data) let reader = OpenSSHReader(data: data)
let rawKeyBlob = try reader.readNextChunk() let rawKeyBlob = try reader.readNextChunk()
let keyBlob = certificatePublicKeyBlob(from: rawKeyBlob) ?? rawKeyBlob let keyBlob = certificatePublicKeyBlob(from: rawKeyBlob) ?? rawKeyBlob
let dataToSign = try reader.readNextChunk() let rawPayload = try reader.readNextChunk()
return SSHAgent.Request.SignatureRequestContext(keyBlob: keyBlob, dataToSign: dataToSign) let payload: SSHAgent.Request.SignatureRequestContext.SignaturePayload
do {
if rawPayload.count > 6 && rawPayload[0..<6] == Constants.sshSigMagic {
payload = .init(raw: rawPayload, decoded: .sshSig(try sshSigPayload(from: rawPayload[6...])))
} else {
payload = .init(raw: rawPayload, decoded: .sshConnection(try sshConnectionPayload(from: rawPayload)))
}
} catch {
payload = .init(raw: rawPayload, decoded: nil)
}
return SSHAgent.Request.SignatureRequestContext(keyBlob: keyBlob, dataToSign: payload)
}
func sshSigPayload(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext.SignaturePayload.DecodedPayload.SSHSigPayload {
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L79
let payloadReader = OpenSSHReader(data: data)
let namespace = try payloadReader.readNextChunkAsString()
_ = try payloadReader.readNextChunk() // reserved
let hashAlgorithm = try payloadReader.readNextChunkAsString()
let hash = try payloadReader.readNextChunk()
return .init(
namespace: namespace,
hashAlgorithm: hashAlgorithm,
hash: hash
)
}
func sshConnectionPayload(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext.SignaturePayload.DecodedPayload.SSHConnectionPayload {
let payloadReader = OpenSSHReader(data: data)
_ = try payloadReader.readNextChunk()
let magic = try payloadReader.readNextBytes(as: UInt8.self, convertEndianness: false)
guard magic == Constants.userAuthMagic else { throw .incorrectFormat }
let username = try payloadReader.readNextChunkAsString()
_ = try payloadReader.readNextChunkAsString() // "ssh-connection"
_ = try payloadReader.readNextChunkAsString() // "publickey-hostbound-v00@openssh.com"
let hasSignature = try payloadReader.readNextByteAsBool()
let algorithm = try payloadReader.readNextChunkAsString()
let publicKeyReader = try payloadReader.readNextChunkAsSubReader()
_ = try publicKeyReader.readNextChunk()
_ = try publicKeyReader.readNextChunk()
let publicKey = try publicKeyReader.readNextChunk()
let hostKeyReader = try payloadReader.readNextChunkAsSubReader()
_ = try hostKeyReader.readNextChunk()
let hostKey = try hostKeyReader.readNextChunk()
return .init(
username: username,
hasSignature: hasSignature,
publicKeyAlgorithm: algorithm,
publicKey: publicKey,
hostKey: hostKey,
)
}
func protocolExtension(from data: Data) throws(AgentParsingError) -> SSHAgent.ProtocolExtension {
do {
let reader = OpenSSHReader(data: data)
let nameRaw = try reader.readNextChunkAsString()
let nameSplit = nameRaw.split(separator: "@")
guard nameSplit.count == 2 else {
throw AgentParsingError.invalidData
}
let (name, domain) = (nameSplit[0], nameSplit[1])
switch domain {
case SSHAgent.ProtocolExtension.OpenSSHExtension.domain:
switch name {
case SSHAgent.ProtocolExtension.OpenSSHExtension.sessionBind(.empty).name:
let hostkeyBlob = try reader.readNextChunkAsSubReader()
let hostKeyType = try hostkeyBlob.readNextChunkAsString()
let hostKeyData = try hostkeyBlob.readNextChunk()
let sessionID = try reader.readNextChunk()
let signatureBlob = try reader.readNextChunkAsSubReader()
_ = try signatureBlob.readNextChunk() // key type again
let signature = try signatureBlob.readNextChunk()
let forwarding = try reader.readNextByteAsBool()
switch hostKeyType {
// FIXME: FACTOR OUT?
case "ssh-ed25519":
let hostKey = try CryptoKit.Curve25519.Signing.PublicKey(rawRepresentation: hostKeyData)
guard hostKey.isValidSignature(signature, for: sessionID) else {
throw AgentParsingError.incorrectSignature
}
case "ecdsa-sha2-nistp256":
let hostKey = try CryptoKit.P256.Signing.PublicKey(rawRepresentation: hostKeyData)
guard hostKey.isValidSignature(try .init(rawRepresentation: signature), for: sessionID) else {
throw AgentParsingError.incorrectSignature
}
case "ecdsa-sha2-nistp384":
let hostKey = try CryptoKit.P384.Signing.PublicKey(rawRepresentation: hostKeyData)
guard hostKey.isValidSignature(try .init(rawRepresentation: signature), for: sessionID) else {
throw AgentParsingError.incorrectSignature
}
case "ssh-mldsa-65":
if #available(macOS 26.0, *) {
let hostKey = try CryptoKit.MLDSA65.PublicKey(rawRepresentation: hostKeyData)
guard hostKey.isValidSignature(signature, for: sessionID) else {
throw AgentParsingError.incorrectSignature
}
} else {
throw AgentParsingError.unhandledRequest
}
case "ssh-mldsa-87":
if #available(macOS 26.0, *) {
let hostKey = try CryptoKit.MLDSA65.PublicKey(rawRepresentation: hostKeyData)
guard hostKey.isValidSignature(signature, for: sessionID) else {
throw AgentParsingError.incorrectSignature
}
} else {
throw AgentParsingError.unhandledRequest
}
case "ssh-rsa":
// FIXME: HANDLE
throw AgentParsingError.unhandledRequest
default:
throw AgentParsingError.unhandledRequest
}
let context = SSHAgent.ProtocolExtension.OpenSSHExtension.SessionBindContext(
hostKey: hostKeyData,
sessionID: sessionID,
signature: signature,
forwarding: forwarding
)
return .openSSH(.sessionBind(context))
default:
return .openSSH(.unknown(String(name)))
}
default:
return .unknown(nameRaw)
}
} catch let error as OpenSSHReaderError {
throw .openSSHReader(error)
} catch let error as AgentParsingError {
throw error
} catch {
throw .unknownRequest
}
} }
func certificatePublicKeyBlob(from hash: Data) -> Data? { func certificatePublicKeyBlob(from hash: Data) -> Data? {
@@ -99,6 +241,7 @@ extension SSHAgentInputParser {
case unknownRequest case unknownRequest
case unhandledRequest case unhandledRequest
case invalidData case invalidData
case incorrectSignature
case openSSHReader(OpenSSHReaderError) case openSSHReader(OpenSSHReaderError)
} }
@@ -19,7 +19,7 @@ extension SSHAgent {
case lock case lock
case unlock case unlock
case addSmartcardKeyConstrained case addSmartcardKeyConstrained
case protocolExtension case protocolExtension(ProtocolExtension)
case unknown(UInt8) case unknown(UInt8)
public var protocolID: UInt8 { public var protocolID: UInt8 {
@@ -60,18 +60,82 @@ extension SSHAgent {
public struct SignatureRequestContext: Sendable, Codable { public struct SignatureRequestContext: Sendable, Codable {
public let keyBlob: Data public let keyBlob: Data
public let dataToSign: Data public let dataToSign: SignaturePayload
public init(keyBlob: Data, dataToSign: Data) { public init(keyBlob: Data, dataToSign: SignaturePayload) {
self.keyBlob = keyBlob self.keyBlob = keyBlob
self.dataToSign = dataToSign self.dataToSign = dataToSign
} }
public static var empty: SignatureRequestContext { public static var empty: SignatureRequestContext {
SignatureRequestContext(keyBlob: Data(), dataToSign: Data()) SignatureRequestContext(keyBlob: Data(), dataToSign: SignaturePayload(raw: Data(), decoded: nil))
}
public struct SignaturePayload: Sendable, Codable {
public let raw: Data
public let decoded: DecodedPayload?
public init(
raw: Data,
decoded: DecodedPayload?
) {
self.raw = raw
self.decoded = decoded
}
public enum DecodedPayload: Sendable, Codable {
case sshConnection(SSHConnectionPayload)
case sshSig(SSHSigPayload)
public struct SSHConnectionPayload: Sendable, Codable {
public let username: String
public let hasSignature: Bool
public let publicKeyAlgorithm: String
public let publicKey: Data
public let hostKey: Data
public init(
username: String,
hasSignature: Bool,
publicKeyAlgorithm: String,
publicKey: Data,
hostKey: Data
) {
self.username = username
self.hasSignature = hasSignature
self.publicKeyAlgorithm = publicKeyAlgorithm
self.publicKey = publicKey
self.hostKey = hostKey
}
}
public struct SSHSigPayload: Sendable, Codable {
public let namespace: String
public let hashAlgorithm: String
public let hash: Data
public init(
namespace: String,
hashAlgorithm: String,
hash: Data,
) {
self.namespace = namespace
self.hashAlgorithm = hashAlgorithm
self.hash = hash
}
}
}
} }
} }
} }
/// The type of the SSH Agent Response, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1 /// The type of the SSH Agent Response, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
@@ -88,8 +152,8 @@ extension SSHAgent {
switch self { switch self {
case .agentFailure: "SSH_AGENT_FAILURE" case .agentFailure: "SSH_AGENT_FAILURE"
case .agentSuccess: "SSH_AGENT_SUCCESS" case .agentSuccess: "SSH_AGENT_SUCCESS"
case .agentIdentitiesAnswer: "SSH_AGENT_IDENTITIES_ANSWER" case .agentIdentitiesAnswer: "SSH2_AGENT_IDENTITIES_ANSWER"
case .agentSignResponse: "SSH_AGENT_SIGN_RESPONSE" case .agentSignResponse: "SSH2_AGENT_SIGN_RESPONSE"
case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE" case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE"
case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE" case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE"
} }
@@ -0,0 +1,76 @@
import Foundation
// Extensions, as defined in https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.agent
extension SSHAgent {
public enum ProtocolExtension: CustomDebugStringConvertible, Codable, Sendable {
case openSSH(OpenSSHExtension)
case unknown(String)
public var debugDescription: String {
switch self {
case let .openSSH(protocolExtension):
protocolExtension.debugDescription
case .unknown(let string):
"Unknown (\(string))"
}
}
public static var empty: ProtocolExtension {
.unknown("empty")
}
private struct ProtocolExtensionParsingError: Error {}
}
}
extension SSHAgent.ProtocolExtension {
public enum OpenSSHExtension: CustomDebugStringConvertible, Codable, Sendable {
case sessionBind(SessionBindContext)
case unknown(String)
public static var domain: String {
"openssh.com"
}
public var name: String {
switch self {
case .sessionBind:
"session-bind"
case .unknown(let name):
name
}
}
public var debugDescription: String {
"\(name)@\(OpenSSHExtension.domain)"
}
}
}
extension SSHAgent.ProtocolExtension.OpenSSHExtension {
public struct SessionBindContext: Codable, Sendable {
public let hostKey: Data
public let sessionID: Data
public let signature: Data
public let forwarding: Bool
public init(hostKey: Data, sessionID: Data, signature: Data, forwarding: Bool) {
self.hostKey = hostKey
self.sessionID = sessionID
self.signature = signature
self.forwarding = forwarding
}
public static let empty = SessionBindContext(hostKey: Data(), sessionID: Data(), signature: Data(), forwarding: false)
}
}
@@ -16,6 +16,8 @@ public final class Agent: Sendable {
private let signatureWriter = OpenSSHSignatureWriter() private let signatureWriter = OpenSSHSignatureWriter()
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent") private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
@MainActor private var sessionID: SSHAgent.ProtocolExtension.OpenSSHExtension.SessionBindContext?
/// Initializes an agent with a store list and a witness. /// Initializes an agent with a store list and a witness.
/// - Parameters: /// - Parameters:
/// - storeList: The `SecretStoreList` to make available. /// - storeList: The `SecretStoreList` to make available.
@@ -31,7 +33,12 @@ public final class Agent: Sendable {
extension Agent { extension Agent {
public func handle(request: SSHAgent.Request, provenance: SigningRequestProvenance) async -> Data { public func handle(
request: SSHAgent.Request,
provenance: SigningRequestProvenance,
hosts: [Data: String]?
) async -> Data {
logger.debug("Agent received request of type \(request.debugDescription)")
// Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting // Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting
await reloadSecretsIfNeccessary() await reloadSecretsIfNeccessary()
var response = Data() var response = Data()
@@ -42,9 +49,51 @@ extension Agent {
response.append(await identities()) response.append(await identities())
logger.debug("Agent returned \(SSHAgent.Response.agentIdentitiesAnswer.debugDescription)") logger.debug("Agent returned \(SSHAgent.Response.agentIdentitiesAnswer.debugDescription)")
case .signRequest(let context): case .signRequest(let context):
let target: SigningRequestTarget?
switch context.dataToSign.decoded {
case .sshConnection(let payload):
target = .connection(
.init(
username: payload.username,
hasSignature: payload.hasSignature,
publicKeyAlgorithm: payload.publicKeyAlgorithm,
publicKey: payload.publicKey,
hostKey: payload.hostKey,
host: hosts?[payload.hostKey]
)
)
if let boundSession = await sessionID {
guard payload.hostKey == boundSession.hostKey else {
logger.error("Agent received bind request, but host key does not match signature request host key.")
throw BindingFailure()
}
}
case .sshSig(let payload):
target = .signature(
.init(
namespace: payload.namespace,
hashAlgorithm: payload.hashAlgorithm,
hash: payload.hash
)
)
default:
target = nil
}
response.append(SSHAgent.Response.agentSignResponse.data) response.append(SSHAgent.Response.agentSignResponse.data)
response.append(try await sign(data: context.dataToSign, keyBlob: context.keyBlob, provenance: provenance)) response.append(try await sign(data: context.dataToSign.raw, keyBlob: context.keyBlob, provenance: provenance, target: target))
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)") logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
case .protocolExtension(.openSSH(.sessionBind(let bind))):
response = try await MainActor.run {
guard sessionID == nil else {
logger.error("Agent received bind request, but already bound.")
// FIXME: This will break forwarding for now.
throw BindingFailure()
}
logger.debug("Agent bound")
sessionID = bind
return SSHAgent.Response.agentSuccess.data
}
logger.debug("Agent returned \(SSHAgent.Response.agentSuccess.debugDescription)")
case .unknown(let value): case .unknown(let value):
logger.error("Agent received unknown request of type \(value).") logger.error("Agent received unknown request of type \(value).")
throw UnhandledRequestError() throw UnhandledRequestError()
@@ -92,19 +141,21 @@ extension Agent {
/// - data: The data to sign. /// - data: The data to sign.
/// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request. /// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request.
/// - Returns: An OpenSSH formatted Data payload containing the signed data response. /// - Returns: An OpenSSH formatted Data payload containing the signed data response.
func sign(data: Data, keyBlob: Data, provenance: SigningRequestProvenance) async throws -> Data { func sign(data: Data, keyBlob: Data, provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws -> Data {
guard let (secret, store) = await secret(matching: keyBlob) else { guard let (secret, store) = await secret(matching: keyBlob) else {
let keyBlobHex = keyBlob.formatted(.hex()) let keyBlobHex = keyBlob.formatted(.hex())
logger.debug("Agent did not have a key matching \(keyBlobHex)") logger.debug("Agent did not have a key matching \(keyBlobHex)")
throw NoMatchingKeyError() throw NoMatchingKeyError()
} }
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance) try evaluateRestrictions(secret.attributes.restrictions, from: provenance, for: target)
let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance) try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance, target: target)
let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance, target: target)
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation) let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
try await witness?.witness(accessTo: secret, from: store, by: provenance) try await witness?.witness(accessTo: secret, from: store, by: provenance, target: target)
logger.debug("Agent signed request") logger.debug("Agent signed request")
@@ -113,6 +164,22 @@ extension Agent {
} }
extension Agent {
func evaluateRestrictions(_ restrictions: Restrictions?, from provenance: SigningRequestProvenance, for target: SigningRequestTarget?) throws(RestrictionError) {
guard let restrictions else { return }
print(restrictions)
throw .connectionsNotPermitted
}
enum RestrictionError: Error {
case signingNotPermitted
case connectionsNotPermitted
case hostNotAllowed(String)
}
}
extension Agent { extension Agent {
/// Gives any store with no loaded secrets a chance to reload. /// Gives any store with no loaded secrets a chance to reload.
@@ -142,6 +209,7 @@ extension Agent {
struct NoMatchingKeyError: Error {} struct NoMatchingKeyError: Error {}
struct UnhandledRequestError: Error {} struct UnhandledRequestError: Error {}
struct BindingFailure: Error {}
} }
@@ -1,5 +1,6 @@
import Foundation import Foundation
import SecretKit import SecretKit
import SSHProtocolKit
/// A protocol that allows conformers to be notified of access to secrets, and optionally prevent access. /// A protocol that allows conformers to be notified of access to secrets, and optionally prevent access.
public protocol SigningWitness: Sendable { public protocol SigningWitness: Sendable {
@@ -10,13 +11,13 @@ public protocol SigningWitness: Sendable {
/// - store: The `Store` being asked to sign the request.. /// - store: The `Store` being asked to sign the request..
/// - provenance: A `SigningRequestProvenance` object describing the origin of the request. /// - provenance: A `SigningRequestProvenance` object describing the origin of the request.
/// - Note: This method being called does not imply that the requst has been authorized. If a secret requires authentication, authentication will still need to be performed by the user before the request will be performed. If the user declines or fails to authenticate, the request will fail. /// - Note: This method being called does not imply that the requst has been authorized. If a secret requires authentication, authentication will still need to be performed by the user before the request will be performed. If the user declines or fails to authenticate, the request will fail.
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws
/// Notifies the callee that a signing operation has been performed for a given secret. /// Notifies the callee that a signing operation has been performed for a given secret.
/// - Parameters: /// - Parameters:
/// - secret: The `Secret` that will was used to sign the request. /// - secret: The `Secret` that will was used to sign the request.
/// - store: The `Store` that signed the request.. /// - store: The `Store` that signed the request..
/// - provenance: A `SigningRequestProvenance` object describing the origin of the request. /// - provenance: A `SigningRequestProvenance` object describing the origin of the request.
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws
} }
@@ -1,7 +1,6 @@
import Foundation import Foundation
import OSLog import OSLog
import SecretKit import SecretKit
import launch
/// A controller that manages socket configuration and request dispatching. /// A controller that manages socket configuration and request dispatching.
public struct SocketController { public struct SocketController {
@@ -13,8 +12,7 @@ public struct SocketController {
private let sessionsContinuation: AsyncStream<Session>.Continuation private let sessionsContinuation: AsyncStream<Session>.Continuation
/// The active SocketPort. Must be retained to be kept valid. /// The active SocketPort. Must be retained to be kept valid.
/// Only applicable for legacy non-launchd sockets. private let port: SocketPort
private let port: SocketPort?
/// The FileHandle for the main socket. /// The FileHandle for the main socket.
private let fileHandle: FileHandle private let fileHandle: FileHandle
@@ -25,41 +23,19 @@ public struct SocketController {
/// Tracer which determines who originates a socket connection. /// Tracer which determines who originates a socket connection.
private let requestTracer = SigningRequestTracer() private let requestTracer = SigningRequestTracer()
public enum Socket { /// Initializes a socket controller with a specified path.
case launchd(String) /// - Parameter path: The path to use as a socket.
case path(String) public init(path: String) {
}
public init(_ socket: Socket) {
(sessions, sessionsContinuation) = AsyncStream<Session>.makeStream() (sessions, sessionsContinuation) = AsyncStream<Session>.makeStream()
switch socket { logger.debug("Socket controller setting up at \(path)")
case .path(let path): if let _ = try? FileManager.default.removeItem(atPath: path) {
logger.debug("Socket controller setting up at \(path)") logger.debug("Socket controller removed existing socket")
if let _ = try? FileManager.default.removeItem(atPath: path) {
logger.debug("Socket controller removed existing socket")
}
let exists = FileManager.default.fileExists(atPath: path)
assert(!exists)
logger.debug("Socket controller path is clear")
let port = SocketPort(path: path)
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
self.port = port
logger.debug("Socket listening at \(path)")
case .launchd(let name):
logger.debug("Socket controller setting for launchd-controlled socket \(name)")
port = nil
var fileDescriptors: UnsafeMutablePointer<Int32>? = nil
var count = 0
let result = unsafe launch_activate_socket(name, &fileDescriptors, &count)
guard result == kOSReturnSuccess, let socket = unsafe fileDescriptors?.pointee else {
fatalError()
}
fileHandle = FileHandle(fileDescriptor: socket, closeOnDealloc: true)
} }
listen() let exists = FileManager.default.fileExists(atPath: path)
} assert(!exists)
logger.debug("Socket controller path is clear")
func listen() { port = SocketPort(path: path)
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
Task { @MainActor [fileHandle, sessionsContinuation, logger] in Task { @MainActor [fileHandle, sessionsContinuation, logger] in
// Create the sequence before triggering the notification to // Create the sequence before triggering the notification to
// ensure it will not be missed. // ensure it will not be missed.
@@ -75,7 +51,7 @@ public struct SocketController {
fileHandle.acceptConnectionInBackgroundAndNotify() fileHandle.acceptConnectionInBackgroundAndNotify()
} }
} }
logger.debug("Socket listening at \(path)")
} }
} }
@@ -149,7 +125,14 @@ private extension SocketPort {
convenience init(path: String) { convenience init(path: String) {
var addr = sockaddr_un() var addr = sockaddr_un()
let length = addr.setPath(path)
let length = withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
path.withCString { cstring in
let len = unsafe strlen(cstring)
unsafe strncpy(pointer, cstring, len)
return len
}
}
// This doesn't seem to be _strictly_ neccessary with SocketPort. // This doesn't seem to be _strictly_ neccessary with SocketPort.
// but just for good form. // but just for good form.
addr.sun_family = sa_family_t(AF_UNIX) addr.sun_family = sa_family_t(AF_UNIX)
@@ -161,31 +144,3 @@ private extension SocketPort {
} }
} }
private extension sockaddr_un {
mutating func setPath(_ path: String) -> Int {
#if compiler(<6.4)
unsafe withUnsafeMutablePointer(to: &self.sun_path.0) { pointer in
unsafe path.withCString { cstring in
let len = unsafe strlen(cstring)
unsafe strncpy(pointer, cstring, len)
return len
}
}
#else
withUnsafeMutablePointer(to: &self.sun_path.0) { pointer in
path.withCString { cstring in
let len = unsafe strlen(cstring)
unsafe strncpy(pointer, cstring, len)
return len
}
}
#endif
}
}
// Changes the header from `UnsafeMutablePointer<UnsafeMutablePointer<Int32>?>?` -> `UnsafeMutablePointer<UnsafeMutablePointer<Int32>?>!`
@_silgen_name("launch_activate_socket")
func launch_activate_socket(_ name: UnsafePointer<CChar>, _ fds: UnsafeMutablePointer<UnsafeMutablePointer<Int32>?>!, _ cnt: UnsafeMutablePointer<Int>!) -> Int32
@@ -8,7 +8,7 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
private let _id: @Sendable () -> UUID private let _id: @Sendable () -> UUID
private let _name: @MainActor @Sendable () -> String private let _name: @MainActor @Sendable () -> String
private let _secrets: @MainActor @Sendable () -> [AnySecret] private let _secrets: @MainActor @Sendable () -> [AnySecret]
private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance) async throws -> Data private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance, SigningRequestTarget?) async throws -> Data
private let _existingPersistedAuthenticationContext: @Sendable (AnySecret) async -> PersistedAuthenticationContext? private let _existingPersistedAuthenticationContext: @Sendable (AnySecret) async -> PersistedAuthenticationContext?
private let _persistAuthentication: @Sendable (AnySecret, TimeInterval) async throws -> Void private let _persistAuthentication: @Sendable (AnySecret, TimeInterval) async throws -> Void
private let _reloadSecrets: @Sendable () async -> Void private let _reloadSecrets: @Sendable () async -> Void
@@ -19,7 +19,7 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
_name = { secretStore.name } _name = { secretStore.name }
_id = { secretStore.id } _id = { secretStore.id }
_secrets = { secretStore.secrets.map { AnySecret($0) } } _secrets = { secretStore.secrets.map { AnySecret($0) } }
_sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) } _sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2, target: $3) }
_existingPersistedAuthenticationContext = { await secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) } _existingPersistedAuthenticationContext = { await secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) }
_persistAuthentication = { try await secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) } _persistAuthentication = { try await secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
_reloadSecrets = { await secretStore.reloadSecrets() } _reloadSecrets = { await secretStore.reloadSecrets() }
@@ -41,8 +41,8 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
return _secrets() return _secrets()
} }
public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) async throws -> Data { public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws -> Data {
try await _sign(data, secret, provenance) try await _sign(data, secret, provenance, target)
} }
public func existingPersistedAuthenticationContext(secret: AnySecret) async -> PersistedAuthenticationContext? { public func existingPersistedAuthenticationContext(secret: AnySecret) async -> PersistedAuthenticationContext? {
@@ -7,7 +7,10 @@ public struct Attributes: Sendable, Codable, Hashable {
/// The authentication requirements for the key. This is simply a description of the option recorded at creation – modifying it doers not modify the key's authentication requirements. /// The authentication requirements for the key. This is simply a description of the option recorded at creation – modifying it doers not modify the key's authentication requirements.
public let authentication: AuthenticationRequirement public let authentication: AuthenticationRequirement
/// The authentication restrictions for the key.
public var restrictions: Restrictions?
/// The string appended to the end of the SSH Public Key. /// The string appended to the end of the SSH Public Key.
/// If nil, a default value will be used. /// If nil, a default value will be used.
public var publicKeyAttribution: String? public var publicKeyAttribution: String?
@@ -15,10 +18,12 @@ public struct Attributes: Sendable, Codable, Hashable {
public init( public init(
keyType: KeyType, keyType: KeyType,
authentication: AuthenticationRequirement, authentication: AuthenticationRequirement,
restrictions: Restrictions?,
publicKeyAttribution: String? = nil publicKeyAttribution: String? = nil
) { ) {
self.keyType = keyType self.keyType = keyType
self.authentication = authentication self.authentication = authentication
self.restrictions = restrictions
self.publicKeyAttribution = publicKeyAttribution self.publicKeyAttribution = publicKeyAttribution
} }
@@ -33,17 +38,17 @@ public enum AuthenticationRequirement: String, Hashable, Sendable, Codable, Iden
/// Authentication is not required for usage. /// Authentication is not required for usage.
case notRequired case notRequired
/// The user needs to authenticate, using either a biometric option, a connected authorized watch, or password entry.. /// The user needs to authenticate, using either a biometric option, a connected authorized watch, or password entry..
case presenceRequired case presenceRequired
/// ONLY the current set of biometric data, as matching at time of creation, is accepted. /// ONLY the current set of biometric data, as matching at time of creation, is accepted.
/// - Warning: This is a dangerous option prone to data loss. The user should be warned before configuring this key that if they modify their enrolled biometry INCLUDING by simply adding a new entry (ie, adding another fingeprting), the key will no longer be able to be accessed. This cannot be overridden with a password. /// - Warning: This is a dangerous option prone to data loss. The user should be warned before configuring this key that if they modify their enrolled biometry INCLUDING by simply adding a new entry (ie, adding another fingeprting), the key will no longer be able to be accessed. This cannot be overridden with a password.
case biometryCurrent case biometryCurrent
/// The authentication requirement was not recorded at creation, and is unknown. /// The authentication requirement was not recorded at creation, and is unknown.
case unknown case unknown
/// Whether or not the key is known to require authentication. /// Whether or not the key is known to require authentication.
public var required: Bool { public var required: Bool {
self == .presenceRequired || self == .biometryCurrent self == .presenceRequired || self == .biometryCurrent
@@ -53,3 +58,46 @@ public enum AuthenticationRequirement: String, Hashable, Sendable, Codable, Iden
self self
} }
} }
/// The restrictions for the key.
public struct Restrictions: Hashable, Sendable, Codable, Identifiable {
public enum AllowedDomains: Hashable, Sendable, Codable {
case all
case specific([String])
public func hash(into hasher: inout Hasher) {
switch self {
case .all:
break
case .specific(let values):
values.hash(into: &hasher)
}
}
}
public enum AllowedProvenancePaths: Hashable, Sendable, Codable, Identifiable {
case all
case specific([String])
public var id: Int {
hashValue
}
}
public var allowForwarding: Bool
public var allowSigning: Bool
public var allowConnections: Bool
public var allowedDomains: AllowedDomains
public var allowedProvenancePaths: AllowedProvenancePaths
public var id: Restrictions {
self
}
// public static let `default` = Restrictions(allowForwarding: true, allowSigning: true, allowConnections: true, allowedDomains: .all, allowedProvenancePaths: .all)
public static let `default` = Restrictions(allowForwarding: true, allowSigning: true, allowConnections: true, allowedDomains: .specific(["example.com"]), allowedProvenancePaths: .all)
}
@@ -20,7 +20,7 @@ public protocol SecretStore<SecretType>: Identifiable, Sendable {
/// - secret: The ``Secret`` to sign with. /// - secret: The ``Secret`` to sign with.
/// - provenance: A ``SigningRequestProvenance`` describing where the request came from. /// - provenance: A ``SigningRequestProvenance`` describing where the request came from.
/// - Returns: The signed data. /// - Returns: The signed data.
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) async throws -> Data func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws -> Data
/// Checks to see if there is currently a valid persisted authentication for a given secret. /// Checks to see if there is currently a valid persisted authentication for a given secret.
/// - Parameters: /// - Parameters:
@@ -0,0 +1,55 @@
import Foundation
import AppKit
/// Describes the target of the signature operation.
public enum SigningRequestTarget: Sendable {
case connection(ConnectionPayload)
case signature(SignaturePayload)
public struct ConnectionPayload: Sendable, Codable{
public let username: String
public let hasSignature: Bool
public let publicKeyAlgorithm: String
public let publicKey: Data
public let hostKey: Data
public let host: String?
public init(
username: String,
hasSignature: Bool,
publicKeyAlgorithm: String,
publicKey: Data,
hostKey: Data,
host: String?
) {
self.username = username
self.hasSignature = hasSignature
self.publicKeyAlgorithm = publicKeyAlgorithm
self.publicKey = publicKey
self.hostKey = hostKey
self.host = host
}
}
public struct SignaturePayload: Sendable, Codable {
public let namespace: String
public let hashAlgorithm: String
public let hash: Data
public init(
namespace: String,
hashAlgorithm: String,
hash: Data,
) {
self.namespace = namespace
self.hashAlgorithm = hashAlgorithm
self.hash = hash
}
}
}
@@ -47,7 +47,7 @@ extension SecureEnclave {
.contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown .contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown
do { do {
let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID) let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID)
let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth)) let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth, restrictions: .default))
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else { guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
logger.log("Skipping \(name), public key already present. Marking as migrated.") logger.log("Skipping \(name), public key already present. Marking as migrated.")
markMigrated(secret: secret, oldID: id) markMigrated(secret: secret, oldID: id)
@@ -37,13 +37,20 @@ extension SecureEnclave {
// MARK: SecretStore // MARK: SecretStore
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data { public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws -> Data {
var context: LAContext var context: LAContext
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) { if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
context = unsafe existing.context context = unsafe existing.context
} else { } else {
let newContext = LAContext() let newContext = LAContext()
newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name)) switch target {
case .connection(let payload) where payload.host != nil:
newContext.localizedReason = String(localized: .authContextRequestSignatureForConnectionDescription(appName: provenance.origin.displayName, targetName: payload.host!, secretName: secret.name))
case .signature(let payload):
newContext.localizedReason = String(localized: .authContextRequestSignatureForSignatureDescription(namespace: payload.namespace, appName: provenance.origin.displayName, secretName: secret.name))
default:
newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name))
}
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton) newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
context = newContext context = newContext
} }
@@ -10,6 +10,7 @@ extension SmartCard {
public let name: String public let name: String
public let publicKey: Data public let publicKey: Data
public var attributes: Attributes public var attributes: Attributes
public var restrictions: Restrictions?
} }
@@ -57,7 +57,7 @@ extension SmartCard {
// MARK: Public API // MARK: Public API
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data { public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws -> Data {
guard let tokenID = await state.tokenID else { fatalError() } guard let tokenID = await state.tokenID else { fatalError() }
var context: LAContext var context: LAContext
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) { if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
@@ -171,7 +171,7 @@ extension SmartCard.Store {
let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)! let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)!
let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any] let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any]
let publicKey = publicKeyAttributes[kSecValueData] as! Data let publicKey = publicKeyAttributes[kSecValueData] as! Data
let attributes = Attributes(keyType: KeyType(secAttr: algorithmSecAttr, size: keySize)!, authentication: .presenceRequired) let attributes = Attributes(keyType: KeyType(secAttr: algorithmSecAttr, size: keySize)!, authentication: .presenceRequired, restrictions: .default)
let secret = SmartCard.Secret(id: tokenID, name: name, publicKey: publicKey, attributes: attributes) let secret = SmartCard.Secret(id: tokenID, name: name, publicKey: publicKey, attributes: attributes)
guard signatureAlgorithm(for: secret) != nil else { return nil } guard signatureAlgorithm(for: secret) != nil else { return nil }
return secret return secret
@@ -49,7 +49,7 @@ extension Stub {
print("Public Key OpenSSH: \(OpenSSHPublicKeyWriter().openSSHString(secret: secret))") print("Public Key OpenSSH: \(OpenSSHPublicKeyWriter().openSSHString(secret: secret))")
} }
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data { public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) throws -> Data {
guard !shouldThrow else { guard !shouldThrow else {
throw NSError(domain: "test", code: 0, userInfo: nil) throw NSError(domain: "test", code: 0, userInfo: nil)
} }
+12 -20
View File
@@ -13,7 +13,7 @@ import SwiftUI
extension EnvironmentValues { extension EnvironmentValues {
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore() @MainActor fileprivate static let _certificateStore = CertificateStore()
@MainActor var certificateStore: CertificateStore { @MainActor var certificateStore: CertificateStore {
EnvironmentValues._certificateStore EnvironmentValues._certificateStore
@@ -41,26 +41,29 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@MainActor private lazy var agent: Agent = { @MainActor private lazy var agent: Agent = {
Agent(storeList: storeList, certificateStore: EnvironmentValues._certificateStore, witness: notifier) Agent(storeList: storeList, certificateStore: EnvironmentValues._certificateStore, witness: notifier)
}() }()
private var shutdownTask: Task<Void, Error>? private lazy var socketController: SocketController = {
private let socketController = SocketController(.launchd("SecureListener")) let path = URL.socketPath as String
return SocketController(path: path)
}()
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate") private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
func applicationDidFinishLaunching(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
logger.debug("SecretAgent finished launching") logger.debug("SecretAgent finished launching")
Task { _ = Task {
for await session in socketController.sessions { for await session in socketController.sessions {
Task { _ = Task {
let inputParser = try await XPCAgentInputParser()
let hostsReader = try await XPCHostsfileReader()
let hosts = try? await hostsReader.read()
do { do {
let inputParser = try await XPCAgentInputParser()
for await message in session.messages { for await message in session.messages {
let request = try await inputParser.parse(data: message) let request = try await inputParser.parse(data: message)
let agentResponse = await agent.handle(request: request, provenance: session.provenance) let agentResponse = await agent.handle(request: request, provenance: session.provenance, hosts: hosts)
try session.write(agentResponse) try session.write(agentResponse)
} }
} catch { } catch {
try? session.close() try session.close()
} }
startCountdownClock()
} }
} }
} }
@@ -89,16 +92,5 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
} }
func startCountdownClock() {
// FIXME: ACCOUNT FOR STORED AUTH
logger.log("Beginning countdown clock")
shutdownTask?.cancel()
shutdownTask = Task { [logger] in
try await Task.sleep(for: .seconds(30))
logger.log("Shutting down")
await NSApplication.shared.terminate(nil)
}
}
} }
+22 -5
View File
@@ -60,12 +60,19 @@ final class Notifier: Sendable {
notificationCenter.requestAuthorization(options: .alert) { _, _ in } notificationCenter.requestAuthorization(options: .alert) { _, _ in }
} }
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async { func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?) async {
await notificationDelegate.state.setPending(secret: secret, store: store) await notificationDelegate.state.setPending(secret: secret, store: store)
let notificationCenter = UNUserNotificationCenter.current() let notificationCenter = UNUserNotificationCenter.current()
let notificationContent = UNMutableNotificationContent() let notificationContent = UNMutableNotificationContent()
notificationContent.title = String(localized: .signedNotificationTitle(appName: provenance.origin.displayName)) notificationContent.title = String(localized: .signedNotificationTitle(appName: provenance.origin.displayName))
notificationContent.subtitle = String(localized: .signedNotificationDescription(secretName: secret.name)) switch target {
case .connection(let payload) where payload.host != nil:
notificationContent.subtitle = String(localized: .signedConnectionNotificationDescription(secretName: secret.name, username: payload.username, host: payload.host!))
case .signature(let payload):
notificationContent.subtitle = String(localized: .signedSignatureNotificationDescription(secretName: secret.name, namespace: payload.namespace))
default:
notificationContent.subtitle = String(localized: .signedNotificationDescription(secretName: secret.name))
}
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
notificationContent.interruptionLevel = .timeSensitive notificationContent.interruptionLevel = .timeSensitive
@@ -100,11 +107,21 @@ final class Notifier: Sendable {
extension Notifier: SigningWitness { extension Notifier: SigningWitness {
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws { func speakNowOrForeverHoldYourPeace(
forAccessTo secret: AnySecret,
from store: AnySecretStore,
by provenance: SigningRequestProvenance,
target: SigningRequestTarget?
) async throws {
} }
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws { func witness(
await notify(accessTo: secret, from: store, by: provenance) accessTo secret: AnySecret,
from store: AnySecretStore,
by provenance: SigningRequestProvenance,
target: SigningRequestTarget?
) async throws {
await notify(accessTo: secret, from: store, by: provenance, target: target)
} }
} }
+2 -2
View File
@@ -16,10 +16,10 @@
<string>1</string> <string>1</string>
<key>com.apple.security.hardened-process.hardened-heap</key> <key>com.apple.security.hardened-process.hardened-heap</key>
<true/> <true/>
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
<string>2</string>
<key>com.apple.security.smartcard</key> <key>com.apple.security.smartcard</key>
<true/> <true/>
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
<string>2</string>
<key>keychain-access-groups</key> <key>keychain-access-groups</key>
<array> <array>
<string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string> <string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string>
@@ -0,0 +1,36 @@
import Foundation
import OSLog
import XPCWrappers
import OSLog
public final class XPCHostsfileReader {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "XPCHostsfileReader")
private let session: XPCTypedSession<[Data: String], HostsfileReaderError>
public init() async throws {
logger.debug("Creating XPCHostsfileReader")
session = try await XPCTypedSession(serviceName: "com.maxgoedjen.Secretive.SecretAgentHostsfileReader", warmup: true)
logger.debug("XPCHostsfileReader is warmed up.")
}
public func read() async throws -> [Data: String] {
logger.debug("Reading hosts file")
defer { logger.debug("Read hosts file") }
return try await session.send(Data())
}
deinit {
session.complete()
}
}
extension XPCHostsfileReader {
public enum HostsfileReaderError: Error, Codable {
case fileDoesNotExist
case parseError
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>XPCService</key>
<dict>
<key>ServiceType</key>
<string>Application</string>
</dict>
</dict>
</plist>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.hardened-process</key>
<true/>
<key>com.apple.security.hardened-process.checked-allocations</key>
<true/>
<key>com.apple.security.hardened-process.dyld-ro</key>
<true/>
<key>com.apple.security.hardened-process.enhanced-security-version-string</key>
<string>2</string>
<key>com.apple.security.hardened-process.hardened-heap</key>
<true/>
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
<string>2</string>
</dict>
</plist>
@@ -0,0 +1,26 @@
import Foundation
import OSLog
import XPCWrappers
import SSHProtocolKit
final class SecretAgentHostsfileReader: NSObject, XPCProtocol {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.SecretAgentHostsfileReader", category: "SecretAgentHostsfileReader")
func process(_ data: Data) async throws -> [Data: String] {
logger.log("Parser parsed certificate")
var result: [Data: String] = [:]
// FIXME: THIS
for try await line in URL(filePath: "/Users/max/.ssh/known_hosts").lines {
let split = line.split(separator: " ").map(String.init)
guard split.count == 3 else { continue }
guard let decoded = Data(base64Encoded: split[2]) else { continue }
let reader = OpenSSHReader(data: decoded)
_ = try reader.readNextChunk()
let key = try reader.readNextChunk()
result[key] = split[0]
}
return result
}
}
@@ -0,0 +1,7 @@
import Foundation
import XPCWrappers
let delegate = XPCServiceDelegate(exportedObject: SecretAgentHostsfileReader())
let listener = NSXPCListener.service()
listener.delegate = delegate
listener.resume()
+246 -43
View File
@@ -21,16 +21,21 @@
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; }; 5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; }; 5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; }; 501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; };
501421652781268000BBAA70 /* SecretAgent.app in Copy SecretAgent */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; }; 50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; }; 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; }; 501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; };
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
502452F92FE2026E009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist in Copy SecretAgent plist */ = {isa = PBXBuildFile; fileRef = 502452F32FE1FF89009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist */; };
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; }; 504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; }; 504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; };
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; }; 504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */; }; 504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */; };
505402A03034B692000C3356 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5054029D3034B692000C3356 /* main.swift */; };
505402A13034B692000C3356 /* SecretAgentHostsfileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5054029E3034B692000C3356 /* SecretAgentHostsfileReader.swift */; };
505402A33034B787000C3356 /* SecretAgentHostsfileReader.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 5054028D3034B5E3000C3356 /* SecretAgentHostsfileReader.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
505402A73034B7A4000C3356 /* XPCWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = 505402A63034B7A4000C3356 /* XPCWrappers */; };
505402AA3034B80E000C3356 /* XPCHostsfileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505402A93034B80E000C3356 /* XPCHostsfileReader.swift */; };
505402AC303594D1000C3356 /* SSHProtocolKit in Frameworks */ = {isa = PBXBuildFile; productRef = 505402AB303594D1000C3356 /* SSHProtocolKit */; };
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; }; 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
505F5EF22FA9635700C45824 /* CertificateKit in Frameworks */ = {isa = PBXBuildFile; productRef = 505F5EF12FA9635700C45824 /* CertificateKit */; }; 505F5EF22FA9635700C45824 /* CertificateKit in Frameworks */ = {isa = PBXBuildFile; productRef = 505F5EF12FA9635700C45824 /* CertificateKit */; };
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; }; 50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
@@ -111,6 +116,13 @@
remoteGlobalIDString = 501577BC2E6BC5B4004A37D0; remoteGlobalIDString = 501577BC2E6BC5B4004A37D0;
remoteInfo = ReleasesDownloader; remoteInfo = ReleasesDownloader;
}; };
505402A43034B787000C3356 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 5054028C3034B5E3000C3356;
remoteInfo = SecretAgentHostsfileReader;
};
50692D1B2E6FDB880043C7BB /* PBXContainerItemProxy */ = { 50692D1B2E6FDB880043C7BB /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 50617D7723FCE48D0099B055 /* Project object */; containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
@@ -175,6 +187,7 @@
dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices";
dstSubfolderSpec = 16; dstSubfolderSpec = 16;
files = ( files = (
505402A33034B787000C3356 /* SecretAgentHostsfileReader.xpc in Embed XPC Services */,
50692E6D2E6FFA5F0043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */, 50692E6D2E6FFA5F0043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */,
50E2052D2FAAB92000402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */, 50E2052D2FAAB92000402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */,
50692E702E6FFA6E0043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */, 50692E702E6FFA6E0043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */,
@@ -182,17 +195,6 @@
name = "Embed XPC Services"; name = "Embed XPC Services";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
502452F82FE2024D009EE753 /* Copy SecretAgent plist */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = Contents/Library/LaunchAgents;
dstSubfolderSpec = 1;
files = (
502452F92FE2026E009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist in Copy SecretAgent plist */,
);
name = "Copy SecretAgent plist";
runOnlyForDeploymentPostprocessing = 0;
};
50617DBF23FCE4AB0099B055 /* Embed Frameworks */ = { 50617DBF23FCE4AB0099B055 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -213,15 +215,14 @@
name = "Embed Frameworks"; name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
50C385AF240E438B00AF2719 /* Copy SecretAgent */ = { 50C385AF240E438B00AF2719 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
dstPath = Contents/Library/LoginItems; dstPath = Contents/Library/LoginItems;
dstSubfolderSpec = 1; dstSubfolderSpec = 1;
files = ( files = (
501421652781268000BBAA70 /* SecretAgent.app in Copy SecretAgent */, 501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */,
); );
name = "Copy SecretAgent";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
@@ -237,11 +238,16 @@
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; }; 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; }; 501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; };
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
502452F32FE1FF89009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = com.maxgoedjen.Secretive.SecretAgent.plist; sourceTree = "<group>"; };
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; }; 504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; }; 504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; }; 504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxBackgroundStyle.swift; sourceTree = "<group>"; }; 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxBackgroundStyle.swift; sourceTree = "<group>"; };
5054028D3034B5E3000C3356 /* SecretAgentHostsfileReader.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretAgentHostsfileReader.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
5054029C3034B692000C3356 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
5054029D3034B692000C3356 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
5054029E3034B692000C3356 /* SecretAgentHostsfileReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretAgentHostsfileReader.swift; sourceTree = "<group>"; };
505402A83034B7D2000C3356 /* SecretAgentHostsfileReader.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgentHostsfileReader.entitlements; sourceTree = "<group>"; };
505402A93034B80E000C3356 /* XPCHostsfileReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCHostsfileReader.swift; sourceTree = "<group>"; };
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; }; 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
5059933F2E7A3B5B0092CFFA /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/Main.storyboard; sourceTree = "<group>"; }; 5059933F2E7A3B5B0092CFFA /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/Main.storyboard; sourceTree = "<group>"; };
50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -304,6 +310,15 @@
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
5054028A3034B5E3000C3356 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
505402AC303594D1000C3356 /* SSHProtocolKit in Frameworks */,
505402A73034B7A4000C3356 /* XPCWrappers in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
50617D7C23FCE48D0099B055 /* Frameworks */ = { 50617D7C23FCE48D0099B055 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -422,6 +437,17 @@
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
5054029F3034B692000C3356 /* SecretAgentHostsfileReader */ = {
isa = PBXGroup;
children = (
505402A83034B7D2000C3356 /* SecretAgentHostsfileReader.entitlements */,
5054029C3034B692000C3356 /* Info.plist */,
5054029D3034B692000C3356 /* main.swift */,
5054029E3034B692000C3356 /* SecretAgentHostsfileReader.swift */,
);
path = SecretAgentHostsfileReader;
sourceTree = "<group>";
};
50617D7623FCE48D0099B055 = { 50617D7623FCE48D0099B055 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -432,6 +458,7 @@
50692D272E6FDB8D0043C7BB /* SecretiveUpdater */, 50692D272E6FDB8D0043C7BB /* SecretiveUpdater */,
50692E662E6FF9E20043C7BB /* SecretAgentInputParser */, 50692E662E6FF9E20043C7BB /* SecretAgentInputParser */,
50E205262FAAB82700402380 /* SecretiveCertificateParser */, 50E205262FAAB82700402380 /* SecretiveCertificateParser */,
5054029F3034B692000C3356 /* SecretAgentHostsfileReader */,
50617D8023FCE48E0099B055 /* Products */, 50617D8023FCE48E0099B055 /* Products */,
5099A08B240243730062B6F2 /* Frameworks */, 5099A08B240243730062B6F2 /* Frameworks */,
); );
@@ -445,6 +472,7 @@
50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */, 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */,
50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */, 50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */,
50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */, 50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */,
5054028D3034B5E3000C3356 /* SecretAgentHostsfileReader.xpc */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -459,7 +487,6 @@
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */, 508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
50E4C4C72E777E4200C73783 /* AppIcon.icon */, 50E4C4C72E777E4200C73783 /* AppIcon.icon */,
50617D8F23FCE48E0099B055 /* Secretive.entitlements */, 50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
502452F32FE1FF89009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist */,
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */, 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */,
50617D8823FCE48E0099B055 /* Preview Content */, 50617D8823FCE48E0099B055 /* Preview Content */,
); );
@@ -544,6 +571,7 @@
50020BAF24064869003D4025 /* AppDelegate.swift */, 50020BAF24064869003D4025 /* AppDelegate.swift */,
5018F54E24064786002EB505 /* Notifier.swift */, 5018F54E24064786002EB505 /* Notifier.swift */,
501578122E6C0479004A37D0 /* XPCInputParser.swift */, 501578122E6C0479004A37D0 /* XPCInputParser.swift */,
505402A93034B80E000C3356 /* XPCHostsfileReader.swift */,
50E2057F2FAB291E00402380 /* CertificateMigrator.swift */, 50E2057F2FAB291E00402380 /* CertificateMigrator.swift */,
50A3B79524026B7600D209EA /* Main.storyboard */, 50A3B79524026B7600D209EA /* Main.storyboard */,
50A3B79824026B7600D209EA /* Info.plist */, 50A3B79824026B7600D209EA /* Info.plist */,
@@ -576,6 +604,27 @@
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
5054028C3034B5E3000C3356 /* SecretAgentHostsfileReader */ = {
isa = PBXNativeTarget;
buildConfigurationList = 5054029B3034B5E3000C3356 /* Build configuration list for PBXNativeTarget "SecretAgentHostsfileReader" */;
buildPhases = (
505402893034B5E3000C3356 /* Sources */,
5054028A3034B5E3000C3356 /* Frameworks */,
5054028B3034B5E3000C3356 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = SecretAgentHostsfileReader;
packageProductDependencies = (
505402A63034B7A4000C3356 /* XPCWrappers */,
505402AB303594D1000C3356 /* SSHProtocolKit */,
);
productName = SecretAgentHostsfileReader;
productReference = 5054028D3034B5E3000C3356 /* SecretAgentHostsfileReader.xpc */;
productType = "com.apple.product-type.xpc-service";
};
50617D7E23FCE48D0099B055 /* Secretive */ = { 50617D7E23FCE48D0099B055 /* Secretive */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 50617D9D23FCE48E0099B055 /* Build configuration list for PBXNativeTarget "Secretive" */; buildConfigurationList = 50617D9D23FCE48E0099B055 /* Build configuration list for PBXNativeTarget "Secretive" */;
@@ -584,8 +633,7 @@
50617D7C23FCE48D0099B055 /* Frameworks */, 50617D7C23FCE48D0099B055 /* Frameworks */,
50617D7D23FCE48D0099B055 /* Resources */, 50617D7D23FCE48D0099B055 /* Resources */,
50617DBF23FCE4AB0099B055 /* Embed Frameworks */, 50617DBF23FCE4AB0099B055 /* Embed Frameworks */,
50C385AF240E438B00AF2719 /* Copy SecretAgent */, 50C385AF240E438B00AF2719 /* CopyFiles */,
502452F82FE2024D009EE753 /* Copy SecretAgent plist */,
501577C92E6BC5B4004A37D0 /* Embed XPC Services */, 501577C92E6BC5B4004A37D0 /* Embed XPC Services */,
); );
buildRules = ( buildRules = (
@@ -671,6 +719,7 @@
50692E6F2E6FFA5F0043C7BB /* PBXTargetDependency */, 50692E6F2E6FFA5F0043C7BB /* PBXTargetDependency */,
50692E722E6FFA6E0043C7BB /* PBXTargetDependency */, 50692E722E6FFA6E0043C7BB /* PBXTargetDependency */,
50E2052F2FAAB92000402380 /* PBXTargetDependency */, 50E2052F2FAAB92000402380 /* PBXTargetDependency */,
505402A53034B787000C3356 /* PBXTargetDependency */,
); );
name = SecretAgent; name = SecretAgent;
packageProductDependencies = ( packageProductDependencies = (
@@ -715,10 +764,13 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 2650; LastSwiftUpdateCheck = 2700;
LastUpgradeCheck = 2640; LastUpgradeCheck = 2640;
ORGANIZATIONNAME = "Max Goedjen"; ORGANIZATIONNAME = "Max Goedjen";
TargetAttributes = { TargetAttributes = {
5054028C3034B5E3000C3356 = {
CreatedOnToolsVersion = 27.0;
};
50617D7E23FCE48D0099B055 = { 50617D7E23FCE48D0099B055 = {
CreatedOnToolsVersion = 11.3; CreatedOnToolsVersion = 11.3;
}; };
@@ -760,13 +812,21 @@
50617D7E23FCE48D0099B055 /* Secretive */, 50617D7E23FCE48D0099B055 /* Secretive */,
50A3B78924026B7500D209EA /* SecretAgent */, 50A3B78924026B7500D209EA /* SecretAgent */,
50692D112E6FDB880043C7BB /* SecretiveUpdater */, 50692D112E6FDB880043C7BB /* SecretiveUpdater */,
50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */,
50E205132FAAB81C00402380 /* SecretiveCertificateParser */, 50E205132FAAB81C00402380 /* SecretiveCertificateParser */,
50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */,
5054028C3034B5E3000C3356 /* SecretAgentHostsfileReader */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */ /* Begin PBXResourcesBuildPhase section */
5054028B3034B5E3000C3356 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
50617D7D23FCE48D0099B055 /* Resources */ = { 50617D7D23FCE48D0099B055 /* Resources */ = {
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -815,6 +875,15 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
505402893034B5E3000C3356 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
505402A03034B692000C3356 /* main.swift in Sources */,
505402A13034B692000C3356 /* SecretAgentHostsfileReader.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
50617D7B23FCE48D0099B055 /* Sources */ = { 50617D7B23FCE48D0099B055 /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -884,6 +953,7 @@
50020BB024064869003D4025 /* AppDelegate.swift in Sources */, 50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
5018F54F24064786002EB505 /* Notifier.swift in Sources */, 5018F54F24064786002EB505 /* Notifier.swift in Sources */,
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */, 501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */,
505402AA3034B80E000C3356 /* XPCHostsfileReader.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -908,6 +978,11 @@
isa = PBXTargetDependency; isa = PBXTargetDependency;
targetProxy = 501577D32E6BC5DD004A37D0 /* PBXContainerItemProxy */; targetProxy = 501577D32E6BC5DD004A37D0 /* PBXContainerItemProxy */;
}; };
505402A53034B787000C3356 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 5054028C3034B5E3000C3356 /* SecretAgentHostsfileReader */;
targetProxy = 505402A43034B787000C3356 /* PBXContainerItemProxy */;
};
50692D1C2E6FDB880043C7BB /* PBXTargetDependency */ = { 50692D1C2E6FDB880043C7BB /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 50692D112E6FDB880043C7BB /* SecretiveUpdater */; target = 50692D112E6FDB880043C7BB /* SecretiveUpdater */;
@@ -952,6 +1027,104 @@
/* End PBXVariantGroup section */ /* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
505402973034B5E3000C3356 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = SecretAgentHostsfileReader/SecretAgentHostsfileReader.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)";
ENABLE_APP_SANDBOX = NO;
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SecretAgentHostsfileReader/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentHostsfileReader;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgentHostsfileReader";
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
505402983034B5E3000C3356 /* Test */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = SecretAgentHostsfileReader/SecretAgentHostsfileReader.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
ENABLE_APP_SANDBOX = NO;
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SecretAgentHostsfileReader/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentHostsfileReader;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgentHostsfileReader";
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Test;
};
505402993034B5E3000C3356 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = SecretAgentHostsfileReader/SecretAgentHostsfileReader.entitlements;
CODE_SIGN_IDENTITY = "Developer ID Application";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)";
ENABLE_APP_SANDBOX = NO;
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SecretAgentHostsfileReader/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentHostsfileReader;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgentHostsfileReader";
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
50617D9B23FCE48E0099B055 /* Debug */ = { 50617D9B23FCE48E0099B055 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 508A58AB241E121B0069DC07 /* Config.xcconfig */; baseConfigurationReference = 508A58AB241E121B0069DC07 /* Config.xcconfig */;
@@ -1531,26 +1704,23 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements; CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements;
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = NO;
ENABLE_ENHANCED_SECURITY = YES; ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = NO;
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO; ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO; ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO; ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO; ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO; ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = SecretAgent/Info.plist; INFOPLIST_FILE = SecretAgent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -1560,6 +1730,13 @@
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgent"; PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgent";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
SWIFT_TREAT_WARNINGS_AS_ERRORS = NO;
}; };
name = Test; name = Test;
}; };
@@ -1567,27 +1744,24 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements; CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)"; DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = NO;
ENABLE_ENHANCED_SECURITY = YES; ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = NO;
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO; ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO; ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO; ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO; ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO; ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = SecretAgent/Info.plist; INFOPLIST_FILE = SecretAgent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -1597,6 +1771,13 @@
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgent"; PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgent";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
SWIFT_TREAT_WARNINGS_AS_ERRORS = NO;
}; };
name = Debug; name = Debug;
}; };
@@ -1604,6 +1785,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements; CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements;
CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_IDENTITY = "Developer ID Application";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
@@ -1611,21 +1793,17 @@
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)"; DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = NO;
ENABLE_ENHANCED_SECURITY = YES; ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = NO;
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO; ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO; ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO; ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO; ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO; ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = SecretAgent/Info.plist; INFOPLIST_FILE = SecretAgent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -1636,6 +1814,13 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgent"; PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgent";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Secretive - Secret Agent"; PROVISIONING_PROFILE_SPECIFIER = "Secretive - Secret Agent";
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
SWIFT_TREAT_WARNINGS_AS_ERRORS = NO;
}; };
name = Release; name = Release;
}; };
@@ -1739,6 +1924,16 @@
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
5054029B3034B5E3000C3356 /* Build configuration list for PBXNativeTarget "SecretAgentHostsfileReader" */ = {
isa = XCConfigurationList;
buildConfigurations = (
505402973034B5E3000C3356 /* Debug */,
505402983034B5E3000C3356 /* Test */,
505402993034B5E3000C3356 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
50617D7A23FCE48D0099B055 /* Build configuration list for PBXProject "Secretive" */ = { 50617D7A23FCE48D0099B055 /* Build configuration list for PBXProject "Secretive" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
@@ -1842,6 +2037,14 @@
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Brief; productName = Brief;
}; };
505402A63034B7A4000C3356 /* XPCWrappers */ = {
isa = XCSwiftPackageProductDependency;
productName = XPCWrappers;
};
505402AB303594D1000C3356 /* SSHProtocolKit */ = {
isa = XCSwiftPackageProductDependency;
productName = SSHProtocolKit;
};
505F5EF12FA9635700C45824 /* CertificateKit */ = { 505F5EF12FA9635700C45824 /* CertificateKit */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = CertificateKit; productName = CertificateKit;
@@ -70,7 +70,7 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "1" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
+13 -23
View File
@@ -1,31 +1,14 @@
import SwiftUI import SwiftUI
@unsafe @preconcurrency import ServiceManagement
import SecretKit import SecretKit
import SecureEnclaveSecretKit import SecureEnclaveSecretKit
import SmartCardSecretKit import SmartCardSecretKit
import Brief import Brief
import CertificateKit import CertificateKit
@Observable
final class LaunchService: Sendable {
private let service = SMAppService.agent(plistName: "com.maxgoedjen.Secretive.SecretAgent.plist")
var status: SMAppService.Status {
service.status
}
func configure() {
try? service.unregister()
try! service.register()
}
func disable() {
try? service.unregister()
}
}
@main @main
struct Secretive: App { struct Secretive: App {
@Environment(\.agentLaunchController) var agentLaunchController
@Environment(\.justUpdatedChecker) var justUpdatedChecker @Environment(\.justUpdatedChecker) var justUpdatedChecker
@SceneBuilder var body: some Scene { @SceneBuilder var body: some Scene {
@@ -33,8 +16,18 @@ struct Secretive: App {
ContentView() ContentView()
.environment(EnvironmentValues._secretStoreList) .environment(EnvironmentValues._secretStoreList)
.environment(EnvironmentValues._certificateStore) .environment(EnvironmentValues._certificateStore)
.onAppear { .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
EnvironmentValues._launchService.configure() Task {
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
guard hasRunSetup && !explicitlyDisabled else { return }
agentLaunchController.check()
guard !agentLaunchController.developmentBuild else { return }
if justUpdatedChecker.justUpdatedBuild || !agentLaunchController.running {
// Relaunch the agent, since it'll be running from earlier update still
try? await agentLaunchController.forceLaunch()
}
}
} }
} }
.commands { .commands {
@@ -122,9 +115,6 @@ extension EnvironmentValues {
private static let _justUpdatedChecker = JustUpdatedChecker() private static let _justUpdatedChecker = JustUpdatedChecker()
@Entry var justUpdatedChecker: any JustUpdatedCheckerProtocol = _justUpdatedChecker @Entry var justUpdatedChecker: any JustUpdatedCheckerProtocol = _justUpdatedChecker
fileprivate static let _launchService = LaunchService()
@Entry var launchService: LaunchService = _launchService
@MainActor var secretStoreList: SecretStoreList { @MainActor var secretStoreList: SecretStoreList {
EnvironmentValues._secretStoreList EnvironmentValues._secretStoreList
} }
@@ -14,6 +14,7 @@ extension Preview {
Attributes( Attributes(
keyType: .init(algorithm: .ecdsa, size: 256), keyType: .init(algorithm: .ecdsa, size: 256),
authentication: .presenceRequired, authentication: .presenceRequired,
restrictions: .default
) )
} }
} }
@@ -38,7 +39,7 @@ extension Preview {
self.init(secrets: new) self.init(secrets: new)
} }
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> Data { func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) throws -> Data {
return data return data
} }
@@ -82,7 +83,7 @@ extension Preview {
self.init(secrets: new) self.init(secrets: new)
} }
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> Data { func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) throws -> Data {
return data return data
} }
@@ -10,8 +10,9 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
@State private var name = "" @State private var name = ""
@State private var keyAttribution = "" @State private var keyAttribution = ""
@State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired @State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired
@State private var restrictions: Restrictions = .default
@State private var keyType: KeyType? @State private var keyType: KeyType?
@State var advanced = false @State var advanced = true // FIXME: Set back
@State var errorText: String? @State var errorText: String?
private var authenticationOptions: [AuthenticationRequirement] { private var authenticationOptions: [AuthenticationRequirement] {
@@ -72,6 +73,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
} }
} }
if advanced { if advanced {
SecretRestrictionsView(restrictions: $restrictions)
Section { Section {
VStack { VStack {
Picker(.createSecretKeyTypeLabel, selection: $keyType) { Picker(.createSecretKeyTypeLabel, selection: $keyType) {
@@ -107,6 +109,8 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} header: {
Text("Key Properties")
} }
} }
if let errorText { if let errorText {
@@ -146,6 +150,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
attributes: .init( attributes: .init(
keyType: keyType!, keyType: keyType!,
authentication: authenticationRequirement, authentication: authenticationRequirement,
restrictions: restrictions,
publicKeyAttribution: attribution publicKeyAttribution: attribution
) )
) )
@@ -158,7 +163,52 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
} }
} }
struct SecretRestrictionsView: View {
//#Preview { @Binding var restrictions: Restrictions
// CreateSecretView(store: Preview.StoreModifiable()) { _ in }
//} struct IdentifiedString: Identifiable {
let value: String
var id: String { value }
}
var body: some View {
Section {
Toggle("Allow Forwarding", isOn: $restrictions.allowForwarding)
Toggle("Allow Signing Operations", isOn: $restrictions.allowSigning)
Toggle("Allow Connection Operations", isOn: $restrictions.allowConnections)
Picker(selection: $restrictions.allowedDomains) {
Text("All")
.tag(Restrictions.AllowedDomains.all)
Text("Specific")
.tag(Restrictions.AllowedDomains.specific([]))
} label: {
Text("Allowed Domains")
}
} header: {
Text("Restrictions")
}
if restrictions.allowedDomains != .all {
Section {
switch restrictions.allowedDomains {
case .all:
EmptyView()
case .specific(let array):
if restrictions.allowedDomains != .all {
ForEach((array + [""]).map({IdentifiedString(value: $0)})) {
TextField("", text: .constant($0.value), prompt: Text("example.com"))
.labelsHidden()
}
}
}
} header: {
Text("Allowed Domains")
}
}
}
}
#Preview {
CreateSecretView(store: Preview.StoreModifiable()) { _ in }
.frame(height: 1000)
}
@@ -2,10 +2,10 @@ import SwiftUI
struct AgentStatusView: View { struct AgentStatusView: View {
@Environment(\.launchService) private var launchService @Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
var body: some View { var body: some View {
if launchService.status == .enabled { if agentLaunchController.running {
AgentRunningView() AgentRunningView()
} else { } else {
AgentNotRunningView() AgentNotRunningView()
@@ -14,53 +14,54 @@ struct AgentStatusView: View {
} }
struct AgentRunningView: View { struct AgentRunningView: View {
@Environment(\.launchService) private var launchService @Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false @AppStorage("explicitlyDisabled") var explicitlyDisabled = false
var body: some View { var body: some View {
Form { Form {
Section { Section {
// if let process = agentLaunchController.process { if let process = agentLaunchController.process {
// ConfigurationItemView( ConfigurationItemView(
// title: .agentDetailsLocationTitle, title: .agentDetailsLocationTitle,
// value: process.bundleURL!.path(), value: process.bundleURL!.path(),
// action: .revealInFinder(process.bundleURL!.path()), action: .revealInFinder(process.bundleURL!.path()),
// ) )
ConfigurationItemView( ConfigurationItemView(
title: .agentDetailsSocketPathTitle, title: .agentDetailsSocketPathTitle,
value: URL.socketPath, value: URL.socketPath,
action: .copy(URL.socketPath), action: .copy(URL.socketPath),
) )
// ConfigurationItemView( ConfigurationItemView(
// title: .agentDetailsVersionTitle, title: .agentDetailsVersionTitle,
// value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
// ) )
// if let launchDate = process.launchDate { if let launchDate = process.launchDate {
// ConfigurationItemView( ConfigurationItemView(
// title: .agentDetailsRunningSinceTitle, title: .agentDetailsRunningSinceTitle,
// value: launchDate.formatted() value: launchDate.formatted()
// ) )
// } }
// } }
} header: { } header: {
Text(.agentReadyNoticeDetailTitle) Text(.agentRunningNoticeDetailTitle)
.font(.headline) .font(.headline)
.padding(.top) .padding(.top)
} footer: { } footer: {
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
Text(.agentReadyNoticeDetailDescription) Text(.agentRunningNoticeDetailDescription)
HStack { HStack {
Spacer() Spacer()
Menu(.agentDetailsRestartAgentButton) { Menu(.agentDetailsRestartAgentButton) {
Button(.agentDetailsDisableAgentButton) { Button(.agentDetailsDisableAgentButton) {
Task { Task {
explicitlyDisabled = true explicitlyDisabled = true
launchService.disable() try? await agentLaunchController
.uninstall()
} }
} }
} primaryAction: { } primaryAction: {
Task { Task {
launchService.configure() try? await agentLaunchController.forceLaunch()
} }
} }
} }
@@ -77,6 +78,7 @@ struct AgentRunningView: View {
struct AgentNotRunningView: View { struct AgentNotRunningView: View {
@Environment(\.agentLaunchController) private var agentLaunchController
@State var triedRestart = false @State var triedRestart = false
@State var loading = false @State var loading = false
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false @AppStorage("explicitlyDisabled") var explicitlyDisabled = false
@@ -85,12 +87,12 @@ struct AgentNotRunningView: View {
Form { Form {
Section { Section {
} header: { } header: {
Text(.agentNotConfiguredNoticeTitle) Text(.agentNotRunningNoticeTitle)
.font(.headline) .font(.headline)
.padding(.top) .padding(.top)
} footer: { } footer: {
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
Text(.agentNotConfiguredNoticeDetailDescription) Text(.agentNotRunningNoticeDetailDescription)
HStack { HStack {
if !triedRestart { if !triedRestart {
Spacer() Spacer()
@@ -98,6 +100,14 @@ struct AgentNotRunningView: View {
explicitlyDisabled = false explicitlyDisabled = false
guard !loading else { return } guard !loading else { return }
loading = true loading = true
Task {
try? await agentLaunchController.forceLaunch()
loading = false
if !agentLaunchController.running {
triedRestart = true
}
}
} label: { } label: {
if !loading { if !loading {
Text(.agentDetailsStartAgentButton) Text(.agentDetailsStartAgentButton)
@@ -19,7 +19,6 @@ struct ContentView: View {
@Environment(\.certificateStore) private var certificateStore @Environment(\.certificateStore) private var certificateStore
@Environment(\.updater) private var updater @Environment(\.updater) private var updater
@Environment(\.agentLaunchController) private var agentLaunchController @Environment(\.agentLaunchController) private var agentLaunchController
@Environment(\.launchService) private var launchService
@AppStorage("defaultsHasRunSetup") private var hasRunSetup = false @AppStorage("defaultsHasRunSetup") private var hasRunSetup = false
@State private var showingCreation = false @State private var showingCreation = false
@@ -148,15 +147,15 @@ extension ContentView {
showingAgentInfo = true showingAgentInfo = true
}, label: { }, label: {
HStack { HStack {
if launchService.status == .enabled { if agentLaunchController.running {
Text(.agentReadyNoticeTitle) Text(.agentRunningNoticeTitle)
.font(.headline) .font(.headline)
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white) .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
Circle() Circle()
.frame(width: 10, height: 10) .frame(width: 10, height: 10)
.foregroundColor(Color.green) .foregroundColor(Color.green)
} else { } else {
Text(.agentNotConfiguredNoticeTitle) Text(.agentNotRunningNoticeTitle)
.font(.headline) .font(.headline)
Circle() Circle()
.frame(width: 10, height: 10) .frame(width: 10, height: 10)
@@ -166,8 +165,8 @@ extension ContentView {
}) })
.buttonStyle( .buttonStyle(
ToolbarStatusButtonStyle( ToolbarStatusButtonStyle(
lightColor: launchService.status == .enabled ? .black.opacity(0.05) : .red.opacity(0.75), lightColor: agentLaunchController.running ? .black.opacity(0.05) : .red.opacity(0.75),
darkColor: launchService.status == .enabled ? .white.opacity(0.05) : .red.opacity(0.5), darkColor: agentLaunchController.running ? .white.opacity(0.05) : .red.opacity(0.5),
) )
) )
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { .popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
@@ -163,8 +163,7 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
} else { } else {
if #available(macOS 26.0, *) { if #available(macOS 26.0, *) {
content content
// Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow. .contentShape(RoundedRectangle(cornerRadius: 15))
.background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15))
.glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15)) .glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15))
.mask(RoundedRectangle(cornerRadius: 15)) .mask(RoundedRectangle(cornerRadius: 15))
.shadow(color: .black.opacity(0.1), radius: 5) .shadow(color: .black.opacity(0.1), radius: 5)
@@ -179,7 +178,12 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
func backgroundColor(interactionState: InteractionState) -> Color { func backgroundColor(interactionState: InteractionState) -> Color {
guard appearsActive else { return Color.clear } guard appearsActive else { return Color.clear }
if #available(macOS 26.0, *) { if #available(macOS 26.0, *) {
let base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1) let base: Color
if #available(macOS 27.0, *) {
base = .clear
} else {
base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1)
}
switch interactionState { switch interactionState {
case .normal: case .normal:
return base return base
@@ -53,7 +53,6 @@ struct MultilineInfoView: View {
} }
} }
.safeAreaPadding(20) .safeAreaPadding(20)
._background(interactionState: interactionStateIndex == item.offset ? interactionState : .normal, cornerRadius: 0)
.onHover { hovering in .onHover { hovering in
withAnimation { withAnimation {
guard item.element.action != nil else { return } guard item.element.action != nil else { return }
@@ -117,8 +116,7 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
func body(content: Content) -> some View { func body(content: Content) -> some View {
if #available(macOS 26.0, *) { if #available(macOS 26.0, *) {
content content
// Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow. .contentShape(RoundedRectangle(cornerRadius: cornerRadius))
.background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15))
.glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: cornerRadius)) .glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: cornerRadius))
.mask(RoundedRectangle(cornerRadius: cornerRadius)) .mask(RoundedRectangle(cornerRadius: cornerRadius))
.shadow(color: .black.opacity(0.1), radius: 5) .shadow(color: .black.opacity(0.1), radius: 5)
@@ -132,7 +130,12 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
func backgroundColor(interactionState: InteractionState) -> Color { func backgroundColor(interactionState: InteractionState) -> Color {
guard appearsActive else { return Color.clear } guard appearsActive else { return Color.clear }
if #available(macOS 26.0, *) { if #available(macOS 26.0, *) {
let base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1) let base: Color
if #available(macOS 27.0, *) {
base = .clear
} else {
base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1)
}
switch interactionState { switch interactionState {
case .normal: case .normal:
return base return base
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.maxgoedjen.Secretive.SecretAgent</string>
<key>BundleProgram</key>
<string>Contents/Library/LoginItems/SecretAgent.app/Contents/MacOS/SecretAgent</string>
<key>Sockets</key>
<dict>
<key>SecureListener</key>
<dict>
<key>SecureSocketWithKey</key>
<string>SECRETAGENT_SOCK</string>
</dict>
</dict>
</dict>
</plist>