🍎 altstore: implement ipa entitlement parser

This adds a parser for reading entitlement values from .ipa files.
Entitlement values are stored in files called
'.../embedded.mobileprovision' packed into .ipa files. These are CMS
signed plist files.

https://en.wikipedia.org/wiki/Cryptographic_Message_Syntax

This also ignores the 2 non-optional entitlements, as mentioned in
altstore docs:

https://faq.altstore.io/distribute-your-apps/make-a-source#entitlements-array-of-strings
This commit is contained in:
Michael Pöhn 2024-03-11 03:11:53 +01:00
parent 90d4051cc6
commit fbd43386bc
No known key found for this signature in database
GPG Key ID: 725F386C05529A5A
1 changed files with 16 additions and 153 deletions

View File

@ -34,6 +34,7 @@ import json
import time
import yaml
import copy
import asn1crypto.cms
import defusedxml.ElementTree as ElementTree
from datetime import datetime, timezone
from argparse import ArgumentParser
@ -598,149 +599,8 @@ IPA_PERMISSIONS = [
]
# known iOS app entitlements, source:
# https://developer.apple.com/documentation/bundleresources/entitlements
IPA_ENTITLEMENTS = [
b"aps-environment",
b"com.apple.developer.ClassKit-environment",
b"com.apple.developer.applesignin",
b"com.apple.developer.aps-environment",
b"com.apple.developer.associated-appclip-app-identifiers",
b"com.apple.developer.associated-domains",
b"com.apple.developer.associated-domains.applinks.read-write",
b"com.apple.developer.authentication-services.autofill-credential-provider",
b"com.apple.developer.automated-device-enrollment.add-devices",
b"com.apple.developer.automatic-assessment-configuration",
b"com.apple.developer.avfoundation.multitasking-camera-access",
b"com.apple.developer.browser.app-installation",
b"com.apple.developer.carplay-audio",
b"com.apple.developer.carplay-charging",
b"com.apple.developer.carplay-communication",
b"com.apple.developer.carplay-maps",
b"com.apple.developer.carplay-messaging",
b"com.apple.developer.carplay-parking",
b"com.apple.developer.carplay-quick-ordering",
b"com.apple.developer.contacts.notes",
b"com.apple.developer.default-data-protection",
b"com.apple.developer.device-information.user-assigned-device-name",
b"com.apple.developer.devicecheck.appattest-environment",
b"com.apple.developer.driverkit",
b"com.apple.developer.driverkit.allow-any-userclient-access",
b"com.apple.developer.driverkit.allow-third-party-userclients",
b"com.apple.developer.driverkit.communicates-with-drivers",
b"com.apple.developer.driverkit.family.audio",
b"com.apple.developer.driverkit.family.block-storage-device",
b"com.apple.developer.driverkit.family.hid.device",
b"com.apple.developer.driverkit.family.hid.eventservice",
b"com.apple.developer.driverkit.family.networking",
b"com.apple.developer.driverkit.family.scsicontroller",
b"com.apple.developer.driverkit.family.serial",
b"com.apple.developer.driverkit.transport.hid",
b"com.apple.developer.driverkit.transport.pci",
b"com.apple.developer.driverkit.transport.usb",
b"com.apple.developer.driverkit.userclient-access",
b"com.apple.developer.endpoint-security.client",
b"com.apple.developer.endpoint-security.client",
b"com.apple.developer.exposure-notification",
b"com.apple.developer.family-controls",
b"com.apple.developer.fileprovider.testing-mode",
b"com.apple.developer.game-center",
b"com.apple.developer.group-session",
b"com.apple.developer.healthkit",
b"com.apple.developer.healthkit.access",
b"com.apple.developer.healthkit.background-delivery",
b"com.apple.developer.healthkit.recalibrate-estimates",
b"com.apple.developer.hid.virtual.device",
b"com.apple.developer.homekit",
b"com.apple.developer.icloud-container-development-container-identifiers",
b"com.apple.developer.icloud-container-environment",
b"com.apple.developer.icloud-container-identifiers",
b"com.apple.developer.icloud-services",
b"com.apple.developer.in-app-identity-presentment",
b"com.apple.developer.in-app-identity-presentment.merchant-identifiers",
b"com.apple.developer.in-app-payments",
b"com.apple.developer.journal.allow",
b"com.apple.developer.kernel.extended-virtual-addressing",
b"com.apple.developer.kernel.increased-memory-limit",
b"com.apple.developer.location.push",
b"com.apple.developer.mail-client",
b"com.apple.developer.managed-app-distribution.install-ui",
b"com.apple.developer.maps",
b"com.apple.developer.marketplace.app-installation",
b"com.apple.developer.matter.allow-setup-payload",
b"com.apple.developer.media-device-discovery-extension",
b"com.apple.developer.networking.HotspotConfiguration",
b"com.apple.developer.networking.custom-protocol",
b"com.apple.developer.networking.manage-thread-network-credentials",
b"com.apple.developer.networking.multicast",
b"com.apple.developer.networking.multipath",
b"com.apple.developer.networking.networkextension",
b"com.apple.developer.networking.networkextension",
b"com.apple.developer.networking.slicing.appcategory",
b"com.apple.developer.networking.slicing.trafficcategory",
b"com.apple.developer.networking.vmnet",
b"com.apple.developer.networking.vpn.api",
b"com.apple.developer.networking.wifi-info",
b"com.apple.developer.nfc.hce",
b"com.apple.developer.nfc.hce.default-contactless-app",
b"com.apple.developer.nfc.hce.iso7816.select-identifier-prefixes",
b"com.apple.developer.nfc.readersession.formats",
b"com.apple.developer.on-demand-install-capable",
b"com.apple.developer.parent-application-identifiers",
b"com.apple.developer.pass-type-identifiers",
b"com.apple.developer.playable-content",
b"com.apple.developer.proximity-reader.identity.display",
b"com.apple.developer.proximity-reader.identity.read",
b"com.apple.developer.push-to-talk",
b"com.apple.developer.sensitivecontentanalysis.client",
b"com.apple.developer.sensorkit.reader.allow",
b"com.apple.developer.severe-vehicular-crash-event",
b"com.apple.developer.siri",
b"com.apple.developer.storekit.external-link.account",
b"com.apple.developer.storekit.external-purchase",
b"com.apple.developer.storekit.external-purchase-link",
b"com.apple.developer.sustained-execution",
b"com.apple.developer.system-extension.install",
b"com.apple.developer.system-extension.redistributable",
b"com.apple.developer.team-identifier",
b"com.apple.developer.ubiquity-kvstore-identifier",
b"com.apple.developer.upi-device-validation",
b"com.apple.developer.user-management",
b"com.apple.developer.usernotifications.filtering",
b"com.apple.developer.video-subscriber-single-sign-on",
b"com.apple.developer.weatherkit",
b"com.apple.developer.web-browser",
b"com.apple.developer.web-browser.public-key-credential",
b"com.apple.external-accessory.wireless-configuration",
b"com.apple.security.app-sandbox",
b"com.apple.security.application-groups",
b"com.apple.security.automation.apple-events",
b"com.apple.security.cs.allow-dyld-environment-variables",
b"com.apple.security.cs.allow-jit",
b"com.apple.security.cs.allow-unsigned-executable-memory",
b"com.apple.security.cs.debugger",
b"com.apple.security.cs.disable-executable-page-protection",
b"com.apple.security.cs.disable-library-validation",
b"com.apple.security.device.audio-input",
b"com.apple.security.device.camera",
b"com.apple.security.hypervisor",
b"com.apple.security.personal-information.addressbook",
b"com.apple.security.personal-information.calendars",
b"com.apple.security.personal-information.location",
b"com.apple.security.personal-information.photos-library",
b"com.apple.security.smartcard",
b"com.apple.security.virtualization",
b"com.apple.smoot.subscriptionservice",
b"com.apple.vm.device-access",
b"com.apple.vm.hypervisor",
b"com.apple.vm.networking",
b"inter-app-audio",
b"keychain-access-groups",
]
def parse_ipa(ipa_path, file_size, sha256):
from biplist import readPlist
import biplist
ipa = {
"apkName": os.path.basename(ipa_path),
@ -755,7 +615,7 @@ def parse_ipa(ipa_path, file_size, sha256):
for info in ipa_zip.infolist():
if re.match("Payload/[^/]*.app/Info.plist", info.filename):
with ipa_zip.open(info) as plist_file:
plist = readPlist(plist_file)
plist = biplist.readPlist(plist_file)
ipa["name"] = plist['CFBundleName']
ipa["packageName"] = plist["CFBundleIdentifier"]
# https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring
@ -768,12 +628,15 @@ def parse_ipa(ipa_path, file_size, sha256):
ipa["ipa_permissions"][ipap] = str(plist[ipap])
if info.filename.endswith("/embedded.mobileprovision"):
print("parsing", info.filename)
with ipa_zip.open(info) as mopro:
for line in mopro.readlines():
for entitlement in IPA_ENTITLEMENTS:
if entitlement in line:
ipa['ipa_entitlements'].add(str(entitlement, encoding="utf-8"))
with ipa_zip.open(info) as mopro_file:
mopro_content_info = cms.ContentInfo.load(mopro_file.read())
mopro_payload_info = mopro_content_info['content']
mopro_payload = mopro_payload_info['encap_content_info']['content'].native
mopro = biplist.readPlistFromString(mopro_payload)
# https://faq.altstore.io/distribute-your-apps/make-a-source#entitlements-array-of-strings
for entitlement in mopro.get('Entitlements', {}).keys():
if entitlement not in ["com.app.developer.team-identifier", 'application-identifier']:
ipa["ipa_entitlements"].add(entitlement)
return ipa
@ -1545,8 +1408,8 @@ def insert_localized_ios_app_metadata(apps_with_packages):
fdroidserver.update.copy_ios_screenshots_to_repo(screenshots, package_name)
# lookup icons, copy them and put them into app
icon_path = _get_ipa_icon(pathlib.Path('build') / package_name)
icon_dest = pathlib.Path('repo') / package_name / 'icon.png' # for now just assume png
icon_path = _get_ipa_icon(Path('build') / package_name)
icon_dest = Path('repo') / package_name / 'icon.png' # for now just assume png
icon_stat = os.stat(icon_path)
app['iconv2'] = {
DEFAULT_LOCALE: {
@ -1759,7 +1622,7 @@ def _get_apk_icons_src(apkfile, icon_name):
def _get_ipa_icon(src_dir):
"""Search source directory of an IPA project for the app icon."""
# parse app icon name from project config file
src_dir = pathlib.Path(src_dir)
src_dir = Path(src_dir)
prj = next(src_dir.glob("**/project.pbxproj"), None)
if not prj or not prj.exists():
return
@ -2670,7 +2533,7 @@ def altstore_index(apps, apks, config, repodir, indent=None):
# idx["subtitle"] F-Droid doesn't have a corresponding value
if config.get("repo_description"):
idx['description'] = config['repo_description']
if (pathlib.Path(repodir) / 'icons' / config['repo_icon']).exists():
if (Path(repodir) / 'icons' / config['repo_icon']).exists():
idx['iconURL'] = f"{config['repo_url']}/icons/{config['repo_icon']}"
# idx["headerURL"] F-Droid doesn't have a corresponding value
# idx["website"] F-Droid doesn't have a corresponding value