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 USER_AGENT = "yobble ios"
 | 
				
			||||||
    static let APP_BUILD = "appstore" // appstore / freestore
 | 
					    static let APP_BUILD = "appstore" // appstore / freestore
 | 
				
			||||||
    static let APP_VERSION = "0.1"
 | 
					    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 {
 | 
					struct AppInfo {
 | 
				
			||||||
 | 
				
			|||||||
@ -6,11 +6,13 @@
 | 
				
			|||||||
//
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import SwiftUI
 | 
					import SwiftUI
 | 
				
			||||||
 | 
					import CoreData
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@main
 | 
					@main
 | 
				
			||||||
struct yobbleApp: App {
 | 
					struct yobbleApp: App {
 | 
				
			||||||
    @StateObject private var themeManager = ThemeManager()
 | 
					    @StateObject private var themeManager = ThemeManager()
 | 
				
			||||||
    @StateObject private var viewModel = LoginViewModel()
 | 
					    @StateObject private var viewModel = LoginViewModel()
 | 
				
			||||||
 | 
					    private let persistenceController = PersistenceController.shared
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    var body: some Scene {
 | 
					    var body: some Scene {
 | 
				
			||||||
        WindowGroup {
 | 
					        WindowGroup {
 | 
				
			||||||
@ -25,6 +27,7 @@ struct yobbleApp: App {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            .environmentObject(themeManager)
 | 
					            .environmentObject(themeManager)
 | 
				
			||||||
            .preferredColorScheme(themeManager.theme.colorScheme)
 | 
					            .preferredColorScheme(themeManager.theme.colorScheme)
 | 
				
			||||||
 | 
					            .environment(\.managedObjectContext, persistenceController.viewContext)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user