add database encrypt
This commit is contained in:
		
							parent
							
								
									7dc78edb02
								
							
						
					
					
						commit
						6fd82e25c1
					
				
							
								
								
									
										101
									
								
								yobble/CoreData/PersistenceController.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								yobble/CoreData/PersistenceController.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,101 @@
 | 
			
		||||
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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										45
									
								
								yobble/Services/DatabaseEncryptionKeyManager.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								yobble/Services/DatabaseEncryptionKeyManager.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
enum DatabaseEncryptionKeyError: Error {
 | 
			
		||||
    case keyNotAvailable
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final class DatabaseEncryptionKeyManager {
 | 
			
		||||
    static let shared = DatabaseEncryptionKeyManager()
 | 
			
		||||
 | 
			
		||||
    private let keychainService: KeychainService
 | 
			
		||||
    private let serviceName = "yobble.database.encryption"
 | 
			
		||||
    private let accountName = "sqlcipher_key"
 | 
			
		||||
    /// Hardcoded dev key used until the user saves their own password-derived key.
 | 
			
		||||
    private let fallbackKey: String
 | 
			
		||||
 | 
			
		||||
    init(
 | 
			
		||||
        keychainService: KeychainService = .shared,
 | 
			
		||||
        fallbackKey: String = AppConfig.DEFAULT_DATABASE_ENCRYPTION_KEY
 | 
			
		||||
    ) {
 | 
			
		||||
        self.keychainService = keychainService
 | 
			
		||||
        self.fallbackKey = fallbackKey
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func currentKey() throws -> String {
 | 
			
		||||
        if let key = keychainService.get(forKey: accountName, service: serviceName), !key.isEmpty {
 | 
			
		||||
            return key
 | 
			
		||||
        }
 | 
			
		||||
        guard !fallbackKey.isEmpty else {
 | 
			
		||||
            throw DatabaseEncryptionKeyError.keyNotAvailable
 | 
			
		||||
        }
 | 
			
		||||
        return fallbackKey
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func persistKey(_ key: String) {
 | 
			
		||||
        keychainService.save(key, forKey: accountName, service: serviceName)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func clearPersistedKey() {
 | 
			
		||||
        keychainService.delete(forKey: accountName, service: serviceName)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func hasPersistedKey() -> Bool {
 | 
			
		||||
        keychainService.get(forKey: accountName, service: serviceName) != nil
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -11,6 +11,10 @@ struct AppConfig {
 | 
			
		||||
    static let USER_AGENT = "yobble ios"
 | 
			
		||||
    static let APP_BUILD = "appstore" // appstore / freestore
 | 
			
		||||
    static let APP_VERSION = "0.1"
 | 
			
		||||
 | 
			
		||||
    static let DISABLE_DB = false
 | 
			
		||||
    /// Fallback SQLCipher key used until the user sets an application password.
 | 
			
		||||
    static let DEFAULT_DATABASE_ENCRYPTION_KEY = "yobble_dev_change_me"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct AppInfo {
 | 
			
		||||
 | 
			
		||||
@ -6,11 +6,13 @@
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import CoreData
 | 
			
		||||
 | 
			
		||||
@main
 | 
			
		||||
struct yobbleApp: App {
 | 
			
		||||
    @StateObject private var themeManager = ThemeManager()
 | 
			
		||||
    @StateObject private var viewModel = LoginViewModel()
 | 
			
		||||
    private let persistenceController = PersistenceController.shared
 | 
			
		||||
    
 | 
			
		||||
    var body: some Scene {
 | 
			
		||||
        WindowGroup {
 | 
			
		||||
@ -25,6 +27,7 @@ struct yobbleApp: App {
 | 
			
		||||
            }
 | 
			
		||||
            .environmentObject(themeManager)
 | 
			
		||||
            .preferredColorScheme(themeManager.theme.colorScheme)
 | 
			
		||||
            .environment(\.managedObjectContext, persistenceController.viewContext)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user