102 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			102 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
import CoreData
 | 
						|
 | 
						|
enum PersistenceControllerError: Error {
 | 
						|
    case encryptionKeyUnavailable
 | 
						|
    case persistentStoreMissing
 | 
						|
    case rekeyingUnavailable
 | 
						|
}
 | 
						|
 | 
						|
final class PersistenceController {
 | 
						|
    static let shared: PersistenceController = {
 | 
						|
        do {
 | 
						|
            return try PersistenceController()
 | 
						|
        } catch {
 | 
						|
            fatalError("Failed to initialize PersistenceController: \(error)")
 | 
						|
        }
 | 
						|
    }()
 | 
						|
 | 
						|
    let container: NSPersistentContainer
 | 
						|
    private let keyManager: DatabaseEncryptionKeyManager
 | 
						|
 | 
						|
    var viewContext: NSManagedObjectContext {
 | 
						|
        container.viewContext
 | 
						|
    }
 | 
						|
 | 
						|
    init(
 | 
						|
        inMemory: Bool = false,
 | 
						|
        keyManager: DatabaseEncryptionKeyManager = .shared,
 | 
						|
        fileManager: FileManager = .default
 | 
						|
    ) throws {
 | 
						|
        self.keyManager = keyManager
 | 
						|
 | 
						|
        let modelName = "YobbleDataModel"
 | 
						|
        guard let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd"),
 | 
						|
              let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL) else {
 | 
						|
            fatalError("Unable to load Core Data model \(modelName)")
 | 
						|
        }
 | 
						|
 | 
						|
        container = NSPersistentContainer(name: modelName, managedObjectModel: managedObjectModel)
 | 
						|
 | 
						|
        let description = container.persistentStoreDescriptions.first ?? NSPersistentStoreDescription()
 | 
						|
        description.type = NSSQLiteStoreType
 | 
						|
 | 
						|
        if inMemory {
 | 
						|
            description.url = URL(fileURLWithPath: "/dev/null")
 | 
						|
        } else {
 | 
						|
            description.url = try Self.makeStoreURL(fileManager: fileManager, fileName: "\(modelName).sqlite")
 | 
						|
        }
 | 
						|
 | 
						|
        description.shouldInferMappingModelAutomatically = true
 | 
						|
        description.shouldMigrateStoreAutomatically = true
 | 
						|
 | 
						|
        let key: String
 | 
						|
        do {
 | 
						|
            key = try keyManager.currentKey()
 | 
						|
        } catch {
 | 
						|
            throw PersistenceControllerError.encryptionKeyUnavailable
 | 
						|
        }
 | 
						|
 | 
						|
        let pragmas: [String: String] = [
 | 
						|
            "journal_mode": "WAL",
 | 
						|
            "cipher_page_size": "4096",
 | 
						|
            "key": key
 | 
						|
        ]
 | 
						|
        description.setOption(pragmas as NSDictionary, forKey: NSSQLitePragmasOption)
 | 
						|
        description.setOption(FileProtectionType.completeUntilFirstUserAuthentication as NSObject, forKey: NSPersistentStoreFileProtectionKey)
 | 
						|
 | 
						|
        container.persistentStoreDescriptions = [description]
 | 
						|
 | 
						|
        container.loadPersistentStores { _, error in
 | 
						|
            if let error {
 | 
						|
                fatalError("Unresolved error \(error)")
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
 | 
						|
        container.viewContext.automaticallyMergesChangesFromParent = true
 | 
						|
    }
 | 
						|
 | 
						|
    func newBackgroundContext() -> NSManagedObjectContext {
 | 
						|
        container.newBackgroundContext()
 | 
						|
    }
 | 
						|
 | 
						|
    /// Placeholder for a future rekey flow once a password-based key is available.
 | 
						|
    ///
 | 
						|
    /// On iOS 16 with SQLCipher you typically re-encrypt by running `PRAGMA rekey` via a raw
 | 
						|
    /// SQLite handle and then persisting the new key to the key manager. This helper keeps
 | 
						|
    /// the signature in place for when that flow is implemented.
 | 
						|
    func rekeyStore(to newKey: String) throws {
 | 
						|
        throw PersistenceControllerError.rekeyingUnavailable
 | 
						|
    }
 | 
						|
 | 
						|
    private static func makeStoreURL(fileManager: FileManager, fileName: String) throws -> URL {
 | 
						|
        guard let baseURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
 | 
						|
            fatalError("Unable to resolve Application Support directory")
 | 
						|
        }
 | 
						|
        if !fileManager.fileExists(atPath: baseURL.path) {
 | 
						|
            try fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
 | 
						|
        }
 | 
						|
        return baseURL.appendingPathComponent(fileName)
 | 
						|
    }
 | 
						|
}
 |