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/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 #import "chrome/browser/mac/keystone_registration.h" 26 #include "chrome/common/chrome_constants.h" 27 #include "chrome/common/chrome_version_info.h" 28 #include "grit/chromium_strings.h" 29 #include "grit/generated_resources.h" 30 #include "ui/base/l10n/l10n_util.h" 31 #include "ui/base/l10n/l10n_util_mac.h" 32 33 namespace { 34 35 namespace ksr = keystone_registration; 36 37 // Constants for the brand file (uses an external file so it can survive 38 // updates to Chrome.) 39 40 #if defined(GOOGLE_CHROME_BUILD) 41 #define kBrandFileName @"Google Chrome Brand.plist"; 42 #elif defined(CHROMIUM_BUILD) 43 #define kBrandFileName @"Chromium Brand.plist"; 44 #else 45 #error Unknown branding 46 #endif 47 48 // These directories are hardcoded in Keystone promotion preflight and the 49 // Keystone install script, so NSSearchPathForDirectoriesInDomains isn't used 50 // since the scripts couldn't use anything like that. 51 NSString* kBrandUserFile = @"~/Library/Google/" kBrandFileName; 52 NSString* kBrandSystemFile = @"/Library/Google/" kBrandFileName; 53 54 NSString* UserBrandFilePath() { 55 return [kBrandUserFile stringByStandardizingPath]; 56 } 57 NSString* SystemBrandFilePath() { 58 return [kBrandSystemFile stringByStandardizingPath]; 59 } 60 61 // Adaptor for scheduling an Objective-C method call on a |WorkerPool| 62 // thread. 63 class PerformBridge : public base::RefCountedThreadSafe<PerformBridge> { 64 public: 65 66 // Call |sel| on |target| with |arg| in a WorkerPool thread. 67 // |target| and |arg| are retained, |arg| may be |nil|. 68 static void PostPerform(id target, SEL sel, id arg) { 69 DCHECK(target); 70 DCHECK(sel); 71 72 scoped_refptr<PerformBridge> op = new PerformBridge(target, sel, arg); 73 base::WorkerPool::PostTask( 74 FROM_HERE, base::Bind(&PerformBridge::Run, op.get()), true); 75 } 76 77 // Convenience for the no-argument case. 78 static void PostPerform(id target, SEL sel) { 79 PostPerform(target, sel, nil); 80 } 81 82 private: 83 // Allow RefCountedThreadSafe<> to delete. 84 friend class base::RefCountedThreadSafe<PerformBridge>; 85 86 PerformBridge(id target, SEL sel, id arg) 87 : target_([target retain]), 88 sel_(sel), 89 arg_([arg retain]) { 90 } 91 92 ~PerformBridge() {} 93 94 // Happens on a WorkerPool thread. 95 void Run() { 96 base::mac::ScopedNSAutoreleasePool pool; 97 [target_ performSelector:sel_ withObject:arg_]; 98 } 99 100 base::scoped_nsobject<id> target_; 101 SEL sel_; 102 base::scoped_nsobject<id> arg_; 103 }; 104 105 } // namespace 106 107 @interface KeystoneGlue (Private) 108 109 // Returns the path to the application's Info.plist file. This returns the 110 // outer application bundle's Info.plist, not the framework's Info.plist. 111 - (NSString*)appInfoPlistPath; 112 113 // Returns a dictionary containing parameters to be used for a KSRegistration 114 // -registerWithParameters: or -promoteWithParameters:authorization: call. 115 - (NSDictionary*)keystoneParameters; 116 117 // Called when Keystone registration completes. 118 - (void)registrationComplete:(NSNotification*)notification; 119 120 // Called periodically to announce activity by pinging the Keystone server. 121 - (void)markActive:(NSTimer*)timer; 122 123 // Called when an update check or update installation is complete. Posts the 124 // kAutoupdateStatusNotification notification to the default notification 125 // center. 126 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version; 127 128 // Returns the version of the currently-installed application on disk. 129 - (NSString*)currentlyInstalledVersion; 130 131 // These three methods are used to determine the version of the application 132 // currently installed on disk, compare that to the currently-running version, 133 // decide whether any updates have been installed, and call 134 // -updateStatus:version:. 135 // 136 // In order to check the version on disk, the installed application's 137 // Info.plist dictionary must be read; in order to see changes as updates are 138 // applied, the dictionary must be read each time, bypassing any caches such 139 // as the one that NSBundle might be maintaining. Reading files can be a 140 // blocking operation, and blocking operations are to be avoided on the main 141 // thread. I'm not quite sure what jank means, but I bet that a blocked main 142 // thread would cause some of it. 143 // 144 // -determineUpdateStatusAsync is called on the main thread to initiate the 145 // operation. It performs initial set-up work that must be done on the main 146 // thread and arranges for -determineUpdateStatus to be called on a work queue 147 // thread managed by WorkerPool. 148 // -determineUpdateStatus then reads the Info.plist, gets the version from the 149 // CFBundleShortVersionString key, and performs 150 // -determineUpdateStatusForVersion: on the main thread. 151 // -determineUpdateStatusForVersion: does the actual comparison of the version 152 // on disk with the running version and calls -updateStatus:version: with the 153 // results of its analysis. 154 - (void)determineUpdateStatusAsync; 155 - (void)determineUpdateStatus; 156 - (void)determineUpdateStatusForVersion:(NSString*)version; 157 158 // Returns YES if registration_ is definitely on a user ticket. If definitely 159 // on a system ticket, or uncertain of ticket type (due to an older version 160 // of Keystone being used), returns NO. 161 - (BOOL)isUserTicket; 162 163 // Returns YES if Keystone is definitely installed at the system level, 164 // determined by the presence of an executable ksadmin program at the expected 165 // system location. 166 - (BOOL)isSystemKeystone; 167 168 // Returns YES if on a system ticket but system Keystone is not present. 169 // Returns NO otherwise. The "doomed" condition will result in the 170 // registration framework appearing to have registered Chrome, but no updates 171 // ever actually taking place. 172 - (BOOL)isSystemTicketDoomed; 173 174 // Called when ticket promotion completes. 175 - (void)promotionComplete:(NSNotification*)notification; 176 177 // Changes the application's ownership and permissions so that all files are 178 // owned by root:wheel and all files and directories are writable only by 179 // root, but readable and executable as needed by everyone. 180 // -changePermissionsForPromotionAsync is called on the main thread by 181 // -promotionComplete. That routine calls 182 // -changePermissionsForPromotionWithTool: on a work queue thread. When done, 183 // -changePermissionsForPromotionComplete is called on the main thread. 184 - (void)changePermissionsForPromotionAsync; 185 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath; 186 - (void)changePermissionsForPromotionComplete; 187 188 // Returns the brand file path to use for Keystone. 189 - (NSString*)brandFilePath; 190 191 @end // @interface KeystoneGlue (Private) 192 193 NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification"; 194 NSString* const kAutoupdateStatusStatus = @"status"; 195 NSString* const kAutoupdateStatusVersion = @"version"; 196 197 namespace { 198 199 NSString* const kChannelKey = @"KSChannelID"; 200 NSString* const kBrandKey = @"KSBrandID"; 201 NSString* const kVersionKey = @"KSVersion"; 202 203 } // namespace 204 205 @implementation KeystoneGlue 206 207 + (id)defaultKeystoneGlue { 208 static bool sTriedCreatingDefaultKeystoneGlue = false; 209 // TODO(jrg): use base::SingletonObjC<KeystoneGlue> 210 static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked 211 212 if (!sTriedCreatingDefaultKeystoneGlue) { 213 sTriedCreatingDefaultKeystoneGlue = true; 214 215 sDefaultKeystoneGlue = [[KeystoneGlue alloc] init]; 216 [sDefaultKeystoneGlue loadParameters]; 217 if (![sDefaultKeystoneGlue loadKeystoneRegistration]) { 218 [sDefaultKeystoneGlue release]; 219 sDefaultKeystoneGlue = nil; 220 } 221 } 222 return sDefaultKeystoneGlue; 223 } 224 225 - (id)init { 226 if ((self = [super init])) { 227 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 228 229 [center addObserver:self 230 selector:@selector(registrationComplete:) 231 name:ksr::KSRegistrationDidCompleteNotification 232 object:nil]; 233 234 [center addObserver:self 235 selector:@selector(promotionComplete:) 236 name:ksr::KSRegistrationPromotionDidCompleteNotification 237 object:nil]; 238 239 [center addObserver:self 240 selector:@selector(checkForUpdateComplete:) 241 name:ksr::KSRegistrationCheckForUpdateNotification 242 object:nil]; 243 244 [center addObserver:self 245 selector:@selector(installUpdateComplete:) 246 name:ksr::KSRegistrationStartUpdateNotification 247 object:nil]; 248 } 249 250 return self; 251 } 252 253 - (void)dealloc { 254 [productID_ release]; 255 [appPath_ release]; 256 [url_ release]; 257 [version_ release]; 258 [channel_ release]; 259 [registration_ release]; 260 [[NSNotificationCenter defaultCenter] removeObserver:self]; 261 [super dealloc]; 262 } 263 264 - (NSDictionary*)infoDictionary { 265 // Use base::mac::OuterBundle() to get the Chrome app's own bundle identifier 266 // and path, not the framework's. For auto-update, the application is 267 // what's significant here: it's used to locate the outermost part of the 268 // application for the existence checker and other operations that need to 269 // see the entire application bundle. 270 return [base::mac::OuterBundle() infoDictionary]; 271 } 272 273 - (void)loadParameters { 274 NSBundle* appBundle = base::mac::OuterBundle(); 275 NSDictionary* infoDictionary = [self infoDictionary]; 276 277 NSString* productID = [infoDictionary objectForKey:@"KSProductID"]; 278 if (productID == nil) { 279 productID = [appBundle bundleIdentifier]; 280 } 281 282 NSString* appPath = [appBundle bundlePath]; 283 NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"]; 284 NSString* version = [infoDictionary objectForKey:kVersionKey]; 285 286 if (!productID || !appPath || !url || !version) { 287 // If parameters required for Keystone are missing, don't use it. 288 return; 289 } 290 291 NSString* channel = [infoDictionary objectForKey:kChannelKey]; 292 // The stable channel has no tag. If updating to stable, remove the 293 // dev and beta tags since we've been "promoted". 294 if (channel == nil) 295 channel = ksr::KSRegistrationRemoveExistingTag; 296 297 productID_ = [productID retain]; 298 appPath_ = [appPath retain]; 299 url_ = [url retain]; 300 version_ = [version retain]; 301 channel_ = [channel retain]; 302 } 303 304 - (NSString*)brandFilePath { 305 DCHECK(version_ != nil) << "-loadParameters must be called first"; 306 307 if (brandFileType_ == kBrandFileTypeNotDetermined) { 308 309 NSFileManager* fm = [NSFileManager defaultManager]; 310 NSString* userBrandFile = UserBrandFilePath(); 311 NSString* systemBrandFile = SystemBrandFilePath(); 312 313 // Default to none. 314 brandFileType_ = kBrandFileTypeNone; 315 316 // Only the stable channel has a brand code. 317 chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel(); 318 319 if (channel == chrome::VersionInfo::CHANNEL_DEV || 320 channel == chrome::VersionInfo::CHANNEL_BETA) { 321 322 // If on the dev or beta channel, this installation may have replaced 323 // an older system-level installation. Check for a user brand file and 324 // nuke it if present. Don't try to remove the system brand file, there 325 // wouldn't be any permission to do so. 326 // 327 // Don't do this on the canary channel. The canary can run side-by-side 328 // with another Google Chrome installation whose brand code, if any, 329 // should remain intact. 330 331 if ([fm fileExistsAtPath:userBrandFile]) { 332 [fm removeItemAtPath:userBrandFile error:NULL]; 333 } 334 335 } else if (channel == chrome::VersionInfo::CHANNEL_STABLE) { 336 337 // If there is a system brand file, use it. 338 if ([fm fileExistsAtPath:systemBrandFile]) { 339 // System 340 341 // Use the system file that is there. 342 brandFileType_ = kBrandFileTypeSystem; 343 344 // Clean up any old user level file. 345 if ([fm fileExistsAtPath:userBrandFile]) { 346 [fm removeItemAtPath:userBrandFile error:NULL]; 347 } 348 349 } else { 350 // User 351 352 NSDictionary* infoDictionary = [self infoDictionary]; 353 NSString* appBundleBrandID = [infoDictionary objectForKey:kBrandKey]; 354 355 NSString* storedBrandID = nil; 356 if ([fm fileExistsAtPath:userBrandFile]) { 357 NSDictionary* storedBrandDict = 358 [NSDictionary dictionaryWithContentsOfFile:userBrandFile]; 359 storedBrandID = [storedBrandDict objectForKey:kBrandKey]; 360 } 361 362 if ((appBundleBrandID != nil) && 363 (![storedBrandID isEqualTo:appBundleBrandID])) { 364 // App and store don't match, update store and use it. 365 NSDictionary* storedBrandDict = 366 [NSDictionary dictionaryWithObject:appBundleBrandID 367 forKey:kBrandKey]; 368 // If Keystone hasn't been installed yet, the location the brand file 369 // is written to won't exist, so manually create the directory. 370 NSString *userBrandFileDirectory = 371 [userBrandFile stringByDeletingLastPathComponent]; 372 if (![fm fileExistsAtPath:userBrandFileDirectory]) { 373 if (![fm createDirectoryAtPath:userBrandFileDirectory 374 withIntermediateDirectories:YES 375 attributes:nil 376 error:NULL]) { 377 LOG(ERROR) << "Failed to create the directory for the brand file"; 378 } 379 } 380 if ([storedBrandDict writeToFile:userBrandFile atomically:YES]) { 381 brandFileType_ = kBrandFileTypeUser; 382 } 383 } else if (storedBrandID) { 384 // Had stored brand, use it. 385 brandFileType_ = kBrandFileTypeUser; 386 } 387 } 388 } 389 390 } 391 392 NSString* result = nil; 393 switch (brandFileType_) { 394 case kBrandFileTypeUser: 395 result = UserBrandFilePath(); 396 break; 397 398 case kBrandFileTypeSystem: 399 result = SystemBrandFilePath(); 400 break; 401 402 case kBrandFileTypeNotDetermined: 403 NOTIMPLEMENTED(); 404 // Fall through 405 case kBrandFileTypeNone: 406 // Clear the value. 407 result = @""; 408 break; 409 410 } 411 return result; 412 } 413 414 - (BOOL)loadKeystoneRegistration { 415 if (!productID_ || !appPath_ || !url_ || !version_) 416 return NO; 417 418 // Load the KeystoneRegistration framework bundle if present. It lives 419 // inside the framework, so use base::mac::FrameworkBundle(); 420 NSString* ksrPath = 421 [[base::mac::FrameworkBundle() privateFrameworksPath] 422 stringByAppendingPathComponent:@"KeystoneRegistration.framework"]; 423 NSBundle* ksrBundle = [NSBundle bundleWithPath:ksrPath]; 424 [ksrBundle load]; 425 426 // Harness the KSRegistration class. 427 Class ksrClass = [ksrBundle classNamed:@"KSRegistration"]; 428 KSRegistration* ksr = [ksrClass registrationWithProductID:productID_]; 429 if (!ksr) 430 return NO; 431 432 registration_ = [ksr retain]; 433 return YES; 434 } 435 436 - (NSString*)appInfoPlistPath { 437 // NSBundle ought to have a way to access this path directly, but it 438 // doesn't. 439 return [[appPath_ stringByAppendingPathComponent:@"Contents"] 440 stringByAppendingPathComponent:@"Info.plist"]; 441 } 442 443 - (NSDictionary*)keystoneParameters { 444 NSNumber* xcType = [NSNumber numberWithInt:ksr::kKSPathExistenceChecker]; 445 NSNumber* preserveTTToken = [NSNumber numberWithBool:YES]; 446 NSString* appInfoPlistPath = [self appInfoPlistPath]; 447 NSString* brandKey = kBrandKey; 448 NSString* brandPath = [self brandFilePath]; 449 450 if ([brandPath length] == 0) { 451 // Brand path and brand key must be cleared together or ksadmin seems 452 // to throw an error. 453 brandKey = @""; 454 } 455 456 return [NSDictionary dictionaryWithObjectsAndKeys: 457 version_, ksr::KSRegistrationVersionKey, 458 appInfoPlistPath, ksr::KSRegistrationVersionPathKey, 459 kVersionKey, ksr::KSRegistrationVersionKeyKey, 460 xcType, ksr::KSRegistrationExistenceCheckerTypeKey, 461 appPath_, ksr::KSRegistrationExistenceCheckerStringKey, 462 url_, ksr::KSRegistrationServerURLStringKey, 463 preserveTTToken, ksr::KSRegistrationPreserveTrustedTesterTokenKey, 464 channel_, ksr::KSRegistrationTagKey, 465 appInfoPlistPath, ksr::KSRegistrationTagPathKey, 466 kChannelKey, ksr::KSRegistrationTagKeyKey, 467 brandPath, ksr::KSRegistrationBrandPathKey, 468 brandKey, ksr::KSRegistrationBrandKeyKey, 469 nil]; 470 } 471 472 - (void)registerWithKeystone { 473 [self updateStatus:kAutoupdateRegistering version:nil]; 474 475 NSDictionary* parameters = [self keystoneParameters]; 476 BOOL result; 477 { 478 // TODO(shess): Allows Keystone to throw an exception when 479 // /usr/bin/python does not exist (really!). 480 // http://crbug.com/86221 and http://crbug.com/87931 481 base::mac::ScopedNSExceptionEnabler enabler; 482 result = [registration_ registerWithParameters:parameters]; 483 } 484 if (!result) { 485 [self updateStatus:kAutoupdateRegisterFailed version:nil]; 486 return; 487 } 488 489 // Upon completion, ksr::KSRegistrationDidCompleteNotification will be 490 // posted, and -registrationComplete: will be called. 491 492 // Mark an active RIGHT NOW; don't wait an hour for the first one. 493 [registration_ setActive]; 494 495 // Set up hourly activity pings. 496 timer_ = [NSTimer scheduledTimerWithTimeInterval:60 * 60 // One hour 497 target:self 498 selector:@selector(markActive:) 499 userInfo:registration_ 500 repeats:YES]; 501 } 502 503 - (void)registrationComplete:(NSNotification*)notification { 504 NSDictionary* userInfo = [notification userInfo]; 505 if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) { 506 if ([self isSystemTicketDoomed]) { 507 [self updateStatus:kAutoupdateNeedsPromotion version:nil]; 508 } else { 509 [self updateStatus:kAutoupdateRegistered version:nil]; 510 } 511 } else { 512 // Dump registration_? 513 [self updateStatus:kAutoupdateRegisterFailed version:nil]; 514 } 515 } 516 517 - (void)stopTimer { 518 [timer_ invalidate]; 519 } 520 521 - (void)markActive:(NSTimer*)timer { 522 KSRegistration* ksr = [timer userInfo]; 523 [ksr setActive]; 524 } 525 526 - (void)checkForUpdate { 527 DCHECK(![self asyncOperationPending]); 528 529 if (!registration_) { 530 [self updateStatus:kAutoupdateCheckFailed version:nil]; 531 return; 532 } 533 534 [self updateStatus:kAutoupdateChecking version:nil]; 535 536 [registration_ checkForUpdate]; 537 538 // Upon completion, ksr::KSRegistrationCheckForUpdateNotification will be 539 // posted, and -checkForUpdateComplete: will be called. 540 } 541 542 - (void)checkForUpdateComplete:(NSNotification*)notification { 543 NSDictionary* userInfo = [notification userInfo]; 544 545 if ([[userInfo objectForKey:ksr::KSRegistrationUpdateCheckErrorKey] 546 boolValue]) { 547 [self updateStatus:kAutoupdateCheckFailed version:nil]; 548 } else if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) { 549 // If an update is known to be available, go straight to 550 // -updateStatus:version:. It doesn't matter what's currently on disk. 551 NSString* version = [userInfo objectForKey:ksr::KSRegistrationVersionKey]; 552 [self updateStatus:kAutoupdateAvailable version:version]; 553 } else { 554 // If no updates are available, check what's on disk, because an update 555 // may have already been installed. This check happens on another thread, 556 // and -updateStatus:version: will be called on the main thread when done. 557 [self determineUpdateStatusAsync]; 558 } 559 } 560 561 - (void)installUpdate { 562 DCHECK(![self asyncOperationPending]); 563 564 if (!registration_) { 565 [self updateStatus:kAutoupdateInstallFailed version:nil]; 566 return; 567 } 568 569 [self updateStatus:kAutoupdateInstalling version:nil]; 570 571 [registration_ startUpdate]; 572 573 // Upon completion, ksr::KSRegistrationStartUpdateNotification will be 574 // posted, and -installUpdateComplete: will be called. 575 } 576 577 - (void)installUpdateComplete:(NSNotification*)notification { 578 NSDictionary* userInfo = [notification userInfo]; 579 580 // http://crbug.com/160308 and b/7517358: when using system Keystone and on 581 // a user ticket, KSUpdateCheckSuccessfulKey will be NO even when an update 582 // was installed correctly, so don't check it. It should be redudnant when 583 // KSUpdateCheckSuccessfullyInstalledKey is checked. 584 if (![[userInfo objectForKey:ksr::KSUpdateCheckSuccessfullyInstalledKey] 585 intValue]) { 586 [self updateStatus:kAutoupdateInstallFailed version:nil]; 587 } else { 588 updateSuccessfullyInstalled_ = YES; 589 590 // Nothing in the notification dictionary reports the version that was 591 // installed. Figure it out based on what's on disk. 592 [self determineUpdateStatusAsync]; 593 } 594 } 595 596 - (NSString*)currentlyInstalledVersion { 597 NSString* appInfoPlistPath = [self appInfoPlistPath]; 598 NSDictionary* infoPlist = 599 [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath]; 600 return [infoPlist objectForKey:@"CFBundleShortVersionString"]; 601 } 602 603 // Runs on the main thread. 604 - (void)determineUpdateStatusAsync { 605 DCHECK([NSThread isMainThread]); 606 607 PerformBridge::PostPerform(self, @selector(determineUpdateStatus)); 608 } 609 610 // Runs on a thread managed by WorkerPool. 611 - (void)determineUpdateStatus { 612 DCHECK(![NSThread isMainThread]); 613 614 NSString* version = [self currentlyInstalledVersion]; 615 616 [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:) 617 withObject:version 618 waitUntilDone:NO]; 619 } 620 621 // Runs on the main thread. 622 - (void)determineUpdateStatusForVersion:(NSString*)version { 623 DCHECK([NSThread isMainThread]); 624 625 AutoupdateStatus status; 626 if (updateSuccessfullyInstalled_) { 627 // If an update was successfully installed and this object saw it happen, 628 // then don't even bother comparing versions. 629 status = kAutoupdateInstalled; 630 } else { 631 NSString* currentVersion = 632 [NSString stringWithUTF8String:chrome::kChromeVersion]; 633 if (!version) { 634 // If the version on disk could not be determined, assume that 635 // whatever's running is current. 636 version = currentVersion; 637 status = kAutoupdateCurrent; 638 } else if ([version isEqualToString:currentVersion]) { 639 status = kAutoupdateCurrent; 640 } else { 641 // If the version on disk doesn't match what's currently running, an 642 // update must have been applied in the background, without this app's 643 // direct participation. Leave updateSuccessfullyInstalled_ alone 644 // because there's no direct knowledge of what actually happened. 645 status = kAutoupdateInstalled; 646 } 647 } 648 649 [self updateStatus:status version:version]; 650 } 651 652 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version { 653 NSNumber* statusNumber = [NSNumber numberWithInt:status]; 654 NSMutableDictionary* dictionary = 655 [NSMutableDictionary dictionaryWithObject:statusNumber 656 forKey:kAutoupdateStatusStatus]; 657 if (version) { 658 [dictionary setObject:version forKey:kAutoupdateStatusVersion]; 659 } 660 661 NSNotification* notification = 662 [NSNotification notificationWithName:kAutoupdateStatusNotification 663 object:self 664 userInfo:dictionary]; 665 recentNotification_.reset([notification retain]); 666 667 [[NSNotificationCenter defaultCenter] postNotification:notification]; 668 } 669 670 - (NSNotification*)recentNotification { 671 return [[recentNotification_ retain] autorelease]; 672 } 673 674 - (AutoupdateStatus)recentStatus { 675 NSDictionary* dictionary = [recentNotification_ userInfo]; 676 return static_cast<AutoupdateStatus>( 677 [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]); 678 } 679 680 - (BOOL)asyncOperationPending { 681 AutoupdateStatus status = [self recentStatus]; 682 return status == kAutoupdateRegistering || 683 status == kAutoupdateChecking || 684 status == kAutoupdateInstalling || 685 status == kAutoupdatePromoting; 686 } 687 688 - (BOOL)isUserTicket { 689 return [registration_ ticketType] == ksr::kKSRegistrationUserTicket; 690 } 691 692 - (BOOL)isSystemKeystone { 693 struct stat statbuf; 694 if (stat("/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/" 695 "Contents/MacOS/ksadmin", 696 &statbuf) != 0) { 697 return NO; 698 } 699 700 if (!(statbuf.st_mode & S_IXUSR)) { 701 return NO; 702 } 703 704 return YES; 705 } 706 707 - (BOOL)isSystemTicketDoomed { 708 BOOL isSystemTicket = ![self isUserTicket]; 709 return isSystemTicket && ![self isSystemKeystone]; 710 } 711 712 - (BOOL)isOnReadOnlyFilesystem { 713 const char* appPathC = [appPath_ fileSystemRepresentation]; 714 struct statfs statfsBuf; 715 716 if (statfs(appPathC, &statfsBuf) != 0) { 717 PLOG(ERROR) << "statfs"; 718 // Be optimistic about the filesystem's writability. 719 return NO; 720 } 721 722 return (statfsBuf.f_flags & MNT_RDONLY) != 0; 723 } 724 725 - (BOOL)needsPromotion { 726 // Don't promote when on a read-only filesystem. 727 if ([self isOnReadOnlyFilesystem]) { 728 return NO; 729 } 730 731 // Promotion is required when a system ticket is present but system Keystone 732 // is not. 733 if ([self isSystemTicketDoomed]) { 734 return YES; 735 } 736 737 // If on a system ticket and system Keystone is present, promotion is not 738 // required. 739 if (![self isUserTicket]) { 740 return NO; 741 } 742 743 // Check the outermost bundle directory, the main executable path, and the 744 // framework directory. It may be enough to just look at the outermost 745 // bundle directory, but checking an interior file and directory can be 746 // helpful in case permissions are set differently only on the outermost 747 // directory. An interior file and directory are both checked because some 748 // file operations, such as Snow Leopard's Finder's copy operation when 749 // authenticating, may actually result in different ownership being applied 750 // to files and directories. 751 NSFileManager* fileManager = [NSFileManager defaultManager]; 752 NSString* executablePath = [base::mac::OuterBundle() executablePath]; 753 NSString* frameworkPath = [base::mac::FrameworkBundle() bundlePath]; 754 return ![fileManager isWritableFileAtPath:appPath_] || 755 ![fileManager isWritableFileAtPath:executablePath] || 756 ![fileManager isWritableFileAtPath:frameworkPath]; 757 } 758 759 - (BOOL)wantsPromotion { 760 if ([self needsPromotion]) { 761 return YES; 762 } 763 764 // These are the same unpromotable cases as in -needsPromotion. 765 if ([self isOnReadOnlyFilesystem] || ![self isUserTicket]) { 766 return NO; 767 } 768 769 return [appPath_ hasPrefix:@"/Applications/"]; 770 } 771 772 - (void)promoteTicket { 773 if ([self asyncOperationPending] || ![self wantsPromotion]) { 774 // Because there are multiple ways of reaching promoteTicket that might 775 // not lock each other out, it may be possible to arrive here while an 776 // asynchronous operation is pending, or even after promotion has already 777 // occurred. Just quietly return without doing anything. 778 return; 779 } 780 781 NSString* prompt = l10n_util::GetNSStringFWithFixup( 782 IDS_PROMOTE_AUTHENTICATION_PROMPT, 783 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); 784 base::mac::ScopedAuthorizationRef authorization( 785 base::mac::AuthorizationCreateToRunAsRoot( 786 base::mac::NSToCFCast(prompt))); 787 if (!authorization.get()) { 788 return; 789 } 790 791 [self promoteTicketWithAuthorization:authorization.release() synchronous:NO]; 792 } 793 794 - (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg 795 synchronous:(BOOL)synchronous { 796 base::mac::ScopedAuthorizationRef authorization(authorization_arg); 797 authorization_arg = NULL; 798 799 if ([self asyncOperationPending]) { 800 // Starting a synchronous operation while an asynchronous one is pending 801 // could be trouble. 802 return; 803 } 804 if (!synchronous && ![self wantsPromotion]) { 805 // If operating synchronously, the call came from the installer, which 806 // means that a system ticket is required. Otherwise, only allow 807 // promotion if it's wanted. 808 return; 809 } 810 811 synchronousPromotion_ = synchronous; 812 813 [self updateStatus:kAutoupdatePromoting version:nil]; 814 815 // TODO(mark): Remove when able! 816 // 817 // keystone_promote_preflight will copy the current brand information out to 818 // the system level so all users can share the data as part of the ticket 819 // promotion. 820 // 821 // It will also ensure that the Keystone system ticket store is in a usable 822 // state for all users on the system. Ideally, Keystone's installer or 823 // another part of Keystone would handle this. The underlying problem is 824 // http://b/2285921, and it causes http://b/2289908, which this workaround 825 // addresses. 826 // 827 // This is run synchronously, which isn't optimal, but 828 // -[KSRegistration promoteWithParameters:authorization:] is currently 829 // synchronous too, and this operation needs to happen before that one. 830 // 831 // TODO(mark): Make asynchronous. That only makes sense if the promotion 832 // operation itself is asynchronous too. http://b/2290009. Hopefully, 833 // the Keystone promotion code will just be changed to do what preflight 834 // now does, and then the preflight script can be removed instead. 835 // However, preflight operation (and promotion) should only be asynchronous 836 // if the synchronous parameter is NO. 837 NSString* preflightPath = 838 [base::mac::FrameworkBundle() 839 pathForResource:@"keystone_promote_preflight" 840 ofType:@"sh"]; 841 const char* preflightPathC = [preflightPath fileSystemRepresentation]; 842 const char* userBrandFile = NULL; 843 const char* systemBrandFile = NULL; 844 if (brandFileType_ == kBrandFileTypeUser) { 845 // Running with user level brand file, promote to the system level. 846 userBrandFile = [UserBrandFilePath() fileSystemRepresentation]; 847 systemBrandFile = [SystemBrandFilePath() fileSystemRepresentation]; 848 } 849 const char* arguments[] = {userBrandFile, systemBrandFile, NULL}; 850 851 int exit_status; 852 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait( 853 authorization, 854 preflightPathC, 855 kAuthorizationFlagDefaults, 856 arguments, 857 NULL, // pipe 858 &exit_status); 859 if (status != errAuthorizationSuccess) { 860 OSSTATUS_LOG(ERROR, status) 861 << "AuthorizationExecuteWithPrivileges preflight"; 862 [self updateStatus:kAutoupdatePromoteFailed version:nil]; 863 return; 864 } 865 if (exit_status != 0) { 866 LOG(ERROR) << "keystone_promote_preflight status " << exit_status; 867 [self updateStatus:kAutoupdatePromoteFailed version:nil]; 868 return; 869 } 870 871 // Hang on to the AuthorizationRef so that it can be used once promotion is 872 // complete. Do this before asking Keystone to promote the ticket, because 873 // -promotionComplete: may be called from inside the Keystone promotion 874 // call. 875 authorization_.swap(authorization); 876 877 NSDictionary* parameters = [self keystoneParameters]; 878 879 // If the brand file is user level, update parameters to point to the new 880 // system level file during promotion. 881 if (brandFileType_ == kBrandFileTypeUser) { 882 NSMutableDictionary* temp_parameters = 883 [[parameters mutableCopy] autorelease]; 884 [temp_parameters setObject:SystemBrandFilePath() 885 forKey:ksr::KSRegistrationBrandPathKey]; 886 parameters = temp_parameters; 887 } 888 889 if (![registration_ promoteWithParameters:parameters 890 authorization:authorization_]) { 891 [self updateStatus:kAutoupdatePromoteFailed version:nil]; 892 authorization_.reset(); 893 return; 894 } 895 896 // Upon completion, ksr::KSRegistrationPromotionDidCompleteNotification will 897 // be posted, and -promotionComplete: will be called. 898 899 // If synchronous, see to it that this happens immediately. Give it a 900 // 10-second deadline. 901 if (synchronous) { 902 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false); 903 } 904 } 905 906 - (void)promotionComplete:(NSNotification*)notification { 907 NSDictionary* userInfo = [notification userInfo]; 908 if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) { 909 if (synchronousPromotion_) { 910 // Short-circuit: if performing a synchronous promotion, the promotion 911 // came from the installer, which already set the permissions properly. 912 // Rather than run a duplicate permission-changing operation, jump 913 // straight to "done." 914 [self changePermissionsForPromotionComplete]; 915 } else { 916 [self changePermissionsForPromotionAsync]; 917 } 918 } else { 919 authorization_.reset(); 920 [self updateStatus:kAutoupdatePromoteFailed version:nil]; 921 } 922 923 if (synchronousPromotion_) { 924 // The run loop doesn't need to wait for this any longer. 925 CFRunLoopRef runLoop = CFRunLoopGetCurrent(); 926 CFRunLoopStop(runLoop); 927 CFRunLoopWakeUp(runLoop); 928 } 929 } 930 931 - (void)changePermissionsForPromotionAsync { 932 // NSBundle is not documented as being thread-safe. Do NSBundle operations 933 // on the main thread before jumping over to a WorkerPool-managed 934 // thread to run the tool. 935 DCHECK([NSThread isMainThread]); 936 937 SEL selector = @selector(changePermissionsForPromotionWithTool:); 938 NSString* toolPath = 939 [base::mac::FrameworkBundle() 940 pathForResource:@"keystone_promote_postflight" 941 ofType:@"sh"]; 942 943 PerformBridge::PostPerform(self, selector, toolPath); 944 } 945 946 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath { 947 const char* toolPathC = [toolPath fileSystemRepresentation]; 948 949 const char* appPathC = [appPath_ fileSystemRepresentation]; 950 const char* arguments[] = {appPathC, NULL}; 951 952 int exit_status; 953 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait( 954 authorization_, 955 toolPathC, 956 kAuthorizationFlagDefaults, 957 arguments, 958 NULL, // pipe 959 &exit_status); 960 if (status != errAuthorizationSuccess) { 961 OSSTATUS_LOG(ERROR, status) 962 << "AuthorizationExecuteWithPrivileges postflight"; 963 } else if (exit_status != 0) { 964 LOG(ERROR) << "keystone_promote_postflight status " << exit_status; 965 } 966 967 SEL selector = @selector(changePermissionsForPromotionComplete); 968 [self performSelectorOnMainThread:selector 969 withObject:nil 970 waitUntilDone:NO]; 971 } 972 973 - (void)changePermissionsForPromotionComplete { 974 authorization_.reset(); 975 976 [self updateStatus:kAutoupdatePromoted version:nil]; 977 } 978 979 - (void)setAppPath:(NSString*)appPath { 980 if (appPath != appPath_) { 981 [appPath_ release]; 982 appPath_ = [appPath copy]; 983 } 984 } 985 986 @end // @implementation KeystoneGlue 987 988 namespace { 989 990 std::string BrandCodeInternal() { 991 KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue]; 992 NSString* brand_path = [keystone_glue brandFilePath]; 993 994 if (![brand_path length]) 995 return std::string(); 996 997 NSDictionary* dict = 998 [NSDictionary dictionaryWithContentsOfFile:brand_path]; 999 NSString* brand_code = 1000 base::mac::ObjCCast<NSString>([dict objectForKey:kBrandKey]); 1001 if (brand_code) 1002 return [brand_code UTF8String]; 1003 1004 return std::string(); 1005 } 1006 1007 } // namespace 1008 1009 namespace keystone_glue { 1010 1011 std::string BrandCode() { 1012 // |s_brand_code| is leaked. 1013 static std::string* s_brand_code = new std::string(BrandCodeInternal()); 1014 return *s_brand_code; 1015 } 1016 1017 bool KeystoneEnabled() { 1018 return [KeystoneGlue defaultKeystoneGlue] != nil; 1019 } 1020 1021 string16 CurrentlyInstalledVersion() { 1022 KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue]; 1023 NSString* version = [keystoneGlue currentlyInstalledVersion]; 1024 return base::SysNSStringToUTF16(version); 1025 } 1026 1027 } // namespace keystone_glue 1028