diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index e1139890a..394cc24ac 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -278,7 +278,7 @@ C32C5A88256DBCF9003C73A2 /* MessageReceiver+LegacyClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C32C5A87256DBCF9003C73A2 /* MessageReceiver+LegacyClosedGroups.swift */; }; C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB75255A581000E217F9 /* AppReadiness.m */; }; C32C5C46256DCBB2003C73A2 /* AppReadiness.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB01255A580700E217F9 /* AppReadiness.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */; }; + C32C5D83256DD5B6003C73A2 /* KeychainStorageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBBC255A581600E217F9 /* KeychainStorageType.swift */; }; C32C5DBF256DD743003C73A2 /* GroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB34255A580B00E217F9 /* GroupPoller.swift */; }; C32C5DC9256DD935003C73A2 /* ProxiedContentDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */; }; C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAFD255A580600E217F9 /* LRUCache.swift */; }; @@ -1548,7 +1548,7 @@ C33FDBA8255A581500E217F9 /* LinkPreviewDraft.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkPreviewDraft.swift; sourceTree = ""; }; C33FDBAB255A581500E217F9 /* OWSFileSystem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSFileSystem.h; sourceTree = ""; }; C33FDBB6255A581600E217F9 /* DataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DataSource.m; sourceTree = ""; }; - C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKKeychainStorage.swift; sourceTree = ""; }; + C33FDBBC255A581600E217F9 /* KeychainStorageType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainStorageType.swift; sourceTree = ""; }; C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSSignalAddress.swift; sourceTree = ""; }; C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushNotificationAPI.swift; sourceTree = ""; }; C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackgroundTask.m; sourceTree = ""; }; @@ -2747,7 +2747,6 @@ FD28A4F527EAD44C00FF65E7 /* Storage.swift */, C33FDBAB255A581500E217F9 /* OWSFileSystem.h */, C33FDA8E255A57FD00E217F9 /* OWSFileSystem.m */, - C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */, ); path = Database; sourceTree = ""; @@ -3790,6 +3789,7 @@ FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */, FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */, FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */, + C33FDBBC255A581600E217F9 /* KeychainStorageType.swift */, FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */, FD30036D2A3AE26000B5A5FB /* CExceptionHelper.mm */, FD12A84A2AD6458800EEBA0D /* DifferenceKit+Utilities.swift */, @@ -6144,7 +6144,7 @@ FDE658A129418C7900A33BC1 /* CryptoKit+Utilities.swift in Sources */, FDFD645927F26C6800808CA1 /* Array+Utilities.swift in Sources */, 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */, - C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */, + C32C5D83256DD5B6003C73A2 /* KeychainStorageType.swift in Sources */, FD559DF52A7368CB00C7C62A /* DispatchQueue+Utilities.swift in Sources */, FDF8488329405A12007DCAE5 /* BatchResponse.swift in Sources */, C3D9E39B256763C20040E4F3 /* AppContext.m in Sources */, diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift index 8e6c8b4b6..851bfb554 100644 --- a/Session/Meta/SessionApp.swift +++ b/Session/Meta/SessionApp.swift @@ -121,10 +121,11 @@ public struct SessionApp { DDLog.flushLog() SessionUtil.clearMemoryState(using: dependencies) - Storage.resetAllStorage() + Storage.resetAllStorage(using: dependencies) DisplayPictureManager.resetStorage(using: dependencies) Attachment.resetAttachmentStorage() AppEnvironment.shared.notificationPresenter.clearAllNotifications() + dependencies[singleton: .keychain].removeAll() onReset?() exit(0) diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index b0b387181..9bcc18b9f 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -123,6 +123,9 @@ enum Onboarding { ) { let sessionId: SessionId = SessionId(.standard, publicKey: x25519KeyPair.publicKey) + // Reset the PushNotificationAPI keys (just in case they were left over from a prior install) + PushNotificationAPI.deleteKeys(using: dependencies) + // Store the user identity information dependencies[singleton: .storage].write { db in try Identity.store( @@ -185,7 +188,7 @@ enum Onboarding { // Only continue if this isn't a new account guard self != .register else { return } - // Fetch the + // Fetch the profile name Onboarding.profileNamePublisher .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) .sinkUntilComplete() diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index 5afd5d629..8c05ea3da 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -225,6 +225,19 @@ public extension ClosedGroup { /// Start polling dependencies[singleton: .groupsPoller].startIfNeeded(for: group.id, using: dependencies) + + /// Subscribe for group push notifications + if let token: String = dependencies[defaults: .standard, key: .deviceToken] { + try? PushNotificationAPI + .preparedSubscribe( + db, + token: Data(hex: token), + sessionId: SessionId(.group, hex: group.id), + using: dependencies + ) + .send(using: dependencies) + .sinkUntilComplete() + } } static func removeData( @@ -271,13 +284,24 @@ public extension ClosedGroup { try? PushNotificationAPI .preparedUnsubscribeFromLegacyGroup( legacyGroupId: threadId, - userSessionId: userSessionId + userSessionId: userSessionId, + using: dependencies ) .send(using: dependencies) .sinkUntilComplete() case .group: - break + if let token: String = dependencies[defaults: .standard, key: .deviceToken] { + try? PushNotificationAPI + .preparedUnsubscribe( + db, + token: Data(hex: token), + sessionId: userSessionId, + using: dependencies + ) + .send(using: dependencies) + .sinkUntilComplete() + } default: break } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index 35f3a4aa8..63d8f69ea 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -132,6 +132,10 @@ extension MessageReceiver { /// or modified clients let inviteSenderIsApproved: Bool = ((try? Contact.fetchOne(db, id: sender))?.isApproved == true) let threadAlreadyExisted: Bool = ((try? SessionThread.exists(db, id: message.groupSessionId.hexString)) ?? false) + let wasKickedFromGroup: Bool = SessionUtil.wasKickedFromGroup( + groupSessionId: message.groupSessionId, + using: dependencies + ) try MessageReceiver.handleNewGroup( db, @@ -155,7 +159,7 @@ extension MessageReceiver { ).upsert(db) /// If the thread didn't already exist then insert an 'invited' info message - guard !threadAlreadyExisted else { return } + guard !threadAlreadyExisted || wasKickedFromGroup else { return } let interaction: Interaction = try Interaction( threadId: message.groupSessionId.hexString, diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift index 10975fec8..b5b3064eb 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift @@ -263,7 +263,8 @@ extension MessageReceiver { ) .asRequest(of: String.self) .fetchSet(db) - .inserting(legacyGroupSessionId) // Insert the new key just to be sure + .inserting(legacyGroupSessionId), // Insert the new key just to be sure + using: dependencies )? .send(using: dependencies) .subscribe(on: DispatchQueue.global(qos: .default), using: dependencies) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift index 3c1f441df..aaa3f1c95 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift @@ -75,12 +75,17 @@ extension MessageSender { try createdInfo.members.forEach { try $0.insert(db) } // Prepare the notification subscription - let preparedNotificationSubscription = try? PushNotificationAPI - .preparedSubscribe( - db, - sessionId: createdInfo.groupSessionId, - using: dependencies - ) + var preparedNotificationSubscription: HTTP.PreparedRequest? + + if let token: String = dependencies[defaults: .standard, key: .deviceToken] { + preparedNotificationSubscription = try? PushNotificationAPI + .preparedSubscribe( + db, + token: Data(hex: token), + sessionId: createdInfo.groupSessionId, + using: dependencies + ) + } return ( createdInfo.groupSessionId, diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift index 1b9e0cb79..4e584be18 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift @@ -144,7 +144,8 @@ extension MessageSender { try? PushNotificationAPI .preparedSubscribeToLegacyGroups( userSessionId: userSessionId, - legacyGroupIds: allActiveLegacyGroupIds + legacyGroupIds: allActiveLegacyGroupIds, + using: dependencies )? .map { _, _ in () } ) diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index 8fe94ca97..9fd862649 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -1,4 +1,6 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable import Foundation import Combine @@ -6,9 +8,14 @@ import GRDB import SessionSnodeKit import SessionUtilitiesKit +// MARK: - KeychainStorage + +public extension KeychainStorage.ServiceKey { static let pushNotificationAPI: Self = "PNKeyChainService" } +public extension KeychainStorage.DataKey { static let pushNotificationEncryptionKey: Self = "PNEncryptionKeyKey" } + +// MARK: - PushNotificationAPI + public enum PushNotificationAPI { - private static let keychainService: String = "PNKeyChainService" - private static let encryptionKeyKey: String = "PNEncryptionKeyKey" private static let encryptionKeyLength: Int = 32 private static let maxRetryCount: Int = 4 private static let tokenExpirationInterval: TimeInterval = (12 * 60 * 60) @@ -23,10 +30,11 @@ public enum PushNotificationAPI { public static func subscribeAll( token: Data, isForcedUpdate: Bool, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) -> AnyPublisher { typealias SubscribeAllPreparedRequests = ( HTTP.PreparedRequest, + [HTTP.PreparedRequest], HTTP.PreparedRequest? ) let hexEncodedToken: String = token.toHexString() @@ -47,6 +55,7 @@ public enum PushNotificationAPI { let preparedUserRequest = try PushNotificationAPI .preparedSubscribe( db, + token: token, sessionId: userSessionId, using: dependencies ) @@ -59,6 +68,21 @@ public enum PushNotificationAPI { dependencies[defaults: .standard, key: .isUsingFullAPNs] = true } ) + let preparedGroupRequests = try ClosedGroup + .select(.threadId) + .filter(ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%")) + .filter(ClosedGroup.Columns.shouldPoll) + .asRequest(of: String.self) + .fetchSet(db) + .map { groupId in + try PushNotificationAPI + .preparedSubscribe( + db, + token: token, + sessionId: SessionId(.group, hex: groupId), + using: dependencies + ) + } let preparedLegacyGroupRequest = try PushNotificationAPI .preparedSubscribeToLegacyGroups( forced: true, @@ -78,10 +102,11 @@ public enum PushNotificationAPI { return ( preparedUserRequest, + preparedGroupRequests, preparedLegacyGroupRequest ) } - .flatMap { userRequest, legacyGroupRequest -> AnyPublisher in + .flatMap { userRequest, preparedGroupRequests, legacyGroupRequest -> AnyPublisher in Publishers .MergeMany( [ @@ -94,7 +119,16 @@ public enum PushNotificationAPI { .send(using: dependencies) .map { _, _ in () } .eraseToAnyPublisher() - ].compactMap { $0 } + ] + .appending( + contentsOf: preparedGroupRequests.map { request in + request + .send(using: dependencies) + .map { _, _ in () } + .eraseToAnyPublisher() + } + ) + .compactMap { $0 } ) .collect() .map { _ in () } @@ -105,27 +139,23 @@ public enum PushNotificationAPI { public static func unsubscribeAll( token: Data, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) -> AnyPublisher { typealias UnsubscribeAllPreparedRequests = ( HTTP.PreparedRequest, + [HTTP.PreparedRequest], [HTTP.PreparedRequest] ) return dependencies[singleton: .storage] .readPublisher(using: dependencies) { db -> UnsubscribeAllPreparedRequests in - guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else { - throw SnodeAPIError.noKeyPair - } - let userSessionId: SessionId = getUserSessionId(db, using: dependencies) let preparedUserRequest = try PushNotificationAPI .preparedUnsubscribe( db, token: token, sessionId: userSessionId, - subkey: nil, - ed25519KeyPair: userED25519KeyPair + using: dependencies ) .handleEvents( receiveOutput: { _, response in @@ -134,6 +164,20 @@ public enum PushNotificationAPI { dependencies[defaults: .standard, key: .deviceToken] = nil } ) + let preparedGroupUnsubscribeRequests = (try? ClosedGroup + .select(.threadId) + .filter(ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%")) + .asRequest(of: String.self) + .fetchSet(db)) + .defaulting(to: []) + .compactMap { groupId in + try? PushNotificationAPI.preparedUnsubscribe( + db, + token: token, + sessionId: SessionId(.group, hex: groupId), + using: dependencies + ) + } // FIXME: Remove this once legacy groups are deprecated let preparedLegacyUnsubscribeRequests = (try? ClosedGroup @@ -145,13 +189,14 @@ public enum PushNotificationAPI { .compactMap { legacyGroupId in try? PushNotificationAPI.preparedUnsubscribeFromLegacyGroup( legacyGroupId: legacyGroupId, - userSessionId: userSessionId + userSessionId: userSessionId, + using: dependencies ) } - return (preparedUserRequest, preparedLegacyUnsubscribeRequests) + return (preparedUserRequest, preparedGroupUnsubscribeRequests, preparedLegacyUnsubscribeRequests) } - .flatMap { preparedUserRequest, preparedLegacyUnsubscribeRequests in + .flatMap { preparedUserRequest, preparedGroupUnsubscribeRequests, preparedLegacyUnsubscribeRequests in // FIXME: Remove this once legacy groups are deprecated /// Unsubscribe from all legacy groups (including ones the user is no longer a member of, just in case) Publishers @@ -161,9 +206,24 @@ public enum PushNotificationAPI { .receive(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) .sinkUntilComplete() - return preparedUserRequest.send(using: dependencies) + return Publishers + .MergeMany( + [ + preparedUserRequest + .send(using: dependencies) + .map { _, _ in () } + .eraseToAnyPublisher() + ] + .appending( + contentsOf: preparedGroupUnsubscribeRequests.map { request in + request + .send(using: dependencies) + .map { _, _ in () } + .eraseToAnyPublisher() + } + ) + ) } - .map { _ in () } .eraseToAnyPublisher() } @@ -171,13 +231,11 @@ public enum PushNotificationAPI { public static func preparedSubscribe( _ db: Database, + token: Data, sessionId: SessionId, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) throws -> HTTP.PreparedRequest { - guard - dependencies[defaults: .standard, key: .isUsingFullAPNs], - let token: String = dependencies[defaults: .standard, key: .deviceToken] - else { throw HTTPError.invalidRequest } + guard dependencies[defaults: .standard, key: .isUsingFullAPNs] else { throw HTTPError.invalidRequest } guard let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(using: dependencies) else { SNLog("Unable to retrieve PN encryption key.") @@ -192,7 +250,7 @@ public enum PushNotificationAPI { body: SubscribeRequest( namespaces: { switch sessionId.prefix { - case .group: return [.default] + case .group: return [.groupMessages] default: return [.default, .configConvoInfoVolatile] } }(), @@ -201,7 +259,7 @@ public enum PushNotificationAPI { // 'generic' notification being shown when receiving things like typing indicator updates includeMessageData: true, serviceInfo: ServiceInfo( - token: token + token: token.toHexString() ), notificationsEncryptionKey: notificationsEncryptionKey, authMethod: try Authentication.with( @@ -215,7 +273,8 @@ public enum PushNotificationAPI { ) ), responseType: SubscribeResponse.self, - retryCount: PushNotificationAPI.maxRetryCount + retryCount: PushNotificationAPI.maxRetryCount, + using: dependencies ) .handleEvents( receiveOutput: { _, response in @@ -236,9 +295,7 @@ public enum PushNotificationAPI { _ db: Database, token: Data, sessionId: SessionId, - subkey: String?, - ed25519KeyPair: KeyPair, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) throws -> HTTP.PreparedRequest { return try PushNotificationAPI .prepareRequest( @@ -260,7 +317,8 @@ public enum PushNotificationAPI { ) ), responseType: UnsubscribeResponse.self, - retryCount: PushNotificationAPI.maxRetryCount + retryCount: PushNotificationAPI.maxRetryCount, + using: dependencies ) .handleEvents( receiveOutput: { _, response in @@ -284,7 +342,7 @@ public enum PushNotificationAPI { recipient: String, with message: String, maxRetryCount: Int? = nil, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) throws -> HTTP.PreparedRequest { return try PushNotificationAPI .prepareRequest( @@ -297,7 +355,8 @@ public enum PushNotificationAPI { ) ), responseType: LegacyPushServerResponse.self, - retryCount: (maxRetryCount ?? PushNotificationAPI.maxRetryCount) + retryCount: (maxRetryCount ?? PushNotificationAPI.maxRetryCount), + using: dependencies ) .handleEvents( receiveOutput: { _, response in @@ -322,7 +381,7 @@ public enum PushNotificationAPI { token: String? = nil, userSessionId: SessionId, legacyGroupIds: Set, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) throws -> HTTP.PreparedRequest? { let isUsingFullAPNs = dependencies[defaults: .standard, key: .isUsingFullAPNs] @@ -346,7 +405,8 @@ public enum PushNotificationAPI { ) ), responseType: LegacyPushServerResponse.self, - retryCount: PushNotificationAPI.maxRetryCount + retryCount: PushNotificationAPI.maxRetryCount, + using: dependencies ) .handleEvents( receiveOutput: { _, response in @@ -367,7 +427,7 @@ public enum PushNotificationAPI { public static func preparedUnsubscribeFromLegacyGroup( legacyGroupId: String, userSessionId: SessionId, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) throws -> HTTP.PreparedRequest { return try PushNotificationAPI .prepareRequest( @@ -380,7 +440,8 @@ public enum PushNotificationAPI { ) ), responseType: LegacyPushServerResponse.self, - retryCount: PushNotificationAPI.maxRetryCount + retryCount: PushNotificationAPI.maxRetryCount, + using: dependencies ) .handleEvents( receiveOutput: { _, response in @@ -401,7 +462,7 @@ public enum PushNotificationAPI { public static func processNotification( notificationContent: UNNotificationContent, - dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) -> (data: Data?, metadata: NotificationMetadata, result: ProcessResult) { // Make sure the notification is from the updated push server guard notificationContent.userInfo["spns"] != nil else { @@ -467,9 +528,9 @@ public enum PushNotificationAPI { @discardableResult private static func getOrGenerateEncryptionKey(using dependencies: Dependencies) throws -> Data { do { - var encryptionKey: Data = try SSKDefaultKeychainStorage.shared.data( - forService: keychainService, - key: encryptionKeyKey + var encryptionKey: Data = try dependencies[singleton: .keychain].data( + forService: .pushNotificationAPI, + key: .pushNotificationEncryptionKey ) defer { encryptionKey.resetBytes(in: 0..( - request: PushNotificationAPIRequest, - using dependencies: Dependencies - ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { - guard - let url: URL = URL(string: "\(request.endpoint.server)/\(request.endpoint.path)"), - let payload: Data = try? JSONEncoder(using: dependencies).encode(request.body) - else { - return Fail(error: HTTPError.invalidJSON) - .eraseToAnyPublisher() - } - - guard Features.useOnionRequests else { - return HTTP - .execute( - .post, - "\(request.endpoint.server)/\(request.endpoint.path)", - body: payload - ) - .map { response in (HTTP.ResponseInfo(code: -1, headers: [:]), response) } - .eraseToAnyPublisher() - } - - var urlRequest: URLRequest = URLRequest(url: url) - urlRequest.httpMethod = "POST" - urlRequest.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ] - urlRequest.httpBody = payload - - return dependencies[singleton: .network] - .send( - .onionRequest( - urlRequest, - to: request.endpoint.server, - with: request.endpoint.serverPublicKey - ) - ) - .eraseToAnyPublisher() + public static func deleteKeys(using dependencies: Dependencies = Dependencies()) { + try? dependencies[singleton: .keychain].remove(service: .pushNotificationAPI, key: .pushNotificationEncryptionKey) } // MARK: - Convenience @@ -563,7 +587,7 @@ public enum PushNotificationAPI { responseType: R.Type, retryCount: Int = 0, timeout: TimeInterval = HTTP.defaultTimeout, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) throws -> HTTP.PreparedRequest { return HTTP.PreparedRequest( request: request, diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 134416282..23b9ec783 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -68,7 +68,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension } let (maybeData, metadata, result) = PushNotificationAPI.processNotification( - notificationContent: notificationContent + notificationContent: notificationContent, + using: dependencies ) guard diff --git a/SessionUtilitiesKit/Database/SSKKeychainStorage.swift b/SessionUtilitiesKit/Database/SSKKeychainStorage.swift deleted file mode 100644 index 0ee3cd907..000000000 --- a/SessionUtilitiesKit/Database/SSKKeychainStorage.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// -// stringlint:disable - -import Foundation -import SAMKeychain - -public enum KeychainStorageError: Error { - case failure(code: Int32?, description: String) - - public var code: Int32? { - switch self { - case .failure(let code, _): return code - } - } -} - -// MARK: - - -@objc public protocol SSKKeychainStorage: AnyObject { - - @objc func string(forService service: String, key: String) throws -> String - - @objc(setString:service:key:error:) func set(string: String, service: String, key: String) throws - - @objc func data(forService service: String, key: String) throws -> Data - - @objc func set(data: Data, service: String, key: String) throws - - @objc func remove(service: String, key: String) throws -} - -// MARK: - - -@objc -public class SSKDefaultKeychainStorage: NSObject, SSKKeychainStorage { - - @objc public static let shared = SSKDefaultKeychainStorage() - - // Force usage as a singleton - override private init() { - super.init() - } - - @objc public func string(forService service: String, key: String) throws -> String { - var error: NSError? - let result = SAMKeychain.password(forService: service, account: key, error: &error) - if let error = error { - throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error retrieving string: \(error)") - } - guard let string = result else { - throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not retrieve string") - } - return string - } - - @objc public func set(string: String, service: String, key: String) throws { - - SAMKeychain.setAccessibilityType(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) - - var error: NSError? - let result = SAMKeychain.setPassword(string, forService: service, account: key, error: &error) - if let error = error { - throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error setting string: \(error)") - } - guard result else { - throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not set string") - } - } - - @objc public func data(forService service: String, key: String) throws -> Data { - var error: NSError? - let result = SAMKeychain.passwordData(forService: service, account: key, error: &error) - if let error = error { - throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error retrieving data: \(error)") - } - guard let data = result else { - throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not retrieve data") - } - return data - } - - @objc public func set(data: Data, service: String, key: String) throws { - - SAMKeychain.setAccessibilityType(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) - - var error: NSError? - let result = SAMKeychain.setPasswordData(data, forService: service, account: key, error: &error) - if let error = error { - throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error setting data: \(error)") - } - guard result else { - throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not set data") - } - } - - @objc public func remove(service: String, key: String) throws { - var error: NSError? - let result = SAMKeychain.deletePassword(forService: service, account: key, error: &error) - if let error = error { - // If deletion failed because the specified item could not be found in the keychain, consider it success. - if error.code == errSecItemNotFound { - return - } - throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error removing data: \(error)") - } - guard result else { - throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not remove data") - } - } -} diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 4f0204b63..696bc660c 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -21,13 +21,16 @@ public extension Singleton { ) } +// MARK: - KeychainStorage + +public extension KeychainStorage.ServiceKey { static let storage: Self = "TSKeyChainService" } +public extension KeychainStorage.DataKey { static let dbCipherKeySpec: Self = "GRDBDatabaseCipherKeySpec" } + // MARK: - Storage open class Storage { public static let queuePrefix: String = "SessionDatabase" private static let dbFileName: String = "Session.sqlite" - private static let keychainService: String = "TSKeyChainService" - private static let dbCipherKeySpecKey: String = "GRDBDatabaseCipherKeySpec" private static let kSQLCipherKeySpecLength: Int = 48 private static let writeWarningThreadshold: TimeInterval = 3 @@ -394,15 +397,15 @@ open class Storage { // MARK: - Security - private static func getDatabaseCipherKeySpec() throws -> Data { - return try SSKDefaultKeychainStorage.shared.data(forService: keychainService, key: dbCipherKeySpecKey) + private static func getDatabaseCipherKeySpec(using dependencies: Dependencies = Dependencies()) throws -> Data { + return try dependencies[singleton: .keychain].data(forService: .storage, key: .dbCipherKeySpec) } @discardableResult private static func getOrGenerateDatabaseKeySpec( using dependencies: Dependencies = Dependencies() ) throws -> Data { do { - var keySpec: Data = try getDatabaseCipherKeySpec() + var keySpec: Data = try getDatabaseCipherKeySpec(using: dependencies) defer { keySpec.resetBytes(in: 0..(singleton singleton: SingletonConfig) -> S { - getValueSettingIfNull(singleton: singleton, &Dependencies.singletonInstances) + guard let value: S = (Dependencies.singletonInstances.wrappedValue[singleton.identifier] as? S) else { + let value: S = singleton.createInstance(self) + Dependencies.singletonInstances.mutate { $0[singleton.identifier] = value } + return value + } + + return value } public subscript(cache cache: CacheConfig) -> I { - getValueSettingIfNull(cache: cache, &Dependencies.cacheInstances) + getValueSettingIfNull(cache: cache) } public subscript(defaults defaults: UserDefaultsConfig) -> UserDefaultsType { - getValueSettingIfNull(defaults: defaults, &Dependencies.userDefaultsInstances) + guard let value: UserDefaultsType = Dependencies.userDefaultsInstances.wrappedValue[defaults.identifier] else { + let value: UserDefaultsType = defaults.createInstance(self) + Dependencies.userDefaultsInstances.mutate { $0[defaults.identifier] = value } + return value + } + + return value } // MARK: - Timing and Async Handling @@ -55,7 +67,7 @@ public class Dependencies { /// the below code we first call `getValueSettingIfNull` to ensure we have a proper instance stored /// in `Dependencies.cacheInstances` so that we can be reliably certail we aren't accessing some /// random instance that will go out of memory as soon as the mutation is completed - getValueSettingIfNull(cache: cache, &Dependencies.cacheInstances) + getValueSettingIfNull(cache: cache) let cacheWrapper: Atomic = ( Dependencies.cacheInstances.wrappedValue[cache.identifier] ?? @@ -77,7 +89,7 @@ public class Dependencies { /// the below code we first call `getValueSettingIfNull` to ensure we have a proper instance stored /// in `Dependencies.cacheInstances` so that we can be reliably certail we aren't accessing some /// random instance that will go out of memory as soon as the mutation is completed - getValueSettingIfNull(cache: cache, &Dependencies.cacheInstances) + getValueSettingIfNull(cache: cache) let cacheWrapper: Atomic = ( Dependencies.cacheInstances.wrappedValue[cache.identifier] ?? @@ -106,45 +118,16 @@ public class Dependencies { // MARK: - Instance upserting - @discardableResult private func getValueSettingIfNull( - singleton: SingletonConfig, - _ store: inout Atomic<[String: Any]> - ) -> S { - guard let value: S = (store.wrappedValue[singleton.identifier] as? S) else { - let value: S = singleton.createInstance(self) - store.mutate { $0[singleton.identifier] = value } - return value - } - - return value - } - - @discardableResult private func getValueSettingIfNull( - cache: CacheConfig, - _ store: inout Atomic<[String: Atomic]> - ) -> I { - guard let value: M = (store.wrappedValue[cache.identifier]?.wrappedValue as? M) else { + @discardableResult private func getValueSettingIfNull(cache: CacheConfig) -> I { + guard let value: M = (Dependencies.cacheInstances.wrappedValue[cache.identifier]?.wrappedValue as? M) else { let value: M = cache.createInstance(self) let mutableInstance: MutableCacheType = cache.mutableInstance(value) - store.mutate { $0[cache.identifier] = Atomic(mutableInstance) } + Dependencies.cacheInstances.mutate { $0[cache.identifier] = Atomic(mutableInstance) } return cache.immutableInstance(value) } return cache.immutableInstance(value) } - - @discardableResult private func getValueSettingIfNull( - defaults: UserDefaultsConfig, - _ store: inout Atomic<[String: UserDefaultsType]> - ) -> UserDefaultsType { - guard let value: UserDefaultsType = store.wrappedValue[defaults.identifier] else { - let value: UserDefaultsType = defaults.createInstance(self) - store.mutate { $0[defaults.identifier] = value } - return value - } - - return value - } } // MARK: - Storage Setting Convenience diff --git a/SessionUtilitiesKit/Utilities/KeychainStorageType.swift b/SessionUtilitiesKit/Utilities/KeychainStorageType.swift new file mode 100644 index 000000000..d187ba6cc --- /dev/null +++ b/SessionUtilitiesKit/Utilities/KeychainStorageType.swift @@ -0,0 +1,195 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SAMKeychain + +// MARK: - Singleton + +public extension Singleton { + static let keychain: SingletonConfig = Dependencies.create( + identifier: "keychain", + createInstance: { _ in KeychainStorage() } + ) +} + +public enum KeychainStorageError: Error { + case failure(code: Int32?, description: String) + + public var code: Int32? { + switch self { + case .failure(let code, _): return code + } + } +} + +// MARK: - KeychainStorageType + +public protocol KeychainStorageType { + func string(forService service: KeychainStorage.ServiceKey, key: KeychainStorage.StringKey) throws -> String + func set(string: String, service: KeychainStorage.ServiceKey, key: KeychainStorage.StringKey) throws + func remove(service: KeychainStorage.ServiceKey, key: KeychainStorage.StringKey) throws + + func data(forService service: KeychainStorage.ServiceKey, key: KeychainStorage.DataKey) throws -> Data + func set(data: Data, service: KeychainStorage.ServiceKey, key: KeychainStorage.DataKey) throws + func remove(service: KeychainStorage.ServiceKey, key: KeychainStorage.DataKey) throws + + func removeAll() +} + +// MARK: - KeychainStorage + +public class KeychainStorage: KeychainStorageType { + public func string(forService service: KeychainStorage.ServiceKey, key: KeychainStorage.StringKey) throws -> String { + var error: NSError? + let result: String? = SAMKeychain.password(forService: service.rawValue, account: key.rawValue, error: &error) + + switch (error, result) { + case (.some(let error), _): + throw KeychainStorageError.failure( + code: Int32(error.code), + description: "[KeychainStorage] Error retrieving string: \(error)" + ) + + case (_, .none): + throw KeychainStorageError.failure(code: nil, description: "[KeychainStorage] Could not retrieve string") + + case (_, .some(let string)): return string + } + } + + public func set(string: String, service: KeychainStorage.ServiceKey, key: KeychainStorage.StringKey) throws { + SAMKeychain.setAccessibilityType(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) + + var error: NSError? + let result: Bool = SAMKeychain.setPassword(string, forService: service.rawValue, account: key.rawValue, error: &error) + + switch (error, result) { + case (.some(let error), _): + throw KeychainStorageError.failure( + code: Int32(error.code), + description: "[KeychainStorage] Error setting string: \(error)" + ) + + case (_, false): + throw KeychainStorageError.failure(code: nil, description: "[KeychainStorage] Could not set string") + + case (_, true): break + } + } + + public func remove(service: KeychainStorage.ServiceKey, key: KeychainStorage.StringKey) throws { + try remove(service: service.rawValue, key: key.rawValue) + } + + public func data(forService service: KeychainStorage.ServiceKey, key: KeychainStorage.DataKey) throws -> Data { + var error: NSError? + let result: Data? = SAMKeychain.passwordData(forService: service.rawValue, account: key.rawValue, error: &error) + + switch (error, result) { + case (.some(let error), _): + throw KeychainStorageError.failure( + code: Int32(error.code), + description: "[KeychainStorage] Error retrieving data: \(error)" + ) + + case (_, .none): + throw KeychainStorageError.failure(code: nil, description: "[KeychainStorage] Could not retrieve data") + + case (_, .some(let data)): return data + } + } + + public func set(data: Data, service: KeychainStorage.ServiceKey, key: KeychainStorage.DataKey) throws { + SAMKeychain.setAccessibilityType(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) + + var error: NSError? + let result: Bool = SAMKeychain.setPasswordData(data, forService: service.rawValue, account: key.rawValue, error: &error) + + switch (error, result) { + case (.some(let error), _): + throw KeychainStorageError.failure( + code: Int32(error.code), + description: "[KeychainStorage] Error setting data: \(error)" + ) + + case (_, false): + throw KeychainStorageError.failure(code: nil, description: "[KeychainStorage] Could not set data") + + case (_, true): break + } + } + + public func remove(service: KeychainStorage.ServiceKey, key: KeychainStorage.DataKey) throws { + try remove(service: service.rawValue, key: key.rawValue) + } + + private func remove(service: String, key: String) throws { + var error: NSError? + let result: Bool = SAMKeychain.deletePassword(forService: service, account: key, error: &error) + + switch (error, result) { + case (.some(let error), _): + /// If deletion failed because the specified item could not be found in the keychain, consider it success + guard error.code != errSecItemNotFound else { return } + + throw KeychainStorageError.failure( + code: Int32(error.code), + description: "[KeychainStorage] Error removing data: \(error)" + ) + + case (_, false): + throw KeychainStorageError.failure(code: nil, description: "[KeychainStorage] Could not remove data") + + case (_, true): break + } + } + + public func removeAll() { + let allData: [[String: Any]] = SAMKeychain.allAccounts().defaulting(to: []) + + allData.forEach { keychainEntry in + guard + let service: String = keychainEntry[kSAMKeychainWhereKey] as? String, + let key: String = keychainEntry[kSAMKeychainAccountKey] as? String + else { return } + + try? remove(service: service, key: key) + } + } +} + +// MARK: - Keys + +public extension KeychainStorage { + struct ServiceKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { + public let rawValue: String + + public init(_ rawValue: String) { self.rawValue = rawValue } + public init?(rawValue: String) { self.rawValue = rawValue } + public init(stringLiteral value: String) { self.init(value) } + public init(unicodeScalarLiteral value: String) { self.init(value) } + public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } + } + + struct DataKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { + public let rawValue: String + + public init(_ rawValue: String) { self.rawValue = rawValue } + public init?(rawValue: String) { self.rawValue = rawValue } + public init(stringLiteral value: String) { self.init(value) } + public init(unicodeScalarLiteral value: String) { self.init(value) } + public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } + } + + struct StringKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { + public let rawValue: String + + public init(_ rawValue: String) { self.rawValue = rawValue } + public init?(rawValue: String) { self.rawValue = rawValue } + public init(stringLiteral value: String) { self.init(value) } + public init(unicodeScalarLiteral value: String) { self.init(value) } + public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } + } +} diff --git a/_SharedTestUtilities/TestDependencies.swift b/_SharedTestUtilities/TestDependencies.swift index 38bbb0165..15fdf75aa 100644 --- a/_SharedTestUtilities/TestDependencies.swift +++ b/_SharedTestUtilities/TestDependencies.swift @@ -14,7 +14,13 @@ public class TestDependencies: Dependencies { // MARK: - Subscript Access override public subscript(singleton singleton: SingletonConfig) -> S { - return getValueSettingIfNull(singleton: singleton, &singletonInstances) + guard let value: S = (singletonInstances[singleton.identifier] as? S) else { + let value: S = singleton.createInstance(self) + singletonInstances[singleton.identifier] = value + return value + } + + return value } public subscript(singleton singleton: SingletonConfig) -> S? { @@ -23,7 +29,14 @@ public class TestDependencies: Dependencies { } override public subscript(cache cache: CacheConfig) -> I { - return getValueSettingIfNull(cache: cache, &cacheInstances) + guard let value: M = (cacheInstances[cache.identifier] as? M) else { + let value: M = cache.createInstance(self) + let mutableInstance: MutableCacheType = cache.mutableInstance(value) + cacheInstances[cache.identifier] = mutableInstance + return cache.immutableInstance(value) + } + + return cache.immutableInstance(value) } public subscript(cache cache: CacheConfig) -> M? { @@ -32,7 +45,13 @@ public class TestDependencies: Dependencies { } override public subscript(defaults defaults: UserDefaultsConfig) -> UserDefaultsType { - return getValueSettingIfNull(defaults: defaults, &defaultsInstances) + guard let value: UserDefaultsType = defaultsInstances[defaults.identifier] else { + let value: UserDefaultsType = defaults.createInstance(self) + defaultsInstances[defaults.identifier] = value + return value + } + + return value } public subscript(defaults defaults: UserDefaultsConfig) -> UserDefaultsType? { @@ -142,48 +161,6 @@ public class TestDependencies: Dependencies { return result.map { elements.remove($0) } } - - // MARK: - Instance upserting - - @discardableResult private func getValueSettingIfNull( - singleton: SingletonConfig, - _ store: inout [String: Any] - ) -> S { - guard let value: S = (store[singleton.identifier] as? S) else { - let value: S = singleton.createInstance(self) - store[singleton.identifier] = value - return value - } - - return value - } - - @discardableResult private func getValueSettingIfNull( - cache: CacheConfig, - _ store: inout [String: MutableCacheType] - ) -> I { - guard let value: M = (store[cache.identifier] as? M) else { - let value: M = cache.createInstance(self) - let mutableInstance: MutableCacheType = cache.mutableInstance(value) - store[cache.identifier] = mutableInstance - return cache.immutableInstance(value) - } - - return cache.immutableInstance(value) - } - - @discardableResult private func getValueSettingIfNull( - defaults: UserDefaultsConfig, - _ store: inout [String: (any UserDefaultsType)] - ) -> UserDefaultsType { - guard let value: UserDefaultsType = store[defaults.identifier] else { - let value: UserDefaultsType = defaults.createInstance(self) - store[defaults.identifier] = value - return value - } - - return value - } } // MARK: - TestState Convenience