// 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], [NSString 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_IN_MIGRATION();
}
- (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_IN_MIGRATION();
}
- (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