TeraRestoreFiles.swift (3780B)
1 import CryptoKit 2 import Foundation 3 import RadrootsKit 4 import TeraKitBindings 5 6 /// Recovery metadata and immutable media stay with the existing file owner. 7 struct TeraRestoreFiles { 8 let roots: RadrootsAppleFileRoots 9 let publicKey: String 10 11 static func readGuard(applicationSupportDirectory: String, publicKey: String) throws -> Data? { 12 let root = URL(fileURLWithPath: applicationSupportDirectory, isDirectory: true) 13 let roots = try RadrootsAppleFileRoots(appIdentifier: "tera.restore.read", dataRoot: root, cacheRoot: root, temporaryRoot: root) 14 return try Self(roots: roots, publicKey: publicKey).readGuard() 15 } 16 17 func readGuard() throws -> Data? { 18 let relative = try applicationRestoreGuardPath(publicKey: publicKey) 19 do { 20 guard case let .inline(bytes) = try RadrootsAppleFileAccess(roots: roots).read( 21 RadrootsFileReference(scope: .data, relativePath: relative), mode: .inline(maxBytes: Int(applicationRestoreGuardLimit())) 22 ) else { throw TeraLocalRestoreHost.failure("restore_recovery_required") } 23 let guardRecord = try validateApplicationRestoreGuard(bytes: bytes) 24 guard guardRecord.relativePath == relative, guardRecord.request.backup.publicKey == publicKey else { 25 throw TeraLocalRestoreHost.failure("restore_identity_mismatch") 26 } 27 return bytes 28 } catch RadrootsAppleFileError.notFound { 29 return nil 30 } 31 } 32 33 func arm(_ guardRecord: FfiRestoreGuard) throws { 34 let validated = try validateApplicationRestoreGuard(bytes: guardRecord.bytes) 35 guard validated == guardRecord, validated.request.backup.publicKey == publicKey else { 36 throw TeraLocalRestoreHost.failure("restore_identity_mismatch") 37 } 38 let relative = validated.relativePath 39 let owner = roots.dataRoot.appendingPathComponent(relative).deletingLastPathComponent() 40 let access = try RadrootsAppleFileAccess(roots: RadrootsAppleFileRoots( 41 appIdentifier: roots.appIdentifier, dataRoot: roots.dataRoot, cacheRoot: roots.cacheRoot, 42 temporaryRoot: roots.temporaryRoot, stagedBlobsRoot: owner 43 )) 44 // The admitted owner directory supplies custody metadata before immutable 45 // installation. Foundation cannot change metadata on a read-only guard. 46 try TeraBackupFiles.protect(owner) 47 try access.installStagedBlob(validated.bytes, reference: RadrootsStagedBlobReference( 48 blobID: URL(fileURLWithPath: relative).lastPathComponent, sizeBytes: validated.bytes.count 49 )) 50 guard try readGuard() == validated.bytes else { throw TeraLocalRestoreHost.failure("restore_recovery_required") } 51 } 52 53 func restoreMedia(_ manifest: FfiBackupManifest) throws { 54 try validateApplicationBackupMedia(request: manifest.request, media: manifest.media) 55 guard manifest.request.publicKey == publicKey else { throw TeraLocalRestoreHost.failure("restore_identity_mismatch") } 56 let access = RadrootsAppleFileAccess(roots: roots) 57 for item in manifest.media { 58 try Task.checkCancellation() 59 guard let size = Int(exactly: item.byteLength), size > 0, 60 item.byteLength <= applicationBackupLimits().mediaBytes 61 else { throw TeraLocalRestoreHost.failure("restore_capacity_exceeded") } 62 let reference = RadrootsFileReference(scope: .data, relativePath: "backups/\(publicKey)/staged_blob_leases/\(item.leaseIdentifier)") 63 guard case let .inline(bytes) = try access.read(reference, mode: .inline(maxBytes: size)), bytes.count == size, 64 SHA256.hash(data: bytes).map({ String(format: "%02x", $0) }).joined() == item.sha256 65 else { throw TeraLocalRestoreHost.failure("restore_media_unavailable") } 66 let blob = try RadrootsStagedBlobReference(blobID: item.sha256, sizeBytes: size) 67 try access.installStagedBlob(bytes, reference: blob) 68 } 69 } 70 }