Skip to content

Commit

Permalink
Add pinentry for admin password retrieval
Browse files Browse the repository at this point in the history
Add support for the pinentry-mac Homebrew package for requesting admin password in a secure manner
Fix apps that have a .pkg installer not launching
  • Loading branch information
milanvarady committed Sep 3, 2023
1 parent 2345a54 commit ba98754
Show file tree
Hide file tree
Showing 20 changed files with 437 additions and 193 deletions.
24 changes: 20 additions & 4 deletions Applite.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
41062C972A3A20F900FD48EA /* UninstallSelf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41062C962A3A20F900FD48EA /* UninstallSelf.swift */; };
41062C992A3A263F00FD48EA /* UninstallSelfView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41062C982A3A263F00FD48EA /* UninstallSelfView.swift */; };
41062C9B2A3E4AFA00FD48EA /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41062C9A2A3E4AFA00FD48EA /* BundleExtension.swift */; };
411EDDD52A9F56020051E07B /* pinentry.ksh in Resources */ = {isa = PBXBuildFile; fileRef = 411EDDD22A9DD5F40051E07B /* pinentry.ksh */; };
411EDDD72A9F58180051E07B /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 411EDDD62A9F58180051E07B /* URLExtension.swift */; };
411EDDD92A9F7E220051E07B /* PinentryScriptHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 411EDDD82A9F7E220051E07B /* PinentryScriptHash.swift */; };
411EDDDB2AA4A0D80051E07B /* PinentryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 411EDDDA2AA4A0D80051E07B /* PinentryError.swift */; };
4120AB652A754B1700F68EFE /* AppliteAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4120AB642A754B1700F68EFE /* AppliteAppView.swift */; };
4120AB682A755B5A00F68EFE /* CheckForUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4120AB672A755B5A00F68EFE /* CheckForUpdatesView.swift */; };
4125BB8A29539907000FBD25 /* PlaceholderAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4125BB8929539907000FBD25 /* PlaceholderAppView.swift */; };
Expand All @@ -20,7 +24,7 @@
412635492A7804E700155034 /* CaskDataLoadError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412635482A7804E700155034 /* CaskDataLoadError.swift */; };
4126354B2A79075900155034 /* ShellResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4126354A2A79075900155034 /* ShellResult.swift */; };
4129FFD92A7A613E00CFE392 /* Fuse in Frameworks */ = {isa = PBXBuildFile; productRef = 4129FFD82A7A613E00CFE392 /* Fuse */; };
413F77A52972B2E70053349A /* BrewInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413F77A42972B2E70053349A /* BrewInstallation.swift */; };
413F77A52972B2E70053349A /* DependencyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413F77A42972B2E70053349A /* DependencyManager.swift */; };
413F77A72972C8000053349A /* ShellOutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413F77A62972C8000053349A /* ShellOutputStream.swift */; };
414074F528DF53E80073EB22 /* AppliteApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 414074F428DF53E80073EB22 /* AppliteApp.swift */; };
414074F728DF53E80073EB22 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 414074F628DF53E80073EB22 /* ContentView.swift */; };
Expand Down Expand Up @@ -70,14 +74,18 @@
41062C962A3A20F900FD48EA /* UninstallSelf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UninstallSelf.swift; sourceTree = "<group>"; };
41062C982A3A263F00FD48EA /* UninstallSelfView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UninstallSelfView.swift; sourceTree = "<group>"; };
41062C9A2A3E4AFA00FD48EA /* BundleExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtension.swift; sourceTree = "<group>"; };
411EDDD22A9DD5F40051E07B /* pinentry.ksh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = pinentry.ksh; sourceTree = "<group>"; };
411EDDD62A9F58180051E07B /* URLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = "<group>"; };
411EDDD82A9F7E220051E07B /* PinentryScriptHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinentryScriptHash.swift; sourceTree = "<group>"; };
411EDDDA2AA4A0D80051E07B /* PinentryError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinentryError.swift; sourceTree = "<group>"; };
4120AB642A754B1700F68EFE /* AppliteAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppliteAppView.swift; sourceTree = "<group>"; };
4120AB672A755B5A00F68EFE /* CheckForUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForUpdatesView.swift; sourceTree = "<group>"; };
4125BB8929539907000FBD25 /* PlaceholderAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAppView.swift; sourceTree = "<group>"; };
4126353D2A77C6EF00155034 /* ArrayExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtension.swift; sourceTree = "<group>"; };
412635432A77FB1600155034 /* BrewInstallationProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewInstallationProgress.swift; sourceTree = "<group>"; };
412635482A7804E700155034 /* CaskDataLoadError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaskDataLoadError.swift; sourceTree = "<group>"; };
4126354A2A79075900155034 /* ShellResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellResult.swift; sourceTree = "<group>"; };
413F77A42972B2E70053349A /* BrewInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewInstallation.swift; sourceTree = "<group>"; };
413F77A42972B2E70053349A /* DependencyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyManager.swift; sourceTree = "<group>"; };
413F77A62972C8000053349A /* ShellOutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellOutputStream.swift; sourceTree = "<group>"; };
414074F128DF53E80073EB22 /* Applite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Applite.app; sourceTree = BUILT_PRODUCTS_DIR; };
414074F428DF53E80073EB22 /* AppliteApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppliteApp.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -159,6 +167,7 @@
4196C90328FC03A900EADDDA /* Shell.swift */,
413F77A62972C8000053349A /* ShellOutputStream.swift */,
4126354A2A79075900155034 /* ShellResult.swift */,
411EDDD82A9F7E220051E07B /* PinentryScriptHash.swift */,
);
path = Shell;
sourceTree = "<group>";
Expand All @@ -168,6 +177,7 @@
children = (
41062C9A2A3E4AFA00FD48EA /* BundleExtension.swift */,
4126353D2A77C6EF00155034 /* ArrayExtension.swift */,
411EDDD62A9F58180051E07B /* URLExtension.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand All @@ -186,7 +196,7 @@
412635422A77FB0000155034 /* Brew Installation */ = {
isa = PBXGroup;
children = (
413F77A42972B2E70053349A /* BrewInstallation.swift */,
413F77A42972B2E70053349A /* DependencyManager.swift */,
412635432A77FB1600155034 /* BrewInstallationProgress.swift */,
);
path = "Brew Installation";
Expand Down Expand Up @@ -265,6 +275,7 @@
isa = PBXGroup;
children = (
41857B722911A2F2004A1894 /* categories.json */,
411EDDD22A9DD5F40051E07B /* pinentry.ksh */,
);
path = Resources;
sourceTree = "<group>";
Expand All @@ -277,6 +288,7 @@
4166EE7C28F73B2300CE305A /* BrewAnalytics.swift */,
4191392B29159B5C00F1D75D /* CaskDTO.swift */,
412635482A7804E700155034 /* CaskDataLoadError.swift */,
411EDDDA2AA4A0D80051E07B /* PinentryError.swift */,
);
path = "Cask Data";
sourceTree = "<group>";
Expand Down Expand Up @@ -440,6 +452,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
411EDDD52A9F56020051E07B /* pinentry.ksh in Resources */,
414074FC28DF53EB0073EB22 /* Preview Assets.xcassets in Resources */,
41857B732911A2F2004A1894 /* categories.json in Resources */,
BD7546572A868DA30083996B /* Localizable.xcstrings in Resources */,
Expand All @@ -466,9 +479,10 @@
4189CE41293C980E009C836D /* BigButtonStyle.swift in Sources */,
41DF006429EAA094004EB7AE /* SendNotification.swift in Sources */,
41857B752912D94A004A1894 /* CategoryView.swift in Sources */,
411EDDDB2AA4A0D80051E07B /* PinentryError.swift in Sources */,
4196C8F528F9CB2600EADDDA /* DiscoverView.swift in Sources */,
4125BB8A29539907000FBD25 /* PlaceholderAppView.swift in Sources */,
413F77A52972B2E70053349A /* BrewInstallation.swift in Sources */,
413F77A52972B2E70053349A /* DependencyManager.swift in Sources */,
418989B42A35D67C004AC23B /* isCommandLineToolsInstalled.swift in Sources */,
419506A42964A27F00FE5802 /* SetupView.swift in Sources */,
41524B99295E352200D0046A /* SettingsView.swift in Sources */,
Expand All @@ -482,6 +496,7 @@
41062C9B2A3E4AFA00FD48EA /* BundleExtension.swift in Sources */,
418989AF2A33B65A004AC23B /* SmallProgressView.swift in Sources */,
41B731392A879353008BF6B9 /* ActiveTasksView.swift in Sources */,
411EDDD72A9F58180051E07B /* URLExtension.swift in Sources */,
419506A62964A5EF00FE5802 /* BrewPathSelectorView.swift in Sources */,
4191392C29159B5C00F1D75D /* CaskDTO.swift in Sources */,
4196C90428FC03A900EADDDA /* Shell.swift in Sources */,
Expand All @@ -494,6 +509,7 @@
41062C972A3A20F900FD48EA /* UninstallSelf.swift in Sources */,
41B731372A8789D4008BF6B9 /* ImportCasks.swift in Sources */,
4126354B2A79075900155034 /* ShellResult.swift in Sources */,
411EDDD92A9F7E220051E07B /* PinentryScriptHash.swift in Sources */,
418F332628EC921D0023D76F /* CaskData.swift in Sources */,
4178CF922A8689AF0037F270 /* ExportCasks.swift in Sources */,
4196C8F928F9CDF700EADDDA /* DownloadView.swift in Sources */,
Expand Down
44 changes: 44 additions & 0 deletions Applite/Extensions/URLExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// URLExtension.swift
// Applite
//
// Created by Milán Várady on 2023. 08. 30..
//

import Foundation
import CryptoKit

extension URL {
func checksumInBase64() -> String? {
let bufferSize = 16*1024

do {
// Open file for reading:
let file = try FileHandle(forReadingFrom: self)
defer {
file.closeFile()
}

// Create and initialize MD5 context:
var md5 = CryptoKit.Insecure.MD5()

// Read up to `bufferSize` bytes, until EOF is reached, and update MD5 context:
while autoreleasepool(invoking: {
let data = file.readData(ofLength: bufferSize)
if data.count > 0 {
md5.update(data: data)
return true // Continue
} else {
return false // End of file
}
}) { }

// Compute the MD5 digest:
let data = Data(md5.finalize())

return data.base64EncodedString()
} catch {
return nil
}
}
}
103 changes: 82 additions & 21 deletions Applite/Model/Cask Data/Cask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,15 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject {
/// - force: If `true` install will be run with the `--force` flag
/// - Returns: `Void`
func install(caskData: CaskData, force: Bool = false) async -> Void {
defer {
resetProgressState(caskData: caskData)
}

Self.logger.info("Cask \"\(self.id)\" installation started")

// Check if pinentry is installed
guard ((try? await checkPinentry()) != nil) else { return }

var cancellables = Set<AnyCancellable>()
let shellOutputStream = ShellOutputStream()
let appdirOn = UserDefaults.standard.bool(forKey: Preferences.appdirOn.rawValue)
Expand All @@ -92,7 +99,7 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject {
}
.store(in: &cancellables)

let result = await shellOutputStream.run("\(BrewPaths.currentBrewExecutable) install --cask \(force ? "--force" : "") \(self.id) \(appdirOn ? appdirArgument : "")", environmentVariables: "HOMEBREW_NO_AUTO_UPDATE=1")
let result = await shellOutputStream.run("\(BrewPaths.currentBrewExecutable) install --cask \(force ? "--force" : "") \(self.id) \(appdirOn ? appdirArgument : "")")

if result.didFail {
Self.logger.error("Failed to install cask \(self.id). Output: \(result.output)")
Expand All @@ -115,11 +122,6 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject {

// Show success for 2 seconds
try? await Task.sleep(for: .seconds(2))

await MainActor.run {
progressState = .idle
caskData.busyCasks.remove(self)
}
}
}

Expand Down Expand Up @@ -148,6 +150,10 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject {
/// - Returns: Bool - Whether the task has failed or not
@discardableResult
func uninstall(caskData: CaskData) async -> Bool {
defer {
resetProgressState(caskData: caskData)
}

_ = await MainActor.run {
caskData.busyCasks.insert(self)
}
Expand All @@ -157,22 +163,17 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject {
taskDescription: "Uninstalling",
notificationSuccess: String(localized:"\(self.name) successfully uninstalled"),
notificationFailure: "Failed to uninstall \(self.name)",
onSuccess: {

self.isInstalled = false

Task {
await MainActor.run {
caskData.busyCasks.remove(self)
}
}
})
onSuccess: { self.isInstalled = false })
}

/// Updates the cask
/// - Returns: Bool - Whether the task has failed or not
@discardableResult
func update(caskData: CaskData) async -> Bool {
defer {
resetProgressState(caskData: caskData)
}

_ = await MainActor.run {
caskData.busyCasks.insert(self)
}
Expand All @@ -187,7 +188,6 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject {
await MainActor.run {
self.isOutdated = false
caskData.outdatedCasks.remove(self)
caskData.busyCasks.remove(self)
}
}
})
Expand All @@ -197,6 +197,10 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject {
/// - Returns: Bool - Whether the task has failed or not
@discardableResult
func reinstall(caskData: CaskData) async -> Bool {
defer {
resetProgressState(caskData: caskData)
}

_ = await MainActor.run {
caskData.busyCasks.insert(self)
}
Expand Down Expand Up @@ -229,6 +233,9 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject {
private func runBrewCommand(command: String, arguments: [String], taskDescription: String,
notificationSuccess: String, notificationFailure: String, onSuccess: (() -> Void)? = nil) async -> Bool {

// Check if pinentry is installed
guard ((try? await checkPinentry()) != nil) else { return true }

await MainActor.run {
let localizedTaskDescription = String.LocalizationValue(stringLiteral: taskDescription)
self.progressState = .busy(withTask: String(localized: localizedTaskDescription))
Expand All @@ -251,19 +258,38 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject {
sendNotification(title: notificationSuccess, reason: .success)
await MainActor.run { self.progressState = .success }
try? await Task.sleep(for: .seconds(2))
await MainActor.run { self.progressState = .idle }
}

return result.didFail
}

@discardableResult
public func launchApp() -> ShellResult {
let brewDirectory = BrewPaths.currentBrewDirectory
let appPath: String

let appPath = "\(brewDirectory.replacingOccurrences(of: " ", with: "\\ ") )/Caskroom/\(self.id)/*/*.app"
if self.pkgInstaller {
// Open PKG type app
var applicationsDirectory = "/Applications"

// Appdir
if UserDefaults.standard.bool(forKey: Preferences.appdirOn.rawValue) {
applicationsDirectory = UserDefaults.standard.string(forKey: Preferences.appdirPath.rawValue) ?? "/Applications"

// Remove trailing "/"
if applicationsDirectory.hasSuffix("/") {
applicationsDirectory.removeLast()
}
}

appPath = "\(applicationsDirectory)/\(self.name).app"
} else {
// Open normal app
let brewDirectory = BrewPaths.currentBrewDirectory

appPath = "\(brewDirectory.replacingOccurrences(of: " ", with: "\\ ") )/Caskroom/\(self.id)/*/*.app"
}

let result = shell("open \(appPath)")
let result = shell("open \"\(appPath)\"")

if result.didFail {
Self.logger.error("Couldn't launch app at path: \(appPath). Output: \(result.output)")
Expand All @@ -272,6 +298,41 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject {
return result
}

/// Checks if pinentry-mac is installed, if not it tries it install it
private func checkPinentry() async throws {
if self.pkgInstaller {
do {
await MainActor.run {
progressState = .busy(withTask: "Preparing")
}

if await BrewPaths.isPinentryInstalled() { return }

Self.logger.notice("pinentry-mac is not installed. Installing now...")

try await DependencyManager.installPinentry(forceInstall: true)
} catch {
Self.logger.error("Cask: Application has PKG installer. Pinentry not installed. Installation attempt failed.")

await MainActor.run {
progressState = .failed(output: "Application has a PKG installer that requires an admin password. Pinentry was not installed and the installation attempt failed.")
}

throw PinentryError.installError
}
}
}

/// Resets progress state and removes self from ``CaskData.busyCasks``
private func resetProgressState(caskData: CaskData) {
Task {
await MainActor.run {
self.progressState = .idle
caskData.busyCasks.remove(self)
}
}
}

static func == (lhs: Cask, rhs: Cask) -> Bool {
lhs.id == rhs.id
}
Expand Down
12 changes: 12 additions & 0 deletions Applite/Model/Cask Data/PinentryError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// PinentryError.swift
// Applite
//
// Created by Milán Várady on 2023. 09. 03..
//

import Foundation

enum PinentryError: Error {
case installError
}
7 changes: 7 additions & 0 deletions Applite/Resources/pinentry.ksh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#! /bin/ksh

# Add homebrew bin directories to path
typeset PATH="/opt/homebrew/bin:/usr/local/bin:${HOME}/Library/Application Support/Applite/homebrew/bin:${PATH}"

# Prompt user for password and return it
printf "%s\n" "SETOK OK" "SETCANCEL Cancel" "SETDESC Applite needs your admin password to complete the task" "SETPROMPT Enter Password:" "SETTITLE Applite Password Request" "GETPIN" | /usr/bin/env pinentry-mac --no-global-grab | /usr/bin/awk '/^D / {print substr($0, index($0, $2))}'
Loading

0 comments on commit ba98754

Please sign in to comment.