mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
Verifies downloaded app’s SHA-256 checksum (if specified)
This commit is contained in:
@@ -16,15 +16,16 @@ import AltSign
|
||||
final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
{
|
||||
let app: AppProtocol
|
||||
let context: AppOperationContext
|
||||
let context: InstallAppOperationContext
|
||||
|
||||
private let appName: String
|
||||
private let bundleIdentifier: String
|
||||
private let destinationURL: URL
|
||||
|
||||
private let session = URLSession(configuration: .default)
|
||||
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||
|
||||
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
|
||||
|
||||
init(app: AppProtocol, destinationURL: URL, context: InstallAppOperationContext)
|
||||
{
|
||||
self.app = app
|
||||
self.context = context
|
||||
@@ -51,8 +52,14 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
print("Downloading App:", self.bundleIdentifier)
|
||||
|
||||
self.localizedFailure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), self.appName)
|
||||
|
||||
guard let storeApp = self.app as? StoreApp else { return self.download(self.app) }
|
||||
|
||||
guard let storeApp = self.app as? StoreApp else {
|
||||
// Only StoreApp allows falling back to previous versions.
|
||||
// AppVersion can only install itself, and ALTApplication doesn't have previous versions.
|
||||
return self.download(self.app)
|
||||
}
|
||||
|
||||
// Verify storeApp
|
||||
storeApp.managedObjectContext?.perform {
|
||||
do {
|
||||
let latestVersion = try self.verify(storeApp)
|
||||
@@ -107,10 +114,19 @@ private extension DownloadAppOperation {
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
func download(@Managed _ app: AppProtocol) {
|
||||
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) }
|
||||
|
||||
|
||||
func download(@Managed _ app: AppProtocol)
|
||||
{
|
||||
guard let sourceURL = $app.url else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) }
|
||||
|
||||
if let appVersion = app as? AppVersion
|
||||
{
|
||||
// All downloads go through this path, and `app` is
|
||||
// always an AppVersion if downloading from a source,
|
||||
// so context.appVersion != nil means downloading from source.
|
||||
self.context.appVersion = appVersion
|
||||
}
|
||||
|
||||
self.downloadIPA(from: sourceURL) { result in
|
||||
do
|
||||
{
|
||||
@@ -178,6 +194,12 @@ private extension DownloadAppOperation {
|
||||
{
|
||||
// File, so assuming this is a .ipa file.
|
||||
appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: self.temporaryDirectory)
|
||||
|
||||
// Use context's temporaryDirectory to ensure .ipa isn't deleted before we're done installing.
|
||||
let ipaURL = self.context.temporaryDirectory.appendingPathComponent("App.ipa")
|
||||
try FileManager.default.copyItem(at: fileURL, to: ipaURL)
|
||||
|
||||
self.context.ipaURL = ipaURL
|
||||
}
|
||||
|
||||
guard let application = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||
|
||||
@@ -109,6 +109,7 @@ class InstallAppOperationContext: AppOperationContext
|
||||
return temporaryDirectory
|
||||
}()
|
||||
|
||||
var ipaURL: URL?
|
||||
var resignedApp: ALTApplication?
|
||||
var installedApp: InstalledApp? {
|
||||
didSet {
|
||||
@@ -120,4 +121,8 @@ class InstallAppOperationContext: AppOperationContext
|
||||
var beginInstallationHandler: ((InstalledApp) -> Void)?
|
||||
|
||||
var alternateIconURL: URL?
|
||||
|
||||
// Non-nil when installing from a source.
|
||||
@AsyncManaged
|
||||
var appVersion: AppVersion?
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ extension VerificationError
|
||||
|
||||
case mismatchedBundleIdentifiers = 1
|
||||
case iOSVersionNotSupported = 2
|
||||
|
||||
case mismatchedHash = 3
|
||||
}
|
||||
|
||||
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
|
||||
@@ -29,6 +31,10 @@ extension VerificationError
|
||||
static func iOSVersionNotSupported(app: AppProtocol, osVersion: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion, requiredOSVersion: OperatingSystemVersion?) -> VerificationError {
|
||||
VerificationError(code: .iOSVersionNotSupported, app: app, deviceOSVersion: osVersion, requiredOSVersion: requiredOSVersion)
|
||||
}
|
||||
|
||||
static func mismatchedHash(_ hash: String, expectedHash: String, app: AppProtocol) -> VerificationError {
|
||||
VerificationError(code: .mismatchedHash, app: app, hash: hash, expectedHash: expectedHash)
|
||||
}
|
||||
}
|
||||
|
||||
struct VerificationError: ALTLocalizedError
|
||||
@@ -43,6 +49,9 @@ struct VerificationError: ALTLocalizedError
|
||||
var deviceOSVersion: OperatingSystemVersion?
|
||||
var requiredOSVersion: OperatingSystemVersion?
|
||||
|
||||
@UserInfoValue var hash: String?
|
||||
@UserInfoValue var expectedHash: String?
|
||||
|
||||
var errorDescription: String? {
|
||||
//TODO: Make this automatic somehow with ALTLocalizedError
|
||||
guard self.errorFailure == nil else { return nil }
|
||||
@@ -104,6 +113,10 @@ struct VerificationError: ALTLocalizedError
|
||||
let failureReason = String(format: NSLocalizedString("%@ requires iOS %@ or later.", comment: ""), appName, requiredOSVersion.stringValue)
|
||||
return failureReason
|
||||
}
|
||||
|
||||
case .mismatchedHash:
|
||||
let appName = self.$app.name ?? NSLocalizedString("the downloaded app", comment: "")
|
||||
return String(format: NSLocalizedString("The SHA-256 hash of %@ does not match the hash specified by the source.", comment: ""), appName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
@@ -96,10 +97,10 @@ struct VerificationError: ALTLocalizedError {
|
||||
@objc(VerifyAppOperation)
|
||||
final class VerifyAppOperation: ResultOperation<Void>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
let context: InstallAppOperationContext
|
||||
var verificationHandler: ((VerificationError) -> Bool)?
|
||||
|
||||
init(context: AppOperationContext)
|
||||
init(context: InstallAppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
@@ -133,7 +134,23 @@ final class VerifyAppOperation: ResultOperation<Void>
|
||||
throw VerificationError.iOSVersionNotSupported(app: app, requiredOSVersion: app.minimumiOSVersion)
|
||||
}
|
||||
|
||||
self.finish(.success(()))
|
||||
guard let appVersion = self.context.appVersion else {
|
||||
return self.finish(.success(()))
|
||||
}
|
||||
|
||||
Task<Void, Never> {
|
||||
do
|
||||
{
|
||||
guard let ipaURL = self.context.ipaURL else { throw OperationError.appNotFound(name: app.name) }
|
||||
try await self.verifyHash(of: app, at: ipaURL, matches: appVersion)
|
||||
|
||||
self.finish(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -141,3 +158,20 @@ final class VerifyAppOperation: ResultOperation<Void>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension VerifyAppOperation
|
||||
{
|
||||
func verifyHash(of app: ALTApplication, at ipaURL: URL, @AsyncManaged matches appVersion: AppVersion) async throws
|
||||
{
|
||||
// Do nothing if source doesn't provide hash.
|
||||
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()
|
||||
|
||||
print("[ALTLog] Comparing app hash (\(hashString)) against expected hash (\(expectedHash))...")
|
||||
|
||||
guard hashString == expectedHash else { throw VerificationError.mismatchedHash(hashString, expectedHash: expectedHash, app: app) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D68" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22D68" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
@@ -38,6 +38,7 @@
|
||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="maxOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="minOSVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="sha256" optional="YES" attributeType="String"/>
|
||||
<attribute name="size" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceID" optional="YES" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
|
||||
@@ -18,6 +18,7 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
|
||||
|
||||
@NSManaged public var downloadURL: URL
|
||||
@NSManaged public var size: Int64
|
||||
@NSManaged public var sha256: String?
|
||||
|
||||
@nonobjc public var minOSVersion: OperatingSystemVersion? {
|
||||
guard let osVersionString = self._minOSVersion else { return nil }
|
||||
@@ -54,6 +55,7 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
|
||||
case localizedDescription
|
||||
case downloadURL
|
||||
case size
|
||||
case sha256
|
||||
case minOSVersion
|
||||
case maxOSVersion
|
||||
}
|
||||
@@ -74,7 +76,9 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
|
||||
|
||||
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||
self.size = try container.decode(Int64.self, forKey: .size)
|
||||
|
||||
|
||||
self.sha256 = try container.decodeIfPresent(String.self, forKey: .sha256)?.lowercased()
|
||||
|
||||
self._minOSVersion = try container.decodeIfPresent(String.self, forKey: .minOSVersion)
|
||||
self._maxOSVersion = try container.decodeIfPresent(String.self, forKey: .maxOSVersion)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user