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.
5 #import "chrome/browser/mac/keystone_glue.h"
13 #include "base/bind.h"
14 #include "base/location.h"
15 #include "base/logging.h"
16 #include "base/mac/authorization_util.h"
17 #include "base/mac/bundle_locations.h"
18 #include "base/mac/mac_logging.h"
19 #include "base/mac/mac_util.h"
20 #include "base/mac/scoped_nsautorelease_pool.h"
21 #include "base/mac/scoped_nsexception_enabler.h"
22 #include "base/memory/ref_counted.h"
23 #include "base/strings/sys_string_conversions.h"
24 #include "base/threading/worker_pool.h"
25 #include "build/build_config.h"
26 #import "chrome/browser/mac/keystone_registration.h"
27 #include "chrome/browser/mac/obsolete_system.h"
28 #include "chrome/common/chrome_constants.h"
29 #include "chrome/common/chrome_version_info.h"
30 #include "chrome/grit/chromium_strings.h"
31 #include "chrome/grit/generated_resources.h"
32 #include "ui/base/l10n/l10n_util.h"
33 #include "ui/base/l10n/l10n_util_mac.h"
37 namespace ksr = keystone_registration;
39 // Constants for the brand file (uses an external file so it can survive
40 // updates to Chrome.)
42 #if defined(GOOGLE_CHROME_BUILD)
43 #define kBrandFileName @"Google Chrome Brand.plist";
44 #elif defined(CHROMIUM_BUILD)
45 #define kBrandFileName @"Chromium Brand.plist";
47 #error Unknown branding
50 // These directories are hardcoded in Keystone promotion preflight and the
51 // Keystone install script, so NSSearchPathForDirectoriesInDomains isn't used
52 // since the scripts couldn't use anything like that.
53 NSString* kBrandUserFile = @"~/Library/Google/" kBrandFileName;
54 NSString* kBrandSystemFile = @"/Library/Google/" kBrandFileName;
56 NSString* UserBrandFilePath() {
57 return [kBrandUserFile stringByStandardizingPath];
59 NSString* SystemBrandFilePath() {
60 return [kBrandSystemFile stringByStandardizingPath];
63 // Adaptor for scheduling an Objective-C method call on a |WorkerPool|
65 class PerformBridge : public base::RefCountedThreadSafe<PerformBridge> {
68 // Call |sel| on |target| with |arg| in a WorkerPool thread.
69 // |target| and |arg| are retained, |arg| may be |nil|.
70 static void PostPerform(id target, SEL sel, id arg) {
74 scoped_refptr<PerformBridge> op = new PerformBridge(target, sel, arg);
75 base::WorkerPool::PostTask(
76 FROM_HERE, base::Bind(&PerformBridge::Run, op.get()), true);
79 // Convenience for the no-argument case.
80 static void PostPerform(id target, SEL sel) {
81 PostPerform(target, sel, nil);
85 // Allow RefCountedThreadSafe<> to delete.
86 friend class base::RefCountedThreadSafe<PerformBridge>;
88 PerformBridge(id target, SEL sel, id arg)
89 : target_([target retain]),
96 // Happens on a WorkerPool thread.
98 base::mac::ScopedNSAutoreleasePool pool;
99 [target_ performSelector:sel_ withObject:arg_];
102 base::scoped_nsobject<id> target_;
104 base::scoped_nsobject<id> arg_;
109 @interface KeystoneGlue (Private)
111 // Returns the path to the application's Info.plist file. This returns the
112 // outer application bundle's Info.plist, not the framework's Info.plist.
113 - (NSString*)appInfoPlistPath;
115 // Returns a dictionary containing parameters to be used for a KSRegistration
116 // -registerWithParameters: or -promoteWithParameters:authorization: call.
117 - (NSDictionary*)keystoneParameters;
119 // Called when Keystone registration completes.
120 - (void)registrationComplete:(NSNotification*)notification;
122 // Called periodically to announce activity by pinging the Keystone server.
123 - (void)markActive:(NSTimer*)timer;
125 // Called when an update check or update installation is complete. Posts the
126 // kAutoupdateStatusNotification notification to the default notification
128 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version;
130 // Returns the version of the currently-installed application on disk.
131 - (NSString*)currentlyInstalledVersion;
133 // These three methods are used to determine the version of the application
134 // currently installed on disk, compare that to the currently-running version,
135 // decide whether any updates have been installed, and call
136 // -updateStatus:version:.
138 // In order to check the version on disk, the installed application's
139 // Info.plist dictionary must be read; in order to see changes as updates are
140 // applied, the dictionary must be read each time, bypassing any caches such
141 // as the one that NSBundle might be maintaining. Reading files can be a
142 // blocking operation, and blocking operations are to be avoided on the main
143 // thread. I'm not quite sure what jank means, but I bet that a blocked main
144 // thread would cause some of it.
146 // -determineUpdateStatusAsync is called on the main thread to initiate the
147 // operation. It performs initial set-up work that must be done on the main
148 // thread and arranges for -determineUpdateStatus to be called on a work queue
149 // thread managed by WorkerPool.
150 // -determineUpdateStatus then reads the Info.plist, gets the version from the
151 // CFBundleShortVersionString key, and performs
152 // -determineUpdateStatusForVersion: on the main thread.
153 // -determineUpdateStatusForVersion: does the actual comparison of the version
154 // on disk with the running version and calls -updateStatus:version: with the
155 // results of its analysis.
156 - (void)determineUpdateStatusAsync;
157 - (void)determineUpdateStatus;
158 - (void)determineUpdateStatusForVersion:(NSString*)version;
160 // Returns YES if registration_ is definitely on a user ticket. If definitely
161 // on a system ticket, or uncertain of ticket type (due to an older version
162 // of Keystone being used), returns NO.
163 - (BOOL)isUserTicket;
165 // Returns YES if Keystone is definitely installed at the system level,
166 // determined by the presence of an executable ksadmin program at the expected
168 - (BOOL)isSystemKeystone;
170 // Returns YES if on a system ticket but system Keystone is not present.
171 // Returns NO otherwise. The "doomed" condition will result in the
172 // registration framework appearing to have registered Chrome, but no updates
173 // ever actually taking place.
174 - (BOOL)isSystemTicketDoomed;
176 // Called when ticket promotion completes.
177 - (void)promotionComplete:(NSNotification*)notification;
179 // Changes the application's ownership and permissions so that all files are
180 // owned by root:wheel and all files and directories are writable only by
181 // root, but readable and executable as needed by everyone.
182 // -changePermissionsForPromotionAsync is called on the main thread by
183 // -promotionComplete. That routine calls
184 // -changePermissionsForPromotionWithTool: on a work queue thread. When done,
185 // -changePermissionsForPromotionComplete is called on the main thread.
186 - (void)changePermissionsForPromotionAsync;
187 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath;
188 - (void)changePermissionsForPromotionComplete;
190 // Returns the brand file path to use for Keystone.
191 - (NSString*)brandFilePath;
193 // YES if no update installation has succeeded since a binary diff patch
194 // installation failed. This signals the need to attempt a full installer
195 // which does not depend on applying a patch to existing files.
196 - (BOOL)wantsFullInstaller;
198 // Returns an NSString* suitable for appending to a Chrome Keystone tag value
199 // or tag key. If the system has a 32-bit-only CPU, the tag suffix will
200 // contain the string "-32bit". If a full installer (as opposed to a binary
201 // diff/delta patch) is required, the tag suffix will contain the string
202 // "-full". If no special treatment is required, the tag suffix will be an
204 - (NSString*)tagSuffix;
206 @end // @interface KeystoneGlue (Private)
208 NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification";
209 NSString* const kAutoupdateStatusStatus = @"status";
210 NSString* const kAutoupdateStatusVersion = @"version";
214 NSString* const kChannelKey = @"KSChannelID";
215 NSString* const kBrandKey = @"KSBrandID";
216 NSString* const kVersionKey = @"KSVersion";
220 @implementation KeystoneGlue
222 + (id)defaultKeystoneGlue {
223 static bool sTriedCreatingDefaultKeystoneGlue = false;
224 // TODO(jrg): use base::SingletonObjC<KeystoneGlue>
225 static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked
227 if (!sTriedCreatingDefaultKeystoneGlue) {
228 sTriedCreatingDefaultKeystoneGlue = true;
230 sDefaultKeystoneGlue = [[KeystoneGlue alloc] init];
231 [sDefaultKeystoneGlue loadParameters];
232 if (![sDefaultKeystoneGlue loadKeystoneRegistration]) {
233 [sDefaultKeystoneGlue release];
234 sDefaultKeystoneGlue = nil;
237 return sDefaultKeystoneGlue;
241 if ((self = [super init])) {
242 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
244 [center addObserver:self
245 selector:@selector(registrationComplete:)
246 name:ksr::KSRegistrationDidCompleteNotification
249 [center addObserver:self
250 selector:@selector(promotionComplete:)
251 name:ksr::KSRegistrationPromotionDidCompleteNotification
254 [center addObserver:self
255 selector:@selector(checkForUpdateComplete:)
256 name:ksr::KSRegistrationCheckForUpdateNotification
259 [center addObserver:self
260 selector:@selector(installUpdateComplete:)
261 name:ksr::KSRegistrationStartUpdateNotification
269 [productID_ release];
274 [registration_ release];
275 [[NSNotificationCenter defaultCenter] removeObserver:self];
279 - (NSDictionary*)infoDictionary {
280 // Use base::mac::OuterBundle() to get the Chrome app's own bundle identifier
281 // and path, not the framework's. For auto-update, the application is
282 // what's significant here: it's used to locate the outermost part of the
283 // application for the existence checker and other operations that need to
284 // see the entire application bundle.
285 return [base::mac::OuterBundle() infoDictionary];
288 - (void)loadParameters {
289 NSBundle* appBundle = base::mac::OuterBundle();
290 NSDictionary* infoDictionary = [self infoDictionary];
292 NSString* productID = [infoDictionary objectForKey:@"KSProductID"];
293 if (productID == nil) {
294 productID = [appBundle bundleIdentifier];
297 NSString* appPath = [appBundle bundlePath];
298 NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"];
299 NSString* version = [infoDictionary objectForKey:kVersionKey];
301 if (!productID || !appPath || !url || !version) {
302 // If parameters required for Keystone are missing, don't use it.
306 NSString* channel = [infoDictionary objectForKey:kChannelKey];
307 // The stable channel has no tag. If updating to stable, remove the
308 // dev and beta tags since we've been "promoted".
310 channel = ksr::KSRegistrationRemoveExistingTag;
312 productID_ = [productID retain];
313 appPath_ = [appPath retain];
315 version_ = [version retain];
316 channel_ = [channel retain];
319 - (NSString*)brandFilePath {
320 DCHECK(version_ != nil) << "-loadParameters must be called first";
322 if (brandFileType_ == kBrandFileTypeNotDetermined) {
324 NSFileManager* fm = [NSFileManager defaultManager];
325 NSString* userBrandFile = UserBrandFilePath();
326 NSString* systemBrandFile = SystemBrandFilePath();
329 brandFileType_ = kBrandFileTypeNone;
331 // Only the stable channel has a brand code.
332 chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
334 if (channel == chrome::VersionInfo::CHANNEL_DEV ||
335 channel == chrome::VersionInfo::CHANNEL_BETA) {
337 // If on the dev or beta channel, this installation may have replaced
338 // an older system-level installation. Check for a user brand file and
339 // nuke it if present. Don't try to remove the system brand file, there
340 // wouldn't be any permission to do so.
342 // Don't do this on the canary channel. The canary can run side-by-side
343 // with another Google Chrome installation whose brand code, if any,
344 // should remain intact.
346 if ([fm fileExistsAtPath:userBrandFile]) {
347 [fm removeItemAtPath:userBrandFile error:NULL];
350 } else if (channel == chrome::VersionInfo::CHANNEL_STABLE) {
352 // If there is a system brand file, use it.
353 if ([fm fileExistsAtPath:systemBrandFile]) {
356 // Use the system file that is there.
357 brandFileType_ = kBrandFileTypeSystem;
359 // Clean up any old user level file.
360 if ([fm fileExistsAtPath:userBrandFile]) {
361 [fm removeItemAtPath:userBrandFile error:NULL];
367 NSDictionary* infoDictionary = [self infoDictionary];
368 NSString* appBundleBrandID = [infoDictionary objectForKey:kBrandKey];
370 NSString* storedBrandID = nil;
371 if ([fm fileExistsAtPath:userBrandFile]) {
372 NSDictionary* storedBrandDict =
373 [NSDictionary dictionaryWithContentsOfFile:userBrandFile];
374 storedBrandID = [storedBrandDict objectForKey:kBrandKey];
377 if ((appBundleBrandID != nil) &&
378 (![storedBrandID isEqualTo:appBundleBrandID])) {
379 // App and store don't match, update store and use it.
380 NSDictionary* storedBrandDict =
381 [NSDictionary dictionaryWithObject:appBundleBrandID
383 // If Keystone hasn't been installed yet, the location the brand file
384 // is written to won't exist, so manually create the directory.
385 NSString *userBrandFileDirectory =
386 [userBrandFile stringByDeletingLastPathComponent];
387 if (![fm fileExistsAtPath:userBrandFileDirectory]) {
388 if (![fm createDirectoryAtPath:userBrandFileDirectory
389 withIntermediateDirectories:YES
392 LOG(ERROR) << "Failed to create the directory for the brand file";
395 if ([storedBrandDict writeToFile:userBrandFile atomically:YES]) {
396 brandFileType_ = kBrandFileTypeUser;
398 } else if (storedBrandID) {
399 // Had stored brand, use it.
400 brandFileType_ = kBrandFileTypeUser;
407 NSString* result = nil;
408 switch (brandFileType_) {
409 case kBrandFileTypeUser:
410 result = UserBrandFilePath();
413 case kBrandFileTypeSystem:
414 result = SystemBrandFilePath();
417 case kBrandFileTypeNotDetermined:
420 case kBrandFileTypeNone:
429 - (BOOL)loadKeystoneRegistration {
430 if (!productID_ || !appPath_ || !url_ || !version_)
433 // Load the KeystoneRegistration framework bundle if present. It lives
434 // inside the framework, so use base::mac::FrameworkBundle();
436 [[base::mac::FrameworkBundle() privateFrameworksPath]
437 stringByAppendingPathComponent:@"KeystoneRegistration.framework"];
438 NSBundle* ksrBundle = [NSBundle bundleWithPath:ksrPath];
441 // Harness the KSRegistration class.
442 Class ksrClass = [ksrBundle classNamed:@"KSRegistration"];
443 KSRegistration* ksr = [ksrClass registrationWithProductID:productID_];
447 registration_ = [ksr retain];
451 - (NSString*)appInfoPlistPath {
452 // NSBundle ought to have a way to access this path directly, but it
454 return [[appPath_ stringByAppendingPathComponent:@"Contents"]
455 stringByAppendingPathComponent:@"Info.plist"];
458 - (NSDictionary*)keystoneParameters {
459 NSNumber* xcType = [NSNumber numberWithInt:ksr::kKSPathExistenceChecker];
460 NSNumber* preserveTTToken = [NSNumber numberWithBool:YES];
461 NSString* appInfoPlistPath = [self appInfoPlistPath];
462 NSString* brandKey = kBrandKey;
463 NSString* brandPath = [self brandFilePath];
465 if ([brandPath length] == 0) {
466 // Brand path and brand key must be cleared together or ksadmin seems
467 // to throw an error.
471 // Note that channel_ is permitted to be an empty string, but it must not be
474 NSString* tagSuffix = [self tagSuffix];
475 NSString* tagValue = [channel_ stringByAppendingString:tagSuffix];
476 NSString* tagKey = [kChannelKey stringByAppendingString:tagSuffix];
478 return [NSDictionary dictionaryWithObjectsAndKeys:
479 version_, ksr::KSRegistrationVersionKey,
480 appInfoPlistPath, ksr::KSRegistrationVersionPathKey,
481 kVersionKey, ksr::KSRegistrationVersionKeyKey,
482 xcType, ksr::KSRegistrationExistenceCheckerTypeKey,
483 appPath_, ksr::KSRegistrationExistenceCheckerStringKey,
484 url_, ksr::KSRegistrationServerURLStringKey,
485 preserveTTToken, ksr::KSRegistrationPreserveTrustedTesterTokenKey,
486 tagValue, ksr::KSRegistrationTagKey,
487 appInfoPlistPath, ksr::KSRegistrationTagPathKey,
488 tagKey, ksr::KSRegistrationTagKeyKey,
489 brandPath, ksr::KSRegistrationBrandPathKey,
490 brandKey, ksr::KSRegistrationBrandKeyKey,
494 - (void)registerWithKeystone {
495 [self updateStatus:kAutoupdateRegistering version:nil];
497 NSDictionary* parameters = [self keystoneParameters];
500 // TODO(shess): Allows Keystone to throw an exception when
501 // /usr/bin/python does not exist (really!).
502 // http://crbug.com/86221 and http://crbug.com/87931
503 base::mac::ScopedNSExceptionEnabler enabler;
504 result = [registration_ registerWithParameters:parameters];
507 [self updateStatus:kAutoupdateRegisterFailed version:nil];
511 // Upon completion, ksr::KSRegistrationDidCompleteNotification will be
512 // posted, and -registrationComplete: will be called.
514 // Mark an active RIGHT NOW; don't wait an hour for the first one.
515 [registration_ setActive];
517 // Set up hourly activity pings.
518 timer_ = [NSTimer scheduledTimerWithTimeInterval:60 * 60 // One hour
520 selector:@selector(markActive:)
521 userInfo:registration_
525 - (void)registrationComplete:(NSNotification*)notification {
526 NSDictionary* userInfo = [notification userInfo];
527 if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
528 if ([self isSystemTicketDoomed]) {
529 [self updateStatus:kAutoupdateNeedsPromotion version:nil];
531 [self updateStatus:kAutoupdateRegistered version:nil];
534 // Dump registration_?
535 [self updateStatus:kAutoupdateRegisterFailed version:nil];
543 - (void)markActive:(NSTimer*)timer {
544 KSRegistration* ksr = [timer userInfo];
548 - (void)checkForUpdate {
549 DCHECK(![self asyncOperationPending]);
551 if (!registration_) {
552 [self updateStatus:kAutoupdateCheckFailed version:nil];
556 [self updateStatus:kAutoupdateChecking version:nil];
558 // All checks from inside Chrome are considered user-initiated, because they
559 // only happen following a user action, such as visiting the about page.
560 // Non-user-initiated checks are the periodic checks automatically made by
561 // Keystone, which don't come through this code path (or even this process).
562 [registration_ checkForUpdateWasUserInitiated:YES];
564 // Upon completion, ksr::KSRegistrationCheckForUpdateNotification will be
565 // posted, and -checkForUpdateComplete: will be called.
568 - (void)checkForUpdateComplete:(NSNotification*)notification {
569 NSDictionary* userInfo = [notification userInfo];
571 if ([[userInfo objectForKey:ksr::KSRegistrationUpdateCheckErrorKey]
573 [self updateStatus:kAutoupdateCheckFailed version:nil];
574 } else if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
575 // If an update is known to be available, go straight to
576 // -updateStatus:version:. It doesn't matter what's currently on disk.
577 NSString* version = [userInfo objectForKey:ksr::KSRegistrationVersionKey];
578 [self updateStatus:kAutoupdateAvailable version:version];
580 // If no updates are available, check what's on disk, because an update
581 // may have already been installed. This check happens on another thread,
582 // and -updateStatus:version: will be called on the main thread when done.
583 [self determineUpdateStatusAsync];
587 - (void)installUpdate {
588 DCHECK(![self asyncOperationPending]);
590 if (!registration_) {
591 [self updateStatus:kAutoupdateInstallFailed version:nil];
595 [self updateStatus:kAutoupdateInstalling version:nil];
597 [registration_ startUpdate];
599 // Upon completion, ksr::KSRegistrationStartUpdateNotification will be
600 // posted, and -installUpdateComplete: will be called.
603 - (void)installUpdateComplete:(NSNotification*)notification {
604 NSDictionary* userInfo = [notification userInfo];
606 // http://crbug.com/160308 and b/7517358: when using system Keystone and on
607 // a user ticket, KSUpdateCheckSuccessfulKey will be NO even when an update
608 // was installed correctly, so don't check it. It should be redudnant when
609 // KSUpdateCheckSuccessfullyInstalledKey is checked.
610 if (![[userInfo objectForKey:ksr::KSUpdateCheckSuccessfullyInstalledKey]
612 [self updateStatus:kAutoupdateInstallFailed version:nil];
614 updateSuccessfullyInstalled_ = YES;
616 // Nothing in the notification dictionary reports the version that was
617 // installed. Figure it out based on what's on disk.
618 [self determineUpdateStatusAsync];
622 - (NSString*)currentlyInstalledVersion {
623 NSString* appInfoPlistPath = [self appInfoPlistPath];
624 NSDictionary* infoPlist =
625 [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath];
626 return [infoPlist objectForKey:@"CFBundleShortVersionString"];
629 // Runs on the main thread.
630 - (void)determineUpdateStatusAsync {
631 DCHECK([NSThread isMainThread]);
633 PerformBridge::PostPerform(self, @selector(determineUpdateStatus));
636 // Runs on a thread managed by WorkerPool.
637 - (void)determineUpdateStatus {
638 DCHECK(![NSThread isMainThread]);
640 NSString* version = [self currentlyInstalledVersion];
642 [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:)
647 // Runs on the main thread.
648 - (void)determineUpdateStatusForVersion:(NSString*)version {
649 DCHECK([NSThread isMainThread]);
651 AutoupdateStatus status;
652 if (updateSuccessfullyInstalled_) {
653 // If an update was successfully installed and this object saw it happen,
654 // then don't even bother comparing versions.
655 status = kAutoupdateInstalled;
657 NSString* currentVersion =
658 [NSString stringWithUTF8String:chrome::kChromeVersion];
660 // If the version on disk could not be determined, assume that
661 // whatever's running is current.
662 version = currentVersion;
663 status = kAutoupdateCurrent;
664 } else if ([version isEqualToString:currentVersion]) {
665 status = kAutoupdateCurrent;
667 // If the version on disk doesn't match what's currently running, an
668 // update must have been applied in the background, without this app's
669 // direct participation. Leave updateSuccessfullyInstalled_ alone
670 // because there's no direct knowledge of what actually happened.
671 status = kAutoupdateInstalled;
675 [self updateStatus:status version:version];
678 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version {
679 NSNumber* statusNumber = [NSNumber numberWithInt:status];
680 NSMutableDictionary* dictionary =
681 [NSMutableDictionary dictionaryWithObject:statusNumber
682 forKey:kAutoupdateStatusStatus];
684 [dictionary setObject:version forKey:kAutoupdateStatusVersion];
687 NSNotification* notification =
688 [NSNotification notificationWithName:kAutoupdateStatusNotification
690 userInfo:dictionary];
691 recentNotification_.reset([notification retain]);
693 [[NSNotificationCenter defaultCenter] postNotification:notification];
696 - (NSNotification*)recentNotification {
697 return [[recentNotification_ retain] autorelease];
700 - (AutoupdateStatus)recentStatus {
701 NSDictionary* dictionary = [recentNotification_ userInfo];
702 return static_cast<AutoupdateStatus>(
703 [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]);
706 - (BOOL)asyncOperationPending {
707 AutoupdateStatus status = [self recentStatus];
708 return status == kAutoupdateRegistering ||
709 status == kAutoupdateChecking ||
710 status == kAutoupdateInstalling ||
711 status == kAutoupdatePromoting;
714 - (BOOL)isUserTicket {
715 return [registration_ ticketType] == ksr::kKSRegistrationUserTicket;
718 - (BOOL)isSystemKeystone {
720 if (stat("/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
721 "Contents/MacOS/ksadmin",
726 if (!(statbuf.st_mode & S_IXUSR)) {
733 - (BOOL)isSystemTicketDoomed {
734 BOOL isSystemTicket = ![self isUserTicket];
735 return isSystemTicket && ![self isSystemKeystone];
738 - (BOOL)isOnReadOnlyFilesystem {
739 const char* appPathC = [appPath_ fileSystemRepresentation];
740 struct statfs statfsBuf;
742 if (statfs(appPathC, &statfsBuf) != 0) {
743 PLOG(ERROR) << "statfs";
744 // Be optimistic about the filesystem's writability.
748 return (statfsBuf.f_flags & MNT_RDONLY) != 0;
751 - (BOOL)needsPromotion {
752 // Don't promote when on a read-only filesystem.
753 if ([self isOnReadOnlyFilesystem]) {
757 // Promotion is required when a system ticket is present but system Keystone
759 if ([self isSystemTicketDoomed]) {
763 // If on a system ticket and system Keystone is present, promotion is not
765 if (![self isUserTicket]) {
769 // Check the outermost bundle directory, the main executable path, and the
770 // framework directory. It may be enough to just look at the outermost
771 // bundle directory, but checking an interior file and directory can be
772 // helpful in case permissions are set differently only on the outermost
773 // directory. An interior file and directory are both checked because some
774 // file operations, such as Snow Leopard's Finder's copy operation when
775 // authenticating, may actually result in different ownership being applied
776 // to files and directories.
777 NSFileManager* fileManager = [NSFileManager defaultManager];
778 NSString* executablePath = [base::mac::OuterBundle() executablePath];
779 NSString* frameworkPath = [base::mac::FrameworkBundle() bundlePath];
780 return ![fileManager isWritableFileAtPath:appPath_] ||
781 ![fileManager isWritableFileAtPath:executablePath] ||
782 ![fileManager isWritableFileAtPath:frameworkPath];
785 - (BOOL)wantsPromotion {
786 if ([self needsPromotion]) {
790 // These are the same unpromotable cases as in -needsPromotion.
791 if ([self isOnReadOnlyFilesystem] || ![self isUserTicket]) {
795 return [appPath_ hasPrefix:@"/Applications/"];
798 - (void)promoteTicket {
799 if ([self asyncOperationPending] || ![self wantsPromotion]) {
800 // Because there are multiple ways of reaching promoteTicket that might
801 // not lock each other out, it may be possible to arrive here while an
802 // asynchronous operation is pending, or even after promotion has already
803 // occurred. Just quietly return without doing anything.
807 NSString* prompt = l10n_util::GetNSStringFWithFixup(
808 IDS_PROMOTE_AUTHENTICATION_PROMPT,
809 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
810 base::mac::ScopedAuthorizationRef authorization(
811 base::mac::AuthorizationCreateToRunAsRoot(
812 base::mac::NSToCFCast(prompt)));
813 if (!authorization.get()) {
817 [self promoteTicketWithAuthorization:authorization.release() synchronous:NO];
820 - (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg
821 synchronous:(BOOL)synchronous {
822 base::mac::ScopedAuthorizationRef authorization(authorization_arg);
823 authorization_arg = NULL;
825 if ([self asyncOperationPending]) {
826 // Starting a synchronous operation while an asynchronous one is pending
830 if (!synchronous && ![self wantsPromotion]) {
831 // If operating synchronously, the call came from the installer, which
832 // means that a system ticket is required. Otherwise, only allow
833 // promotion if it's wanted.
837 synchronousPromotion_ = synchronous;
839 [self updateStatus:kAutoupdatePromoting version:nil];
841 // TODO(mark): Remove when able!
843 // keystone_promote_preflight will copy the current brand information out to
844 // the system level so all users can share the data as part of the ticket
847 // It will also ensure that the Keystone system ticket store is in a usable
848 // state for all users on the system. Ideally, Keystone's installer or
849 // another part of Keystone would handle this. The underlying problem is
850 // http://b/2285921, and it causes http://b/2289908, which this workaround
853 // This is run synchronously, which isn't optimal, but
854 // -[KSRegistration promoteWithParameters:authorization:] is currently
855 // synchronous too, and this operation needs to happen before that one.
857 // TODO(mark): Make asynchronous. That only makes sense if the promotion
858 // operation itself is asynchronous too. http://b/2290009. Hopefully,
859 // the Keystone promotion code will just be changed to do what preflight
860 // now does, and then the preflight script can be removed instead.
861 // However, preflight operation (and promotion) should only be asynchronous
862 // if the synchronous parameter is NO.
863 NSString* preflightPath =
864 [base::mac::FrameworkBundle()
865 pathForResource:@"keystone_promote_preflight"
867 const char* preflightPathC = [preflightPath fileSystemRepresentation];
868 const char* userBrandFile = NULL;
869 const char* systemBrandFile = NULL;
870 if (brandFileType_ == kBrandFileTypeUser) {
871 // Running with user level brand file, promote to the system level.
872 userBrandFile = [UserBrandFilePath() fileSystemRepresentation];
873 systemBrandFile = [SystemBrandFilePath() fileSystemRepresentation];
875 const char* arguments[] = {userBrandFile, systemBrandFile, NULL};
878 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
881 kAuthorizationFlagDefaults,
885 if (status != errAuthorizationSuccess) {
886 OSSTATUS_LOG(ERROR, status)
887 << "AuthorizationExecuteWithPrivileges preflight";
888 [self updateStatus:kAutoupdatePromoteFailed version:nil];
891 if (exit_status != 0) {
892 LOG(ERROR) << "keystone_promote_preflight status " << exit_status;
893 [self updateStatus:kAutoupdatePromoteFailed version:nil];
897 // Hang on to the AuthorizationRef so that it can be used once promotion is
898 // complete. Do this before asking Keystone to promote the ticket, because
899 // -promotionComplete: may be called from inside the Keystone promotion
901 authorization_.swap(authorization);
903 NSDictionary* parameters = [self keystoneParameters];
905 // If the brand file is user level, update parameters to point to the new
906 // system level file during promotion.
907 if (brandFileType_ == kBrandFileTypeUser) {
908 NSMutableDictionary* temp_parameters =
909 [[parameters mutableCopy] autorelease];
910 [temp_parameters setObject:SystemBrandFilePath()
911 forKey:ksr::KSRegistrationBrandPathKey];
912 parameters = temp_parameters;
915 if (![registration_ promoteWithParameters:parameters
916 authorization:authorization_]) {
917 [self updateStatus:kAutoupdatePromoteFailed version:nil];
918 authorization_.reset();
922 // Upon completion, ksr::KSRegistrationPromotionDidCompleteNotification will
923 // be posted, and -promotionComplete: will be called.
925 // If synchronous, see to it that this happens immediately. Give it a
926 // 10-second deadline.
928 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
932 - (void)promotionComplete:(NSNotification*)notification {
933 NSDictionary* userInfo = [notification userInfo];
934 if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
935 if (synchronousPromotion_) {
936 // Short-circuit: if performing a synchronous promotion, the promotion
937 // came from the installer, which already set the permissions properly.
938 // Rather than run a duplicate permission-changing operation, jump
939 // straight to "done."
940 [self changePermissionsForPromotionComplete];
942 [self changePermissionsForPromotionAsync];
945 authorization_.reset();
946 [self updateStatus:kAutoupdatePromoteFailed version:nil];
949 if (synchronousPromotion_) {
950 // The run loop doesn't need to wait for this any longer.
951 CFRunLoopRef runLoop = CFRunLoopGetCurrent();
952 CFRunLoopStop(runLoop);
953 CFRunLoopWakeUp(runLoop);
957 - (void)changePermissionsForPromotionAsync {
958 // NSBundle is not documented as being thread-safe. Do NSBundle operations
959 // on the main thread before jumping over to a WorkerPool-managed
960 // thread to run the tool.
961 DCHECK([NSThread isMainThread]);
963 SEL selector = @selector(changePermissionsForPromotionWithTool:);
965 [base::mac::FrameworkBundle()
966 pathForResource:@"keystone_promote_postflight"
969 PerformBridge::PostPerform(self, selector, toolPath);
972 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath {
973 const char* toolPathC = [toolPath fileSystemRepresentation];
975 const char* appPathC = [appPath_ fileSystemRepresentation];
976 const char* arguments[] = {appPathC, NULL};
979 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
982 kAuthorizationFlagDefaults,
986 if (status != errAuthorizationSuccess) {
987 OSSTATUS_LOG(ERROR, status)
988 << "AuthorizationExecuteWithPrivileges postflight";
989 } else if (exit_status != 0) {
990 LOG(ERROR) << "keystone_promote_postflight status " << exit_status;
993 SEL selector = @selector(changePermissionsForPromotionComplete);
994 [self performSelectorOnMainThread:selector
999 - (void)changePermissionsForPromotionComplete {
1000 authorization_.reset();
1002 [self updateStatus:kAutoupdatePromoted version:nil];
1005 - (void)setAppPath:(NSString*)appPath {
1006 if (appPath != appPath_) {
1008 appPath_ = [appPath copy];
1012 - (BOOL)wantsFullInstaller {
1013 // It's difficult to check the tag prior to Keystone registration, and
1014 // performing registration replaces the tag. keystone_install.sh
1015 // communicates a need for a full installer with Chrome in this file,
1016 // .want_full_installer.
1017 NSString* wantFullInstallerPath =
1018 [appPath_ stringByAppendingPathComponent:@".want_full_installer"];
1019 NSString* wantFullInstallerContents =
1020 [NSString stringWithContentsOfFile:wantFullInstallerPath
1021 encoding:NSUTF8StringEncoding
1023 if (!wantFullInstallerContents) {
1027 NSString* wantFullInstallerVersion =
1028 [wantFullInstallerContents stringByTrimmingCharactersInSet:
1029 [NSCharacterSet newlineCharacterSet]];
1030 return [wantFullInstallerVersion isEqualToString:version_];
1033 - (NSString*)tagSuffix {
1034 // Tag suffix components are not entirely arbitrary: all possible tag keys
1035 // must be present in the application's Info.plist, there must be
1036 // server-side agreement on the processing and meaning of tag suffix
1037 // components, and other code that manipulates tag values (such as the
1038 // Keystone update installation script) must be tag suffix-aware. To reduce
1039 // the number of tag suffix combinations that need to be listed in
1040 // Info.plist, tag suffix components should only be appended to the tag
1041 // suffix in ASCII sort order.
1042 NSString* tagSuffix = @"";
1043 if (ObsoleteSystemMac::Has32BitOnlyCPU()) {
1044 tagSuffix = [tagSuffix stringByAppendingString:@"-32bit"];
1046 if ([self wantsFullInstaller]) {
1047 tagSuffix = [tagSuffix stringByAppendingString:@"-full"];
1052 @end // @implementation KeystoneGlue
1056 std::string BrandCodeInternal() {
1057 KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
1058 NSString* brand_path = [keystone_glue brandFilePath];
1060 if (![brand_path length])
1061 return std::string();
1063 NSDictionary* dict =
1064 [NSDictionary dictionaryWithContentsOfFile:brand_path];
1065 NSString* brand_code =
1066 base::mac::ObjCCast<NSString>([dict objectForKey:kBrandKey]);
1068 return [brand_code UTF8String];
1070 return std::string();
1075 namespace keystone_glue {
1077 std::string BrandCode() {
1078 // |s_brand_code| is leaked.
1079 static std::string* s_brand_code = new std::string(BrandCodeInternal());
1080 return *s_brand_code;
1083 bool KeystoneEnabled() {
1084 return [KeystoneGlue defaultKeystoneGlue] != nil;
1087 base::string16 CurrentlyInstalledVersion() {
1088 KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue];
1089 NSString* version = [keystoneGlue currentlyInstalledVersion];
1090 return base::SysNSStringToUTF16(version);
1093 } // namespace keystone_glue