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