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