Fixed an issue with push notifications in legacy groups

• Reworked the NotificationServiceExtension to just always reset and reload it's state to avoid weird bugs
• Updated the legacy group messages to fallback to using a locally generated serverHash if one isn't provided (always happens for legacy PNs)
• Include error info when failing to process extension logs
• Made a bunch of the Storage functions instance functions instead of static functions
pull/1018/head
Morgan Pretty 7 months ago
parent 533afa2af0
commit 7e771467d6

@ -1 +1 @@
Subproject commit 12df14a6fc4c3276c651dbc612377ff0d80fb323 Subproject commit e1a76ebb7b8f90f4dc07e05ccaf88643da2829ad

@ -205,7 +205,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
// Stop all jobs except for message sending and when completed suspend the database // Stop all jobs except for message sending and when completed suspend the database
JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend, using: dependencies) { _ in JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend, using: dependencies) { _ in
LibSession.suspendNetworkAccess() LibSession.suspendNetworkAccess()
Storage.suspendDatabaseAccess(using: dependencies) Storage.shared.suspendDatabaseAccess()
Log.flush() Log.flush()
} }
} }

@ -147,7 +147,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
/// Apple's documentation on the matter) /// Apple's documentation on the matter)
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
Storage.resumeDatabaseAccess(using: dependencies) dependencies.storage.resumeDatabaseAccess()
LibSession.resumeNetworkAccess() LibSession.resumeNetworkAccess()
// Reset the 'startTime' (since it would be invalid from the last launch) // Reset the 'startTime' (since it would be invalid from the last launch)
@ -212,7 +212,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend, using: dependencies) { [dependencies] neededBackgroundProcessing in JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend, using: dependencies) { [dependencies] neededBackgroundProcessing in
if !self.hasCallOngoing() && (!neededBackgroundProcessing || Singleton.hasAppContext && Singleton.appContext.isInBackground) { if !self.hasCallOngoing() && (!neededBackgroundProcessing || Singleton.hasAppContext && Singleton.appContext.isInBackground) {
LibSession.suspendNetworkAccess() LibSession.suspendNetworkAccess()
Storage.suspendDatabaseAccess(using: dependencies) dependencies.storage.suspendDatabaseAccess()
Log.info("[AppDelegate] completed network and database shutdowns.") Log.info("[AppDelegate] completed network and database shutdowns.")
Log.flush() Log.flush()
} }
@ -238,7 +238,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
UserDefaults.sharedLokiProject?[.isMainAppActive] = true UserDefaults.sharedLokiProject?[.isMainAppActive] = true
// FIXME: Seems like there are some discrepancies between the expectations of how the iOS lifecycle methods work, we should look into them and ensure the code behaves as expected (in this case there were situations where these two wouldn't get called when returning from the background) // FIXME: Seems like there are some discrepancies between the expectations of how the iOS lifecycle methods work, we should look into them and ensure the code behaves as expected (in this case there were situations where these two wouldn't get called when returning from the background)
Storage.resumeDatabaseAccess(using: dependencies) dependencies.storage.resumeDatabaseAccess()
LibSession.resumeNetworkAccess() LibSession.resumeNetworkAccess()
ensureRootViewController(calledFrom: .didBecomeActive) ensureRootViewController(calledFrom: .didBecomeActive)
@ -288,7 +288,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
Log.appResumedExecution() Log.appResumedExecution()
Log.info("Starting background fetch.") Log.info("Starting background fetch.")
Storage.resumeDatabaseAccess(using: dependencies) dependencies.storage.resumeDatabaseAccess()
LibSession.resumeNetworkAccess() LibSession.resumeNetworkAccess()
let queue: DispatchQueue = .global(qos: .userInitiated) let queue: DispatchQueue = .global(qos: .userInitiated)
@ -312,7 +312,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
if Singleton.hasAppContext && Singleton.appContext.isInBackground { if Singleton.hasAppContext && Singleton.appContext.isInBackground {
LibSession.suspendNetworkAccess() LibSession.suspendNetworkAccess()
Storage.suspendDatabaseAccess(using: dependencies) dependencies.storage.suspendDatabaseAccess()
Log.flush() Log.flush()
} }
@ -338,7 +338,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
if Singleton.hasAppContext && Singleton.appContext.isInBackground { if Singleton.hasAppContext && Singleton.appContext.isInBackground {
LibSession.suspendNetworkAccess() LibSession.suspendNetworkAccess()
Storage.suspendDatabaseAccess(using: dependencies) dependencies.storage.suspendDatabaseAccess()
Log.flush() Log.flush()
} }
@ -471,7 +471,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
case .databaseError: case .databaseError:
alert.addAction(UIAlertAction(title: "vc_restore_title".localized(), style: .destructive) { [dependencies] _ in alert.addAction(UIAlertAction(title: "vc_restore_title".localized(), style: .destructive) { [dependencies] _ in
// Reset the current database for a clean migration // Reset the current database for a clean migration
Storage.resetForCleanMigration() dependencies.storage.resetForCleanMigration()
// Hide the top banner if there was one // Hide the top banner if there was one
TopBannerController.hide() TopBannerController.hide()

@ -117,7 +117,7 @@ public struct SessionApp {
LibSession.clearSnodeCache() LibSession.clearSnodeCache()
LibSession.suspendNetworkAccess() LibSession.suspendNetworkAccess()
PushNotificationAPI.resetKeys() PushNotificationAPI.resetKeys()
Storage.resetAllStorage() dependencies.storage.resetAllStorage()
ProfileManager.resetProfileStorage() ProfileManager.resetProfileStorage()
Attachment.resetAttachmentStorage() Attachment.resetAttachmentStorage()
AppEnvironment.shared.notificationPresenter.clearAllNotifications() AppEnvironment.shared.notificationPresenter.clearAllNotifications()

@ -293,7 +293,7 @@ public enum PushRegistrationError: Error {
// FIXME: Initialise the `PushRegistrationManager` with a dependencies instance // FIXME: Initialise the `PushRegistrationManager` with a dependencies instance
let dependencies: Dependencies = Dependencies() let dependencies: Dependencies = Dependencies()
Storage.resumeDatabaseAccess(using: dependencies) Storage.shared.resumeDatabaseAccess()
LibSession.resumeNetworkAccess() LibSession.resumeNetworkAccess()
let maybeCall: SessionCall? = Storage.shared.write { db in let maybeCall: SessionCall? = Storage.shared.write { db in

@ -4,6 +4,7 @@
import Foundation import Foundation
import GRDB import GRDB
import SessionSnodeKit
import SessionUtil import SessionUtil
import SessionUtilitiesKit import SessionUtilitiesKit
@ -256,4 +257,25 @@ public extension Crypto.Generator {
return try decryptedData ?? { throw MessageReceiverError.decryptionFailed }() return try decryptedData ?? { throw MessageReceiverError.decryptionFailed }()
} }
} }
static func messageServerHash(
swarmPubkey: String,
namespace: SnodeAPI.Namespace,
data: Data
) -> Crypto.Generator<String> {
return Crypto.Generator(
id: "messageServerHash",
args: [swarmPubkey, namespace, data]
) {
var cSwarmPubkey: [CChar] = try swarmPubkey.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }()
var cData: [CChar] = try data.base64EncodedString().cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }()
var cHash: [CChar] = [CChar](repeating: 0, count: 65)
guard session_compute_message_hash(cSwarmPubkey, Int16(namespace.rawValue), cData, &cHash) else {
throw MessageReceiverError.decryptionFailed
}
return String(cString: cHash)
}
}
} }

@ -203,7 +203,7 @@ public extension ConfigurationSyncJob {
static func enqueue( static func enqueue(
_ db: Database, _ db: Database,
publicKey: String, publicKey: String,
dependencies: Dependencies = Dependencies() using dependencies: Dependencies = Dependencies()
) { ) {
// Upsert a config sync job if needed // Upsert a config sync job if needed
dependencies.jobRunner.upsert( dependencies.jobRunner.upsert(

@ -21,6 +21,7 @@ public enum MessageReceiver {
var customMessage: Message? = nil var customMessage: Message? = nil
let sender: String let sender: String
let sentTimestamp: UInt64 let sentTimestamp: UInt64
let serverHash: String?
let openGroupServerMessageId: UInt64? let openGroupServerMessageId: UInt64?
let threadVariant: SessionThread.Variant let threadVariant: SessionThread.Variant
let threadIdGenerator: (Message) throws -> String let threadIdGenerator: (Message) throws -> String
@ -40,6 +41,7 @@ public enum MessageReceiver {
plaintext = data.removePadding() // Remove the padding plaintext = data.removePadding() // Remove the padding
sender = messageSender sender = messageSender
sentTimestamp = UInt64(floor(timestamp * 1000)) // Convert to ms for database consistency sentTimestamp = UInt64(floor(timestamp * 1000)) // Convert to ms for database consistency
serverHash = nil
openGroupServerMessageId = UInt64(messageServerId) openGroupServerMessageId = UInt64(messageServerId)
threadVariant = .community threadVariant = .community
threadIdGenerator = { message in threadIdGenerator = { message in
@ -50,10 +52,6 @@ public enum MessageReceiver {
} }
case (_, .openGroupInbox(let timestamp, let messageServerId, let serverPublicKey, let senderId, let recipientId)): case (_, .openGroupInbox(let timestamp, let messageServerId, let serverPublicKey, let senderId, let recipientId)):
guard let userEd25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
throw MessageReceiverError.noUserED25519KeyPair
}
(plaintext, sender) = try dependencies.crypto.tryGenerate( (plaintext, sender) = try dependencies.crypto.tryGenerate(
.plaintextWithSessionBlindingProtocol( .plaintextWithSessionBlindingProtocol(
db, db,
@ -67,11 +65,12 @@ public enum MessageReceiver {
plaintext = plaintext.removePadding() // Remove the padding plaintext = plaintext.removePadding() // Remove the padding
sentTimestamp = UInt64(floor(timestamp * 1000)) // Convert to ms for database consistency sentTimestamp = UInt64(floor(timestamp * 1000)) // Convert to ms for database consistency
serverHash = nil
openGroupServerMessageId = UInt64(messageServerId) openGroupServerMessageId = UInt64(messageServerId)
threadVariant = .contact threadVariant = .contact
threadIdGenerator = { _ in sender } threadIdGenerator = { _ in sender }
case (_, .swarm(let publicKey, let namespace, _, _, _)): case (_, .swarm(let publicKey, let namespace, let swarmServerHash, _, _)):
switch namespace { switch namespace {
case .default: case .default:
guard guard
@ -81,9 +80,6 @@ public enum MessageReceiver {
SNLog("Failed to unwrap data for message from 'default' namespace.") SNLog("Failed to unwrap data for message from 'default' namespace.")
throw MessageReceiverError.invalidMessage throw MessageReceiverError.invalidMessage
} }
guard let userX25519KeyPair: KeyPair = Identity.fetchUserKeyPair(db) else {
throw MessageReceiverError.noUserX25519KeyPair
}
(plaintext, sender) = try dependencies.crypto.tryGenerate( (plaintext, sender) = try dependencies.crypto.tryGenerate(
.plaintextWithSessionProtocol( .plaintextWithSessionProtocol(
@ -94,6 +90,7 @@ public enum MessageReceiver {
) )
plaintext = plaintext.removePadding() // Remove the padding plaintext = plaintext.removePadding() // Remove the padding
sentTimestamp = envelope.timestamp sentTimestamp = envelope.timestamp
serverHash = swarmServerHash
openGroupServerMessageId = nil openGroupServerMessageId = nil
threadVariant = .contact threadVariant = .contact
threadIdGenerator = { message in threadIdGenerator = { message in
@ -148,6 +145,16 @@ public enum MessageReceiver {
(plaintext, sender) = try decrypt(keyPairs: encryptionKeyPairs) (plaintext, sender) = try decrypt(keyPairs: encryptionKeyPairs)
plaintext = plaintext.removePadding() // Remove the padding plaintext = plaintext.removePadding() // Remove the padding
sentTimestamp = envelope.timestamp sentTimestamp = envelope.timestamp
/// If we weren't given a `serverHash` then compute one locally using the same logic the swarm would
switch swarmServerHash.isEmpty {
case false: serverHash = swarmServerHash
case true:
serverHash = dependencies.crypto.generate(
.messageServerHash(swarmPubkey: publicKey, namespace: namespace, data: data)
).defaulting(to: "")
}
openGroupServerMessageId = nil openGroupServerMessageId = nil
threadVariant = .legacyGroup threadVariant = .legacyGroup
threadIdGenerator = { _ in publicKey } threadIdGenerator = { _ in publicKey }
@ -170,7 +177,7 @@ public enum MessageReceiver {
let message: Message = try (customMessage ?? Message.createMessageFrom(proto, sender: sender)) let message: Message = try (customMessage ?? Message.createMessageFrom(proto, sender: sender))
message.sender = sender message.sender = sender
message.recipient = userSessionId message.recipient = userSessionId
message.serverHash = origin.serverHash message.serverHash = serverHash
message.sentTimestamp = sentTimestamp message.sentTimestamp = sentTimestamp
message.receivedTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs()) message.receivedTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
message.openGroupServerMessageId = openGroupServerMessageId message.openGroupServerMessageId = openGroupServerMessageId

@ -12,8 +12,8 @@ import SignalUtilitiesKit
import SessionUtilitiesKit import SessionUtilitiesKit
public final class NotificationServiceExtension: UNNotificationServiceExtension { public final class NotificationServiceExtension: UNNotificationServiceExtension {
private let dependencies: Dependencies = Dependencies() private var dependencies: Dependencies = Dependencies()
private var didPerformSetup = false private var startTime: CFTimeInterval = 0
private var contentHandler: ((UNNotificationContent) -> Void)? private var contentHandler: ((UNNotificationContent) -> Void)?
private var request: UNNotificationRequest? private var request: UNNotificationRequest?
private var hasCompleted: Atomic<Bool> = Atomic(false) private var hasCompleted: Atomic<Bool> = Atomic(false)
@ -27,6 +27,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// MARK: Did receive a remote push notification request // MARK: Did receive a remote push notification request
override public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { override public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.startTime = CACurrentMediaTime()
self.contentHandler = contentHandler self.contentHandler = contentHandler
self.request = request self.request = request
@ -51,37 +52,23 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
Singleton.setup(appContext: NotificationServiceExtensionContext()) Singleton.setup(appContext: NotificationServiceExtensionContext())
} }
// Perform main setup /// Perform main setup (create a new `Dependencies` instance each time so we don't need to worry about state from previous
Storage.resumeDatabaseAccess(using: dependencies) /// notifications causing issues with new notifications
self.dependencies = Dependencies()
DispatchQueue.main.sync { DispatchQueue.main.sync {
self.setUpIfNecessary() { [weak self] in self.performSetup { [weak self] in
self?.handleNotification(notificationContent, isPerformingResetup: false) self?.handleNotification(notificationContent, isPerformingResetup: false)
} }
} }
} }
private func handleNotification(_ notificationContent: UNMutableNotificationContent, isPerformingResetup: Bool) { private func handleNotification(_ notificationContent: UNMutableNotificationContent, isPerformingResetup: Bool) {
let userSessionId: String = getUserHexEncodedPublicKey(using: dependencies)
let (maybeData, metadata, result) = PushNotificationAPI.processNotification( let (maybeData, metadata, result) = PushNotificationAPI.processNotification(
notificationContent: notificationContent, notificationContent: notificationContent,
using: dependencies using: dependencies
) )
/// There is an annoying issue where clearing account data and creating a new account can result in the user receiving push notifications
/// for the new account but the NotificationServiceExtension having cached state based on the old account
///
/// In order to avoid this we check if the account the notification was sent to matches the current users sessionId and if it doesn't (and the
/// notification is for a message stored in one of the users namespaces) then try to re-setup the notification extension
guard !metadata.namespace.isCurrentUserNamespace || metadata.accountId == userSessionId else {
guard !isPerformingResetup else {
Log.error("Received notification for an accountId that isn't the current user, resetup failed.")
return self.completeSilenty(handledNotification: false)
}
Log.warn("Received notification for an accountId that isn't the current user, attempting to resetup.")
return self.forceResetup(notificationContent)
}
guard guard
(result == .success || result == .legacySuccess), (result == .success || result == .legacySuccess),
let data: Data = maybeData let data: Data = maybeData
@ -246,6 +233,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// If an error occurred we want to rollback the transaction (by throwing) and then handle // If an error occurred we want to rollback the transaction (by throwing) and then handle
// the error outside of the database // the error outside of the database
let handleError = { let handleError = {
// Dispatch to the next run loop to ensure we are out of the database write thread before
// handling the result (and suspending the database)
DispatchQueue.main.async {
switch error { switch error {
case MessageReceiverError.noGroupKeyPair: case MessageReceiverError.noGroupKeyPair:
Log.warn("Failed due to having no legacy group decryption keys.") Log.warn("Failed due to having no legacy group decryption keys.")
@ -273,6 +263,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
default: self?.handleFailure(for: notificationContent, error: .other(error)) default: self?.handleFailure(for: notificationContent, error: .other(error))
} }
} }
}
db.afterNextTransaction( db.afterNextTransaction(
onCommit: { _ in handleError() }, onCommit: { _ in handleError() },
@ -285,19 +276,30 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// MARK: Setup // MARK: Setup
private func setUpIfNecessary(completion: @escaping () -> Void) { private func performSetup(completion: @escaping () -> Void) {
Log.assertOnMainThread()
// The NSE will often re-use the same process, so if we're
// already set up we want to do nothing; we're already ready
// to process new messages.
guard !didPerformSetup else { return completion() }
Log.info("Performing setup.") Log.info("Performing setup.")
didPerformSetup = true
_ = AppVersion.shared _ = AppVersion.shared
// FIXME: Remove these once the database instance is fully managed via `Dependencies`
if AppSetup.hasRun {
dependencies.storage.resumeDatabaseAccess()
dependencies.storage.reconfigureDatabase()
dependencies.caches.mutate(cache: .general) { $0.clearCachedUserPublicKey() }
// If we had already done a setup then `libSession` won't have been re-setup so
// we need to do so now (this ensures it has the correct user keys as well)
LibSession.clearMemoryState(using: dependencies)
dependencies.storage.read { [dependencies] db in
LibSession.loadState(
db,
userPublicKey: getUserHexEncodedPublicKey(db, using: dependencies),
ed25519SecretKey: Identity.fetchUserEd25519KeyPair(db)?.secretKey,
using: dependencies
)
}
}
AppSetup.setupEnvironment( AppSetup.setupEnvironment(
retrySetupIfDatabaseInvalid: true, retrySetupIfDatabaseInvalid: true,
appSpecificBlock: { appSpecificBlock: {
@ -315,49 +317,32 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// Setup LibSession // Setup LibSession
LibSession.addLogger() LibSession.addLogger()
}, },
migrationsCompletion: { [weak self] result, needsConfigSync in migrationsCompletion: { [weak self, dependencies] result, _ in
switch result { switch result {
case .failure(let error): case .failure(let error):
Log.error("Failed to complete migrations: \(error).") Log.error("Failed to complete migrations: \(error).")
self?.completeSilenty(handledNotification: false) self?.completeSilenty(handledNotification: false)
case .success: case .success:
DispatchQueue.main.async {
// Ensure storage is actually valid
guard dependencies.storage.isValid else {
Log.error("Storage invalid.")
self?.completeSilenty(handledNotification: false)
return
}
// We should never receive a non-voip notification on an app that doesn't support // We should never receive a non-voip notification on an app that doesn't support
// app extensions since we have to inform the service we wanted these, so in theory // app extensions since we have to inform the service we wanted these, so in theory
// this path should never occur. However, the service does have our push token // this path should never occur. However, the service does have our push token
// so it is possible that could change in the future. If it does, do nothing // so it is possible that could change in the future. If it does, do nothing
// and don't disturb the user. Messages will be processed when they open the app. // and don't disturb the user. Messages will be processed when they open the app.
guard Storage.shared[.isReadyForAppExtensions] else { guard dependencies.storage[.isReadyForAppExtensions] else {
Log.error("Not ready for extensions.") Log.error("Not ready for extensions.")
self?.completeSilenty(handledNotification: false) self?.completeSilenty(handledNotification: false)
return return
} }
DispatchQueue.main.async {
self?.versionMigrationsDidComplete(needsConfigSync: needsConfigSync, completion: completion)
}
}
},
using: dependencies
)
}
private func versionMigrationsDidComplete(needsConfigSync: Bool, completion: @escaping () -> Void) {
Log.assertOnMainThread()
// If we need a config sync then trigger it now
if needsConfigSync {
Storage.shared.write { db in
ConfigurationSyncJob.enqueue(db, publicKey: getUserHexEncodedPublicKey(db))
}
}
// App isn't ready until storage is ready AND all version migrations are complete.
guard Storage.shared.isValid else {
Log.error("Storage invalid.")
return self.completeSilenty(handledNotification: false)
}
// If the app wasn't ready then mark it as ready now // If the app wasn't ready then mark it as ready now
if !Singleton.appReadiness.isAppReady { if !Singleton.appReadiness.isAppReady {
// Note that this does much more than set a flag; it will also run all deferred blocks. // Note that this does much more than set a flag; it will also run all deferred blocks.
@ -366,36 +351,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
completion() completion()
} }
}
/// It's possible for the NotificationExtension to still have some kind of cached data from the old database after it's been deleted },
/// when a new account is created shortly after, this results in weird errors when receiving PNs for the new account
///
/// In order to avoid this situation we check to see whether the received PN is targetting the current user and, if not, we call this
/// method to force a resetup of the notification extension
///
/// **Note:** We need to reconfigure the database here because if the database was deleted it's possible for the NotificationExtension
/// to somehow still have some form of access to the old one
private func forceResetup(_ notificationContent: UNMutableNotificationContent) {
Storage.reconfigureDatabase()
LibSession.clearMemoryState(using: dependencies)
dependencies.caches.mutate(cache: .general) { $0.clearCachedUserPublicKey() }
self.setUpIfNecessary() { [weak self, dependencies] in
// If we had already done a setup then `libSession` won't have been re-setup so
// we need to do so now (this ensures it has the correct user keys as well)
Storage.shared.read { db in
LibSession.loadState(
db,
userPublicKey: getUserHexEncodedPublicKey(db),
ed25519SecretKey: Identity.fetchUserEd25519KeyPair(db)?.secretKey,
using: dependencies using: dependencies
) )
} }
self?.handleNotification(notificationContent, isPerformingResetup: true)
}
}
// MARK: Handle completion // MARK: Handle completion
override public func serviceExtensionTimeWillExpire() { override public func serviceExtensionTimeWillExpire() {
@ -416,15 +377,17 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
else { return } else { return }
let silentContent: UNMutableNotificationContent = UNMutableNotificationContent() let silentContent: UNMutableNotificationContent = UNMutableNotificationContent()
silentContent.badge = Storage.shared
if !isMainAppAndActive {
silentContent.badge = dependencies.storage
.read { db in try Interaction.fetchUnreadCount(db) } .read { db in try Interaction.fetchUnreadCount(db) }
.map { NSNumber(value: $0) } .map { NSNumber(value: $0) }
.defaulting(to: NSNumber(value: 0)) .defaulting(to: NSNumber(value: 0))
dependencies.storage.suspendDatabaseAccess()
Log.info(handledNotification ? "Completed after handling notification." : "Completed silently.")
if !isMainAppAndActive {
Storage.suspendDatabaseAccess(using: dependencies)
} }
let duration: CFTimeInterval = (CACurrentMediaTime() - startTime)
Log.info(handledNotification ? "Completed after handling notification in \(.seconds(duration), unit: .ms)." : "Completed silently after \(.seconds(duration), unit: .ms).")
Log.flush() Log.flush()
self.contentHandler!(silentContent) self.contentHandler!(silentContent)
@ -502,8 +465,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
} }
private func handleFailure(for content: UNMutableNotificationContent, error: NotificationError) { private func handleFailure(for content: UNMutableNotificationContent, error: NotificationError) {
Log.error("Show generic failure message due to error: \(error).") dependencies.storage.suspendDatabaseAccess()
Storage.suspendDatabaseAccess(using: dependencies)
let duration: CFTimeInterval = (CACurrentMediaTime() - startTime)
Log.error("Show generic failure message after \(.seconds(duration), unit: .ms) due to error: \(error).")
Log.flush() Log.flush()
content.title = "Session" content.title = "Session"

@ -111,7 +111,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
// When the thread picker disappears it means the user has left the screen (this will be called // When the thread picker disappears it means the user has left the screen (this will be called
// whether the user has sent the message or cancelled sending) // whether the user has sent the message or cancelled sending)
LibSession.suspendNetworkAccess() LibSession.suspendNetworkAccess()
Storage.suspendDatabaseAccess(using: viewModel.dependencies) viewModel.dependencies.storage.suspendDatabaseAccess()
Log.flush() Log.flush()
} }
@ -240,7 +240,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
shareNavController?.dismiss(animated: true, completion: nil) shareNavController?.dismiss(animated: true, completion: nil)
ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { [dependencies = viewModel.dependencies] activityIndicator in ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { [dependencies = viewModel.dependencies] activityIndicator in
Storage.resumeDatabaseAccess(using: dependencies) dependencies.storage.resumeDatabaseAccess()
LibSession.resumeNetworkAccess() LibSession.resumeNetworkAccess()
let swarmPublicKey: String = { let swarmPublicKey: String = {
@ -336,7 +336,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { [weak self] result in receiveCompletion: { [weak self] result in
LibSession.suspendNetworkAccess() LibSession.suspendNetworkAccess()
Storage.suspendDatabaseAccess(using: dependencies) dependencies.storage.suspendDatabaseAccess()
Log.flush() Log.flush()
activityIndicator.dismiss { } activityIndicator.dismiss { }

@ -91,7 +91,7 @@ open class Storage {
/// **Note:** If we fail to get/generate the keySpec then don't bother continuing to setup the Database as it'll just be invalid, /// **Note:** If we fail to get/generate the keySpec then don't bother continuing to setup the Database as it'll just be invalid,
/// in this case the App/Extensions will have logic that checks the `isValid` flag of the database /// in this case the App/Extensions will have logic that checks the `isValid` flag of the database
do { do {
var tmpKeySpec: Data = try Storage.getOrGenerateDatabaseKeySpec() var tmpKeySpec: Data = try getOrGenerateDatabaseKeySpec()
tmpKeySpec.resetBytes(in: 0..<tmpKeySpec.count) tmpKeySpec.resetBytes(in: 0..<tmpKeySpec.count)
} }
catch { return } catch { return }
@ -108,8 +108,8 @@ open class Storage {
config.busyMode = .timeout(Storage.writeTransactionStartTimeout) config.busyMode = .timeout(Storage.writeTransactionStartTimeout)
/// Load in the SQLCipher keys /// Load in the SQLCipher keys
config.prepareDatabase { db in config.prepareDatabase { [weak self] db in
var keySpec: Data = try Storage.getOrGenerateDatabaseKeySpec() var keySpec: Data = try self?.getOrGenerateDatabaseKeySpec() ?? { throw StorageError.invalidKeySpec }()
defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use
// Use a raw key spec, where the 96 hexadecimal digits are provided // Use a raw key spec, where the 96 hexadecimal digits are provided
@ -358,12 +358,12 @@ open class Storage {
return try Singleton.keychain.data(forKey: .dbCipherKeySpec) return try Singleton.keychain.data(forKey: .dbCipherKeySpec)
} }
@discardableResult private static func getOrGenerateDatabaseKeySpec() throws -> Data { @discardableResult private func getOrGenerateDatabaseKeySpec() throws -> Data {
do { do {
var keySpec: Data = try getDatabaseCipherKeySpec() var keySpec: Data = try Storage.getDatabaseCipherKeySpec()
defer { keySpec.resetBytes(in: 0..<keySpec.count) } defer { keySpec.resetBytes(in: 0..<keySpec.count) }
guard keySpec.count == kSQLCipherKeySpecLength else { throw StorageError.invalidKeySpec } guard keySpec.count == Storage.kSQLCipherKeySpecLength else { throw StorageError.invalidKeySpec }
return keySpec return keySpec
} }
@ -382,7 +382,7 @@ open class Storage {
case (_, errSecItemNotFound): case (_, errSecItemNotFound):
// No keySpec was found so we need to generate a new one // No keySpec was found so we need to generate a new one
do { do {
var keySpec: Data = try Randomness.generateRandomBytes(numberBytes: kSQLCipherKeySpecLength) var keySpec: Data = try Randomness.generateRandomBytes(numberBytes: Storage.kSQLCipherKeySpecLength)
defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use
try Singleton.keychain.set(data: keySpec, forKey: .dbCipherKeySpec) try Singleton.keychain.set(data: keySpec, forKey: .dbCipherKeySpec)
@ -426,40 +426,41 @@ open class Storage {
/// The generally suggested approach is to avoid this entirely by not storing the database in an AppGroup folder and sharing it /// The generally suggested approach is to avoid this entirely by not storing the database in an AppGroup folder and sharing it
/// with extensions - this may be possible but will require significant refactoring and a potentially painful migration to move the /// with extensions - this may be possible but will require significant refactoring and a potentially painful migration to move the
/// database and other files into the App folder /// database and other files into the App folder
public static func suspendDatabaseAccess(using dependencies: Dependencies) { public func suspendDatabaseAccess() {
guard !dependencies.storage.isSuspended else { return } guard !isSuspended else { return }
dependencies.storage.isSuspended = true isSuspended = true
Log.info("[Storage] Database access suspended.") Log.info("[Storage] Database access suspended.")
/// Interrupt any open transactions (if this function is called then we are expecting that all processes have finished running /// Interrupt any open transactions (if this function is called then we are expecting that all processes have finished running
/// and don't actually want any more transactions to occur) /// and don't actually want any more transactions to occur)
dependencies.storage.dbWriter?.interrupt() dbWriter?.interrupt()
} }
/// This method reverses the database suspension used to prevent the `0xdead10cc` exception (see `suspendDatabaseAccess()` /// This method reverses the database suspension used to prevent the `0xdead10cc` exception (see `suspendDatabaseAccess()`
/// above for more information /// above for more information
public static func resumeDatabaseAccess(using dependencies: Dependencies) { public func resumeDatabaseAccess() {
guard dependencies.storage.isSuspended else { return } guard isSuspended else { return }
dependencies.storage.isSuspended = false
isSuspended = false
Log.info("[Storage] Database access resumed.") Log.info("[Storage] Database access resumed.")
} }
public static func resetAllStorage() { public func resetAllStorage() {
Storage.shared.isValid = false isValid = false
Storage.internalHasCreatedValidInstance.mutate { $0 = false } Storage.internalHasCreatedValidInstance.mutate { $0 = false }
Storage.shared.migrationsCompleted.mutate { $0 = false } migrationsCompleted.mutate { $0 = false }
Storage.shared.dbWriter = nil dbWriter = nil
deleteDatabaseFiles() deleteDatabaseFiles()
do { try deleteDbKeys() } catch { Log.warn("Failed to delete database keys.") } do { try deleteDbKeys() } catch { Log.warn("Failed to delete database keys.") }
} }
public static func reconfigureDatabase() { public func reconfigureDatabase() {
Storage.shared.configureDatabase() configureDatabase()
} }
public static func resetForCleanMigration() { public func resetForCleanMigration() {
// Clear existing content // Clear existing content
resetAllStorage() resetAllStorage()
@ -467,13 +468,13 @@ open class Storage {
reconfigureDatabase() reconfigureDatabase()
} }
private static func deleteDatabaseFiles() { private func deleteDatabaseFiles() {
do { try FileSystem.deleteFile(at: databasePath) } catch { Log.warn("Failed to delete database.") } do { try FileSystem.deleteFile(at: Storage.databasePath) } catch { Log.warn("Failed to delete database.") }
do { try FileSystem.deleteFile(at: databasePathShm) } catch { Log.warn("Failed to delete database-shm.") } do { try FileSystem.deleteFile(at: Storage.databasePathShm) } catch { Log.warn("Failed to delete database-shm.") }
do { try FileSystem.deleteFile(at: databasePathWal) } catch { Log.warn("Failed to delete database-wal.") } do { try FileSystem.deleteFile(at: Storage.databasePathWal) } catch { Log.warn("Failed to delete database-wal.") }
} }
private static func deleteDbKeys() throws { private func deleteDbKeys() throws {
try Singleton.keychain.remove(key: .dbCipherKeySpec) try Singleton.keychain.remove(key: .dbCipherKeySpec)
} }
@ -734,7 +735,7 @@ public extension ValueObservation {
#if DEBUG #if DEBUG
public extension Storage { public extension Storage {
func exportInfo(password: String) throws -> (dbPath: String, keyPath: String) { func exportInfo(password: String) throws -> (dbPath: String, keyPath: String) {
var keySpec: Data = try Storage.getOrGenerateDatabaseKeySpec() var keySpec: Data = try getOrGenerateDatabaseKeySpec()
defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use
guard var passwordData: Data = password.data(using: .utf8) else { throw StorageError.generic } guard var passwordData: Data = password.data(using: .utf8) else { throw StorageError.generic }

@ -446,7 +446,7 @@ public class Logger {
} }
} }
catch { catch {
self?.completeResumeLogging(error: "Unable to write extension logs to current log file") self?.completeResumeLogging(error: "Unable to write extension logs to current log file due to error: \(error)")
return return
} }

@ -8,7 +8,8 @@ import SessionUIKit
import SessionSnodeKit import SessionSnodeKit
public enum AppSetup { public enum AppSetup {
private static let hasRun: Atomic<Bool> = Atomic(false) private static let _hasRun: Atomic<Bool> = Atomic(false)
public static var hasRun: Bool { _hasRun.wrappedValue }
public static func setupEnvironment( public static func setupEnvironment(
retrySetupIfDatabaseInvalid: Bool = false, retrySetupIfDatabaseInvalid: Bool = false,
@ -18,13 +19,13 @@ public enum AppSetup {
using dependencies: Dependencies using dependencies: Dependencies
) { ) {
// If we've already run the app setup then only continue under certain circumstances // If we've already run the app setup then only continue under certain circumstances
guard !AppSetup.hasRun.wrappedValue else { guard !AppSetup._hasRun.wrappedValue else {
let storageIsValid: Bool = Storage.shared.isValid let storageIsValid: Bool = dependencies.storage.isValid
switch (retrySetupIfDatabaseInvalid, storageIsValid) { switch (retrySetupIfDatabaseInvalid, storageIsValid) {
case (true, false): case (true, false):
Storage.reconfigureDatabase() dependencies.storage.reconfigureDatabase()
AppSetup.hasRun.mutate { $0 = false } AppSetup._hasRun.mutate { $0 = false }
AppSetup.setupEnvironment( AppSetup.setupEnvironment(
retrySetupIfDatabaseInvalid: false, // Don't want to get stuck in a loop retrySetupIfDatabaseInvalid: false, // Don't want to get stuck in a loop
appSpecificBlock: appSpecificBlock, appSpecificBlock: appSpecificBlock,
@ -42,7 +43,7 @@ public enum AppSetup {
return return
} }
AppSetup.hasRun.mutate { $0 = true } AppSetup._hasRun.mutate { $0 = true }
var backgroundTask: SessionBackgroundTask? = SessionBackgroundTask(label: #function) var backgroundTask: SessionBackgroundTask? = SessionBackgroundTask(label: #function)
@ -91,7 +92,7 @@ public enum AppSetup {
) { ) {
var backgroundTask: SessionBackgroundTask? = (backgroundTask ?? SessionBackgroundTask(label: #function)) var backgroundTask: SessionBackgroundTask? = (backgroundTask ?? SessionBackgroundTask(label: #function))
Storage.shared.perform( dependencies.storage.perform(
migrationTargets: [ migrationTargets: [
SNUtilitiesKit.self, SNUtilitiesKit.self,
SNSnodeKit.self, SNSnodeKit.self,

Loading…
Cancel
Save