field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

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 }