1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "chrome/browser/mac/keystone_glue.h"
6
7#include <sys/mount.h>
8#include <sys/param.h>
9#include <sys/stat.h>
10
11#include <vector>
12
13#include "base/bind.h"
14#include "base/file_version_info.h"
15#include "base/location.h"
16#include "base/logging.h"
17#include "base/mac/authorization_util.h"
18#include "base/mac/bundle_locations.h"
19#include "base/mac/foundation_util.h"
20#include "base/mac/mac_logging.h"
21#include "base/memory/ref_counted.h"
22#include "base/no_destructor.h"
23#include "base/strings/string_number_conversions.h"
24#include "base/strings/sys_string_conversions.h"
25#include "base/task/post_task.h"
26#include "base/task/thread_pool.h"
27#include "base/version.h"
28#include "build/branding_buildflags.h"
29#include "build/build_config.h"
30#import "chrome/browser/mac/keystone_registration.h"
31#include "chrome/common/channel_info.h"
32#include "chrome/common/chrome_constants.h"
33#include "chrome/grit/chromium_strings.h"
34#include "chrome/grit/generated_resources.h"
35#include "components/version_info/version_info.h"
36#include "ui/base/l10n/l10n_util.h"
37#include "ui/base/l10n/l10n_util_mac.h"
38
39namespace {
40
41namespace ksr = keystone_registration;
42
43#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
44
45// Functions to handle the brand file.
46//
47// Note that an external file is used so it can survive updates to Chrome.
48//
49// Note that these directories are hard-coded in Keystone scripts, so
50// NSSearchPathForDirectoriesInDomains isn't used since the scripts couldn't use
51// anything like that.
52
53NSString* BrandFileName(version_info::Channel channel) {
54  NSString* fragment;
55
56  switch (channel) {
57    case version_info::Channel::CANARY:
58      fragment = @" Canary";
59      break;
60    case version_info::Channel::DEV:
61      fragment = @" Dev";
62      break;
63    case version_info::Channel::BETA:
64      fragment = @" Beta";
65      break;
66    default:
67      fragment = @"";
68      break;
69  }
70
71  return [NSString stringWithFormat:@"Google Chrome%@ Brand.plist", fragment];
72}
73
74NSString* UserBrandFilePath(version_info::Channel channel) {
75  return [[@"~/Library/Google/" stringByAppendingString:BrandFileName(channel)]
76      stringByStandardizingPath];
77}
78
79NSString* SystemBrandFilePath(version_info::Channel channel) {
80  return [[@"/Library/Google/" stringByAppendingString:BrandFileName(channel)]
81      stringByStandardizingPath];
82}
83
84#endif
85
86// Adaptor for scheduling an Objective-C method call in ThreadPool.
87class PerformBridge : public base::RefCountedThreadSafe<PerformBridge> {
88 public:
89
90  // Call |sel| on |target| with |arg| in a WorkerPool thread.
91  // |target| and |arg| are retained, |arg| may be |nil|.
92  static void PostPerform(id target, SEL sel, id arg) {
93    DCHECK(target);
94    DCHECK(sel);
95
96    scoped_refptr<PerformBridge> op = new PerformBridge(target, sel, arg);
97    base::ThreadPool::PostTask(
98        FROM_HERE,
99        {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
100         base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
101        base::BindOnce(&PerformBridge::Run, op.get()));
102  }
103
104  // Convenience for the no-argument case.
105  static void PostPerform(id target, SEL sel) {
106    PostPerform(target, sel, nil);
107  }
108
109 private:
110  // Allow RefCountedThreadSafe<> to delete.
111  friend class base::RefCountedThreadSafe<PerformBridge>;
112
113  PerformBridge(id target, SEL sel, id arg)
114      : target_([target retain]),
115        sel_(sel),
116        arg_([arg retain]) {
117  }
118
119  ~PerformBridge() {}
120
121  // Happens on a WorkerPool thread.
122  void Run() {
123    @autoreleasepool {
124      [target_ performSelector:sel_ withObject:arg_];
125    }
126  }
127
128  base::scoped_nsobject<id> target_;
129  SEL sel_;
130  base::scoped_nsobject<id> arg_;
131};
132
133}  // namespace
134
135@interface KeystoneGlue (Private)
136
137// Returns the path to the application's Info.plist file.  This returns the
138// outer application bundle's Info.plist, not the framework's Info.plist.
139- (NSString*)appInfoPlistPath;
140
141// Returns a dictionary containing parameters to be used for a KSRegistration
142// -registerWithParameters: or -promoteWithParameters:authorization: call.
143- (NSDictionary*)keystoneParameters;
144
145// Called when Keystone registration completes.
146- (void)registrationComplete:(NSNotification*)notification;
147
148// Called periodically to announce activity by pinging the Keystone server.
149- (void)markActive:(NSTimer*)timer;
150
151// Called when an update check or update installation is complete.  Posts the
152// kAutoupdateStatusNotification notification to the default notification
153// center.
154- (void)updateStatus:(AutoupdateStatus)status
155             version:(NSString*)version
156               error:(NSString*)error;
157
158// Returns the version of the currently-installed application on disk.
159- (NSString*)currentlyInstalledVersion;
160
161// These three methods are used to determine the version of the application
162// currently installed on disk, compare that to the currently-running version,
163// decide whether any updates have been installed, and call
164// -updateStatus:version:error:.
165//
166// In order to check the version on disk, the installed application's
167// Info.plist dictionary must be read; in order to see changes as updates are
168// applied, the dictionary must be read each time, bypassing any caches such
169// as the one that NSBundle might be maintaining.  Reading files can be a
170// blocking operation, and blocking operations are to be avoided on the main
171// thread.  I'm not quite sure what jank means, but I bet that a blocked main
172// thread would cause some of it.
173//
174// -determineUpdateStatusAsync is called on the main thread to initiate the
175// operation.  It performs initial set-up work that must be done on the main
176// thread and arranges for -determineUpdateStatus to be called on a work queue
177// thread managed by WorkerPool.
178// -determineUpdateStatus then reads the Info.plist, gets the version from the
179// CFBundleShortVersionString key, and performs
180// -determineUpdateStatusForVersion: on the main thread.
181// -determineUpdateStatusForVersion: does the actual comparison of the version
182// on disk with the running version and calls -updateStatus:version:error: with
183// the results of its analysis.
184- (void)determineUpdateStatusAsync;
185- (void)determineUpdateStatus;
186- (void)determineUpdateStatusForVersion:(NSString*)version;
187
188// Returns YES if registration_ is definitely on a system ticket.
189- (BOOL)isSystemTicket;
190
191// Returns YES if Keystone is definitely installed at the system level,
192// determined by the presence of an executable ksadmin program at the expected
193// system location.
194- (BOOL)isSystemKeystone;
195
196// Called when ticket promotion completes.
197- (void)promotionComplete:(NSNotification*)notification;
198
199// Changes the application's ownership and permissions so that all files are
200// owned by root:wheel and all files and directories are writable only by
201// root, but readable and executable as needed by everyone.
202// -changePermissionsForPromotionAsync is called on the main thread by
203// -promotionComplete.  That routine calls
204// -changePermissionsForPromotionWithTool: on a work queue thread.  When done,
205// -changePermissionsForPromotionComplete is called on the main thread.
206- (void)changePermissionsForPromotionAsync;
207- (void)changePermissionsForPromotionWithTool:(NSString*)toolPath;
208- (void)changePermissionsForPromotionComplete;
209
210#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
211// Returns the brand file path to use for Keystone.
212- (NSString*)brandFilePath;
213#endif
214
215// YES if no update installation has succeeded since a binary diff patch
216// installation failed. This signals the need to attempt a full installer
217// which does not depend on applying a patch to existing files.
218- (BOOL)wantsFullInstaller;
219
220// Returns an NSString* suitable for appending to a Chrome Keystone tag value or
221// tag key.  If a full installer (as opposed to a binary diff/delta patch) is
222// required, the tag suffix will contain the string "-full". If no special
223// treatment is required, the tag suffix will be an empty string.
224- (NSString*)tagSuffix;
225
226@end  // @interface KeystoneGlue (Private)
227
228NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification";
229NSString* const kAutoupdateStatusStatus = @"status";
230NSString* const kAutoupdateStatusVersion = @"version";
231NSString* const kAutoupdateStatusErrorMessages = @"errormessages";
232
233namespace {
234
235NSString* const kChannelKey = @"KSChannelID";
236NSString* const kBrandKey = @"KSBrandID";
237NSString* const kVersionKey = @"KSVersion";
238
239}  // namespace
240
241@implementation KeystoneGlue
242
243+ (KeystoneGlue*)defaultKeystoneGlue {
244  static bool sTriedCreatingDefaultKeystoneGlue = false;
245  static KeystoneGlue* sDefaultKeystoneGlue = nil;  // leaked
246
247  if (!sTriedCreatingDefaultKeystoneGlue) {
248    sTriedCreatingDefaultKeystoneGlue = true;
249
250    sDefaultKeystoneGlue = [[KeystoneGlue alloc] init];
251    [sDefaultKeystoneGlue loadParameters];
252    if (![sDefaultKeystoneGlue loadKeystoneRegistration]) {
253      [sDefaultKeystoneGlue release];
254      sDefaultKeystoneGlue = nil;
255    }
256  }
257  return sDefaultKeystoneGlue;
258}
259
260- (instancetype)init {
261  if ((self = [super init])) {
262    NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
263
264    [center addObserver:self
265               selector:@selector(registrationComplete:)
266                   name:ksr::KSRegistrationDidCompleteNotification
267                 object:nil];
268
269    [center addObserver:self
270               selector:@selector(promotionComplete:)
271                   name:ksr::KSRegistrationPromotionDidCompleteNotification
272                 object:nil];
273
274    [center addObserver:self
275               selector:@selector(checkForUpdateComplete:)
276                   name:ksr::KSRegistrationCheckForUpdateNotification
277                 object:nil];
278
279    [center addObserver:self
280               selector:@selector(installUpdateComplete:)
281                   name:ksr::KSRegistrationStartUpdateNotification
282                 object:nil];
283  }
284
285  return self;
286}
287
288- (void)dealloc {
289  [[NSNotificationCenter defaultCenter] removeObserver:self];
290  [super dealloc];
291}
292
293- (NSDictionary*)infoDictionary {
294  // Use base::mac::OuterBundle() to get the Chrome app's own bundle identifier
295  // and path, not the framework's.  For auto-update, the application is
296  // what's significant here: it's used to locate the outermost part of the
297  // application for the existence checker and other operations that need to
298  // see the entire application bundle.
299  return [base::mac::OuterBundle() infoDictionary];
300}
301
302- (void)loadParameters {
303  NSBundle* appBundle = base::mac::OuterBundle();
304  NSDictionary* infoDictionary = [self infoDictionary];
305
306  NSString* productID =
307      base::mac::ObjCCast<NSString>(infoDictionary[@"KSProductID"]);
308  if (productID == nil) {
309    productID = [appBundle bundleIdentifier];
310  }
311
312  NSString* appPath = [appBundle bundlePath];
313  NSString* url = base::mac::ObjCCast<NSString>(infoDictionary[@"KSUpdateURL"]);
314  NSString* version =
315      base::mac::ObjCCast<NSString>(infoDictionary[kVersionKey]);
316
317  if (!productID || !appPath || !url || !version) {
318    // If parameters required for Keystone are missing, don't use it.
319    return;
320  }
321
322  std::string channel = chrome::GetChannelName();
323  // The stable channel has no tag.  If updating to stable, remove the
324  // dev and beta tags since we've been "promoted".
325  version_info::Channel channelType = chrome::GetChannelByName(channel);
326  if (channelType == version_info::Channel::STABLE) {
327    channel = base::SysNSStringToUTF8(ksr::KSRegistrationRemoveExistingTag);
328    DCHECK(chrome::GetChannelByName(channel) == version_info::Channel::STABLE)
329        << "-channel name modification has side effect";
330  }
331
332  _productID.reset([productID copy]);
333  _appPath.reset([appPath copy]);
334  _url.reset([url copy]);
335  _version.reset([version copy]);
336  _channel = channel;
337}
338
339#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
340
341- (NSString*)brandFilePath {
342  DCHECK(_version != nil) << "-loadParameters must be called first";
343
344  if (_brandFile)
345    return _brandFile;
346
347  NSFileManager* fm = [NSFileManager defaultManager];
348  version_info::Channel channel = chrome::GetChannelByName(_channel);
349  NSString* userBrandFile = UserBrandFilePath(channel);
350  NSString* systemBrandFile = SystemBrandFilePath(channel);
351
352  // Default to none.
353  _brandFile.reset(@"", base::scoped_policy::RETAIN);
354
355  // Only a side-by-side capable Chromium can have an independent brand code.
356
357  if (!chrome::IsSideBySideCapable()) {
358    // If on the older dev or beta channels that were not side-by-side capable,
359    // this installation may have replaced an older system-level installation.
360    // Check for a user brand file and nuke it if present. Don't try to remove
361    // the system brand file, there wouldn't be any permission to do so.
362
363    // Don't do this on a side-by-side capable channel. Those can run
364    // side-by-side with another Google Chrome installation whose brand code, if
365    // any, should remain intact.
366
367    if ([fm fileExistsAtPath:userBrandFile]) {
368      [fm removeItemAtPath:userBrandFile error:NULL];
369    }
370  } else {
371    // If there is a system brand file, use it.
372    if ([fm fileExistsAtPath:systemBrandFile]) {
373      // System
374
375      // Use the system file that is there.
376      _brandFile.reset(systemBrandFile, base::scoped_policy::RETAIN);
377
378      // Clean up any old user level file.
379      if ([fm fileExistsAtPath:userBrandFile]) {
380        [fm removeItemAtPath:userBrandFile error:NULL];
381      }
382
383    } else {
384      // User
385
386      NSDictionary* infoDictionary = [self infoDictionary];
387      NSString* appBundleBrandID =
388          base::mac::ObjCCast<NSString>(infoDictionary[kBrandKey]);
389
390      NSString* storedBrandID = nil;
391      if ([fm fileExistsAtPath:userBrandFile]) {
392        NSDictionary* storedBrandDict =
393            [NSDictionary dictionaryWithContentsOfFile:userBrandFile];
394        storedBrandID =
395            base::mac::ObjCCast<NSString>(storedBrandDict[kBrandKey]);
396      }
397
398      if ((appBundleBrandID != nil) &&
399          (![storedBrandID isEqualTo:appBundleBrandID])) {
400        // App and store don't match, update store and use it.
401        NSDictionary* storedBrandDict = @{kBrandKey : appBundleBrandID};
402        // If Keystone hasn't been installed yet, the location the brand file
403        // is written to won't exist, so manually create the directory.
404        NSString* userBrandFileDirectory =
405            [userBrandFile stringByDeletingLastPathComponent];
406        if (![fm fileExistsAtPath:userBrandFileDirectory]) {
407          if (![fm createDirectoryAtPath:userBrandFileDirectory
408                  withIntermediateDirectories:YES
409                                   attributes:nil
410                                        error:NULL]) {
411            LOG(ERROR) << "Failed to create the directory for the brand file";
412          }
413        }
414        if ([storedBrandDict writeToFile:userBrandFile atomically:YES]) {
415          _brandFile.reset(userBrandFile, base::scoped_policy::RETAIN);
416        }
417      } else if (storedBrandID) {
418        // Had stored brand, use it.
419        _brandFile.reset(userBrandFile, base::scoped_policy::RETAIN);
420      }
421    }
422  }
423
424  return _brandFile;
425}
426
427#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
428
429- (BOOL)loadKeystoneRegistration {
430  if (!_productID || !_appPath || !_url || !_version)
431    return NO;
432
433  // Load the KeystoneRegistration framework bundle if present.  It lives
434  // inside the framework, so use base::mac::FrameworkBundle();
435  NSString* ksrPath =
436      [[base::mac::FrameworkBundle() privateFrameworksPath]
437          stringByAppendingPathComponent:@"KeystoneRegistration.framework"];
438  NSBundle* ksrBundle = [NSBundle bundleWithPath:ksrPath];
439  [ksrBundle load];
440
441  // Harness the KSRegistration class.
442  Class ksrClass = [ksrBundle classNamed:@"KSRegistration"];
443  KSRegistration* ksr = [ksrClass registrationWithProductID:_productID];
444  if (!ksr)
445    return NO;
446
447  _registration.reset([ksr retain]);
448  _ksUnsignedReportingAttributeClass =
449      [ksrBundle classNamed:@"KSUnsignedReportingAttribute"];
450  return YES;
451}
452
453- (NSString*)appInfoPlistPath {
454  // NSBundle ought to have a way to access this path directly, but it
455  // doesn't.
456  return [[_appPath stringByAppendingPathComponent:@"Contents"]
457             stringByAppendingPathComponent:@"Info.plist"];
458}
459
460- (NSDictionary*)keystoneParameters {
461  NSNumber* xcType = [NSNumber numberWithInt:ksr::kKSPathExistenceChecker];
462  NSNumber* preserveTTToken = @YES;
463  NSString* appInfoPlistPath = [self appInfoPlistPath];
464  NSString* brandKey = kBrandKey;
465  NSString* brandPath = @"";
466#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
467  brandPath = [self brandFilePath];
468#endif
469
470  if ([brandPath length] == 0) {
471    // Brand path and brand key must be cleared together or ksadmin seems
472    // to throw an error.
473    brandKey = @"";
474  }
475
476  // Note that _channel is permitted to be an empty string, but it must not be
477  // nil.
478  NSString* tagSuffix = [self tagSuffix];
479  NSString* tagValue =
480      [NSString stringWithFormat:@"%s%@", _channel.c_str(), tagSuffix];
481  NSString* tagKey = [kChannelKey stringByAppendingString:tagSuffix];
482
483  return @{
484    ksr::KSRegistrationVersionKey : _version,
485    ksr::KSRegistrationVersionPathKey : appInfoPlistPath,
486    ksr::KSRegistrationVersionKeyKey : kVersionKey,
487    ksr::KSRegistrationExistenceCheckerTypeKey : xcType,
488    ksr::KSRegistrationExistenceCheckerStringKey : _appPath.get(),
489    ksr::KSRegistrationServerURLStringKey : _url.get(),
490    ksr::KSRegistrationPreserveTrustedTesterTokenKey : preserveTTToken,
491    ksr::KSRegistrationTagKey : tagValue,
492    ksr::KSRegistrationTagPathKey : appInfoPlistPath,
493    ksr::KSRegistrationTagKeyKey : tagKey,
494    ksr::KSRegistrationBrandPathKey : brandPath,
495    ksr::KSRegistrationBrandKeyKey : brandKey
496  };
497}
498
499- (void)setRegistrationActive {
500  DCHECK(_registration);
501  _registrationActive = YES;
502  NSError* setActiveError = nil;
503  if (![_registration setActiveWithError:&setActiveError]) {
504    VLOG(1) << [setActiveError localizedDescription];
505  }
506}
507
508- (void)registerWithKeystone {
509  DCHECK(_registration);
510
511  [self updateStatus:kAutoupdateRegistering version:nil error:nil];
512
513  NSDictionary* parameters = [self keystoneParameters];
514  BOOL result = [_registration registerWithParameters:parameters];
515  if (!result) {
516    // TODO: If Keystone ever makes a variant of this API with a withError:
517    // parameter, include the error message here in the call to updateStatus:.
518    [self updateStatus:kAutoupdateRegisterFailed version:nil error:nil];
519    return;
520  }
521
522  // Upon completion, ksr::KSRegistrationDidCompleteNotification will be
523  // posted, and -registrationComplete: will be called.
524
525  // Set up hourly activity pings.
526  _timer = [NSTimer scheduledTimerWithTimeInterval:60 * 60  // One hour
527                                            target:self
528                                          selector:@selector(markActive:)
529                                          userInfo:nil
530                                           repeats:YES];
531}
532
533- (BOOL)isRegisteredAndActive {
534  return _registrationActive;
535}
536
537- (void)registrationComplete:(NSNotification*)notification {
538  NSDictionary* userInfo = [notification userInfo];
539  NSNumber* status =
540      base::mac::ObjCCast<NSNumber>(userInfo[ksr::KSRegistrationStatusKey]);
541  NSString* errorMessages = base::mac::ObjCCast<NSString>(
542      userInfo[ksr::KSRegistrationUpdateCheckRawErrorMessagesKey]);
543
544  if ([status boolValue]) {
545    if ([self needsPromotion]) {
546      [self updateStatus:kAutoupdateNeedsPromotion
547                 version:nil
548                   error:errorMessages];
549    } else {
550      [self updateStatus:kAutoupdateRegistered
551                 version:nil
552                   error:errorMessages];
553    }
554  } else {
555    // Dump registration_?
556    [self updateStatus:kAutoupdateRegisterFailed
557               version:nil
558                 error:errorMessages];
559  }
560}
561
562- (void)stopTimer {
563  [_timer invalidate];
564}
565
566- (void)markActive:(NSTimer*)timer {
567  [self setRegistrationActive];
568}
569
570- (void)checkForUpdate {
571  DCHECK(_registration);
572
573  if ([self asyncOperationPending]) {
574    // Update check already in process; return without doing anything.
575    return;
576  }
577
578  [self updateStatus:kAutoupdateChecking version:nil error:nil];
579
580  // All checks from inside Chrome are considered user-initiated, because they
581  // only happen following a user action, such as visiting the about page.
582  // Non-user-initiated checks are the periodic checks automatically made by
583  // Keystone, which don't come through this code path (or even this process).
584  [_registration checkForUpdateWasUserInitiated:YES];
585
586  // Upon completion, ksr::KSRegistrationCheckForUpdateNotification will be
587  // posted, and -checkForUpdateComplete: will be called.
588}
589
590- (void)checkForUpdateComplete:(NSNotification*)notification {
591  NSDictionary* userInfo = [notification userInfo];
592  NSNumber* error = base::mac::ObjCCast<NSNumber>(
593      userInfo[ksr::KSRegistrationUpdateCheckErrorKey]);
594  NSNumber* status =
595      base::mac::ObjCCast<NSNumber>(userInfo[ksr::KSRegistrationStatusKey]);
596  NSString* errorMessages = base::mac::ObjCCast<NSString>(
597      userInfo[ksr::KSRegistrationUpdateCheckRawErrorMessagesKey]);
598
599  if ([error boolValue]) {
600    [self updateStatus:kAutoupdateCheckFailed
601               version:nil
602                 error:errorMessages];
603  } else if ([status boolValue]) {
604    // If an update is known to be available, go straight to
605    // -updateStatus:version:.  It doesn't matter what's currently on disk.
606    NSString* version =
607        base::mac::ObjCCast<NSString>(userInfo[ksr::KSRegistrationVersionKey]);
608    [self updateStatus:kAutoupdateAvailable
609               version:version
610                 error:errorMessages];
611  } else {
612    // If no updates are available, check what's on disk, because an update
613    // may have already been installed.  This check happens on another thread,
614    // and -updateStatus:version: will be called on the main thread when done.
615    [self determineUpdateStatusAsync];
616  }
617}
618
619- (void)installUpdate {
620  DCHECK(_registration);
621
622  if ([self asyncOperationPending]) {
623    // Update check already in process; return without doing anything.
624    return;
625  }
626
627  [self updateStatus:kAutoupdateInstalling version:nil error:nil];
628
629  [_registration startUpdate];
630
631  // Upon completion, ksr::KSRegistrationStartUpdateNotification will be
632  // posted, and -installUpdateComplete: will be called.
633}
634
635- (void)installUpdateComplete:(NSNotification*)notification {
636  NSDictionary* userInfo = [notification userInfo];
637  NSNumber* successfulInstall = base::mac::ObjCCast<NSNumber>(
638      userInfo[ksr::KSUpdateCheckSuccessfullyInstalledKey]);
639  NSString* errorMessages = base::mac::ObjCCast<NSString>(
640      userInfo[ksr::KSRegistrationUpdateCheckRawErrorMessagesKey]);
641
642  // http://crbug.com/160308 and b/7517358: when using system Keystone and on
643  // a user ticket, KSUpdateCheckSuccessfulKey will be NO even when an update
644  // was installed correctly, so don't check it. It should be redudnant when
645  // KSUpdateCheckSuccessfullyInstalledKey is checked.
646  if (![successfulInstall intValue]) {
647    [self updateStatus:kAutoupdateInstallFailed
648               version:nil
649                 error:errorMessages];
650  } else {
651    _updateSuccessfullyInstalled = YES;
652
653    // Nothing in the notification dictionary reports the version that was
654    // installed.  Figure it out based on what's on disk.
655    [self determineUpdateStatusAsync];
656  }
657}
658
659- (NSString*)currentlyInstalledVersion {
660  NSString* appInfoPlistPath = [self appInfoPlistPath];
661  NSDictionary* infoPlist =
662      [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath];
663  return base::mac::ObjCCast<NSString>(
664      infoPlist[@"CFBundleShortVersionString"]);
665}
666
667// Runs on the main thread.
668- (void)determineUpdateStatusAsync {
669  DCHECK([NSThread isMainThread]);
670
671  PerformBridge::PostPerform(self, @selector(determineUpdateStatus));
672}
673
674// Runs on a thread managed by WorkerPool.
675- (void)determineUpdateStatus {
676  DCHECK(![NSThread isMainThread]);
677
678  NSString* version = [self currentlyInstalledVersion];
679
680  [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:)
681                         withObject:version
682                      waitUntilDone:NO];
683}
684
685// Runs on the main thread.
686- (void)determineUpdateStatusForVersion:(NSString*)version {
687  DCHECK([NSThread isMainThread]);
688
689  AutoupdateStatus status;
690  if (_updateSuccessfullyInstalled) {
691    // If an update was successfully installed and this object saw it happen,
692    // then don't even bother comparing versions.
693    status = kAutoupdateInstalled;
694  } else {
695    NSString* currentVersion = base::SysUTF8ToNSString(chrome::kChromeVersion);
696    if (!version) {
697      // If the version on disk could not be determined, assume that
698      // whatever's running is current.
699      version = currentVersion;
700      status = kAutoupdateCurrent;
701    } else if ([version isEqualToString:currentVersion]) {
702      status = kAutoupdateCurrent;
703    } else {
704      // If the version on disk doesn't match what's currently running, an
705      // update must have been applied in the background, without this app's
706      // direct participation.  Leave updateSuccessfullyInstalled_ alone
707      // because there's no direct knowledge of what actually happened.
708      status = kAutoupdateInstalled;
709    }
710  }
711
712  [self updateStatus:status version:version error:nil];
713}
714
715- (void)updateStatus:(AutoupdateStatus)status
716             version:(NSString*)version
717               error:(NSString*)error {
718  NSNumber* statusNumber = [NSNumber numberWithInt:status];
719  NSMutableDictionary* dictionary =
720      [NSMutableDictionary dictionaryWithObject:statusNumber
721                                         forKey:kAutoupdateStatusStatus];
722  if ([version length]) {
723    dictionary[kAutoupdateStatusVersion] = version;
724  }
725  if ([error length]) {
726    dictionary[kAutoupdateStatusErrorMessages] = error;
727  }
728
729  NSNotification* notification =
730      [NSNotification notificationWithName:kAutoupdateStatusNotification
731                                    object:self
732                                  userInfo:dictionary];
733  _recentNotification.reset([notification retain]);
734
735  [[NSNotificationCenter defaultCenter] postNotification:notification];
736}
737
738- (NSNotification*)recentNotification {
739  return [[_recentNotification retain] autorelease];
740}
741
742- (AutoupdateStatus)recentStatus {
743  NSDictionary* dictionary = [_recentNotification userInfo];
744  NSNumber* status =
745      base::mac::ObjCCastStrict<NSNumber>(dictionary[kAutoupdateStatusStatus]);
746  return static_cast<AutoupdateStatus>([status intValue]);
747}
748
749- (BOOL)asyncOperationPending {
750  AutoupdateStatus status = [self recentStatus];
751  return status == kAutoupdateRegistering ||
752         status == kAutoupdateChecking ||
753         status == kAutoupdateInstalling ||
754         status == kAutoupdatePromoting;
755}
756
757- (BOOL)isSystemTicket {
758  DCHECK(_registration);
759  return [_registration ticketType] == ksr::kKSRegistrationSystemTicket;
760}
761
762- (BOOL)isSystemKeystone {
763  // ksadmin moved from MacOS to Helpers in Keystone 1.2.13.112, 2019-11-12. A
764  // symbolic link from the old location was left in place, but may not remain
765  // indefinitely. Try the new location first, falling back to the old if
766  // needed.
767  struct stat statbuf;
768  if (stat("/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
769           "Contents/Helpers/ksadmin",
770           &statbuf) != 0 &&
771      stat("/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
772           "Contents/MacOS/ksadmin",
773           &statbuf) != 0) {
774    return NO;
775  }
776
777  if (!(statbuf.st_mode & S_IXUSR)) {
778    return NO;
779  }
780
781  return YES;
782}
783
784- (BOOL)isOnReadOnlyFilesystem {
785  const char* appPathC = [_appPath fileSystemRepresentation];
786  struct statfs statfsBuf;
787
788  if (statfs(appPathC, &statfsBuf) != 0) {
789    PLOG(ERROR) << "statfs";
790    // Be optimistic about the filesystem's writability.
791    return NO;
792  }
793
794  return (statfsBuf.f_flags & MNT_RDONLY) != 0;
795}
796
797- (BOOL)isAutoupdateEnabledForAllUsers {
798  return [self isSystemKeystone] && [self isSystemTicket];
799}
800
801// Compares the version of the installed system Keystone to the version of
802// KeystoneRegistration.framework. The method is a class method, so that
803// tests can pick it up.
804+ (BOOL)isValidSystemKeystone:(NSDictionary*)systemKeystonePlistContents
805            comparedToBundled:(NSDictionary*)bundledKeystonePlistContents {
806  NSString* versionKey = base::mac::CFToNSCast(kCFBundleVersionKey);
807
808  // If the bundled version is missing or broken, this question is irrelevant.
809  NSString* bundledKeystoneVersionString =
810      base::mac::ObjCCast<NSString>(bundledKeystonePlistContents[versionKey]);
811  if (!bundledKeystoneVersionString.length)
812    return YES;
813  base::Version bundled_version(
814      base::SysNSStringToUTF8(bundledKeystoneVersionString));
815  if (!bundled_version.IsValid())
816    return YES;
817
818  NSString* systemKeystoneVersionString =
819      base::mac::ObjCCast<NSString>(systemKeystonePlistContents[versionKey]);
820  if (!systemKeystoneVersionString.length)
821    return NO;
822
823  // Installed Keystone's version should always be >= than the bundled one.
824  base::Version system_version(
825      base::SysNSStringToUTF8(systemKeystoneVersionString));
826  if (!system_version.IsValid() || system_version < bundled_version)
827    return NO;
828
829  return YES;
830}
831
832- (BOOL)isSystemKeystoneBroken {
833  DCHECK([self isSystemKeystone])
834      << "Call this method only for system Keystone.";
835
836  NSDictionary* systemKeystonePlist =
837      [NSDictionary dictionaryWithContentsOfFile:
838                        @"/Library/Google/GoogleSoftwareUpdate/"
839                        @"GoogleSoftwareUpdate.bundle/Contents/Info.plist"];
840  NSBundle* keystoneFramework = [NSBundle bundleForClass:[_registration class]];
841  return ![[self class] isValidSystemKeystone:systemKeystonePlist
842                            comparedToBundled:keystoneFramework.infoDictionary];
843}
844
845- (BOOL)needsPromotion {
846  // Don't promote when on a read-only filesystem.
847  if ([self isOnReadOnlyFilesystem]) {
848    return NO;
849  }
850
851  BOOL isSystemKeystone = [self isSystemKeystone];
852  if (isSystemKeystone) {
853    // We can recover broken user keystone, but not broken system one.
854    if ([self isSystemKeystoneBroken])
855      return YES;
856  }
857
858  // System ticket requires system Keystone for the updates to work.
859  if ([self isSystemTicket])
860    return !isSystemKeystone;
861
862  // Check the outermost bundle directory, the main executable path, and the
863  // framework directory.  It may be enough to just look at the outermost
864  // bundle directory, but checking an interior file and directory can be
865  // helpful in case permissions are set differently only on the outermost
866  // directory.  An interior file and directory are both checked because some
867  // file operations, such as Snow Leopard's Finder's copy operation when
868  // authenticating, may actually result in different ownership being applied
869  // to files and directories.
870  NSFileManager* fileManager = [NSFileManager defaultManager];
871  NSString* executablePath = [base::mac::OuterBundle() executablePath];
872  NSString* frameworkPath = [base::mac::FrameworkBundle() bundlePath];
873  return ![fileManager isWritableFileAtPath:_appPath] ||
874         ![fileManager isWritableFileAtPath:executablePath] ||
875         ![fileManager isWritableFileAtPath:frameworkPath];
876}
877
878- (BOOL)wantsPromotion {
879  if ([self needsPromotion]) {
880    return YES;
881  }
882
883  // These are the same unpromotable cases as in -needsPromotion.
884  if ([self isOnReadOnlyFilesystem] || [self isSystemTicket]) {
885    return NO;
886  }
887
888  return [_appPath hasPrefix:@"/Applications/"];
889}
890
891- (void)promoteTicket {
892  if ([self asyncOperationPending] || ![self wantsPromotion]) {
893    // Because there are multiple ways of reaching promoteTicket that might
894    // not lock each other out, it may be possible to arrive here while an
895    // asynchronous operation is pending, or even after promotion has already
896    // occurred.  Just quietly return without doing anything.
897    return;
898  }
899
900  NSString* prompt = l10n_util::GetNSStringFWithFixup(
901      IDS_PROMOTE_AUTHENTICATION_PROMPT,
902      l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
903  base::mac::ScopedAuthorizationRef authorization(
904      base::mac::AuthorizationCreateToRunAsRoot(
905          base::mac::NSToCFCast(prompt)));
906  if (!authorization.get()) {
907    return;
908  }
909
910  [self promoteTicketWithAuthorization:authorization.release() synchronous:NO];
911}
912
913- (void)promoteTicketWithAuthorization:(AuthorizationRef)anAuthorization
914                           synchronous:(BOOL)synchronous {
915  DCHECK(_registration);
916
917  base::mac::ScopedAuthorizationRef authorization(anAuthorization);
918  anAuthorization = nullptr;
919
920  if ([self asyncOperationPending]) {
921    // Starting a synchronous operation while an asynchronous one is pending
922    // could be trouble.
923    return;
924  }
925  if (!synchronous && ![self wantsPromotion]) {
926    // If operating synchronously, the call came from the installer, which
927    // means that a system ticket is required.  Otherwise, only allow
928    // promotion if it's wanted.
929    return;
930  }
931
932  _synchronousPromotion = synchronous;
933
934  [self updateStatus:kAutoupdatePromoting version:nil error:nil];
935
936#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
937  // TODO(mark): Remove when able!
938  //
939  // keystone_promote_preflight will copy the current brand information out to
940  // the system level so all users can share the data as part of the ticket
941  // promotion.
942  //
943  // This is run synchronously, which isn't optimal, but
944  // -[KSRegistration promoteWithParameters:authorization:] is currently
945  // synchronous too, and this operation needs to happen before that one.
946  //
947  // Hopefully, the Keystone promotion code will just be changed to do what
948  // preflight now does, and then the preflight script can be removed instead.
949  // However, preflight operation (and promotion) should only be asynchronous if
950  // the synchronous parameter is NO.
951  NSString* preflightPath =
952      [base::mac::FrameworkBundle()
953          pathForResource:@"keystone_promote_preflight"
954                   ofType:@"sh"];
955  const char* preflightPathC = [preflightPath fileSystemRepresentation];
956
957  // This is typically a once per machine operation, so it is not worth caching
958  // the type of brand file (user vs system). Figure it out here:
959  version_info::Channel channel = chrome::GetChannelByName(_channel);
960  NSString* userBrandFile = UserBrandFilePath(channel);
961  NSString* systemBrandFile = SystemBrandFilePath(channel);
962  const char* arguments[] = {NULL, NULL, NULL};
963  BOOL userBrand = NO;
964  if ([_brandFile isEqualToString:userBrandFile]) {
965    // Running with user level brand file, promote to the system level.
966    userBrand = YES;
967    arguments[0] = userBrandFile.UTF8String;
968    arguments[1] = systemBrandFile.UTF8String;
969  }
970
971  int exit_status;
972  OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
973      authorization,
974      preflightPathC,
975      kAuthorizationFlagDefaults,
976      arguments,
977      NULL,  // pipe
978      &exit_status);
979  if (status != errAuthorizationSuccess) {
980    // It's possible to get an OS-provided error string for this return code
981    // using base::mac::DescriptionFromOSStatus, but most of those strings are
982    // not useful/actionable for users, so we stick with the error code instead.
983    NSString* errorMessage = l10n_util::GetNSStringFWithFixup(
984        IDS_PROMOTE_PREFLIGHT_LAUNCH_ERROR, base::NumberToString16(status));
985    [self updateStatus:kAutoupdatePromoteFailed
986               version:nil
987                 error:errorMessage];
988    return;
989  }
990  if (exit_status != 0) {
991    NSString* errorMessage = l10n_util::GetNSStringFWithFixup(
992        IDS_PROMOTE_PREFLIGHT_SCRIPT_ERROR, base::NumberToString16(status));
993    [self updateStatus:kAutoupdatePromoteFailed
994               version:nil
995                 error:errorMessage];
996    return;
997  }
998
999  // Hang on to the AuthorizationRef so that it can be used once promotion is
1000  // complete.  Do this before asking Keystone to promote the ticket, because
1001  // -promotionComplete: may be called from inside the Keystone promotion
1002  // call.
1003  _authorization.swap(authorization);
1004
1005  NSDictionary* parameters = [self keystoneParameters];
1006
1007  // If the brand file is user level, update parameters to point to the new
1008  // system level file during promotion.
1009  if (userBrand) {
1010    NSMutableDictionary* tempParameters =
1011        [[parameters mutableCopy] autorelease];
1012    tempParameters[ksr::KSRegistrationBrandPathKey] = systemBrandFile;
1013    _brandFile.reset(systemBrandFile, base::scoped_policy::RETAIN);
1014    parameters = tempParameters;
1015  }
1016
1017  if (![_registration promoteWithParameters:parameters
1018                              authorization:_authorization]) {
1019    // TODO: If Keystone ever makes a variant of this API with a withError:
1020    // parameter, include the error message here in the call to updateStatus:.
1021    [self updateStatus:kAutoupdatePromoteFailed version:nil error:nil];
1022    _authorization.reset();
1023    return;
1024  }
1025
1026  // Upon completion, ksr::KSRegistrationPromotionDidCompleteNotification will
1027  // be posted, and -promotionComplete: will be called.
1028
1029  // If synchronous, see to it that this happens immediately. Give it a
1030  // 10-second deadline.
1031  if (synchronous) {
1032    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
1033  }
1034#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
1035}
1036
1037- (void)promotionComplete:(NSNotification*)notification {
1038  NSDictionary* userInfo = [notification userInfo];
1039  NSNumber* status =
1040      base::mac::ObjCCast<NSNumber>(userInfo[ksr::KSRegistrationStatusKey]);
1041
1042  if ([status boolValue]) {
1043    if (_synchronousPromotion) {
1044      // Short-circuit: if performing a synchronous promotion, the promotion
1045      // came from the installer, which already set the permissions properly.
1046      // Rather than run a duplicate permission-changing operation, jump
1047      // straight to "done."
1048      [self changePermissionsForPromotionComplete];
1049    } else {
1050      [self changePermissionsForPromotionAsync];
1051    }
1052  } else {
1053    _authorization.reset();
1054    [self updateStatus:kAutoupdatePromoteFailed version:nil error:nil];
1055  }
1056
1057  if (_synchronousPromotion) {
1058    // The run loop doesn't need to wait for this any longer.
1059    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
1060    CFRunLoopStop(runLoop);
1061    CFRunLoopWakeUp(runLoop);
1062  }
1063}
1064
1065- (void)changePermissionsForPromotionAsync {
1066  // NSBundle is not documented as being thread-safe.  Do NSBundle operations
1067  // on the main thread before jumping over to a WorkerPool-managed
1068  // thread to run the tool.
1069  DCHECK([NSThread isMainThread]);
1070
1071  SEL selector = @selector(changePermissionsForPromotionWithTool:);
1072  NSString* toolPath =
1073      [base::mac::FrameworkBundle()
1074          pathForResource:@"keystone_promote_postflight"
1075                   ofType:@"sh"];
1076
1077  PerformBridge::PostPerform(self, selector, toolPath);
1078}
1079
1080- (void)changePermissionsForPromotionWithTool:(NSString*)toolPath {
1081  const char* toolPathC = [toolPath fileSystemRepresentation];
1082
1083  const char* appPathC = [_appPath fileSystemRepresentation];
1084  const char* arguments[] = {appPathC, NULL};
1085
1086  int exit_status;
1087  OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
1088      _authorization,
1089      toolPathC,
1090      kAuthorizationFlagDefaults,
1091      arguments,
1092      NULL,  // pipe
1093      &exit_status);
1094  if (status != errAuthorizationSuccess) {
1095    OSSTATUS_LOG(ERROR, status)
1096        << "AuthorizationExecuteWithPrivileges postflight";
1097  } else if (exit_status != 0) {
1098    LOG(ERROR) << "keystone_promote_postflight status " << exit_status;
1099  }
1100
1101  SEL selector = @selector(changePermissionsForPromotionComplete);
1102  [self performSelectorOnMainThread:selector
1103                         withObject:nil
1104                      waitUntilDone:NO];
1105}
1106
1107- (void)changePermissionsForPromotionComplete {
1108  _authorization.reset();
1109
1110  [self updateStatus:kAutoupdatePromoted version:nil error:nil];
1111}
1112
1113- (void)setAppPath:(NSString*)appPath {
1114  if (appPath != _appPath) {
1115    _appPath.reset([appPath copy]);
1116  }
1117}
1118
1119- (BOOL)wantsFullInstaller {
1120  // It's difficult to check the tag prior to Keystone registration, and
1121  // performing registration replaces the tag. keystone_install.sh
1122  // communicates a need for a full installer with Chrome in this file,
1123  // .want_full_installer.
1124  NSString* wantFullInstallerPath =
1125      [_appPath stringByAppendingPathComponent:@".want_full_installer"];
1126  NSString* wantFullInstallerContents =
1127      [NSString stringWithContentsOfFile:wantFullInstallerPath
1128                                encoding:NSUTF8StringEncoding
1129                                   error:NULL];
1130  if (!wantFullInstallerContents) {
1131    return NO;
1132  }
1133
1134  NSString* wantFullInstallerVersion =
1135      [wantFullInstallerContents stringByTrimmingCharactersInSet:
1136          [NSCharacterSet newlineCharacterSet]];
1137  return [wantFullInstallerVersion isEqualToString:_version];
1138}
1139
1140- (NSString*)tagSuffix {
1141  // Tag suffix components are not entirely arbitrary: all possible tag keys
1142  // must be present in the application's Info.plist, there must be
1143  // server-side agreement on the processing and meaning of tag suffix
1144  // components, and other code that manipulates tag values (such as the
1145  // Keystone update installation script) must be tag suffix-aware. To reduce
1146  // the number of tag suffix combinations that need to be listed in
1147  // Info.plist, tag suffix components should only be appended to the tag
1148  // suffix in ASCII sort order.
1149  NSString* tagSuffix = @"";
1150  if ([self wantsFullInstaller]) {
1151    tagSuffix = [tagSuffix stringByAppendingString:@"-full"];
1152  }
1153  return tagSuffix;
1154}
1155
1156@end  // @implementation KeystoneGlue
1157
1158#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
1159
1160namespace {
1161
1162std::string BrandCodeInternal() {
1163  KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
1164  NSString* brand_path = [keystone_glue brandFilePath];
1165
1166  if (![brand_path length])
1167    return std::string();
1168
1169  NSDictionary* dict =
1170      [NSDictionary dictionaryWithContentsOfFile:brand_path];
1171  NSString* brand_code = base::mac::ObjCCast<NSString>(dict[kBrandKey]);
1172  if (brand_code)
1173    return base::SysNSStringToUTF8(brand_code);
1174
1175  return std::string();
1176}
1177
1178}  // namespace
1179
1180#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
1181
1182namespace keystone_glue {
1183
1184std::string BrandCode() {
1185#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
1186  static base::NoDestructor<std::string> s_brand_code(BrandCodeInternal());
1187  return *s_brand_code;
1188#else
1189  return std::string();
1190#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
1191}
1192
1193bool KeystoneEnabled() {
1194  return [KeystoneGlue defaultKeystoneGlue] != nil;
1195}
1196
1197base::string16 CurrentlyInstalledVersion() {
1198  KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue];
1199  NSString* version = [keystoneGlue currentlyInstalledVersion];
1200  return base::SysNSStringToUTF16(version);
1201}
1202
1203}  // namespace keystone_glue
1204