mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-17 02:33:27 +01:00
Verifies downloaded app’s SHA-256 checksum (if specified)
This commit is contained in:
@@ -16,7 +16,8 @@ import AltSign
|
|||||||
final class DownloadAppOperation: ResultOperation<ALTApplication>
|
final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||||
{
|
{
|
||||||
let app: AppProtocol
|
let app: AppProtocol
|
||||||
let context: AppOperationContext
|
let context: InstallAppOperationContext
|
||||||
|
|
||||||
private let appName: String
|
private let appName: String
|
||||||
private let bundleIdentifier: String
|
private let bundleIdentifier: String
|
||||||
private let destinationURL: URL
|
private let destinationURL: URL
|
||||||
@@ -24,7 +25,7 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
private let session = URLSession(configuration: .default)
|
private let session = URLSession(configuration: .default)
|
||||||
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||||
|
|
||||||
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
|
init(app: AppProtocol, destinationURL: URL, context: InstallAppOperationContext)
|
||||||
{
|
{
|
||||||
self.app = app
|
self.app = app
|
||||||
self.context = context
|
self.context = context
|
||||||
@@ -52,7 +53,13 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
|
|
||||||
self.localizedFailure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), self.appName)
|
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 {
|
storeApp.managedObjectContext?.perform {
|
||||||
do {
|
do {
|
||||||
let latestVersion = try self.verify(storeApp)
|
let latestVersion = try self.verify(storeApp)
|
||||||
@@ -108,8 +115,17 @@ private extension DownloadAppOperation {
|
|||||||
return version
|
return version
|
||||||
}
|
}
|
||||||
|
|
||||||
func download(@Managed _ app: AppProtocol) {
|
func download(@Managed _ app: AppProtocol)
|
||||||
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) }
|
{
|
||||||
|
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
|
self.downloadIPA(from: sourceURL) { result in
|
||||||
do
|
do
|
||||||
@@ -178,6 +194,12 @@ private extension DownloadAppOperation {
|
|||||||
{
|
{
|
||||||
// File, so assuming this is a .ipa file.
|
// File, so assuming this is a .ipa file.
|
||||||
appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: self.temporaryDirectory)
|
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 }
|
guard let application = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ class InstallAppOperationContext: AppOperationContext
|
|||||||
return temporaryDirectory
|
return temporaryDirectory
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
var ipaURL: URL?
|
||||||
var resignedApp: ALTApplication?
|
var resignedApp: ALTApplication?
|
||||||
var installedApp: InstalledApp? {
|
var installedApp: InstalledApp? {
|
||||||
didSet {
|
didSet {
|
||||||
@@ -120,4 +121,8 @@ class InstallAppOperationContext: AppOperationContext
|
|||||||
var beginInstallationHandler: ((InstalledApp) -> Void)?
|
var beginInstallationHandler: ((InstalledApp) -> Void)?
|
||||||
|
|
||||||
var alternateIconURL: URL?
|
var alternateIconURL: URL?
|
||||||
|
|
||||||
|
// Non-nil when installing from a source.
|
||||||
|
@AsyncManaged
|
||||||
|
var appVersion: AppVersion?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ extension VerificationError
|
|||||||
|
|
||||||
case mismatchedBundleIdentifiers = 1
|
case mismatchedBundleIdentifiers = 1
|
||||||
case iOSVersionNotSupported = 2
|
case iOSVersionNotSupported = 2
|
||||||
|
|
||||||
|
case mismatchedHash = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
|
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 {
|
static func iOSVersionNotSupported(app: AppProtocol, osVersion: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion, requiredOSVersion: OperatingSystemVersion?) -> VerificationError {
|
||||||
VerificationError(code: .iOSVersionNotSupported, app: app, deviceOSVersion: osVersion, requiredOSVersion: requiredOSVersion)
|
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
|
struct VerificationError: ALTLocalizedError
|
||||||
@@ -43,6 +49,9 @@ struct VerificationError: ALTLocalizedError
|
|||||||
var deviceOSVersion: OperatingSystemVersion?
|
var deviceOSVersion: OperatingSystemVersion?
|
||||||
var requiredOSVersion: OperatingSystemVersion?
|
var requiredOSVersion: OperatingSystemVersion?
|
||||||
|
|
||||||
|
@UserInfoValue var hash: String?
|
||||||
|
@UserInfoValue var expectedHash: String?
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
//TODO: Make this automatic somehow with ALTLocalizedError
|
//TODO: Make this automatic somehow with ALTLocalizedError
|
||||||
guard self.errorFailure == nil else { return nil }
|
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)
|
let failureReason = String(format: NSLocalizedString("%@ requires iOS %@ or later.", comment: ""), appName, requiredOSVersion.stringValue)
|
||||||
return failureReason
|
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 Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
@@ -96,10 +97,10 @@ struct VerificationError: ALTLocalizedError {
|
|||||||
@objc(VerifyAppOperation)
|
@objc(VerifyAppOperation)
|
||||||
final class VerifyAppOperation: ResultOperation<Void>
|
final class VerifyAppOperation: ResultOperation<Void>
|
||||||
{
|
{
|
||||||
let context: AppOperationContext
|
let context: InstallAppOperationContext
|
||||||
var verificationHandler: ((VerificationError) -> Bool)?
|
var verificationHandler: ((VerificationError) -> Bool)?
|
||||||
|
|
||||||
init(context: AppOperationContext)
|
init(context: InstallAppOperationContext)
|
||||||
{
|
{
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
@@ -133,7 +134,23 @@ final class VerifyAppOperation: ResultOperation<Void>
|
|||||||
throw VerificationError.iOSVersionNotSupported(app: app, requiredOSVersion: app.minimumiOSVersion)
|
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
|
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"?>
|
<?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">
|
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||||
<attribute name="appleID" attributeType="String"/>
|
<attribute name="appleID" attributeType="String"/>
|
||||||
<attribute name="firstName" attributeType="String"/>
|
<attribute name="firstName" attributeType="String"/>
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
|
||||||
<attribute name="maxOSVersion" optional="YES" attributeType="String"/>
|
<attribute name="maxOSVersion" optional="YES" attributeType="String"/>
|
||||||
<attribute name="minOSVersion" 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="size" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="sourceID" optional="YES" attributeType="String"/>
|
<attribute name="sourceID" optional="YES" attributeType="String"/>
|
||||||
<attribute name="version" 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 downloadURL: URL
|
||||||
@NSManaged public var size: Int64
|
@NSManaged public var size: Int64
|
||||||
|
@NSManaged public var sha256: String?
|
||||||
|
|
||||||
@nonobjc public var minOSVersion: OperatingSystemVersion? {
|
@nonobjc public var minOSVersion: OperatingSystemVersion? {
|
||||||
guard let osVersionString = self._minOSVersion else { return nil }
|
guard let osVersionString = self._minOSVersion else { return nil }
|
||||||
@@ -54,6 +55,7 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
|
|||||||
case localizedDescription
|
case localizedDescription
|
||||||
case downloadURL
|
case downloadURL
|
||||||
case size
|
case size
|
||||||
|
case sha256
|
||||||
case minOSVersion
|
case minOSVersion
|
||||||
case maxOSVersion
|
case maxOSVersion
|
||||||
}
|
}
|
||||||
@@ -75,6 +77,8 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
|
|||||||
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||||
self.size = try container.decode(Int64.self, forKey: .size)
|
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._minOSVersion = try container.decodeIfPresent(String.self, forKey: .minOSVersion)
|
||||||
self._maxOSVersion = try container.decodeIfPresent(String.self, forKey: .maxOSVersion)
|
self._maxOSVersion = try container.decodeIfPresent(String.self, forKey: .maxOSVersion)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user