diff --git a/Sources/PubNub/Helpers/Crypto/CryptoModule.swift b/Sources/PubNub/Helpers/Crypto/CryptoModule.swift index f75db9b3..fdfe29fa 100644 --- a/Sources/PubNub/Helpers/Crypto/CryptoModule.swift +++ b/Sources/PubNub/Helpers/Crypto/CryptoModule.swift @@ -15,6 +15,7 @@ public class CryptorModule { public static func aesCbcCryptoModule(with key: String, withRandomIV: Bool = true) -> CryptoModule { preconditionFailure("This method is no longer available") } + public static func legacyCryptoModule(with key: String, withRandomIV: Bool = true) -> CryptoModule { preconditionFailure("This method is no longer available") } @@ -22,22 +23,23 @@ public class CryptorModule { /// Object capable of encryption/decryption public struct CryptoModule { - private let defaultCryptor: Cryptor - private let cryptors: [Cryptor] + private let defaultCryptor: any Cryptor + private let cryptors: [any Cryptor] private let legacyCryptorId: CryptorId = [] typealias Base64EncodedString = String /// Initializes `CryptoModule` with custom ``Cryptor`` objects capable of encryption and decryption /// - /// Use this constructor if you would like to provide **custom** objects for decryption and encryption and don't want to use PubNub's built-in `Cryptors`. - /// Otherwise, refer to convenience static factory methods such as ``aesCbcCryptoModule(with:withRandomIV:)`` - /// and ``legacyCryptoModule(with:withRandomIV:)`` that return `CryptoModule` configured for you. + /// Use this constructor if you would like to provide **custom** objects for decryption and encryption + /// and don't want to use PubNub's built-in `Cryptors`. Otherwise, refer to convenience static factory methods + /// such as ``aesCbcCryptoModule(with:withRandomIV:)``and ``legacyCryptoModule(with:withRandomIV:)`` + /// that return `CryptoModule` configured for you. /// /// - Parameters: /// - default: Primary ``Cryptor`` instance used for encryption and decryption /// - cryptors: An optional list of ``Cryptor`` instances which older messages/files were encoded - public init(default cryptor: Cryptor, cryptors: [Cryptor] = []) { + public init(default cryptor: any Cryptor, cryptors: [any Cryptor] = []) { self.defaultCryptor = cryptor self.cryptors = cryptors } @@ -46,13 +48,15 @@ public struct CryptoModule { /// /// - Parameters: /// - data: Data to encrypt - /// - Returns: A success, storing encrypted `Data` if operation succeeds. Otherwise, a failure storing `PubNubError` is returned + /// - Returns: + /// - **Success**: An encrypted `Data` object + /// - **Failure**: `PubNubError` describing the reason of failure public func encrypt(data: Data) -> Result { guard !data.isEmpty else { return .failure(PubNubError( .encryptionFailure, - additional: ["Cannot encrypt empty Data"]) - ) + additional: ["Cannot encrypt empty Data"] + )) } return defaultCryptor.encrypt(data: data).map { if defaultCryptor.id == LegacyCryptor.ID { @@ -71,13 +75,15 @@ public struct CryptoModule { /// /// - Parameters: /// - data: Data to decrypt - /// - Returns: A success, storing decrypted `Data` if operation succeeds. Otherwise, a failure storing `PubNubError` is returned + /// - Returns: + /// - **Success**: A decrypted `Data` object + /// - **Failure**: `PubNubError` describing the reason of failure public func decrypt(data: Data) -> Result { guard !data.isEmpty else { return .failure(PubNubError( .decryptionFailure, - additional: ["Cannot decrypt empty Data in \(String(describing: self))"]) - ) + additional: ["Cannot decrypt empty Data in \(String(describing: self))"] + )) } do { let header = try CryptorHeader.from(data: data) @@ -87,7 +93,7 @@ public struct CryptoModule { .unknownCryptorFailure, additional: [ "Could not find matching Cryptor for \(header.cryptorId()) while decrypting Data. " + - "Ensure the corresponding instance is registered in \(String(describing: Self.self))" + "Ensure the corresponding instance is registered in \(String(describing: Self.self))" ] )) } @@ -115,8 +121,8 @@ public struct CryptoModule { if $0.isEmpty { return .failure(PubNubError( .decryptionFailure, - additional: ["Decrypting resulted with empty Data"]) - ) + additional: ["Decrypting resulted with empty Data"] + )) } return .success($0) } @@ -129,8 +135,8 @@ public struct CryptoModule { return .failure(PubNubError( .decryptionFailure, underlying: error, - additional: ["Cannot decrypt InputStream"]) - ) + additional: ["Cannot decrypt InputStream"] + )) } } @@ -139,7 +145,9 @@ public struct CryptoModule { /// - Parameters: /// - stream: Stream to encrypt /// - contentLength: Content length of encoded stream - /// - Returns: A success, storing an `InputStream` value if operation succeeds. Otherwise, a failure storing `PubNubError` is returned + /// - Returns: + /// - **Success**: An `InputStream` value + /// - **Failure**: `PubNubError` describing the reason of failure public func encrypt(stream: InputStream, contentLength: Int) -> Result { guard contentLength > 0 else { return .failure(PubNubError( @@ -179,7 +187,9 @@ public struct CryptoModule { /// - stream: Stream to decrypt /// - contentLength: Content length of encrypted stream /// - to: URL where the stream should be decrypted to - /// - Returns: A success, storing a decrypted `InputStream` value if operation succeeds. Otherwise, a failure storing `PubNubError` is returned + /// - Returns: + /// - **Success**: A decrypted `InputStream` object + /// - **Failure**: `PubNubError` describing the reason of failure @discardableResult public func decrypt( stream: InputStream, @@ -203,7 +213,7 @@ public struct CryptoModule { .unknownCryptorFailure, additional: [ "Could not find matching Cryptor for \(readHeaderResp.header.cryptorId()) while decrypting InputStream. " + - "Ensure the corresponding instance is registered in \(String(describing: Self.self))" + "Ensure the corresponding instance is registered in \(String(describing: Self.self))" ] )) } @@ -218,8 +228,8 @@ public struct CryptoModule { if outputPath.sizeOf == 0 { return .failure(PubNubError( .decryptionFailure, - additional: ["Decrypting resulted with an empty File"]) - ) + additional: ["Decrypting resulted with an empty File"] + )) } return .success($0) } @@ -237,7 +247,7 @@ public struct CryptoModule { } } - private func cryptor(matching header: CryptorHeader) -> Cryptor? { + private func cryptor(matching header: CryptorHeader) -> (any Cryptor)? { header.cryptorId() == defaultCryptor.id ? defaultCryptor : cryptors.first(where: { $0.id == header.cryptorId() }) @@ -246,7 +256,6 @@ public struct CryptoModule { /// Convenience methods for creating `CryptoModule` public extension CryptoModule { - /// Returns **recommended** `CryptoModule` for encryption/decryption /// /// - Parameters: @@ -279,7 +288,9 @@ extension CryptoModule: Equatable { extension CryptoModule: Hashable { public func hash(into hasher: inout Hasher) { - hasher.combine(cryptors.map { $0.id }) + for cryptor in cryptors { + hasher.combine(cryptor) + } } } @@ -289,7 +300,7 @@ extension CryptoModule: CustomStringConvertible { } } -internal extension CryptoModule { +extension CryptoModule { func encrypt(string: String) -> Result { guard let data = string.data(using: .utf8) else { return .failure(PubNubError( @@ -309,8 +320,8 @@ internal extension CryptoModule { } else { return .failure(PubNubError( .decryptionFailure, - additional: ["Cannot create String from provided Data"]) - ) + additional: ["Cannot create String from provided Data"] + )) } } } diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift index 5b935c32..a5b067ac 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift @@ -147,3 +147,10 @@ public struct AESCBCCryptor: Cryptor { } } } + +extension AESCBCCryptor: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(key) + hasher.combine(id) + } +} diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift index c3a0d533..e8c1d983 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift @@ -33,7 +33,7 @@ public struct EncryptedStreamData { public typealias CryptorId = [UInt8] /// Protocol for all types that encapsulate concrete encryption/decryption operations -public protocol Cryptor { +public protocol Cryptor: Hashable { /// Unique 4-byte identifier across all `Cryptor` /// /// - Important: `[0x41, 0x43, 0x52, 0x48]` and `[0x00, 0x00, 0x00, 0x00]` values are reserved diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift index 28f324ce..af0fa871 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift @@ -171,3 +171,11 @@ public struct LegacyCryptor: Cryptor { } } } + +extension LegacyCryptor: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(key) + hasher.combine(withRandomIV) + hasher.combine(LegacyCryptor.ID) + } +} diff --git a/Sources/PubNub/PubNub.swift b/Sources/PubNub/PubNub.swift index 32f2e541..98a9350b 100644 --- a/Sources/PubNub/PubNub.swift +++ b/Sources/PubNub/PubNub.swift @@ -1381,9 +1381,9 @@ public extension PubNub { // MARK: - Crypto extension PubNub { - /// Encrypt some `Data` using the configuration `CryptoModule` value + /// Encrypts the `Data` object using `CryptoModule` provided in configuration /// - Parameter message: The plain text message to be encrypted - /// - Returns: A `Result` containing either the encryped Data (mapped to Base64-encoded data) or the Crypto Error + /// - Returns: A `Result` containing either the encryped `Data` (mapped to Base64-encoded data) or the `CryptoError` public func encrypt(message: String) -> Result { guard let cryptoModule = configuration.cryptoModule else { PubNub.log.error(ErrorDescription.missingCryptoKey) @@ -1400,9 +1400,9 @@ extension PubNub { } } - /// Decrypt some `Data` using the configuration CryptoModule value + /// Decrypts the given `Data` object using `CryptoModule` provided in `configuration` /// - Parameter message: The encrypted `Data` to decrypt - /// - Returns: A `Result` containing either the decrypted plain text message or the Crypto Error + /// - Returns: A `Result` containing either the decrypted plain text message or the `CryptoError` public func decrypt(data: Data) -> Result { guard let cryptoModule = configuration.cryptoModule else { PubNub.log.error(ErrorDescription.missingCryptoKey) diff --git a/Tests/PubNubTests/Integration/PublishEndpointIntegrationTests.swift b/Tests/PubNubTests/Integration/PublishEndpointIntegrationTests.swift index 130372bc..5fc0c138 100644 --- a/Tests/PubNubTests/Integration/PublishEndpointIntegrationTests.swift +++ b/Tests/PubNubTests/Integration/PublishEndpointIntegrationTests.swift @@ -16,7 +16,6 @@ class PublishEndpointIntegrationTests: XCTestCase { func testPublishEndpoint() { let publishExpect = expectation(description: "Publish Response") - // Instantiate PubNub let configuration = PubNubConfiguration(from: testsBundle) let client = PubNub(configuration: configuration) @@ -37,19 +36,19 @@ class PublishEndpointIntegrationTests: XCTestCase { func testSignalTooLong() { let publishExpect = expectation(description: "Publish Response") - // Instantiate PubNub let configuration = PubNubConfiguration(from: testsBundle) let client = PubNub(configuration: configuration) - client.signal(channel: "SwiftITest", - message: ["$": "35.75", "HI": "b62", "t": "BO"]) { result in + client.signal( + channel: "SwiftITest", + message: ["$": "35.75", "HI": "b62", "t": "BO"] + ) { result in switch result { case .success: XCTFail("Publish should fail") case let .failure(error): - XCTAssertEqual(error.pubNubError?.reason, - PubNubError.Reason.messageTooLong) + XCTAssertEqual(error.pubNubError?.reason, PubNubError.Reason.messageTooLong) } publishExpect.fulfill() } @@ -59,15 +58,16 @@ class PublishEndpointIntegrationTests: XCTestCase { func testCompressedPublishEndpoint() { let compressedPublishExpect = expectation(description: "Compressed Publish Response") - // Instantiate PubNub let configuration = PubNubConfiguration(from: testsBundle) let client = PubNub(configuration: configuration) // Publish a simple message to the demo_tutorial channel - client.publish(channel: "SwiftITest", - message: "TestCompressedPublish", - shouldCompress: true) { result in + client.publish( + channel: "SwiftITest", + message: "TestCompressedPublish", + shouldCompress: true + ) { result in switch result { case .success: break @@ -82,7 +82,6 @@ class PublishEndpointIntegrationTests: XCTestCase { func testFireEndpoint() { let fireExpect = expectation(description: "Fire Response") - // Instantiate PubNub let configuration = PubNubConfiguration(from: testsBundle) let client = PubNub(configuration: configuration) @@ -103,7 +102,6 @@ class PublishEndpointIntegrationTests: XCTestCase { func testSignalEndpoint() { let signalExpect = expectation(description: "Signal Response") - // Instantiate PubNub let configuration = PubNubConfiguration(from: testsBundle) let client = PubNub(configuration: configuration) @@ -124,7 +122,6 @@ class PublishEndpointIntegrationTests: XCTestCase { func testPushblishEscapedString() { let message = "{\"text\": \"bob\", \"duckName\": \"swiftduck\"}" - let publishExpect = expectation(description: "Publish Response") // Instantiate PubNub @@ -137,8 +134,7 @@ class PublishEndpointIntegrationTests: XCTestCase { case .success: XCTFail("Publish should fail") case let .failure(error): - XCTAssertEqual(error.pubNubError?.reason, - PubNubError.Reason.requestContainedInvalidJSON) + XCTAssertEqual(error.pubNubError?.reason, PubNubError.Reason.requestContainedInvalidJSON) } publishExpect.fulfill() } @@ -181,4 +177,64 @@ class PublishEndpointIntegrationTests: XCTestCase { wait(for: [publishExpect], timeout: 10.0) } + + func testPublish_WithCryptoModulesFromDifferentClients() { + let firstClient = PubNub(configuration: PubNubConfiguration( + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: PubNubConfiguration(from: testsBundle).userId, + cryptoModule: CryptoModule.aesCbcCryptoModule(with: "someKey") + )) + let secondClient = PubNub(configuration: PubNubConfiguration( + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: PubNubConfiguration(from: testsBundle).userId, + cryptoModule: CryptoModule.aesCbcCryptoModule(with: "anotherKey") + )) + + let channelForFistClient = "ChannelA" + let channelForSecondClient = "ChannelB" + + let publishExpect = expectation(description: "Publish Response") + publishExpect.assertForOverFulfill = true + publishExpect.expectedFulfillmentCount = 2 + + let subscribeExpect = expectation(description: "Subscribe Response") + subscribeExpect.assertForOverFulfill = true + subscribeExpect.assertForOverFulfill = true + + for client in [firstClient, secondClient] { + client.onConnectionStateChange = { [unowned client] newStatus in + if newStatus == .connected { + client.publish( + channel: client === firstClient ? channelForFistClient : channelForSecondClient, + message: "This is a message" + ) { result in + switch result { + case .success: + publishExpect.fulfill() + case let .failure(error): + XCTFail("Unexpected failure: \(error)") + } + } + } + } + } + + let subscription = firstClient.channel(channelForFistClient).subscription() + let subscriptionFromSecondClient = secondClient.channel(channelForSecondClient).subscription() + + subscription.onMessage = { message in + XCTAssertEqual(message.payload.stringOptional, "This is a message") + subscribeExpect.fulfill() + } + subscriptionFromSecondClient.onMessage = { message in + XCTAssertEqual(message.payload.stringOptional, "This is a message") + subscribeExpect.fulfill() + } + subscription.subscribe() + subscriptionFromSecondClient.subscribe() + + wait(for: [publishExpect, subscribeExpect], timeout: 10.0) + } } diff --git a/Tests/PubNubTests/PubNubConfigurationTests.swift b/Tests/PubNubTests/PubNubConfigurationTests.swift index fe7de13a..2424ade5 100644 --- a/Tests/PubNubTests/PubNubConfigurationTests.swift +++ b/Tests/PubNubTests/PubNubConfigurationTests.swift @@ -55,11 +55,30 @@ class PubNubConfigurationTests: XCTestCase { } func testInit_RawValues() { - let config = PubNubConfiguration(publishKey: publishKeyValue, - subscribeKey: subscribeKeyValue, - userId: UUID().uuidString) + let config = PubNubConfiguration( + publishKey: publishKeyValue, + subscribeKey: subscribeKeyValue, + userId: UUID().uuidString + ) XCTAssertEqual(config.publishKey, publishKeyValue) XCTAssertEqual(config.subscribeKey, subscribeKeyValue) } + + func testConfigurations_DifferentCryptoModules() { + let firstConfig = PubNubConfiguration( + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: PubNubConfiguration(from: testsBundle).userId, + cryptoModule: CryptoModule.aesCbcCryptoModule(with: "someKey") + ) + let secondConfig = PubNubConfiguration( + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: PubNubConfiguration(from: testsBundle).userId, + cryptoModule: CryptoModule.aesCbcCryptoModule(with: "anotherKey") + ) + + XCTAssertNotEqual(firstConfig.hashValue, secondConfig.hashValue) + } }