1
0
mirror of https://gitlab.com/fdroid/fdroidserver.git synced 2024-11-14 02:50:12 +01:00

🍎 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 2658c22933
commit 301f0c8273
No known key found for this signature in database
GPG Key ID: 725F386C05529A5A

View File

@ -34,6 +34,7 @@ import json
import time import time
import yaml import yaml
import copy import copy
import asn1crypto.cms
import defusedxml.ElementTree as ElementTree import defusedxml.ElementTree as ElementTree
from datetime import datetime, timezone from datetime import datetime, timezone
from argparse import ArgumentParser 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): def parse_ipa(ipa_path, file_size, sha256):
from biplist import readPlist import biplist
ipa = { ipa = {
"apkName": os.path.basename(ipa_path), "apkName": os.path.basename(ipa_path),
@ -755,7 +615,7 @@ def parse_ipa(ipa_path, file_size, sha256):
for info in ipa_zip.infolist(): for info in ipa_zip.infolist():
if re.match("Payload/[^/]*.app/Info.plist", info.filename): if re.match("Payload/[^/]*.app/Info.plist", info.filename):
with ipa_zip.open(info) as plist_file: with ipa_zip.open(info) as plist_file:
plist = readPlist(plist_file) plist = biplist.readPlist(plist_file)
ipa["name"] = plist['CFBundleName'] ipa["name"] = plist['CFBundleName']
ipa["packageName"] = plist["CFBundleIdentifier"] ipa["packageName"] = plist["CFBundleIdentifier"]
# https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring # 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]) ipa["ipa_permissions"][ipap] = str(plist[ipap])
if info.filename.endswith("/embedded.mobileprovision"): if info.filename.endswith("/embedded.mobileprovision"):
print("parsing", info.filename) print("parsing", info.filename)
with ipa_zip.open(info) as mopro: with ipa_zip.open(info) as mopro_file:
for line in mopro.readlines(): mopro_content_info = cms.ContentInfo.load(mopro_file.read())
for entitlement in IPA_ENTITLEMENTS: mopro_payload_info = mopro_content_info['content']
if entitlement in line: mopro_payload = mopro_payload_info['encap_content_info']['content'].native
ipa['ipa_entitlements'].add(str(entitlement, encoding="utf-8")) 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 return ipa
@ -1563,8 +1426,8 @@ def insert_localized_ios_app_metadata(apps_with_packages):
fdroidserver.update.copy_ios_screenshots_to_repo(screenshots, package_name) fdroidserver.update.copy_ios_screenshots_to_repo(screenshots, package_name)
# lookup icons, copy them and put them into app # lookup icons, copy them and put them into app
icon_path = _get_ipa_icon(pathlib.Path('build') / package_name) icon_path = _get_ipa_icon(Path('build') / package_name)
icon_dest = pathlib.Path('repo') / package_name / 'icon.png' # for now just assume png icon_dest = Path('repo') / package_name / 'icon.png' # for now just assume png
icon_stat = os.stat(icon_path) icon_stat = os.stat(icon_path)
app['iconv2'] = { app['iconv2'] = {
DEFAULT_LOCALE: { DEFAULT_LOCALE: {
@ -1777,7 +1640,7 @@ def _get_apk_icons_src(apkfile, icon_name):
def _get_ipa_icon(src_dir): def _get_ipa_icon(src_dir):
"""Search source directory of an IPA project for the app icon.""" """Search source directory of an IPA project for the app icon."""
# parse app icon name from project config file # 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) prj = next(src_dir.glob("**/project.pbxproj"), None)
if not prj or not prj.exists(): if not prj or not prj.exists():
return return
@ -2688,7 +2551,7 @@ def altstore_index(apps, apks, config, repodir, indent=None):
# idx["subtitle"] F-Droid doesn't have a corresponding value # idx["subtitle"] F-Droid doesn't have a corresponding value
if config.get("repo_description"): if config.get("repo_description"):
idx['description'] = config['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['iconURL'] = f"{config['repo_url']}/icons/{config['repo_icon']}"
# idx["headerURL"] F-Droid doesn't have a corresponding value # idx["headerURL"] F-Droid doesn't have a corresponding value
# idx["website"] F-Droid doesn't have a corresponding value # idx["website"] F-Droid doesn't have a corresponding value