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