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