blob: 742852c1b9ae64390de37cd1e0b945b5c28e5749 [file] [log] [blame]
/*
* Copyright (C) 2022 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "ICAppBundle.h"
#if ENABLE(INSTALL_COORDINATION_BUNDLES)
#import "AppBundleRequest.h"
#import "LaunchServicesSPI.h"
#import "PushClientConnection.h"
#import "WebPushDaemon.h"
#import <JavaScriptCore/ConsoleTypes.h>
#import <pal/crypto/CryptoDigest.h>
#import <wtf/MainThread.h>
#import <wtf/text/Base64.h>
#import <wtf/text/StringBuilder.h>
NSString *pushBundleAppManagementDomain = @"com.apple.WebKit.PushBundles";
@interface _WKAppInstallCoordinatorObserver : NSObject <IXAppInstallCoordinatorObserver>
- (instancetype)initWithICAppBundle:(WebPushD::ICAppBundle&)icAppBundle;
@property WeakPtr<WebPushD::ICAppBundle> bundle;
@end
@implementation _WKAppInstallCoordinatorObserver
- (instancetype)initWithICAppBundle:(WebPushD::ICAppBundle&)icAppBundle
{
if (!(self = [super init]))
return nil;
_bundle = icAppBundle;
return self;
}
- (void)coordinatorDidCompleteSuccessfully:(nonnull IXAppInstallCoordinator *)coordinator forApplicationRecord:(nullable LSApplicationRecord *)appRecord
{
callOnMainRunLoop([self, protectedSelf = RetainPtr<id>(self)] {
if (_bundle)
_bundle->bundleCreationSucceeded();
});
}
- (void)coordinator:(nonnull IXAppInstallCoordinator *)coordinator canceledWithReason:(nonnull NSError *)cancelReason client:(IXClientIdentifier)client
{
callOnMainRunLoop([self, protectedSelf = RetainPtr<id>(self), protectedCancelReason = RetainPtr<NSError>(cancelReason)] {
if (_bundle)
_bundle->bundleCreationFailed(protectedCancelReason.get());
});
}
@end
using PAL::CryptoDigest;
namespace WebPushD {
static void broadcastDebugMessage(const String& message)
{
Daemon::singleton().broadcastDebugMessage(makeString("ICAppBundle: ", message));
}
ICAppBundle::ICAppBundle(ClientConnection& clientConnection, const String& originString, PushAppBundleClient& client)
: PushAppBundle(client)
, m_originString(originString)
, m_clientConnection(&clientConnection)
{
}
static String encodeOriginStringForBundleIdentifier(const String& input)
{
// Bundle identifiers have exactly 64 valid characters.
// a-z, A-Z, 0-9, '-', and '.'
// Base64 URL encoding uses '-' and '_' for the final 2 characters
// For our bundle identifier, we'll Base64 URL encode then replace '_' with '.'
// We also get rid of the typical '=' padding for base64 encoding.
auto base64 = base64URLEncodeToVector(input.utf8());
for (size_t i = 0; i < base64.size(); ++i) {
if (base64[i] == '_')
base64[i] = '.';
}
size_t numberEqualSigns = 0;
if (base64.size() >= 1 && base64.last() == '=')
++numberEqualSigns;
if (base64.size() >= 2 && base64[base64.size() - 2] == '=')
++numberEqualSigns;
return String::fromUTF8(base64.data(), base64.size() - numberEqualSigns);
}
static String originStringFromEncodedBundleIdentifier(const String& input)
{
auto utf8 = input.utf8();
auto buffer = utf8.mutableData();
for (size_t i = 0; i < utf8.length(); ++i) {
if (buffer[i] == '.')
buffer[i] = '_';
}
auto decoded = base64URLDecode(buffer, utf8.length());
if (!decoded)
return { };
return String::fromUTF8(decoded->data(), decoded->size());
}
static String bundlePrefixForHostAppIdentifier(const String& hostAppIdentifier)
{
auto appDigest = PAL::CryptoDigest::create(CryptoDigest::Algorithm::SHA_256);
auto utf8Identifier = hostAppIdentifier.utf8();
appDigest->addBytes(utf8Identifier.data(), utf8Identifier.length());
return makeString("com.apple.WebKit.PushBundle.", appDigest->toHexString(), ".");
}
static String bundleNameForOriginStringAndHostAppIdentifier(const String& originString, const String& hostAppIdentifier)
{
// FIXME: Once we can store the origin string within the app bundle, hash the origin string as well.
// For now we base64 encode it to make a valid bundle identifier that carries the information.
return makeString(bundlePrefixForHostAppIdentifier(hostAppIdentifier), encodeOriginStringForBundleIdentifier(originString));
}
void ICAppBundle::getOriginsWithRegistrations(ClientConnection& connection, CompletionHandler<void(const Vector<String>&)>&& completionHandler)
{
Vector<String> results;
RetainPtr<NSString> bundleIdentifierPrefix = (NSString *)bundlePrefixForHostAppIdentifier(connection.hostAppCodeSigningIdentifier());
LSEnumerator<LSApplicationRecord *> *enumerator = [LSApplicationRecord enumeratorWithOptions:LSApplicationEnumerationOptionsEnumeratePlaceholders];
for (LSApplicationRecord *record = [enumerator nextObject]; record; record = [enumerator nextObject]) {
if (![record.managementDomain isEqualToString:pushBundleAppManagementDomain])
continue;
if (![record.bundleIdentifier hasPrefix:bundleIdentifierPrefix.get()])
continue;
// FIXME: For now we store the origin string in the bundle identifier base64 encoded
// Once we store it in the bundle itself, we'll hash the origin string in the bundle identifier and extract it from the bundle
NSArray *components = [record.bundleIdentifier componentsSeparatedByString:bundleIdentifierPrefix.get()];
ASSERT(components.count >= 2);
auto originString = originStringFromEncodedBundleIdentifier(components[1]);
if (originString.isEmpty())
continue;
results.append(originString);
}
completionHandler(results);
}
const String& ICAppBundle::getBundleIdentifier()
{
if (m_bundleIdentifier.isEmpty() && m_clientConnection)
m_bundleIdentifier = bundleNameForOriginStringAndHostAppIdentifier(m_originString, m_clientConnection->hostAppCodeSigningIdentifier());
return m_bundleIdentifier;
}
void ICAppBundle::checkForExistingBundle()
{
NSError *error = nil;
RetainPtr<LSApplicationRecord> appRecord = adoptNS([[LSApplicationRecord alloc] initWithBundleIdentifier:(NSString *)getBundleIdentifier() allowPlaceholder:YES error:&error]);
if (m_client)
m_client->didCheckForExistingBundle(*this, appRecord ? PushAppBundleExists::Yes : PushAppBundleExists::No);
}
void ICAppBundle::deleteExistingBundle()
{
[IXAppInstallCoordinator uninstallAppWithBundleID:(NSString *)getBundleIdentifier() requestUserConfirmation:NO completion:[this, protectedThis = Ref { *this }] (NSError *error) {
callOnMainRunLoop([this, protectedThis = Ref { *this }, error = RetainPtr<NSError>(error)] {
didDeleteExistingBundleWithError(error.get());
});
}];
}
void ICAppBundle::didDeleteExistingBundleWithError(NSError *error)
{
if (error)
broadcastDebugMessage(makeString("Error deleting existing bundle: ", String([error description])));
if (m_client)
m_client->didDeleteExistingBundleWithError(*this, error);
}
void ICAppBundle::stop()
{
m_appInstallCoordinator.get().observer = nil;
m_appInstallCoordinator = nil;
m_appInstallObserver = nil;
}
void ICAppBundle::createBundle()
{
RetainPtr<NSString> bundleIdentifier = (NSString *)getBundleIdentifier();
// Cancel any previous install coordinator that might've been left hanging in a partially finished state
NSError *error = nil;
[IXAppInstallCoordinator cancelCoordinatorForAppWithBundleID:bundleIdentifier.get() withReason:[NSError errorWithDomain:@"WKErrorDomain" code:1 userInfo:nil] client:IXClientIdentifierAppliedFor error:&error];
BOOL created = NO;
error = nil;
m_appInstallCoordinator = [IXRestoringDemotedAppInstallCoordinator coordinatorForAppWithBundleID:bundleIdentifier.get() withClientID:IXClientIdentifierAppliedFor createIfNotExisting:YES created:&created error:&error];
if (!m_appInstallCoordinator || error) {
broadcastDebugMessage(makeString("Unable to create install coordinatior for app bundle identifier ", String(bundleIdentifier.get())));
bundleCreationFailed(error);
return;
}
if (!created) {
broadcastDebugMessage(makeString("Existing install coordinatior for app bundle identifier ", String(bundleIdentifier.get()), " was found. This is unexpected."));
bundleCreationFailed(error);
return;
}
m_appInstallObserver = adoptNS([[_WKAppInstallCoordinatorObserver alloc] initWithICAppBundle:*this]);
m_appInstallCoordinator.get().observer = m_appInstallObserver.get();
RetainPtr<IXPlaceholder> placeholder = adoptNS([[IXPlaceholder alloc] initAppPlaceholderWithBundleName:(NSString *)m_originString bundleID:bundleIdentifier.get() installType:LSInstallTypeIntentionalDowngrade client:IXClientIdentifierAppliedFor]);
RetainPtr<IXPlaceholderAttributes> placeholderAttributes = adoptNS([IXPlaceholderAttributes new]);
placeholderAttributes.get().launchProhibited = YES;
[placeholder setPlaceholderAttributes:placeholderAttributes.get() error:&error];
if (error) {
broadcastDebugMessage(makeString("Unable to set placeholder attributes for bundle ", String(bundleIdentifier.get()), " Error: ", String([error description])));
bundleCreationFailed(error);
return;
}
NSDictionary *entitlementsDictionary = @{
@"com.apple.private.usernotifications.bundle-identifiers": bundleIdentifier.get(),
@"com.apple.private.usernotifications.notification-simulator": @YES,
@"com.apple.developer.app-management-domain": pushBundleAppManagementDomain
};
RetainPtr<IXPromisedInMemoryDictionary> entitlementsPromise = adoptNS([[IXPromisedInMemoryDictionary alloc] initWithName:[NSString stringWithFormat:@"Entitlements Promise for %@", bundleIdentifier.get()] client:IXClientIdentifierAppliedFor dictionary:entitlementsDictionary]);
if (!entitlementsPromise) {
broadcastDebugMessage(makeString("Unable to create placeholder entitlements promise for bundle ", String(bundleIdentifier.get())));
bundleCreationFailed(error);
return;
}
error = nil;
if (![placeholder setEntitlementsPromise:entitlementsPromise.get() error:&error] || error) {
broadcastDebugMessage(makeString("Failed to set placeholder entitlements promise for bundle ", String(bundleIdentifier.get()), " Error: ", String([error description])));
bundleCreationFailed(error);
return;
}
BOOL result = [placeholder setConfigurationCompleteWithError:&error];
if (!result || error) {
broadcastDebugMessage(makeString("Failed to set placeholder entitlements promise complete for bundle ", String(bundleIdentifier.get()), " Error: ", String([error description])));
bundleCreationFailed(error);
return;
}
[m_appInstallCoordinator setPlaceholderPromise:placeholder.get() error:&error];
if (error) {
broadcastDebugMessage(makeString("Failed to set placeholder promise for bundle ", String(bundleIdentifier.get()), " Error: ", String([error description])));
bundleCreationFailed(error);
return;
}
RetainPtr<IXPromisedOutOfBandTransfer> dataPromise = adoptNS([[IXPromisedOutOfBandTransfer alloc] initWithName:@"Empty user data promise" client:IXClientIdentifierAppliedFor diskSpaceNeeded:0]);
dataPromise.get().percentComplete = 1.0;
dataPromise.get().complete = YES;
[m_appInstallCoordinator setUserDataPromise:dataPromise.get() error:&error];
if (error) {
broadcastDebugMessage(makeString("Failed to set user data promise for bundle ", String(bundleIdentifier.get()), " Error: ", String([error description])));
bundleCreationFailed(error);
return;
}
}
void ICAppBundle::bundleCreationSucceeded()
{
if (m_client)
m_client->didCreateAppBundle(*this, PushAppBundleCreationResult::Success);
}
void ICAppBundle::bundleCreationFailed(NSError *error)
{
if (m_client)
m_client->didCreateAppBundle(*this, PushAppBundleCreationResult::Failure);
}
} // namespace WebPushD
#endif // ENABLE(INSTALL_COORDINATION_BUNDLES)