123 lines
5.1 KiB
Swift
123 lines
5.1 KiB
Swift
import AppKit
|
|
import Foundation
|
|
|
|
/// Detects whether Full Disk Access (FDA) has been granted to PureMac.
|
|
/// Without FDA, macOS TCC blocks access to ~/Library/Mail, ~/Library/Safari,
|
|
/// /Library/Application Support/com.apple.TCC, and other protected locations.
|
|
final class FullDiskAccessManager {
|
|
static let shared = FullDiskAccessManager()
|
|
|
|
private init() {}
|
|
|
|
/// Check if Full Disk Access is granted by attempting a real read of a
|
|
/// TCC-protected file. We use FileHandle/Data — not isReadableFile or
|
|
/// fileExists — because the metadata APIs short-circuit before TCC fires
|
|
/// and so don't register the calling app in the FDA list.
|
|
var hasFullDiskAccess: Bool {
|
|
let probes = [
|
|
"/Library/Application Support/com.apple.TCC/TCC.db",
|
|
FileManager.default.homeDirectoryForCurrentUser
|
|
.appendingPathComponent("Library/Safari/CloudTabs.db").path,
|
|
FileManager.default.homeDirectoryForCurrentUser
|
|
.appendingPathComponent("Library/Mail").path,
|
|
]
|
|
|
|
for path in probes {
|
|
if canActuallyRead(path: path) { return true }
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Real-read probe. Performs the syscall TCC actually evaluates, so
|
|
/// the OS records PureMac as the requester and adds it to the FDA pane.
|
|
/// Directories use contentsOfDirectory; files use FileHandle.
|
|
@discardableResult
|
|
private func canActuallyRead(path: String) -> Bool {
|
|
var isDir: ObjCBool = false
|
|
guard FileManager.default.fileExists(atPath: path, isDirectory: &isDir) else {
|
|
return false
|
|
}
|
|
if isDir.boolValue {
|
|
do {
|
|
_ = try FileManager.default.contentsOfDirectory(atPath: path)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
} else {
|
|
guard let handle = try? FileHandle(forReadingFrom: URL(fileURLWithPath: path)) else {
|
|
return false
|
|
}
|
|
defer { try? handle.close() }
|
|
return (try? handle.read(upToCount: 1)) != nil
|
|
}
|
|
}
|
|
|
|
/// Force PureMac to appear in the Full Disk Access list.
|
|
///
|
|
/// The OS only registers an app in the FDA pane after that app itself
|
|
/// makes a TCC-gated syscall. Metadata lookups (fileExists, isReadableFile)
|
|
/// don't qualify, and delegating to Finder via AppleScript registers
|
|
/// *Finder* — not PureMac. So at launch we touch a small set of paths
|
|
/// gated specifically by the SystemPolicyAllFiles (FDA) service. The reads
|
|
/// will fail until the user grants access — that's fine, the failed
|
|
/// attempts are what register us.
|
|
///
|
|
/// We intentionally do NOT probe Messages, Contacts, Calendars, or other
|
|
/// paths gated by separate TCC services. Touching those would register
|
|
/// PureMac in unrelated permission lists ("Messages", "Contacts", etc.)
|
|
/// even though we never use them — confusing and a privacy regression.
|
|
func triggerRegistration() {
|
|
DispatchQueue.global(qos: .utility).async {
|
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
|
// FDA-only probes. Each path here is read-gated by
|
|
// SystemPolicyAllFiles, nothing else.
|
|
let probePaths = [
|
|
"/Library/Application Support/com.apple.TCC/TCC.db",
|
|
"\(home)/Library/Mail",
|
|
"\(home)/Library/Safari/CloudTabs.db",
|
|
"\(home)/Library/Safari/Bookmarks.plist",
|
|
"\(home)/Library/Cookies",
|
|
"\(home)/Library/HTTPStorages",
|
|
"\(home)/Library/Application Support/com.apple.TCC",
|
|
]
|
|
for path in probePaths {
|
|
_ = self.canActuallyRead(path: path)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Opens System Settings to the Full Disk Access pane.
|
|
func openFullDiskAccessSettings() {
|
|
let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles")!
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
|
|
/// Reveal the running PureMac.app bundle in Finder so the user can drag
|
|
/// it into the Full Disk Access list when the OS hasn't auto-registered
|
|
/// it (common with Homebrew installs that strip the quarantine attribute).
|
|
func revealAppInFinder() {
|
|
let bundleURL = Bundle.main.bundleURL
|
|
NSWorkspace.shared.activateFileViewerSelecting([bundleURL])
|
|
}
|
|
|
|
/// Reset PureMac's TCC entries so the OS can re-register the bundle.
|
|
/// Useful when the bundle was replaced (Homebrew upgrade, manual move)
|
|
/// and the existing TCC row points at a stale code-signing identity.
|
|
/// Returns true if the reset command exited cleanly.
|
|
@discardableResult
|
|
func resetFullDiskAccess() -> Bool {
|
|
let bundleID = Bundle.main.bundleIdentifier ?? "com.puremac.app"
|
|
let process = Process()
|
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/tccutil")
|
|
process.arguments = ["reset", "SystemPolicyAllFiles", bundleID]
|
|
do {
|
|
try process.run()
|
|
process.waitUntilExit()
|
|
return process.terminationStatus == 0
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
}
|