| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/updater/mac/setup/ks_tickets.h" |
| |
| #import <Foundation/Foundation.h> |
| |
| #include "base/apple/foundation_util.h" |
| #include "base/files/file_path.h" |
| #include "base/logging.h" |
| #include "base/notreached.h" |
| #include "base/strings/sys_string_conversions.h" |
| |
| NSString* const kCRUTicketBrandKey = @"KSBrandID"; |
| NSString* const kCRUTicketTagKey = @"KSChannelID"; |
| |
| @interface KSLaunchServicesExistenceChecker : NSObject <NSSecureCoding> |
| @property(nonnull, readonly) NSString* bundle_id; |
| @end |
| |
| @interface KSSpotlightExistenceChecker : NSObject <NSSecureCoding> |
| @property(nonnull, readonly) NSString* query; |
| @end |
| |
| @implementation KSTicketStore |
| |
| + (nullable NSDictionary<NSString*, KSTicket*>*)readStoreWithPath: |
| (nonnull NSString*)path { |
| if (![NSFileManager.defaultManager fileExistsAtPath:path]) { |
| VLOG(0) << "Ticket store does not exist at " |
| << base::SysNSStringToUTF8(path); |
| return [NSDictionary dictionary]; |
| } |
| |
| NSError* error = nil; |
| NSData* storeData = [NSData dataWithContentsOfFile:path |
| options:0 // Use normal IO |
| error:&error]; |
| if (!storeData) { |
| VLOG(0) << "Failed to load ticket store at " |
| << base::SysNSStringToUTF8(path) << ": " << error; |
| return nil; |
| } |
| if (!storeData.length) { |
| return [NSDictionary dictionary]; |
| } |
| |
| NSDictionary* store = nil; |
| NSKeyedUnarchiver* unpacker = |
| [[NSKeyedUnarchiver alloc] initForReadingFromData:storeData error:&error]; |
| if (!unpacker) { |
| VLOG(0) << base::SysNSStringToUTF8( |
| [NSString stringWithFormat:@"Ticket error %@", error]); |
| return nil; |
| } |
| unpacker.requiresSecureCoding = YES; |
| NSSet* classes = |
| [NSSet setWithObjects:[NSDictionary class], [KSTicket class], |
| [KSPathExistenceChecker class], |
| [KSLaunchServicesExistenceChecker class], |
| [KSSpotlightExistenceChecker class], |
| [NSArray class], [NSSet class], [NSURL class], nil]; |
| store = [unpacker decodeObjectOfClasses:classes |
| forKey:NSKeyedArchiveRootObjectKey]; |
| [unpacker finishDecoding]; |
| if (unpacker.error) { |
| VLOG(0) << "Error unpacking ticket store: " << unpacker.error; |
| return nil; |
| } |
| if (!store || ![store isKindOfClass:[NSDictionary class]]) { |
| VLOG(0) << "Ticket store is not a dictionary."; |
| return nil; |
| } |
| return store; |
| } |
| |
| @end |
| |
| @implementation KSPathExistenceChecker |
| |
| @synthesize path = path_; |
| |
| + (BOOL)supportsSecureCoding { |
| return YES; |
| } |
| |
| - (instancetype)initWithCoder:(NSCoder*)coder { |
| if ((self = [super init])) { |
| path_ = [coder decodeObjectOfClass:[NSString class] forKey:@"path"]; |
| } |
| return self; |
| } |
| |
| - (instancetype)initWithFilePath:(const base::FilePath&)filePath { |
| if ((self = [super init])) { |
| path_ = base::apple::FilePathToNSString(filePath); |
| } |
| return self; |
| } |
| |
| - (void)encodeWithCoder:(NSCoder*)coder { |
| [coder encodeObject:path_ forKey:@"path"]; |
| } |
| |
| - (NSString*)description { |
| // Formatting must stay the same in ksadmin output. |
| return [NSString |
| stringWithFormat:@"<%@:0x222222222222 path=%@>", [self class], path_]; |
| } |
| |
| @end |
| |
| @implementation KSLaunchServicesExistenceChecker |
| |
| @synthesize bundle_id = bundle_id_; |
| |
| + (BOOL)supportsSecureCoding { |
| return YES; |
| } |
| |
| - (instancetype)initWithCoder:(NSCoder*)coder { |
| if ((self = [super init])) { |
| bundle_id_ = [coder decodeObjectOfClass:[NSString class] |
| forKey:@"bundle_id"]; |
| } |
| return self; |
| } |
| |
| - (void)encodeWithCoder:(NSCoder*)coder { |
| NOTREACHED(); |
| } |
| |
| - (NSString*)description { |
| // Formatting must stay the same in ksadmin output. |
| return [NSString stringWithFormat:@"<%@:0x222222222222 bundle_id=%@>", |
| [self class], bundle_id_]; |
| } |
| |
| @end |
| |
| @implementation KSSpotlightExistenceChecker |
| |
| @synthesize query = query_; |
| |
| + (BOOL)supportsSecureCoding { |
| return YES; |
| } |
| |
| - (instancetype)initWithCoder:(NSCoder*)coder { |
| if ((self = [super init])) { |
| query_ = [coder decodeObjectOfClass:[NSString class] forKey:@"query"]; |
| } |
| return self; |
| } |
| |
| - (void)encodeWithCoder:(NSCoder*)coder { |
| NOTREACHED(); |
| } |
| |
| - (NSString*)description { |
| // Formatting must stay the same in ksadmin output. |
| return [NSString |
| stringWithFormat:@"<%@:0x222222222222 query=%@>", [self class], query_]; |
| } |
| |
| @end |
| |
| // All these keys must be same as those from Keystone. |
| NSString* const kKSTicketBrandKeyKey = @"brandKey"; |
| NSString* const kKSTicketBrandPathKey = @"brandPath"; |
| NSString* const kKSTicketCohortKey = @"Cohort"; |
| NSString* const kKSTicketCohortHintKey = @"CohortHint"; |
| NSString* const kKSTicketCohortNameKey = @"CohortName"; |
| NSString* const kKSTicketCreationDateKey = @"creation_date"; |
| NSString* const kKSTicketExistenceCheckerKey = @"existence_checker"; |
| NSString* const kKSTicketProductIDKey = @"product_id"; |
| NSString* const kKSTicketServerTypeKey = @"serverType"; |
| NSString* const kKSTicketServerURLKey = @"server_url"; |
| NSString* const kKSTicketTagKey = @"tag"; |
| NSString* const kKSTicketTagKeyKey = @"tagKey"; |
| NSString* const kKSTicketTagPathKey = @"tagPath"; |
| NSString* const kKSTicketTicketVersionKey = @"ticketVersion"; |
| NSString* const kKSTicketVersionKey = @"version"; |
| NSString* const kKSTicketVersionPathKey = @"versionPath"; |
| NSString* const kKSTicketVersionKeyKey = @"versionKey"; |
| |
| @implementation KSTicket |
| |
| @synthesize productID = productID_; |
| @synthesize version = version_; |
| @synthesize existenceChecker = existenceChecker_; |
| @synthesize serverURL = serverURL_; |
| @synthesize serverType = serverType_; |
| @synthesize creationDate = creationDate_; |
| @synthesize tag = tag_; |
| @synthesize tagPath = tagPath_; |
| @synthesize tagKey = tagKey_; |
| @synthesize brandPath = brandPath_; |
| @synthesize brandKey = brandKey_; |
| @synthesize versionPath = versionPath_; |
| @synthesize versionKey = versionKey_; |
| @synthesize cohort = cohort_; |
| @synthesize cohortHint = cohortHint_; |
| @synthesize cohortName = cohortName_; |
| @synthesize ticketVersion = ticketVersion_; |
| |
| + (BOOL)supportsSecureCoding { |
| return YES; |
| } |
| |
| // Tries to obtain the server URL which may be NSURL or NSString object. |
| // Verifies the read objects and guarantees that the returned object is NSURL. |
| // The method may throw. |
| - (NSURL*)decodeServerURL:(NSCoder*)decoder { |
| id serverURL = [decoder decodeObjectOfClasses:[NSSet setWithArray:@[ |
| [NSString class], |
| [NSURL class], |
| ]] |
| forKey:kKSTicketServerURLKey]; |
| if (!serverURL) |
| return nil; |
| if ([serverURL isKindOfClass:[NSString class]]) { |
| return [NSURL URLWithString:serverURL]; // May throw |
| } |
| return (NSURL*)serverURL; |
| } |
| |
| - (instancetype)initWithCoder:(NSCoder*)coder { |
| if ((self = [super init])) { |
| productID_ = [coder decodeObjectOfClass:[NSString class] |
| forKey:kKSTicketProductIDKey]; |
| version_ = [coder decodeObjectOfClass:[NSString class] |
| forKey:kKSTicketVersionKey]; |
| if ([[coder decodeObjectForKey:kKSTicketExistenceCheckerKey] |
| isKindOfClass:[KSPathExistenceChecker class]]) { |
| existenceChecker_ = |
| [coder decodeObjectOfClass:[KSPathExistenceChecker class] |
| forKey:kKSTicketExistenceCheckerKey]; |
| } |
| serverURL_ = [self decodeServerURL:coder]; |
| creationDate_ = [coder decodeObjectOfClass:[NSDate class] |
| forKey:kKSTicketCreationDateKey]; |
| serverType_ = [coder decodeObjectOfClass:[NSString class] |
| forKey:kKSTicketServerTypeKey]; |
| tag_ = [coder decodeObjectOfClass:[NSString class] forKey:kKSTicketTagKey]; |
| tagPath_ = [coder decodeObjectOfClass:[NSString class] |
| forKey:kKSTicketTagPathKey]; |
| tagKey_ = [coder decodeObjectOfClass:[NSString class] |
| forKey:kKSTicketTagKeyKey]; |
| brandPath_ = [coder decodeObjectOfClass:[NSString class] |
| forKey:kKSTicketBrandPathKey]; |
| brandKey_ = [coder decodeObjectOfClass:[NSString class] |
| forKey:kKSTicketBrandKeyKey]; |
| versionPath_ = [coder decodeObjectOfClass:[NSString class] |
| forKey:kKSTicketVersionPathKey]; |
| versionKey_ = [coder decodeObjectOfClass:[NSString class] |
| forKey:kKSTicketVersionKeyKey]; |
| cohort_ = [coder decodeObjectOfClass:[NSString class] |
| forKey:kKSTicketCohortKey]; |
| cohortHint_ = [coder decodeObjectOfClass:[NSString class] |
| forKey:kKSTicketCohortHintKey]; |
| cohortName_ = [coder decodeObjectOfClass:[NSString class] |
| forKey:kKSTicketCohortNameKey]; |
| ticketVersion_ = [coder decodeInt32ForKey:kKSTicketTicketVersionKey]; |
| } |
| return self; |
| } |
| |
| - (instancetype)initWithAppId:(NSString*)appId |
| version:(NSString*)version |
| ecp:(const base::FilePath&)ecp |
| tag:(NSString*)tag |
| brandCode:(NSString*)brandCode |
| brandPath:(const base::FilePath&)brandPath { |
| if ((self = [super init])) { |
| productID_ = appId; |
| version_ = version; |
| if (!ecp.empty()) { |
| existenceChecker_ = [[KSPathExistenceChecker alloc] initWithFilePath:ecp]; |
| |
| tagPath_ = |
| [NSString stringWithFormat:@"%@/Contents/Info.plist", |
| base::apple::FilePathToNSString(ecp)]; |
| tagKey_ = kCRUTicketTagKey; |
| } |
| tag_ = tag; |
| |
| brandCode_ = brandCode; |
| if (!brandPath.empty()) { |
| brandPath_ = base::apple::FilePathToNSString(brandPath); |
| brandKey_ = kCRUTicketBrandKey; |
| } |
| serverURL_ = |
| [NSURL URLWithString:@"https://tools.google.com/service/update2"]; |
| serverType_ = @"Omaha"; |
| ticketVersion_ = 1; |
| } |
| return self; |
| } |
| |
| - (void)encodeWithCoder:(NSCoder*)coder { |
| [coder encodeObject:productID_ forKey:kKSTicketProductIDKey]; |
| [coder encodeObject:version_ forKey:kKSTicketVersionKey]; |
| [coder encodeObject:existenceChecker_ forKey:kKSTicketExistenceCheckerKey]; |
| [coder encodeObject:serverURL_ forKey:kKSTicketServerURLKey]; |
| [coder encodeObject:creationDate_ forKey:kKSTicketCreationDateKey]; |
| if (serverType_.length) { |
| [coder encodeObject:serverType_ forKey:kKSTicketServerTypeKey]; |
| } |
| if (tag_.length) { |
| [coder encodeObject:tag_ forKey:kKSTicketTagKey]; |
| } |
| if (tagPath_.length) { |
| [coder encodeObject:tagPath_ forKey:kKSTicketTagPathKey]; |
| } |
| if (tagKey_.length) { |
| [coder encodeObject:tagKey_ forKey:kKSTicketTagKeyKey]; |
| } |
| if (brandPath_.length) { |
| [coder encodeObject:brandPath_ forKey:kKSTicketBrandPathKey]; |
| } |
| if (brandKey_.length) { |
| [coder encodeObject:brandKey_ forKey:kKSTicketBrandKeyKey]; |
| } |
| if (versionPath_.length) { |
| [coder encodeObject:versionPath_ forKey:kKSTicketVersionPathKey]; |
| } |
| if (versionKey_.length) { |
| [coder encodeObject:versionKey_ forKey:kKSTicketVersionKeyKey]; |
| } |
| if (cohort_.length) { |
| [coder encodeObject:cohort_ forKey:kKSTicketCohortKey]; |
| } |
| if (cohortHint_.length) { |
| [coder encodeObject:cohortHint_ forKey:kKSTicketCohortHintKey]; |
| } |
| if (cohortName_.length) { |
| [coder encodeObject:cohortName_ forKey:kKSTicketCohortNameKey]; |
| } |
| [coder encodeInt32:ticketVersion_ forKey:kKSTicketTicketVersionKey]; |
| } |
| |
| - (NSUInteger)hash { |
| return [productID_ hash] + [version_ hash] + [existenceChecker_ hash] + |
| [serverURL_ hash] + [creationDate_ hash]; |
| } |
| |
| - (NSString*)description { |
| // Keep the description stable. Clients depend on the output formatting |
| // as "fieldname=value" without any additional quoting. We cannot use |
| // KSDescription() here because of these legacy formatting restrictions. In |
| // particular, ksadmin output must not be substantially changed. |
| NSString* serverTypeString = @""; |
| if (serverType_) { |
| serverTypeString = |
| [NSString stringWithFormat:@"\n\tserverType=%@", serverType_]; |
| } |
| NSString* tagString = @""; |
| if (tag_) { |
| tagString = [NSString stringWithFormat:@"\n\ttag=%@", tag_]; |
| } |
| NSString* tagPathString = @""; |
| if (tagPath_ && tagKey_) { |
| tagPathString = [NSString |
| stringWithFormat:@"\n\ttagPath=%@\n\ttagKey=%@", tagPath_, tagKey_]; |
| } |
| NSString* brandPathString = @""; |
| if (brandPath_ && brandKey_) { |
| brandPathString = |
| [NSString stringWithFormat:@"\n\tbrandPath=%@\n\tbrandKey=%@", |
| brandPath_, brandKey_]; |
| } |
| NSString* versionPathString = @""; |
| if (versionPath_ && versionKey_) { |
| versionPathString = |
| [NSString stringWithFormat:@"\n\tversionPath=%@\n\tversionKey=%@", |
| versionPath_, versionKey_]; |
| } |
| NSString* cohortString = @""; |
| if ([cohort_ length]) { |
| cohortString = [NSString stringWithFormat:@"\n\tcohort=%@", cohort_]; |
| if ([cohortName_ length]) { |
| cohortString = [cohortString |
| stringByAppendingFormat:@"\n\tcohortName=%@", cohortName_]; |
| } |
| } |
| NSString* cohortHintString = @""; |
| if ([cohortHint_ length]) { |
| cohortHintString = |
| [NSString stringWithFormat:@"\n\tcohortHint=%@", cohortHint_]; |
| } |
| NSString* ticketVersionString = |
| [NSString stringWithFormat:@"\n\tticketVersion=%d", ticketVersion_]; |
| // Dates used to be parsed and stored as GMT and printed in GMT. That |
| // changed in 10.7 to be GMT with timezone information, so use a custom |
| // description string that matches our old output. |
| NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init]; |
| [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; |
| [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; |
| NSString* gmtDate = [dateFormatter stringFromDate:creationDate_]; |
| return [NSString |
| stringWithFormat:@"<%@:0x222222222222\n\tproductID=%@\n\tversion=%@\n\t" |
| @"xc=%@%@\n\turl=%@\n\tcreationDate=%@%@%@%@%@%@%@%@\n>", |
| [self class], productID_, version_, existenceChecker_, |
| serverTypeString, serverURL_, gmtDate, tagString, |
| tagPathString, brandPathString, versionPathString, |
| cohortString, cohortHintString, ticketVersionString]; |
| } |
| |
| - (NSString*)readExternalPropertyAtPath:(NSString*)path withKey:(NSString*)key { |
| // Standardize (expands tilde, symlink resolve, etc.) |
| NSString* fullPath = [path stringByStandardizingPath]; |
| |
| if (!fullPath.length || !key.length) |
| return nil; |
| |
| NSData* plistData = [NSData dataWithContentsOfFile:fullPath]; |
| if (!plistData.length) { |
| LOG(ERROR) << "Failed to read external property from file: " |
| << base::SysNSStringToUTF8(path); |
| return nil; |
| } |
| |
| id plistContent = |
| [NSPropertyListSerialization propertyListWithData:plistData |
| options:NSPropertyListImmutable |
| format:nil |
| error:nil]; |
| if (!plistContent || ![plistContent isKindOfClass:[NSDictionary class]]) { |
| return nil; |
| } |
| |
| id value = [plistContent objectForKey:key]; |
| if (![value isKindOfClass:[NSString class]]) |
| return nil; |
| |
| return (NSString*)value; |
| } |
| |
| - (NSString*)determineTag { |
| NSString* externalTag = [self readExternalPropertyAtPath:tagPath_ |
| withKey:tagKey_]; |
| return externalTag ? externalTag : tag_; |
| } |
| |
| - (NSString*)determineBrand { |
| return [self readExternalPropertyAtPath:brandPath_ withKey:brandKey_]; |
| } |
| |
| - (NSString*)determineVersion { |
| NSString* externalVersion = [self readExternalPropertyAtPath:versionPath_ |
| withKey:versionKey_]; |
| return externalVersion ? externalVersion : version_; |
| } |
| |
| @end |