diff --git a/Mist.xcodeproj/project.pbxproj b/Mist.xcodeproj/project.pbxproj index 0cb3602..6669ad8 100644 --- a/Mist.xcodeproj/project.pbxproj +++ b/Mist.xcodeproj/project.pbxproj @@ -130,6 +130,8 @@ 39FF05F62859850F00A86670 /* SettingsFirmwaresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39FF05F52859850F00A86670 /* SettingsFirmwaresView.swift */; }; 39FF05F82859851800A86670 /* SettingsApplicationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39FF05F72859851800A86670 /* SettingsApplicationsView.swift */; }; 39FF05FA285985DD00A86670 /* SettingsAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39FF05F9285985DD00A86670 /* SettingsAboutView.swift */; }; + 573A235E2A285E8900EC9470 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = 573A235D2A285E8900EC9470 /* SQLite */; }; + 573A23602A285EAE00EC9470 /* FullDiskAccessVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 573A235F2A285EAE00EC9470 /* FullDiskAccessVerifier.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -270,6 +272,7 @@ 39FF05F52859850F00A86670 /* SettingsFirmwaresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFirmwaresView.swift; sourceTree = ""; }; 39FF05F72859851800A86670 /* SettingsApplicationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsApplicationsView.swift; sourceTree = ""; }; 39FF05F9285985DD00A86670 /* SettingsAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAboutView.swift; sourceTree = ""; }; + 573A235F2A285EAE00EC9470 /* FullDiskAccessVerifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullDiskAccessVerifier.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -277,6 +280,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 573A235E2A285E8900EC9470 /* SQLite in Frameworks */, 3935F4C5286B546A00760AB0 /* Sparkle in Frameworks */, 390451DF28573FAA00E0B563 /* Yams in Frameworks */, 39CF55AA286154A5006FB5D2 /* Blessed in Frameworks */, @@ -374,6 +378,7 @@ 39CA25E22941D8BB0030711E /* FileAttributesUpdater.swift */, 39CF56162861BE66006FB5D2 /* FileCopier.swift */, 398734C728601FFC00B4C357 /* FileMover.swift */, + 573A235F2A285EAE00EC9470 /* FullDiskAccessVerifier.swift */, 39D68B882861369B00A7848C /* InstallerCreator.swift */, 39CF56302862A8C5006FB5D2 /* InstallMediaCreator.swift */, 39CF562E2862A797006FB5D2 /* ISOConverter.swift */, @@ -560,6 +565,7 @@ 39CF55A9286154A5006FB5D2 /* Blessed */, 39CF55B128615D30006FB5D2 /* SecureXPC */, 3935F4C4286B546A00760AB0 /* Sparkle */, + 573A235D2A285E8900EC9470 /* SQLite */, ); productName = Mist; productReference = 390451A62856E1D900E0B563 /* Mist.app */; @@ -639,6 +645,7 @@ 39CF55A8286154A5006FB5D2 /* XCRemoteSwiftPackageReference "Blessed" */, 39CF55B028615D30006FB5D2 /* XCRemoteSwiftPackageReference "SecureXPC" */, 3935F4C3286B546A00760AB0 /* XCRemoteSwiftPackageReference "Sparkle" */, + 573A235C2A285E8900EC9470 /* XCRemoteSwiftPackageReference "SQLite" */, ); productRefGroup = 390451A72856E1D900E0B563 /* Products */; projectDirPath = ""; @@ -803,6 +810,7 @@ 39CA25E32941D8BB0030711E /* FileAttributesUpdater.swift in Sources */, 3935F4AB286B04BC00760AB0 /* HelperToolInfoPropertyList.swift in Sources */, 393F35BC28641181005B7165 /* RefreshState.swift in Sources */, + 573A23602A285EAE00EC9470 /* FullDiskAccessVerifier.swift in Sources */, 390451CA2856F1D300E0B563 /* ScaledImage.swift in Sources */, 39252A95285BF83D00956C74 /* MistTask.swift in Sources */, 39CF56272861E10F006FB5D2 /* Codesigner.swift in Sources */, @@ -1238,6 +1246,14 @@ minimumVersion = 0.8.0; }; }; + 573A235C2A285E8900EC9470 /* XCRemoteSwiftPackageReference "SQLite" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/stephencelis/SQLite.swift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.14.1; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1271,6 +1287,11 @@ package = 39CF55B028615D30006FB5D2 /* XCRemoteSwiftPackageReference "SecureXPC" */; productName = SecureXPC; }; + 573A235D2A285E8900EC9470 /* SQLite */ = { + isa = XCSwiftPackageProductDependency; + package = 573A235C2A285E8900EC9470 /* XCRemoteSwiftPackageReference "SQLite" */; + productName = SQLite; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 3904519E2856E1D800E0B563 /* Project object */; diff --git a/Mist/Helpers/FullDiskAccessVerifier.swift b/Mist/Helpers/FullDiskAccessVerifier.swift new file mode 100644 index 0000000..a8e8b57 --- /dev/null +++ b/Mist/Helpers/FullDiskAccessVerifier.swift @@ -0,0 +1,46 @@ +// +// FullDiskAccessVerifier.swift +// Mist +// +// Created by Nindi Gill on 1/6/2023. +// + +import SQLite + +/// Helper struct to verify Full Disk Access. +struct FullDiskAccessVerifier { + + private enum AuthValue: Int { + case denied = 0 + case unknown = 1 + case allowed = 2 + case limited = 3 + } + + /// TCC Service identifier for Full Disk Access + private static let kTCCServiceSystemPolicyAllFiles: String = "kTCCServiceSystemPolicyAllFiles" + + /// Verifies if the app has Full Disk Access. + /// + /// - Returns: `true` if the app has Full Disk Access, otherwise `false`. + static func isAllowed() -> Bool { + do { + let database: Connection = try Connection("/Library/Application Support/com.apple.TCC/TCC.db") + let service: Expression = Expression("service") + let client: Expression = Expression("client") + let authValue: Expression = Expression("auth_value") + let access: Table = Table("access").filter(service == kTCCServiceSystemPolicyAllFiles && client == String.appIdentifier) + var allowed: Bool = false + + for row in try database.prepare(access) where row[authValue] == AuthValue.allowed.rawValue { + allowed = true + break + } + + return allowed + } catch { + // print(error.localizedDescription) + return false + } + } +} diff --git a/Mist/Model/DownloadAlertType.swift b/Mist/Model/DownloadAlertType.swift index 479f4cc..bbe9b53 100644 --- a/Mist/Model/DownloadAlertType.swift +++ b/Mist/Model/DownloadAlertType.swift @@ -8,5 +8,6 @@ enum DownloadAlertType: String { case compatibility = "Compatiblity" case helperTool = "Helper Tool" + case fullDiskAccess = "Full Disk Access" case cacheDirectory = "Cache Directory" } diff --git a/Mist/Views/List/ListRow.swift b/Mist/Views/List/ListRow.swift index 557bfbe..778e9dc 100644 --- a/Mist/Views/List/ListRow.swift +++ b/Mist/Views/List/ListRow.swift @@ -49,6 +49,12 @@ struct ListRow: View { private var privilegedHelperToolMessage: String { "The Mist Privileged Helper Tool is required to perform Administrator tasks when \(type == .firmware ? "downloading macOS Firmwares" : "creating macOS Installers")." } + private var fullDiskAccessTitle: String { + "Full Disk Access required!" + } + private var fullDiskAccessMessage: String { + "Mist requires Full Disk Access to perform Administrator tasks when creating macOS Installers." + } private var cacheDirectoryTitle: String { "Cache directory settings incorrect!" } @@ -99,6 +105,13 @@ struct ListRow: View { primaryButton: .default(Text("Install...")) { installPrivilegedHelperTool() }, secondaryButton: .default(Text("Cancel")) ) + case .fullDiskAccess: + return Alert( + title: Text(fullDiskAccessTitle), + message: Text(fullDiskAccessMessage), + primaryButton: .default(Text("Allow...")) { openFullDiskAccessPreferences() }, + secondaryButton: .default(Text("Cancel")) + ) case .cacheDirectory: return Alert( title: Text(cacheDirectoryTitle), @@ -123,6 +136,13 @@ struct ListRow: View { return } + guard type == .installer, + FullDiskAccessVerifier.isAllowed() else { + alertType = .fullDiskAccess + showAlert = true + return + } + if cacheDownloads { do { @@ -165,6 +185,15 @@ struct ListRow: View { try? PrivilegedHelperManager.shared.authorizeAndBless() } + private func openFullDiskAccessPreferences() { + + guard let url: URL = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") else { + return + } + + NSWorkspace.shared.open(url) + } + private func repairCacheDirectoryOwnershipAndPermissions() async throws { let url: URL = URL(fileURLWithPath: cacheDirectory) let ownerAccountName: String = NSUserName()