RadrootsAppleMobileStore.swift (5402B)
1 import Foundation 2 3 #if canImport(UIKit) 4 import UIKit 5 #endif 6 7 public enum RadrootsAppleProtectedDataAvailability: Sendable, Equatable { 8 case available 9 case unavailable 10 11 @MainActor 12 public static var current: Self { 13 #if canImport(UIKit) 14 UIApplication.shared.isProtectedDataAvailable ? .available : .unavailable 15 #else 16 .available 17 #endif 18 } 19 } 20 21 public struct RadrootsAppleMobileStoreConfiguration: Sendable, Equatable { 22 public let applicationSupportDirectory: URL 23 public let ownerDirectory: URL 24 public let protectedDataAvailability: RadrootsAppleProtectedDataAvailability 25 } 26 27 public enum RadrootsAppleMobileStoreError: Error, Sendable, Equatable { 28 case invalidPublicKey 29 case protectedDataUnavailable 30 case invalidDirectoryLayout 31 case fileSystemFailure 32 } 33 34 extension RadrootsAppleMobileStoreError: LocalizedError { 35 public var errorDescription: String? { 36 switch self { 37 case .invalidPublicKey: "The public key is invalid." 38 case .protectedDataUnavailable: "Protected data is unavailable." 39 case .invalidDirectoryLayout: "The mobile-store directory layout is invalid." 40 case .fileSystemFailure: "The mobile store could not be prepared." 41 } 42 } 43 } 44 45 public enum RadrootsAppleMobileStore { 46 private static let productDirectory = "radroots" 47 private static let userDirectory = "users" 48 49 /// Prepares the Apple-owned directory consumed by the Rust mobile runtime. 50 /// 51 /// The returned Application Support directory is passed to Rust, which 52 /// independently derives and validates the same identity-scoped suffix. 53 public static func prepare( 54 roots: RadrootsAppleFileRoots, 55 publicKeyHex: String, 56 protectedDataAvailability: RadrootsAppleProtectedDataAvailability, 57 fileManager: FileManager = .default 58 ) throws -> RadrootsAppleMobileStoreConfiguration { 59 guard protectedDataAvailability == .available else { 60 throw RadrootsAppleMobileStoreError.protectedDataUnavailable 61 } 62 guard isCanonicalPublicKey(publicKeyHex) else { 63 throw RadrootsAppleMobileStoreError.invalidPublicKey 64 } 65 66 let applicationSupportDirectory = roots.dataRoot 67 let productRoot = 68 applicationSupportDirectory 69 .appendingPathComponent(productDirectory, isDirectory: true) 70 let userRoot = 71 productRoot 72 .appendingPathComponent(userDirectory, isDirectory: true) 73 let ownerRoot = 74 userRoot 75 .appendingPathComponent(publicKeyHex, isDirectory: true) 76 77 do { 78 // swiftlint:disable trailing_comma 79 for directory in [ 80 applicationSupportDirectory, 81 productRoot, 82 userRoot, 83 ownerRoot, 84 ] { 85 try createOrValidateDirectory(directory, fileManager: fileManager) 86 } 87 // swiftlint:enable trailing_comma 88 try excludeFromBackup(ownerRoot) 89 try applyFileProtection(ownerRoot, fileManager: fileManager) 90 } catch let error as RadrootsAppleMobileStoreError { 91 throw error 92 } catch { 93 throw RadrootsAppleMobileStoreError.fileSystemFailure 94 } 95 96 return RadrootsAppleMobileStoreConfiguration( 97 applicationSupportDirectory: applicationSupportDirectory, 98 ownerDirectory: ownerRoot, 99 protectedDataAvailability: protectedDataAvailability 100 ) 101 } 102 103 private static func isCanonicalPublicKey(_ value: String) -> Bool { 104 value.utf8.count == 64 105 && value.utf8.allSatisfy { 106 ($0 >= 48 && $0 <= 57) || ($0 >= 97 && $0 <= 102) 107 } 108 } 109 110 private static func createOrValidateDirectory( 111 _ directory: URL, 112 fileManager: FileManager 113 ) throws { 114 var isDirectory: ObjCBool = false 115 if fileManager.fileExists(atPath: directory.path, isDirectory: &isDirectory) { 116 let values = try directory.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey]) 117 guard isDirectory.boolValue, values.isDirectory == true, values.isSymbolicLink != true else { 118 throw RadrootsAppleMobileStoreError.invalidDirectoryLayout 119 } 120 return 121 } 122 try fileManager.createDirectory( 123 at: directory, 124 withIntermediateDirectories: false, 125 attributes: nil 126 ) 127 let values = try directory.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey]) 128 guard values.isDirectory == true, values.isSymbolicLink != true else { 129 throw RadrootsAppleMobileStoreError.invalidDirectoryLayout 130 } 131 } 132 133 private static func excludeFromBackup(_ directory: URL) throws { 134 var directory = directory 135 var values = URLResourceValues() 136 values.isExcludedFromBackup = true 137 try directory.setResourceValues(values) 138 } 139 140 private static func applyFileProtection( 141 _ directory: URL, 142 fileManager: FileManager 143 ) throws { 144 #if os(iOS) 145 try fileManager.setAttributes( 146 [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], 147 ofItemAtPath: directory.path 148 ) 149 #endif 150 } 151 }