add database encrypt

This commit is contained in:
cheykrym 2025-10-21 17:56:12 +03:00
parent 7dc78edb02
commit 6fd82e25c1
4 changed files with 153 additions and 0 deletions

View 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)
}
}

View 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
}
}

View File

@ -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 {

View File

@ -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)
} }
} }
} }