diff --git a/DB/Sources/DB/Content/ContentDatabase.swift b/DB/Sources/DB/Content/ContentDatabase.swift index e2d6fef..00c4bd8 100644 --- a/DB/Sources/DB/Content/ContentDatabase.swift +++ b/DB/Sources/DB/Content/ContentDatabase.swift @@ -17,21 +17,19 @@ public struct ContentDatabase { useHomeTimelineLastReadId: Bool, useNotificationsLastReadId: Bool, inMemory: Bool, + appGroup: String, keychain: Keychain.Type) throws { if inMemory { databaseWriter = DatabaseQueue() + try Self.migrator.migrate(databaseWriter) } else { - let path = try Self.fileURL(id: id).path - var configuration = Configuration() - - configuration.prepareDatabase { - try $0.usePassphrase(Secrets.databaseKey(identityId: id, keychain: keychain)) + databaseWriter = try DatabasePool.withFileCoordinator( + url: Self.fileURL(id: id, appGroup: appGroup), + migrator: Self.migrator) { + try Secrets.databaseKey(identityId: id, keychain: keychain) } - - databaseWriter = try DatabasePool(path: path, configuration: configuration) } - try Self.migrator.migrate(databaseWriter) try Self.clean( databaseWriter, useHomeTimelineLastReadId: useHomeTimelineLastReadId, @@ -47,8 +45,8 @@ public struct ContentDatabase { } public extension ContentDatabase { - static func delete(id: Identity.Id) throws { - try FileManager.default.removeItem(at: fileURL(id: id)) + static func delete(id: Identity.Id, appGroup: String) throws { + try FileManager.default.removeItem(at: fileURL(id: id, appGroup: appGroup)) } func insert(status: Status) -> AnyPublisher { @@ -411,8 +409,8 @@ public extension ContentDatabase { private extension ContentDatabase { static let cleanAfterLastReadIdCount = 40 - static func fileURL(id: Identity.Id) throws -> URL { - try FileManager.default.databaseDirectoryURL(name: id.uuidString) + static func fileURL(id: Identity.Id, appGroup: String) throws -> URL { + try FileManager.default.databaseDirectoryURL(name: id.uuidString, appGroup: appGroup) } // swiftlint:disable:next function_body_length diff --git a/DB/Sources/DB/Extensions/DatabasePool+Extensions.swift b/DB/Sources/DB/Extensions/DatabasePool+Extensions.swift new file mode 100644 index 0000000..bc7582c --- /dev/null +++ b/DB/Sources/DB/Extensions/DatabasePool+Extensions.swift @@ -0,0 +1,51 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import GRDB + +// https://github.com/groue/GRDB.swift/blob/master/Documentation/SharingADatabase.md + +extension DatabasePool { + class func withFileCoordinator(url: URL, + migrator: DatabaseMigrator, + passphrase: @escaping (() throws -> String)) throws -> Self { + let coordinator = NSFileCoordinator(filePresenter: nil) + var coordinatorError: NSError? + var dbPool: Self? + var dbError: Error? + + coordinator.coordinate(writingItemAt: url, options: .forMerging, error: &coordinatorError) { coordinatedURL in + do { + var configuration = Configuration() + + configuration.prepareDatabase { db in + try db.usePassphrase(passphrase()) + try db.execute(sql: "PRAGMA cipher_plaintext_header_size = 32") + + if !db.configuration.readonly { + var flag: CInt = 1 + let code = withUnsafeMutablePointer(to: &flag) { + sqlite3_file_control(db.sqliteConnection, nil, SQLITE_FCNTL_PERSIST_WAL, $0) + } + + guard code == SQLITE_OK else { + throw DatabaseError(resultCode: ResultCode(rawValue: code)) + } + } + } + + dbPool = try Self(path: coordinatedURL.path, configuration: configuration) + + try migrator.migrate(dbPool!) + } catch { + dbError = error + } + } + + if let error = dbError ?? coordinatorError { + throw error + } + + return dbPool! + } +} diff --git a/DB/Sources/DB/Extensions/FileManager+Extensions.swift b/DB/Sources/DB/Extensions/FileManager+Extensions.swift index 360c799..f8d731f 100644 --- a/DB/Sources/DB/Extensions/FileManager+Extensions.swift +++ b/DB/Sources/DB/Extensions/FileManager+Extensions.swift @@ -3,12 +3,17 @@ import Foundation extension FileManager { - func databaseDirectoryURL(name: String) throws -> URL { - let databaseDirectoryURL = try url(for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true) - .appendingPathComponent("DB") + enum DatabaseDirectoryError: Error { + case containerURLNotFound + case unexpectedFileExistsWithDBDirectoryName + } + + func databaseDirectoryURL(name: String, appGroup: String) throws -> URL { + guard let containerURL = containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { + throw DatabaseDirectoryError.containerURLNotFound + } + + let databaseDirectoryURL = containerURL.appendingPathComponent("DB") var isDirectory: ObjCBool = false if !fileExists(atPath: databaseDirectoryURL.path, isDirectory: &isDirectory) { @@ -16,7 +21,7 @@ extension FileManager { withIntermediateDirectories: false, attributes: [.protectionKey: FileProtectionType.complete]) } else if !isDirectory.boolValue { - throw NSError(domain: NSCocoaErrorDomain, code: NSFileWriteFileExistsError, userInfo: nil) + throw DatabaseDirectoryError.unexpectedFileExistsWithDBDirectoryName } return databaseDirectoryURL.appendingPathComponent(name) diff --git a/DB/Sources/DB/Identity/IdentityDatabase+Migration.swift b/DB/Sources/DB/Identity/IdentityDatabase+Migration.swift index da1713d..c6f6335 100644 --- a/DB/Sources/DB/Identity/IdentityDatabase+Migration.swift +++ b/DB/Sources/DB/Identity/IdentityDatabase+Migration.swift @@ -3,7 +3,7 @@ import GRDB extension IdentityDatabase { - var migrator: DatabaseMigrator { + static var migrator: DatabaseMigrator { var migrator = DatabaseMigrator() migrator.registerMigration("0.1.0") { db in diff --git a/DB/Sources/DB/Identity/IdentityDatabase.swift b/DB/Sources/DB/Identity/IdentityDatabase.swift index 7e3a99b..f76ea82 100644 --- a/DB/Sources/DB/Identity/IdentityDatabase.swift +++ b/DB/Sources/DB/Identity/IdentityDatabase.swift @@ -14,21 +14,17 @@ public enum IdentityDatabaseError: Error { public struct IdentityDatabase { private let databaseWriter: DatabaseWriter - public init(inMemory: Bool, keychain: Keychain.Type) throws { + public init(inMemory: Bool, appGroup: String, keychain: Keychain.Type) throws { if inMemory { databaseWriter = DatabaseQueue() + try Self.migrator.migrate(databaseWriter) } else { - let path = try FileManager.default.databaseDirectoryURL(name: Self.name).path - var configuration = Configuration() + let url = try FileManager.default.databaseDirectoryURL(name: Self.name, appGroup: appGroup) - configuration.prepareDatabase { - try $0.usePassphrase(Secrets.databaseKey(identityId: nil, keychain: keychain)) + databaseWriter = try DatabasePool.withFileCoordinator(url: url, migrator: Self.migrator) { + try Secrets.databaseKey(identityId: nil, keychain: keychain) } - - databaseWriter = try DatabasePool(path: path, configuration: configuration) } - - try migrator.migrate(databaseWriter) } } diff --git a/Secrets/Sources/Secrets/Secrets.swift b/Secrets/Sources/Secrets/Secrets.swift index db9352e..7bdd516 100644 --- a/Secrets/Sources/Secrets/Secrets.swift +++ b/Secrets/Sources/Secrets/Secrets.swift @@ -172,7 +172,7 @@ public extension Secrets { private extension Secrets { static let keychainServiceName = "com.metabolist.metatext" - static let databaseKeyLength = 32 + static let databaseKeyLength = 48 private static func set(_ data: SecretsStorable, forAccount account: String, keychain: Keychain.Type) throws { try keychain.setGenericPassword( diff --git a/ServiceLayer/Sources/ServiceLayer/Entities/AppEnvironment.swift b/ServiceLayer/Sources/ServiceLayer/Entities/AppEnvironment.swift index dbdcf21..c2bf3ad 100644 --- a/ServiceLayer/Sources/ServiceLayer/Entities/AppEnvironment.swift +++ b/ServiceLayer/Sources/ServiceLayer/Entities/AppEnvironment.swift @@ -40,12 +40,14 @@ public struct AppEnvironment { } public extension AppEnvironment { + static let appGroup = "group.metabolist.metatext" + static func live(userNotificationCenter: UNUserNotificationCenter, reduceMotion: @escaping () -> Bool) -> Self { Self( session: URLSession.shared, webAuthSessionType: LiveWebAuthSession.self, keychain: LiveKeychain.self, - userDefaults: .standard, + userDefaults: UserDefaults(suiteName: appGroup)!, userNotificationClient: .live(userNotificationCenter), reduceMotion: reduceMotion, uuid: UUID.init, diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AllIdentitiesService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AllIdentitiesService.swift index 5a6acdc..d925dc8 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AllIdentitiesService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AllIdentitiesService.swift @@ -18,6 +18,7 @@ public struct AllIdentitiesService { self.environment = environment self.database = try environment.fixtureDatabase ?? IdentityDatabase( inMemory: environment.inMemoryContent, + appGroup: AppEnvironment.appGroup, keychain: environment.keychain) identitiesCreated = identitiesCreatedSubject.eraseToAnyPublisher() } @@ -88,7 +89,7 @@ public extension AllIdentitiesService { database.deleteIdentity(id: id) .collect() .tryMap { _ -> AnyPublisher in - try ContentDatabase.delete(id: id) + try ContentDatabase.delete(id: id, appGroup: AppEnvironment.appGroup) let secrets = Secrets(identityId: id, keychain: environment.keychain) diff --git a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift index 4ed4d42..732f38b 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift @@ -34,6 +34,7 @@ public struct IdentityService { useHomeTimelineLastReadId: appPreferences.homeTimelineBehavior == .rememberPosition, useNotificationsLastReadId: appPreferences.notificationsTabBehavior == .rememberPosition, inMemory: environment.inMemoryContent, + appGroup: AppEnvironment.appGroup, keychain: environment.keychain) } } diff --git a/Supporting Files/Metatext.entitlements b/Supporting Files/Metatext.entitlements index 439ee85..a9ea328 100644 --- a/Supporting Files/Metatext.entitlements +++ b/Supporting Files/Metatext.entitlements @@ -11,6 +11,10 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + group.metabolist.metatext + com.apple.security.network.client keychain-access-groups diff --git a/ViewModels/Sources/PreviewViewModels/PreviewViewModels.swift b/ViewModels/Sources/PreviewViewModels/PreviewViewModels.swift index 67c34a2..1ba9bfe 100644 --- a/ViewModels/Sources/PreviewViewModels/PreviewViewModels.swift +++ b/ViewModels/Sources/PreviewViewModels/PreviewViewModels.swift @@ -15,7 +15,7 @@ import ViewModels let db: IdentityDatabase = { let id = Identity.Id() - let db = try! IdentityDatabase(inMemory: true, keychain: MockKeychain.self) + let db = try! IdentityDatabase(inMemory: true, appGroup: "", keychain: MockKeychain.self) let secrets = Secrets(identityId: id, keychain: MockKeychain.self) try! secrets.setInstanceURL(.previewInstanceURL)