2020-05-02 22:06:57 -07:00
//
// V e r i f y A p p O p e r a t i o n . s w i f t
// A l t S t o r e
//
// C r e a t e d b y R i l e y T e s t u t o n 5 / 2 / 2 0 .
// C o p y r i g h t © 2 0 2 0 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import Foundation
2023-05-11 17:02:20 -05:00
import CryptoKit
2020-05-02 22:06:57 -07:00
2024-08-06 10:43:52 +09:00
import AltStoreCore
2020-05-02 22:06:57 -07:00
import AltSign
import Roxas
2024-08-06 10:43:52 +09:00
extension VerificationError
2020-05-02 22:06:57 -07:00
{
2024-08-06 10:43:52 +09:00
enum Code : Int , ALTErrorCode , CaseIterable {
typealias Error = VerificationError
case privateEntitlements
case mismatchedBundleIdentifiers
case iOSVersionNotSupported
2020-05-02 22:06:57 -07:00
}
2024-08-06 10:43:52 +09:00
static func privateEntitlements ( _ entitlements : [ String : Any ] , app : ALTApplication ) -> VerificationError {
VerificationError ( code : . privateEntitlements , app : app , entitlements : entitlements )
}
static func mismatchedBundleIdentifiers ( sourceBundleID : String , app : ALTApplication ) -> VerificationError {
VerificationError ( code : . mismatchedBundleIdentifiers , app : app , sourceBundleID : sourceBundleID )
2020-05-02 22:06:57 -07:00
}
2024-08-06 10:43:52 +09:00
static func iOSVersionNotSupported ( app : AppProtocol , osVersion : OperatingSystemVersion = ProcessInfo . processInfo . operatingSystemVersion , requiredOSVersion : OperatingSystemVersion ? ) -> VerificationError {
VerificationError ( code : . iOSVersionNotSupported , app : app )
}
}
struct VerificationError : ALTLocalizedError {
let code : Code
var errorTitle : String ?
var errorFailure : String ?
@ Managed var app : AppProtocol ?
var sourceBundleID : String ?
var deviceOSVersion : OperatingSystemVersion ?
var requiredOSVersion : OperatingSystemVersion ?
2020-05-02 22:06:57 -07:00
2024-08-06 10:43:52 +09:00
var errorDescription : String ? {
switch self . code {
case . iOSVersionNotSupported :
guard let deviceOSVersion else { return nil }
var failureReason = self . errorFailureReason
if self . app = = nil {
let firstLetter = failureReason . prefix ( 1 ) . lowercased ( )
failureReason = firstLetter + failureReason . dropFirst ( )
}
return String ( formatted : " This device is running iOS %@, but %@ " , deviceOSVersion . stringValue , failureReason )
default : return nil
}
2022-12-12 15:34:09 -06:00
return self . errorFailureReason
2024-08-06 10:43:52 +09:00
}
var errorFailureReason : String {
switch self . code
2020-05-02 22:06:57 -07:00
{
2024-08-06 10:43:52 +09:00
case . privateEntitlements :
let appName = self . $ app . name ? ? NSLocalizedString ( " The app " , comment : " " )
return String ( formatted : " “%@” requires private permissions. " , appName )
case . mismatchedBundleIdentifiers :
if let appBundleID = self . $ app . bundleIdentifier , let bundleID = self . sourceBundleID {
return String ( formatted : " The bundle ID '%@' does not match the one specified by the source ('%@'). " , appBundleID , bundleID )
} else {
return NSLocalizedString ( " The bundle ID does not match the one specified by the source. " , comment : " " )
}
case . iOSVersionNotSupported :
let appName = self . $ app . name ? ? NSLocalizedString ( " The app " , comment : " " )
let deviceOSVersion = self . deviceOSVersion ? ? ProcessInfo . processInfo . operatingSystemVersion
guard let requiredOSVersion else {
return String ( formatted : " %@ does not support iOS %@. " , appName , deviceOSVersion . stringValue )
}
if deviceOSVersion > requiredOSVersion {
return String ( formatted : " %@ requires iOS %@ or earlier " , appName , requiredOSVersion . stringValue )
} else {
return String ( formatted : " %@ requires iOS %@ or later " , appName , requiredOSVersion . stringValue )
2021-02-26 13:46:49 -06:00
}
2020-05-02 22:06:57 -07:00
}
}
}
2023-05-12 18:26:24 -05:00
import RegexBuilder
private extension ALTEntitlement
{
static var ignoredEntitlements : Set < ALTEntitlement > = [
. applicationIdentifier ,
. teamIdentifier
]
}
2023-05-15 15:13:13 -05:00
extension VerifyAppOperation
{
enum PermissionReviewMode
{
case none
case all
case added
}
}
2020-05-02 22:06:57 -07:00
@objc ( VerifyAppOperation )
2023-01-04 09:52:12 -05:00
final class VerifyAppOperation : ResultOperation < Void >
2020-05-02 22:06:57 -07:00
{
2023-05-15 15:13:13 -05:00
let permissionsMode : PermissionReviewMode
2023-05-11 17:02:20 -05:00
let context : InstallAppOperationContext
2023-05-15 15:13:13 -05:00
init ( permissionsMode : PermissionReviewMode , context : InstallAppOperationContext )
2020-05-02 22:06:57 -07:00
{
2023-05-15 15:13:13 -05:00
self . permissionsMode = permissionsMode
2020-05-02 22:06:57 -07:00
self . context = context
super . init ( )
}
override func main ( )
{
super . main ( )
do
{
if let error = self . context . error
{
throw error
}
2024-11-10 02:54:18 +05:30
let appName = self . context . app ? . name ? ? NSLocalizedString ( " The app " , comment : " " )
self . localizedFailure = String ( format : NSLocalizedString ( " %@ could not be installed. " , comment : " " ) , appName )
2020-05-02 22:06:57 -07:00
2024-11-09 14:35:18 +05:30
guard let app = self . context . app else {
throw OperationError . invalidParameters ( " VerifyAppOperation.main: self.context.app is nil " )
}
2020-05-07 13:13:05 -07:00
2024-08-06 10:43:52 +09:00
if ! [ " ny.litritt.ignited " , " com.litritt.ignited " ] . contains ( where : { $0 = = app . bundleIdentifier } ) {
guard app . bundleIdentifier = = self . context . bundleIdentifier else {
throw VerificationError . mismatchedBundleIdentifiers ( sourceBundleID : self . context . bundleIdentifier , app : app )
}
2020-05-07 13:13:05 -07:00
}
2021-02-26 13:46:49 -06:00
guard ProcessInfo . processInfo . isOperatingSystemAtLeast ( app . minimumiOSVersion ) else {
2024-08-06 10:43:52 +09:00
throw VerificationError . iOSVersionNotSupported ( app : app , requiredOSVersion : app . minimumiOSVersion )
2021-02-26 13:46:49 -06:00
}
2023-05-11 17:02:20 -05:00
guard let appVersion = self . context . appVersion else {
return self . finish ( . success ( ( ) ) )
}
Task < Void , Never > {
do
{
2023-05-22 15:02:26 -05:00
do
{
guard let ipaURL = self . context . ipaURL else { throw OperationError . appNotFound ( name : app . name ) }
try await self . verifyHash ( of : app , at : ipaURL , matches : appVersion )
try await self . verifyDownloadedVersion ( of : app , matches : appVersion )
// V e r i f y p e r m i s s i o n s l a s t i n c a s e u s e r b y p a s s e s e r r o r .
try await self . verifyPermissions ( of : app , match : appVersion )
}
catch let error as VerificationError where error . code = = . undeclaredPermissions
{
#if ! BETA
throw error
#endif
if let trustedSources = UserDefaults . shared . trustedSources , let sourceID = await self . context . $ appVersion . sourceID
{
let isTrusted = trustedSources . contains { $0 . identifier = = sourceID }
guard ! isTrusted else {
// D o n ' t e n f o r c e p e r m i s s i o n c h e c k i n g f o r T r u s t e d S o u r c e s w h i l e 2 . 0 i s i n b e t a .
return self . finish ( . success ( ( ) ) )
}
}
// W h i l e i n b e t a , a l l o w u s e r s t o t e m p o r a r i l y b y p a s s p e r m i s s i o n s a l e r t
// s o s o u r c e m a i n t a i n e r s h a v e t i m e t o u p d a t e t h e i r s o u r c e s .
guard let presentingViewController = self . context . presentingViewController else { throw error }
let message = NSLocalizedString ( " While AltStore 2.0 is in beta, you may choose to ignore this warning at your own risk until the source is updated. " , comment : " " )
let ignoreAction = await UIAlertAction ( title : NSLocalizedString ( " Install Anyway " , comment : " " ) , style : . destructive )
let viewPermissionsAction = await UIAlertAction ( title : NSLocalizedString ( " View Permisions " , comment : " " ) , style : . default )
while true
{
let action = try await presentingViewController . presentConfirmationAlert ( title : error . errorFailureReason ,
message : message ,
actions : [ ignoreAction , viewPermissionsAction ] )
guard action = = viewPermissionsAction else { break } // b r e a k l o o p t o c o n t i n u e w i t h i n s t a l l a t i o n ( u n l e s s w e ' r e v i e w i n g p e r m i s s i o n s ) .
await presentingViewController . presentAlert ( title : NSLocalizedString ( " Undeclared Permissions " , comment : " " ) , message : error . recoverySuggestion )
}
}
2023-05-12 18:26:24 -05:00
2023-05-11 17:02:20 -05:00
self . finish ( . success ( ( ) ) )
}
catch
{
self . finish ( . failure ( error ) )
}
}
2020-05-02 22:06:57 -07:00
}
catch
{
self . finish ( . failure ( error ) )
}
}
}
2023-05-11 17:02:20 -05:00
private extension VerifyAppOperation
{
func verifyHash ( of app : ALTApplication , at ipaURL : URL , @ AsyncManaged matches appVersion : AppVersion ) async throws
{
// D o n o t h i n g i f s o u r c e d o e s n ' t p r o v i d e h a s h .
guard let expectedHash = await $ appVersion . sha256 else { return }
let data = try Data ( contentsOf : ipaURL )
let sha256Hash = SHA256 . hash ( data : data )
let hashString = sha256Hash . compactMap { String ( format : " %02x " , $0 ) } . joined ( )
2023-10-18 14:06:10 -05:00
Logger . sideload . debug ( " Comparing app hash ( \( hashString , privacy : . public ) ) against expected hash ( \( expectedHash , privacy : . public ) )... " )
2023-05-11 17:02:20 -05:00
guard hashString = = expectedHash else { throw VerificationError . mismatchedHash ( hashString , expectedHash : expectedHash , app : app ) }
}
2023-05-11 17:47:03 -05:00
func verifyDownloadedVersion ( of app : ALTApplication , @ AsyncManaged matches appVersion : AppVersion ) async throws
{
2023-05-18 15:55:26 -05:00
let ( version , buildVersion ) = await $ appVersion . perform { ( $0 . version , $0 . buildVersion ) }
2023-05-11 17:47:03 -05:00
guard version = = app . version else { throw VerificationError . mismatchedVersion ( app . version , expectedVersion : version , app : app ) }
2023-05-18 15:55:26 -05:00
if let buildVersion
{
guard buildVersion = = app . buildVersion else { throw VerificationError . mismatchedBuildVersion ( app . buildVersion , expectedVersion : buildVersion , app : app ) }
}
2023-05-11 17:47:03 -05:00
}
2023-05-12 18:26:24 -05:00
2023-05-15 15:13:13 -05:00
func verifyPermissions ( of app : ALTApplication , @ AsyncManaged match appVersion : AppVersion ) async throws
{
guard self . permissionsMode != . none else { return }
guard let storeApp = await $ appVersion . app else { throw OperationError . invalidParameters }
// V e r i f y s o u r c e p e r m i s s i o n s m a t c h f i r s t .
2023-05-26 14:47:15 -05:00
_ = try await self . verifyPermissions ( of : app , match : storeApp )
2023-05-15 15:13:13 -05:00
2023-05-26 14:47:15 -05:00
// TODO: U n c o m m e n t t o v e r i f y a d d e d p e r m i s s i o n s .
// s w i t c h s e l f . p e r m i s s i o n s M o d e
// {
// c a s e . n o n e , . a l l : b r e a k
// c a s e . a d d e d :
// l e t i n s t a l l e d A p p U R L = I n s t a l l e d A p p . f i l e U R L ( f o r : a p p )
// g u a r d l e t p r e v i o u s A p p = A L T A p p l i c a t i o n ( f i l e U R L : i n s t a l l e d A p p U R L ) e l s e { t h r o w O p e r a t i o n E r r o r . a p p N o t F o u n d ( n a m e : a p p . n a m e ) }
//
// v a r p r e v i o u s E n t i t l e m e n t s = S e t ( p r e v i o u s A p p . e n t i t l e m e n t s . k e y s )
// f o r a p p E x t e n s i o n i n p r e v i o u s A p p . a p p E x t e n s i o n s
// {
// p r e v i o u s E n t i t l e m e n t s . f o r m U n i o n ( a p p E x t e n s i o n . e n t i t l e m e n t s . k e y s )
// }
//
// / / M a k e s u r e a l l e n t i t l e m e n t s a l r e a d y e x i s t i n p r e v i o u s A p p .
// l e t a d d e d E n t i t l e m e n t s = A r r a y ( a l l P e r m i s s i o n s . l a z y . c o m p a c t M a p { $ 0 a s ? A L T E n t i t l e m e n t } . f i l t e r { ! p r e v i o u s E n t i t l e m e n t s . c o n t a i n s ( $ 0 ) } )
// g u a r d a d d e d E n t i t l e m e n t s . i s E m p t y e l s e { t h r o w V e r i f i c a t i o n E r r o r . a d d e d P e r m i s s i o n s ( a d d e d E n t i t l e m e n t s , a p p V e r s i o n : a p p V e r s i o n ) }
// }
2023-05-15 15:13:13 -05:00
}
2023-05-12 18:26:24 -05:00
@ discardableResult
func verifyPermissions ( of app : ALTApplication , @ AsyncManaged match storeApp : StoreApp ) async throws -> [ any ALTAppPermission ]
{
// E n t i t l e m e n t s
var allEntitlements = Set ( app . entitlements . keys )
for appExtension in app . appExtensions
{
allEntitlements . formUnion ( appExtension . entitlements . keys )
}
// F i l t e r o u t i g n o r e d e n t i t l e m e n t s .
allEntitlements = allEntitlements . filter { ! ALTEntitlement . ignoredEntitlements . contains ( $0 ) }
// P r i v a c y
let allPrivacyPermissions : Set < ALTAppPrivacyPermission >
if #available ( iOS 16 , * )
{
let regex = Regex {
" NS "
// C a p t u r e p e r m i s s i o n " n a m e "
Capture {
OneOrMore ( . anyGraphemeCluster )
}
" UsageDescription "
// O p t i o n a l s u f f i x
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 = [ ]
}
// V e r i f y p e r m i s s i o n s .
let sourcePermissions : Set < AnyHashable > = Set ( await $ storeApp . perform { $0 . permissions . map { AnyHashable ( $0 . permission ) } } )
2023-05-26 14:58:52 -05:00
let localPermissions : [ any ALTAppPermission ] = Array ( allEntitlements ) + Array ( allPrivacyPermissions )
2023-05-12 18:26:24 -05:00
// T o p a s s : E V E R Y p e r m i s s i o n i n l o c a l P e r m i s s i o n s m u s t a l s o a p p e a r i n s o u r c e P e r m i s s i o n s .
// I f t h e r e i s a s i n g l e m i s s i n g p e r m i s s i o n , t h r o w e r r o r .
let missingPermissions : [ any ALTAppPermission ] = localPermissions . filter { ! sourcePermissions . contains ( AnyHashable ( $0 ) ) }
guard missingPermissions . isEmpty else { throw VerificationError . undeclaredPermissions ( missingPermissions , app : app ) }
return localPermissions
}
2023-05-11 17:02:20 -05:00
}