Verifies downloaded app’s permissions match source

Renames source JSON permissions key to “appPermissions” in order to preserve backwards compatibility, since we’ve changed the schema for permissions.
This commit is contained in:
Riley Testut
2023-05-12 18:26:24 -05:00
committed by Magesh K
parent f884d72a8b
commit ee410605e8
16 changed files with 452 additions and 127 deletions

View File

@@ -179,10 +179,10 @@
BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */; };
BF66EE862501AE50007EE018 /* AltStoreCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BF66EE8C2501AEB2007EE018 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE8B2501AEB1007EE018 /* Keychain.swift */; };
BF66EE942501AEBC007EE018 /* ALTAppPermission.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE8E2501AEBC007EE018 /* ALTAppPermission.h */; settings = {ATTRIBUTES = (Public, ); }; };
BF66EE942501AEBC007EE018 /* ALTAppPermissions.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */; settings = {ATTRIBUTES = (Public, ); }; };
BF66EE952501AEBC007EE018 /* ALTSourceUserInfoKey.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */; settings = {ATTRIBUTES = (Public, ); }; };
BF66EE962501AEBC007EE018 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */; };
BF66EE972501AEBC007EE018 /* ALTAppPermission.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE912501AEBC007EE018 /* ALTAppPermission.m */; };
BF66EE972501AEBC007EE018 /* ALTAppPermissions.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */; };
BF66EE982501AEBC007EE018 /* ALTPatreonBenefitType.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */; settings = {ATTRIBUTES = (Public, ); }; };
BF66EE992501AEBC007EE018 /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE932501AEBC007EE018 /* ALTSourceUserInfoKey.m */; };
BF66EE9D2501AEC1007EE018 /* AppProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */; };
@@ -357,6 +357,7 @@
D561B2ED28EF5A4F006752E4 /* AltSign-Dynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
D5708417292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
D570841A2924680D00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
D571ADD02A02FC7200B24B63 /* ALTAppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */; };
D5728CA72A0D79D30014E73C /* OptionalProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */; };
D57968CB29CB99EF00539069 /* VibrantButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57968CA29CB99EF00539069 /* VibrantButton.swift */; };
D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; };
@@ -749,10 +750,10 @@
BF66EE802501AE50007EE018 /* AltStoreCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AltStoreCore.h; sourceTree = "<group>"; };
BF66EE812501AE50007EE018 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BF66EE8B2501AEB1007EE018 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
BF66EE8E2501AEBC007EE018 /* ALTAppPermission.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTAppPermission.h; sourceTree = "<group>"; };
BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTAppPermissions.h; sourceTree = "<group>"; };
BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTSourceUserInfoKey.h; sourceTree = "<group>"; };
BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTPatreonBenefitType.m; sourceTree = "<group>"; };
BF66EE912501AEBC007EE018 /* ALTAppPermission.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermission.m; sourceTree = "<group>"; };
BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermissions.m; sourceTree = "<group>"; };
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTPatreonBenefitType.h; sourceTree = "<group>"; };
BF66EE932501AEBC007EE018 /* ALTSourceUserInfoKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTSourceUserInfoKey.m; sourceTree = "<group>"; };
BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppProtocol.swift; sourceTree = "<group>"; };
@@ -914,6 +915,7 @@
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = "<group>"; };
D55E163528776CB000A627A1 /* ComplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationView.swift; sourceTree = "<group>"; };
D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = "<group>"; };
D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTAppPermission.swift; sourceTree = "<group>"; };
D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalProtocol.swift; sourceTree = "<group>"; };
D57968CA29CB99EF00539069 /* VibrantButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantButton.swift; sourceTree = "<group>"; };
D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = "<group>"; };
@@ -1428,8 +1430,8 @@
children = (
BFB39B5B252BC10E00D1BE50 /* Managed.swift */,
D5F48B4929CD0B67002B52A4 /* AsyncManaged.swift */,
BF66EE8E2501AEBC007EE018 /* ALTAppPermission.h */,
BF66EE912501AEBC007EE018 /* ALTAppPermission.m */,
BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */,
BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */,
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */,
BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */,
BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */,
@@ -1444,6 +1446,7 @@
BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */,
BF66EE9C2501AEC1007EE018 /* Fetchable.swift */,
D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */,
D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */,
);
path = Protocols;
sourceTree = "<group>";
@@ -1998,7 +2001,7 @@
BF66EE982501AEBC007EE018 /* ALTPatreonBenefitType.h in Headers */,
BFAECC5F2501B0BF00528F27 /* ALTConstants.h in Headers */,
BFAECC5D2501B0BF00528F27 /* ALTConnection.h in Headers */,
BF66EE942501AEBC007EE018 /* ALTAppPermission.h in Headers */,
BF66EE942501AEBC007EE018 /* ALTAppPermissions.h in Headers */,
BFAECC602501B0BF00528F27 /* NSError+ALTServerError.h in Headers */,
0EE7FDC82BE8CF4800D1E390 /* ALTWrappedError.h in Headers */,
BFAECC5E2501B0BF00528F27 /* CFNotificationName+AltStore.h in Headers */,
@@ -2618,7 +2621,7 @@
0E05025A2BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift in Sources */,
BF66EE8C2501AEB2007EE018 /* Keychain.swift in Sources */,
BF66EED42501AECA007EE018 /* AltStore5ToAltStore6.xcmappingmodel in Sources */,
BF66EE972501AEBC007EE018 /* ALTAppPermission.m in Sources */,
BF66EE972501AEBC007EE018 /* ALTAppPermissions.m in Sources */,
BFAECC552501B0A400528F27 /* Connection.swift in Sources */,
BF66EEDA2501AECA007EE018 /* RefreshAttempt.swift in Sources */,
0E05025C2BEC947000879B5C /* String+SideStore.swift in Sources */,
@@ -2654,6 +2657,7 @@
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */,
BFAECC562501B0A400528F27 /* ALTServerError+Conveniences.swift in Sources */,
BFAECC592501B0A400528F27 /* Result+Conveniences.swift in Sources */,
D571ADD02A02FC7200B24B63 /* ALTAppPermission.swift in Sources */,
D5E3FB9828FDFAD90034B72C /* NSError+AltStore.swift in Sources */,
BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */,
BF66EEE12501AECA007EE018 /* DatabaseManager.swift in Sources */,

View File

@@ -177,12 +177,17 @@ private extension AppContentViewController
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
{
let dataSource = RSTArrayCollectionViewDataSource(items: self.app.permissions)
let dataSource = RSTArrayCollectionViewDataSource(items: Array(self.app.permissions))
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
let cell = cell as! PermissionCollectionViewCell
cell.button.setImage(permission.type.icon, for: .normal)
cell.button.tintColor = .label
cell.textLabel.text = permission.type.localizedShortName ?? permission.type.localizedName
// cell.button.setImage(permission.type.icon, for: .normal)
// cell.button.tintColor = .label
// cell.textLabel.text = permission.type.localizedShortName ?? permission.type.localizedName
let icon = UIImage(systemName: permission.symbolName ?? "lock")
cell.button.setImage(icon, for: .normal)
cell.textLabel.text = permission.localizedDisplayName
}
return dataSource

View File

@@ -21,7 +21,7 @@ final class PermissionPopoverViewController: UIViewController
{
super.viewDidLoad()
self.nameLabel.text = self.permission.type.localizedName
self.nameLabel.text = self.permission.localizedName ?? self.permission.permission.rawValue
self.descriptionLabel.text = self.permission.usageDescription
}
}

View File

@@ -23,6 +23,8 @@ extension VerificationError
case mismatchedHash = 3
case mismatchedVersion = 4
case undeclaredPermissions = 6
}
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
@@ -40,6 +42,10 @@ extension VerificationError
static func mismatchedVersion(_ version: String, expectedVersion: String, app: AppProtocol) -> VerificationError {
VerificationError(code: .mismatchedVersion, app: app, version: version, expectedVersion: expectedVersion)
}
static func undeclaredPermissions(_ permissions: [any ALTAppPermission], app: AppProtocol) -> VerificationError {
VerificationError(code: .undeclaredPermissions, app: app, permissions: permissions)
}
}
struct VerificationError: ALTLocalizedError
@@ -60,6 +66,9 @@ struct VerificationError: ALTLocalizedError
@UserInfoValue var version: String?
@UserInfoValue var expectedVersion: String?
@UserInfoValue
var permissions: [any ALTAppPermission]?
var errorDescription: String? {
//TODO: Make this automatic somehow with ALTLocalizedError
guard self.errorFailure == nil else { return nil }
@@ -129,6 +138,52 @@ struct VerificationError: ALTLocalizedError
case .mismatchedVersion:
let appName = self.$app.name ?? NSLocalizedString("the app", comment: "")
return String(format: NSLocalizedString("The downloaded version of %@ does not match the version specified by the source.", comment: ""), appName)
case .undeclaredPermissions:
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
return String(format: NSLocalizedString("%@ requires additional permissions not specified by the source.", comment: ""), appName)
}
}
var recoverySuggestion: String? {
switch self.code
{
case .undeclaredPermissions:
guard let permissions, !permissions.isEmpty else { return nil }
let baseMessage = NSLocalizedString("These permissions must be declared by the source in order for AltStore to install this app:", comment: "")
let permissionsByType = Dictionary(grouping: permissions) { $0.type }
let permissionSections = [ALTAppPermissionType.entitlement, .privacy, .backgroundMode].compactMap { (type) -> String? in
guard let permissions = permissionsByType[type] else { return nil }
// "Privacy:"
var sectionText = "\(type.localizedName ?? type.rawValue):\n"
// Sort permissions + join into single string.
let sortedList = permissions.map { permission -> String in
if let localizedName = permission.localizedName
{
// "Entitlement Name (com.apple.entitlement.name)"
return "\(localizedName) (\(permission.rawValue))"
}
else
{
// "com.apple.entitlement.name"
return permission.rawValue
}
}
.sorted { $0.localizedStandardCompare($1) == .orderedAscending } // Case-insensitive sorting
.joined(separator: "\n")
sectionText += sortedList
return sectionText
}
let recoverySuggestion = ([baseMessage] + permissionSections).joined(separator: "\n\n")
return recoverySuggestion
default: return nil
}
}
}

View File

@@ -94,6 +94,16 @@ struct VerificationError: ALTLocalizedError {
}
}
import RegexBuilder
private extension ALTEntitlement
{
static var ignoredEntitlements: Set<ALTEntitlement> = [
.applicationIdentifier,
.teamIdentifier
]
}
@objc(VerifyAppOperation)
final class VerifyAppOperation: ResultOperation<Void>
{
@@ -146,6 +156,11 @@ final class VerifyAppOperation: ResultOperation<Void>
try await self.verifyHash(of: app, at: ipaURL, matches: appVersion)
try await self.verifyDownloadedVersion(of: app, matches: appVersion)
if let storeApp = await self.context.$appVersion.app
{
try await self.verifyPermissions(of: app, match: storeApp)
}
self.finish(.success(()))
}
catch
@@ -183,4 +198,81 @@ private extension VerifyAppOperation
guard version == app.version else { throw VerificationError.mismatchedVersion(app.version, expectedVersion: version, app: app) }
}
@discardableResult
func verifyPermissions(of app: ALTApplication, @AsyncManaged match storeApp: StoreApp) async throws -> [any ALTAppPermission]
{
// Entitlements
var allEntitlements = Set(app.entitlements.keys)
for appExtension in app.appExtensions
{
allEntitlements.formUnion(appExtension.entitlements.keys)
}
// Filter out ignored entitlements.
allEntitlements = allEntitlements.filter { !ALTEntitlement.ignoredEntitlements.contains($0) }
// Background Modes
// App extensions can't have background modes, so don't need to worry about them.
let allBackgroundModes: Set<ALTAppBackgroundMode>
if let backgroundModes = app.bundle.infoDictionary?[Bundle.Info.backgroundModes] as? [String]
{
let backgroundModes = backgroundModes.lazy.map { ALTAppBackgroundMode($0) }
allBackgroundModes = Set(backgroundModes)
}
else
{
allBackgroundModes = []
}
// Privacy
let allPrivacyPermissions: Set<ALTAppPrivacyPermission>
if #available(iOS 16, *)
{
let regex = Regex {
"NS"
// Capture permission "name"
Capture {
OneOrMore(.anyGraphemeCluster)
}
"UsageDescription"
// Optional suffix
Optionally(OneOrMore(.anyGraphemeCluster))
}
let privacyPermissions = ([app] + app.appExtensions).flatMap { (app) in
let permissions = app.bundle.infoDictionary?.keys.compactMap { key -> ALTAppPrivacyPermission? in
guard let match = key.wholeMatch(of: regex) else { return nil }
let permission = ALTAppPrivacyPermission(rawValue: String(match.1))
return permission
} ?? []
return permissions
}
allPrivacyPermissions = Set(privacyPermissions)
}
else
{
allPrivacyPermissions = []
}
// Verify permissions.
let sourcePermissions: Set<AnyHashable> = Set(await $storeApp.perform { $0.permissions.map { AnyHashable($0.permission) } })
let localPermissions: [any ALTAppPermission] = Array(allEntitlements) + Array(allBackgroundModes) + Array(allPrivacyPermissions)
// To pass: EVERY permission in localPermissions must also appear in sourcePermissions.
// If there is a single missing permission, throw error.
let missingPermissions: [any ALTAppPermission] = localPermissions.filter { !sourcePermissions.contains(AnyHashable($0)) }
guard missingPermissions.isEmpty else { throw VerificationError.undeclaredPermissions(missingPermissions, app: app) }
return localPermissions
}
}

View File

@@ -16,7 +16,7 @@ FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <AltStoreCore/PublicHeader.h>
#import <AltStoreCore/ALTAppPermission.h>
#import <AltStoreCore/ALTAppPermissions.h>
#import <AltStoreCore/ALTSourceUserInfoKey.h>
#import <AltStoreCore/ALTPatreonBenefitType.h>

View File

@@ -27,9 +27,18 @@
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="appBundleID" optional="YES" attributeType="String"/>
<attribute name="permission" attributeType="String"/>
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<attribute name="usageDescription" optional="YES" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="appBundleID"/>
<constraint value="permission"/>
<constraint value="type"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
<attribute name="appBundleID" attributeType="String"/>
@@ -191,7 +200,7 @@
<relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/>
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
<uniquenessConstraints>

View File

@@ -9,6 +9,7 @@
import CoreData
import UIKit
import AltSign
public extension ALTAppPermissionType
{
var localizedShortName: String? {
@@ -71,15 +72,29 @@ public extension ALTAppPermissionType
}
}
@objc(AppPermission)
@objc(AppPermission) @dynamicMemberLookup
public class AppPermission: NSManagedObject, Decodable, Fetchable
{
/* Properties */
@NSManaged public var type: ALTAppPermissionType
@NSManaged public var usageDescription: String
@NSManaged public var usageDescription: String?
@nonobjc public var permission: any ALTAppPermission {
switch self.type
{
case .entitlement: return ALTEntitlement(rawValue: self._permission)
case .privacy: return ALTAppPrivacyPermission(rawValue: self._permission)
case .backgroundMode: return ALTAppBackgroundMode(rawValue: self._permission)
default: return UnknownAppPermission(rawValue: self._permission)
}
}
@NSManaged @objc(permission) private var _permission: String
// Set by StoreApp.
@NSManaged public var appBundleID: String?
/* Relationships */
@NSManaged public private(set) var app: StoreApp!
@NSManaged public internal(set) var app: StoreApp?
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
@@ -88,7 +103,10 @@ public class AppPermission: NSManagedObject, Decodable, Fetchable
private enum CodingKeys: String, CodingKey
{
case type
case entitlement
case privacyType = "privacy"
case backgroundMode = "background"
case usageDescription
}
@@ -101,10 +119,33 @@ public class AppPermission: NSManagedObject, Decodable, Fetchable
do
{
let container = try decoder.container(keyedBy: CodingKeys.self)
self.usageDescription = try container.decode(String.self, forKey: .usageDescription)
let rawType = try container.decode(String.self, forKey: .type)
self.type = ALTAppPermissionType(rawValue: rawType)
self.usageDescription = try container.decodeIfPresent(String.self, forKey: .usageDescription)
if let entitlement = try container.decodeIfPresent(String.self, forKey: .entitlement)
{
self._permission = entitlement
self.type = .entitlement
}
else if let privacyType = try container.decodeIfPresent(String.self, forKey: .privacyType)
{
self._permission = privacyType
self.type = .privacy
}
else if let backgroundMode = try container.decodeIfPresent(String.self, forKey: .backgroundMode)
{
self._permission = backgroundMode
self.type = .backgroundMode
}
else
{
self._permission = ""
self.type = .unknown
// We don't want to save any unknown permissions, but can't throw error
// without making the entire decoding fail, so just delete self instead.
context.delete(self)
}
}
catch
{
@@ -125,3 +166,14 @@ public extension AppPermission
return NSFetchRequest<AppPermission>(entityName: "AppPermission")
}
}
// @dynamicMemberLookup
public extension AppPermission
{
// Convenience for accessing .permission properties.
subscript<T>(dynamicMember keyPath: KeyPath<any ALTAppPermission, T>) -> T {
get {
return self.permission[keyPath: keyPath]
}
}
}

View File

@@ -19,10 +19,12 @@ extension MergeError
case noVersions
case incorrectVersionOrder
case incorrectPermissions
}
static func noVersions(for app: StoreApp) -> MergeError { .init(code: .noVersions, appName: app.name, appBundleID: app.bundleIdentifier, sourceID: app.sourceIdentifier) }
static func incorrectVersionOrder(for app: StoreApp) -> MergeError { .init(code: .incorrectVersionOrder, appName: app.name, appBundleID: app.bundleIdentifier, sourceID: app.sourceIdentifier) }
static func incorrectPermissions(for app: StoreApp) -> MergeError { .init(code: .incorrectPermissions, appName: app.name, appBundleID: app.bundleIdentifier, sourceID: app.sourceIdentifier) }
}
public struct MergeError: ALTLocalizedError
@@ -57,6 +59,15 @@ public struct MergeError: ALTLocalizedError
}
return String(format: NSLocalizedString("The cached versions for %@ do not match the source.", comment: ""), appName)
case .incorrectPermissions:
var appName = NSLocalizedString("one or more apps", comment: "")
if let name = self.appName, let bundleID = self.appBundleID
{
appName = name + " (\(bundleID))"
}
return String(format: NSLocalizedString("The cached permissions for %@ do not match the source.", comment: ""), appName)
}
}
@@ -134,8 +145,8 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
if let previousApp = conflict.conflictingObjects.first(where: { !$0.isInserted }) as? StoreApp
{
// Delete previous permissions (same as below).
for permission in previousApp.permissions
// Delete previous permissions (different than below).
for case let permission as AppPermission in previousApp._permissions where permission.app == nil
{
permission.managedObjectContext?.delete(permission)
}
@@ -171,6 +182,8 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
}
var sortedVersionsByGlobalAppID = [String: NSOrderedSet]()
var permissionsByGlobalAppID = [String: Set<AnyHashable>]()
var featuredAppIDsBySourceID = [String: [String]]()
for conflict in conflicts
@@ -178,28 +191,28 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
switch conflict.databaseObject
{
case let databaseObject as StoreApp:
// Delete previous permissions
for permission in databaseObject.permissions
guard let contextApp = conflict.conflictingObjects.first as? StoreApp else { break }
// Permissions
let contextPermissions = Set(contextApp._permissions.lazy.compactMap { $0 as? AppPermission }.map { AnyHashable($0.permission) })
for case let databasePermission as AppPermission in databaseObject._permissions where !contextPermissions.contains(AnyHashable(databasePermission.permission))
{
permission.managedObjectContext?.delete(permission)
// Permission does NOT exist in context, so delete existing databasePermission.
databasePermission.managedObjectContext?.delete(databasePermission)
}
if let contextApp = conflict.conflictingObjects.first as? StoreApp
// Versions
let contextVersions = NSOrderedSet(array: contextApp._versions.lazy.compactMap { $0 as? AppVersion }.map { $0.version })
for case let databaseVersion as AppVersion in databaseObject._versions where !contextVersions.contains(databaseVersion.version)
{
let contextVersions = NSOrderedSet(array: contextApp._versions.lazy.compactMap { $0 as? AppVersion }.map { $0.version })
for case let appVersion as AppVersion in databaseObject._versions where !contextVersions.contains(appVersion.version)
{
// Version # does NOT exist in context, so delete existing appVersion.
appVersion.managedObjectContext?.delete(appVersion)
}
if let globallyUniqueID = contextApp.globallyUniqueID
{
// Core Data _normally_ preserves the correct ordering of versions when merging,
// but just in case we cache the order and reorder the versions post-merge if needed.
sortedVersionsByGlobalAppID[globallyUniqueID] = contextVersions
}
// Version # does NOT exist in context, so delete existing databaseVersion.
databaseVersion.managedObjectContext?.delete(databaseVersion)
}
if let globallyUniqueID = contextApp.globallyUniqueID
{
permissionsByGlobalAppID[globallyUniqueID] = contextPermissions
sortedVersionsByGlobalAppID[globallyUniqueID] = contextVersions
}
case let databaseObject as Source:
@@ -244,36 +257,44 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
case let databaseObject as StoreApp:
do
{
let appVersions: [AppVersion]
var appVersions = databaseObject.versions
if let globallyUniqueID = databaseObject.globallyUniqueID,
let sortedAppVersions = sortedVersionsByGlobalAppID[globallyUniqueID],
let sortedAppVersionsArray = sortedAppVersions.array as? [String],
case let databaseVersions = databaseObject.versions.map({ $0.version }),
databaseVersions != sortedAppVersionsArray
if let globallyUniqueID = databaseObject.globallyUniqueID
{
// databaseObject.versions post-merge doesn't match contextApp.versions pre-merge, so attempt to fix by re-sorting.
let fixedAppVersions = databaseObject.versions.sorted { (versionA, versionB) in
let indexA = sortedAppVersions.index(of: versionA.version)
let indexB = sortedAppVersions.index(of: versionB.version)
return indexA < indexB
// Permissions
if let appPermissions = permissionsByGlobalAppID[globallyUniqueID],
case let databasePermissions = Set(databaseObject.permissions.map({ AnyHashable($0.permission) })),
databasePermissions != appPermissions
{
// Sorting order doesn't matter, but elements themselves don't match so throw error.
throw MergeError.incorrectPermissions(for: databaseObject)
}
let appVersionValues = fixedAppVersions.map { $0.version }
guard appVersionValues == sortedAppVersionsArray else {
// fixedAppVersions still doesn't match source's versions, so throw MergeError.
throw MergeError.incorrectVersionOrder(for: databaseObject)
// App versions
if let sortedAppVersions = sortedVersionsByGlobalAppID[globallyUniqueID],
let sortedAppVersionsArray = sortedAppVersions.array as? [String],
case let databaseVersions = databaseObject.versions.map({ $0.version }),
databaseVersions != sortedAppVersionsArray
{
// databaseObject.versions post-merge doesn't match contextApp.versions pre-merge, so attempt to fix by re-sorting.
let fixedAppVersions = databaseObject.versions.sorted { (versionA, versionB) in
let indexA = sortedAppVersions.index(of: versionA.version)
let indexB = sortedAppVersions.index(of: versionB.version)
return indexA < indexB
}
let appVersionValues = fixedAppVersions.map { $0.version }
guard appVersionValues == sortedAppVersionsArray else {
// fixedAppVersions still doesn't match source's versions, so throw MergeError.
throw MergeError.incorrectVersionOrder(for: databaseObject)
}
appVersions = fixedAppVersions
}
appVersions = fixedAppVersions
}
else
{
appVersions = databaseObject.versions
}
// Update versions post-merging to make sure latestSupportedVersion is correct.
// Always update versions post-merging to make sure latestSupportedVersion is correct.
try databaseObject.setVersions(appVersions)
}
catch

View File

@@ -146,8 +146,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
@NSManaged @objc(source) public var _source: Source?
@NSManaged public internal(set) var featuringSource: Source?
@NSManaged @objc(permissions) public var _permissions: NSOrderedSet
@NSManaged @objc(latestVersion) public private(set) var latestSupportedVersion: AppVersion?
@NSManaged @objc(versions) public private(set) var _versions: NSOrderedSet
@@ -163,9 +161,10 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
}
}
@nonobjc public var permissions: [AppPermission] {
return self._permissions.array as! [AppPermission]
@nonobjc public var permissions: Set<AppPermission> {
return self._permissions as! Set<AppPermission>
}
@NSManaged @objc(permissions) internal private(set) var _permissions: NSSet // Use NSSet to avoid eagerly fetching values.
@nonobjc public var versions: [AppVersion] {
return self._versions.array as! [AppVersion]
@@ -216,7 +215,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
case platformURLs
case tintColor
case subtitle
case permissions
case permissions = "appPermissions"
case size
case isBeta = "beta"
case versions
@@ -287,7 +286,11 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
self._permissions = NSOrderedSet(array: permissions)
for permission in permissions
{
permission.appBundleID = self.bundleIdentifier
}
self._permissions = NSSet(array: permissions)
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions)
{
@@ -367,6 +370,23 @@ internal extension StoreApp
self._downloadURL = latestVersion.downloadURL
self._size = Int32(latestVersion.size)
}
func setPermissions(_ permissions: Set<AppPermission>)
{
for case let permission as AppPermission in self._permissions
{
if permissions.contains(permission)
{
permission.app = self
}
else
{
permission.app = nil
}
}
self._permissions = permissions as NSSet
}
}
public extension StoreApp

View File

@@ -0,0 +1,89 @@
//
// ALTAppPermission.swift
// AltStoreCore
//
// Created by Riley Testut on 5/3/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import AltSign
public extension ALTAppPermissionType
{
var localizedName: String? {
switch self
{
case .unknown: return NSLocalizedString("Permission", comment: "")
case .entitlement: return NSLocalizedString("Entitlement", comment: "")
case .privacy: return NSLocalizedString("Privacy Permission", comment: "")
case .backgroundMode: return NSLocalizedString("Background Mode", comment: "")
default: return nil
}
}
}
public protocol ALTAppPermission: RawRepresentable<String>, Hashable
{
var type: ALTAppPermissionType { get }
var symbolName: String? { get }
var localizedName: String? { get }
var localizedDisplayName: String { get } // Default implementation
}
public extension ALTAppPermission
{
var localizedDisplayName: String {
return self.localizedName ?? self.rawValue
}
func isEqual(_ permission: any ALTAppPermission) -> Bool
{
guard let permission = permission as? Self else { return false }
return self == permission
}
static func ==(lhs: Self, rhs: any ALTAppPermission) -> Bool
{
return lhs.isEqual(rhs)
}
}
public struct UnknownAppPermission: ALTAppPermission
{
public var type: ALTAppPermissionType { .unknown }
public var symbolName: String? { nil }
public var localizedName: String? { nil }
public var rawValue: String
public init(rawValue: String)
{
self.rawValue = rawValue
}
}
extension ALTEntitlement: ALTAppPermission
{
public var type: ALTAppPermissionType { .entitlement }
public var symbolName: String? { nil }
public var localizedName: String? { nil }
}
extension ALTAppPrivacyPermission: ALTAppPermission
{
public var type: ALTAppPermissionType { .privacy }
public var symbolName: String? { nil }
public var localizedName: String? { nil }
}
extension ALTAppBackgroundMode: ALTAppPermission
{
public var type: ALTAppPermissionType { .backgroundMode }
public var symbolName: String? { nil }
public var localizedName: String? { nil }
}

View File

@@ -1,28 +0,0 @@
//
// ALTAppPermission.h
// AltStore
//
// Created by Riley Testut on 7/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef NSString *ALTAppPermissionType NS_TYPED_EXTENSIBLE_ENUM;
extern ALTAppPermissionType const ALTAppPermissionTypePhotos;
extern ALTAppPermissionType const ALTAppPermissionTypeCamera;
extern ALTAppPermissionType const ALTAppPermissionTypeLocation;
extern ALTAppPermissionType const ALTAppPermissionTypeContacts;
extern ALTAppPermissionType const ALTAppPermissionTypeReminders;
extern ALTAppPermissionType const ALTAppPermissionTypeAppleMusic;
extern ALTAppPermissionType const ALTAppPermissionTypeMicrophone;
extern ALTAppPermissionType const ALTAppPermissionTypeSpeechRecognition;
extern ALTAppPermissionType const ALTAppPermissionTypeBackgroundAudio;
extern ALTAppPermissionType const ALTAppPermissionTypeBackgroundFetch;
extern ALTAppPermissionType const ALTAppPermissionTypeBluetooth;
extern ALTAppPermissionType const ALTAppPermissionTypeNetwork;
extern ALTAppPermissionType const ALTAppPermissionTypeCalendars;
extern ALTAppPermissionType const ALTAppPermissionTypeTouchID;
extern ALTAppPermissionType const ALTAppPermissionTypeFaceID;
extern ALTAppPermissionType const ALTAppPermissionTypeSiri;
extern ALTAppPermissionType const ALTAppPermissionTypeMotion;

View File

@@ -1,27 +0,0 @@
//
// ALTAppPermission.m
// AltStore
//
// Created by Riley Testut on 7/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
#import "ALTAppPermission.h"
ALTAppPermissionType const ALTAppPermissionTypePhotos = @"photos";
ALTAppPermissionType const ALTAppPermissionTypeCamera = @"camera";
ALTAppPermissionType const ALTAppPermissionTypeLocation = @"location";
ALTAppPermissionType const ALTAppPermissionTypeContacts = @"contacts";
ALTAppPermissionType const ALTAppPermissionTypeReminders = @"reminders";
ALTAppPermissionType const ALTAppPermissionTypeAppleMusic = @"music";
ALTAppPermissionType const ALTAppPermissionTypeMicrophone = @"microphone";
ALTAppPermissionType const ALTAppPermissionTypeSpeechRecognition = @"speech-recognition";
ALTAppPermissionType const ALTAppPermissionTypeBackgroundAudio = @"background-audio";
ALTAppPermissionType const ALTAppPermissionTypeBackgroundFetch = @"background-fetch";
ALTAppPermissionType const ALTAppPermissionTypeBluetooth = @"bluetooth";
ALTAppPermissionType const ALTAppPermissionTypeNetwork = @"network";
ALTAppPermissionType const ALTAppPermissionTypeCalendars = @"calendars";
ALTAppPermissionType const ALTAppPermissionTypeTouchID = @"touchid";
ALTAppPermissionType const ALTAppPermissionTypeFaceID = @"faceid";
ALTAppPermissionType const ALTAppPermissionTypeSiri = @"siri";
ALTAppPermissionType const ALTAppPermissionTypeMotion = @"motion";

View File

@@ -0,0 +1,18 @@
//
// ALTAppPermissions.h
// AltStore
//
// Created by Riley Testut on 7/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef NSString *ALTAppPermissionType NS_TYPED_EXTENSIBLE_ENUM;
extern ALTAppPermissionType const ALTAppPermissionTypeUnknown;
extern ALTAppPermissionType const ALTAppPermissionTypeEntitlement;
extern ALTAppPermissionType const ALTAppPermissionTypePrivacy;
extern ALTAppPermissionType const ALTAppPermissionTypeBackgroundMode;
typedef NSString *ALTAppPrivacyPermission NS_TYPED_EXTENSIBLE_ENUM;
typedef NSString *ALTAppBackgroundMode NS_TYPED_EXTENSIBLE_ENUM;

View File

@@ -0,0 +1,14 @@
//
// ALTAppPermissions.m
// AltStore
//
// Created by Riley Testut on 7/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
#import "ALTAppPermissions.h"
ALTAppPermissionType const ALTAppPermissionTypeUnknown = @"unknown";
ALTAppPermissionType const ALTAppPermissionTypeEntitlement = @"entitlement";
ALTAppPermissionType const ALTAppPermissionTypePrivacy = @"privacy";
ALTAppPermissionType const ALTAppPermissionTypeBackgroundMode = @"background";

View File

@@ -22,6 +22,7 @@ public extension Bundle
public static let devicePairingString = "ALTPairingFile"
public static let urlTypes = "CFBundleURLTypes"
public static let exportedUTIs = "UTExportedTypeDeclarations"
public static let backgroundModes = "UIBackgroundModes"
public static let untetherURL = "ALTFugu14UntetherURL"
public static let untetherRequired = "ALTFugu14UntetherRequired"