1// 2// KBHelperTool.m 3// Keybase 4// 5// Created by Gabriel on 5/10/15. 6// Copyright (c) 2015 Gabriel Handford. All rights reserved. 7// 8 9#import "KBHelperTool.h" 10 11#import "KBDebugPropertiesView.h" 12#import "KBPrivilegedTask.h" 13 14#import <ObjectiveSugar/ObjectiveSugar.h> 15#import <ServiceManagement/ServiceManagement.h> 16 17#import "KBSemVersion.h" 18#import "KBFormatter.h" 19 20#define HELPER_LOCATION (@"/Library/PrivilegedHelperTools/keybase.Helper") 21 22@interface KBHelperTool () <MPLog> 23@property KBDebugPropertiesView *infoView; 24@property (nonatomic) MPXPCClient *helper; 25@end 26 27@implementation KBHelperTool 28 29- (instancetype)initWithConfig:(KBEnvConfig *)config { 30 if ((self = [self initWithConfig:config name:@"Privileged Helper" info:@"Runs privileged tasks" image:[KBIcons imageForIcon:KBIconExtension]])) { 31 32 } 33 return self; 34} 35 36- (NSView *)componentView { 37 [self componentDidUpdate]; 38 return _infoView; 39} 40 41- (KBSemVersion *)bundleVersion { 42 return [KBSemVersion version:NSBundle.mainBundle.infoDictionary[@"KBHelperVersion"] build:nil]; 43} 44 45+ (MPXPCClient *)helper { 46 MPXPCClient *client = [[MPXPCClient alloc] initWithServiceName:@"keybase.Helper" privileged:YES readOptions:MPMessagePackReaderOptionsUseOrderedDictionary]; 47 client.retryMaxAttempts = 4; 48 client.retryDelay = 0.5; 49 client.timeout = 60.0; 50 return client; 51} 52 53- (MPXPCClient *)helper { 54 if (!_helper) { 55 _helper = [KBHelperTool helper]; 56 _helper.logDelegate = self; 57 } 58 return _helper; 59} 60 61- (void)componentDidUpdate { 62 GHODictionary *info = [GHODictionary dictionary]; 63 GHODictionary *statusInfo = [self.componentStatus statusInfo]; 64 if (statusInfo) [info addEntriesFromOrderedDictionary:statusInfo]; 65 66 if (!_infoView) _infoView = [[KBDebugPropertiesView alloc] init]; 67 [_infoView setProperties:info]; 68} 69 70- (void)log:(MPLogLevel)level format:(NSString *)format, ... { 71 va_list args; 72 va_start(args, format); 73 DDLogInfo(@"%@", [[NSString alloc] initWithFormat:format arguments:args]); 74 va_end(args); 75} 76 77- (void)doInstallAlert:(KBSemVersion *)bundleVersion runningVersion:(KBSemVersion *)runningVersion { 78 if ([runningVersion.version length] == 0) { 79 // No need to show anything if the user is explicitly choosing to install 80 // and just clicked on something. 81 return; 82 } 83 84 NSString *alertText = @"Keybase is about to upgrade the Keybase file system, allowing end-to-end encrypted files from right inside your Finder."; 85 NSString *infoText = @""; 86 87 BOOL multiUser = [bundleVersion isOrderedSame:[KBSemVersion version:@"1.0.31"]]; 88 BOOL activeDirectory = [bundleVersion isOrderedSame:[KBSemVersion version:@"1.0.35"]]; 89 BOOL bigSurFuse = [bundleVersion isOrderedSame:[KBSemVersion version:@"1.0.47"]]; 90 91 if (multiUser) { 92 alertText = @"New Keybase feature: multiple users in macOS"; 93 // Use a division slash instead of a regular / to avoid weird line breaks. 94 infoText = @"Previously, only one user of this computer could find their Keybase files at \u2215keybase. With this update, \u2215keybase will now support multiple users on the same computer by linking to user-specific Keybase directories in \u2215Volumes.\n\nYou may need to enter your password for this update."; 95 } else if (activeDirectory) { 96 alertText = @"Keybase helper update"; 97 infoText = @"This Keybase release fixes a regression in macOS installs that use Active Directory for user management.\n\nYou may need to enter your password for this update."; 98 } else if (bigSurFuse) { 99 alertText = @"Keybase helper update"; 100 infoText = @"This Keybase release contains a new version of macFuse which adds KBFS support for Big Sur and M1 devices.\n\nYou may need to enter your password for this update. A system reboot may also be needed."; 101 } else { 102 alertText = @"Keybase helper update"; 103 infoText = @"This Keybase release contains bugfixes and security updates to the Keybase installer helper tool.\n\nYou may need to enter your password for this update."; 104 } 105 NSAlert *alert = [[NSAlert alloc] init]; 106 [alert setMessageText:alertText]; 107 [alert setInformativeText:infoText]; 108 [alert addButtonWithTitle:@"Got it!"]; 109 [alert setAlertStyle:NSAlertStyleInformational]; 110 [alert runModal]; // ignore response 111} 112 113- (BOOL)exists { 114 return [NSFileManager.defaultManager fileExistsAtPath:HELPER_LOCATION isDirectory:nil]; 115} 116 117- (BOOL)isCriticalUpdate:(KBSemVersion *)runningVersion { 118 return [runningVersion isLessThan:[KBSemVersion version:@"1.0.44"]]; 119} 120 121- (void)refreshComponent:(KBRefreshComponentCompletion)completion { 122 GHODictionary *info = [GHODictionary dictionary]; 123 KBSemVersion *bundleVersion = [self bundleVersion]; 124 info[@"Bundle Version"] = [bundleVersion description]; 125 126 if (![NSFileManager.defaultManager fileExistsAtPath:HELPER_LOCATION isDirectory:nil]) { 127 self.componentStatus = [KBComponentStatus componentStatusWithInstallStatus:KBRInstallStatusNotInstalled installAction:KBRInstallActionInstall info:info error:nil]; 128 completion(self.componentStatus); 129 return; 130 } 131 132 [self.helper sendRequest:@"version" params:nil completion:^(NSError *error, NSDictionary *versions) { 133 if (error) { 134 self.componentStatus = [KBComponentStatus componentStatusWithInstallStatus:KBRInstallStatusNotInstalled installAction:KBRInstallActionReinstall info:info error:nil]; 135 // If we couldn't run this, just act like it is a very old version running that we don't know how to 136 // talk to so we can still run checks on the bundle version 137 KBSemVersion *runningVersion = [KBSemVersion version:@"1.0.0" build:nil]; 138 [self doInstallAlert:bundleVersion runningVersion:runningVersion]; 139 completion(self.componentStatus); 140 } else { 141 DDLogDebug(@"Helper version: %@", versions); 142 KBSemVersion *runningVersion = [KBSemVersion version:KBIfNull(versions[@"version"], @"") build:nil]; 143 if (runningVersion) info[@"Version"] = [runningVersion description]; 144 DDLogInfo(@"Running Version: %@", runningVersion); 145 if ([bundleVersion isGreaterThan:runningVersion]) { 146 if ([self isCriticalUpdate:runningVersion]) { 147 DDLogInfo(@"Critical update found: bundle: %@ running: %@", bundleVersion, runningVersion); 148 self.componentStatus = [KBComponentStatus componentStatusWithInstallStatus:KBRInstallStatusError installAction:KBRInstallActionUpgrade info:info error:KBMakeError(KBErrorCodeFuseCriticalUpdate, @"FUSE critical update")]; 149 completion(self.componentStatus); 150 } else { 151 if (bundleVersion) info[@"Bundle Version"] = [bundleVersion description]; 152 self.componentStatus = [KBComponentStatus componentStatusWithInstallStatus:KBRInstallStatusInstalled installAction:KBRInstallActionUpgrade info:info error:nil]; 153 [self doInstallAlert:bundleVersion runningVersion:runningVersion]; 154 completion(self.componentStatus); 155 } 156 } else { 157 self.componentStatus = [KBComponentStatus componentStatusWithInstallStatus:KBRInstallStatusInstalled installAction:KBRInstallActionNone info:info error:nil]; 158 completion(self.componentStatus); 159 } 160 } 161 }]; 162} 163 164- (void)install:(KBCompletion)completion { 165 [self refreshComponent:^(KBComponentStatus *cs) { 166 // check for an error from refresh, and just abort if it gives us one 167 if (cs.error != nil) { 168 completion(cs.error); 169 return; 170 } 171 if ([cs needsInstallOrUpgrade]) { 172 [self _install:completion]; 173 } else { 174 completion(nil); 175 } 176 }]; 177} 178 179- (void)_install:(KBCompletion)completion { 180 NSError *error = nil; 181 if ([self installPrivilegedServiceWithName:@"keybase.Helper" error:&error]) { 182 completion(nil); 183 } else { 184 if (!error) error = KBMakeError(KBErrorCodeInstallError, @"Failed to install privileged helper"); 185 completion(error); 186 } 187} 188 189- (AuthorizationRef)authorization:(NSError **)error { 190 AuthorizationRef authRef; 191 OSStatus createStatus = AuthorizationCreate(NULL, NULL, 0, &authRef); 192 if (createStatus != errAuthorizationSuccess) { 193 if (error) *error = KBMakeError(createStatus, @"Error creating auth: %@", @(createStatus)); 194 return nil; 195 } 196 197 AuthorizationItem authItem = {kSMRightBlessPrivilegedHelper, 0, NULL, 0}; 198 AuthorizationRights authRights = {1, &authItem}; 199 AuthorizationFlags flags = kAuthorizationFlagDefaults | kAuthorizationFlagInteractionAllowed | kAuthorizationFlagPreAuthorize | kAuthorizationFlagExtendRights; 200 OSStatus authResult = AuthorizationCopyRights(authRef, &authRights, kAuthorizationEmptyEnvironment, flags, NULL); 201 if (authResult != errAuthorizationSuccess) { 202 if (error) { 203 *error = [NSError errorWithDomain:@"keybase.Helper" code:authResult userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Error copying rights: %@", @(authResult)], NSLocalizedRecoveryOptionsErrorKey: @[@"Quit"]}]; 204 } 205 return nil; 206 } 207 208 return authRef; 209} 210 211- (BOOL)installPrivilegedServiceWithName:(NSString *)name error:(NSError **)error { 212 AuthorizationRef authRef = [self authorization:error]; 213 if (!authRef) { 214 return NO; 215 } 216 217 NSString *helperPath = HELPER_LOCATION; 218 // It's unsafe to update privileged helper tools. 219 // https://openradar.appspot.com/20446733 220 DDLogDebug(@"Removing %@", helperPath); 221 if ([NSFileManager.defaultManager fileExistsAtPath:helperPath]) { 222 char *tool = "/bin/rm"; 223 char *args[] = {"-f", (char *)[helperPath UTF8String], NULL}; 224 FILE *pipe = NULL; 225 AuthorizationExecuteWithPrivileges(authRef, tool, kAuthorizationFlagDefaults, args, &pipe); 226 } 227 228 CFErrorRef cerror = NULL; 229 DDLogDebug(@"Installing helper tool via SMJobBless"); 230 Boolean success = SMJobBless(kSMDomainSystemLaunchd, (__bridge CFStringRef)name, authRef, &cerror); 231 232 // Let's attempt it again on error (since it's flakey) 233 if (!success) { 234 DDLogDebug(@"Failed, retrying"); 235 success = SMJobBless(kSMDomainSystemLaunchd, (__bridge CFStringRef)name, authRef, &cerror); 236 } 237 238 AuthorizationFree(authRef, kAuthorizationFlagDestroyRights); 239 240 if (!success) { 241 if (error) *error = (NSError *)CFBridgingRelease(cerror); 242 return NO; 243 } else { 244 DDLogDebug(@"Helper tool installed"); 245 return YES; 246 } 247} 248 249- (void)uninstall:(KBCompletion)completion { 250 [self removeIfExists:HELPER_LOCATION completion:completion]; 251 // TODO: Maybe, we should also kill the running helper process, but that requires more privilege, and 252 // isn't a big deal that we don't kill it on uninstall. The plist should be left untouched because if 253 // removed seems to cause problems if the helper tool needs to be re-installed. 254} 255 256- (void)removeIfExists:(NSString *)path completion:(KBCompletion)completion { 257 if ([NSFileManager.defaultManager fileExistsAtPath:path]) { 258 DDLogDebug(@"Removing %@", path); 259 [self.helper sendRequest:@"remove" params:@[@{@"path": path}] completion:^(NSError *err, id value) { 260 completion(err); 261 }]; 262 } else { 263 completion(nil); 264 } 265} 266 267/* 268- (BOOL)uninstallPrivilegedServiceWithName:(NSString *)name error:(NSError **)error { 269 AuthorizationRef authRef = [self authorization:error]; 270 if (!authRef) { 271 return NO; 272 } 273 CFErrorRef cerror = NULL; 274 BOOL success = SMJobRemove(kSMDomainSystemLaunchd, (__bridge CFStringRef)(name), authRef, true, &cerror); 275 AuthorizationFree(authRef, kAuthorizationFlagDefaults); 276 277 if (!success) { 278 if (error) *error = (NSError *)CFBridgingRelease(cerror); 279 return NO; 280 } else { 281 return YES; 282 } 283} 284 */ 285 286@end 287