From b7bd1430d8799f3b4049081e134eb0d770c1736b Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Fri, 26 Jun 2026 13:17:04 +0200 Subject: [PATCH 1/7] Harden AES-CBC decryption against padding-oracle and timing side channels --- .../Crypto/Cryptors/AESCBCCryptor.swift | 41 +++---- .../Crypto/Cryptors/LegacyCryptor.swift | 32 ++--- .../Miscellaneous/CryptoInputStream.swift | 17 +-- .../Miscellaneous/Data+CommonCrypto.swift | 16 +++ .../PubNubUnitTests/Helpers/CryptoTests.swift | 112 ++++++++++++++++++ 5 files changed, 170 insertions(+), 48 deletions(-) diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift index 21fd75a9..c1119aa3 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift @@ -58,13 +58,14 @@ public struct AESCBCCryptor: Cryptor { } public func decrypt(data: EncryptedData) -> Result { + guard CBCDecrypt.isValidInput( + iv: data.metadata, + cipherText: data.data, + blockSize: kCCBlockSizeAES128 + ) else { + return .failure(CBCDecrypt.failure) + } do { - if data.data.isEmpty { - return .failure(PubNubError( - .decryptionFailure, - additional: ["Cannot decrypt empty Data in \(String(describing: self))"]) - ) - } return .success( try data.data.crypt( operation: CCOperation(kCCDecrypt), @@ -77,10 +78,7 @@ public struct AESCBCCryptor: Cryptor { ) ) } catch { - return .failure(PubNubError( - .decryptionFailure, - underlying: error - )) + return .failure(CBCDecrypt.failure) } } @@ -120,6 +118,15 @@ public struct AESCBCCryptor: Cryptor { } public func decrypt(data: EncryptedStreamData, outputPath: URL) -> Result { + // The IV lives in `metadata` and the stream carries only ciphertext, so the same length and + // alignment checks as the in-memory path apply (CWE-20, CWE-208). + guard + data.metadata.count == kCCBlockSizeAES128, + data.contentLength > 0, + data.contentLength % kCCBlockSizeAES128 == 0 + else { + return .failure(CBCDecrypt.failure) + } do { let cryptoInputStreamCipher = CryptoInputStream.Cipher( algorithm: CCAlgorithm(kCCAlgorithmAES128), @@ -140,18 +147,12 @@ public struct AESCBCCryptor: Cryptor { try cryptoInputStream.writeEncodedData( to: outputPath ) - if let stream = InputStream(url: outputPath) { - return .success(stream) + guard let stream = InputStream(url: outputPath) else { + return .failure(CBCDecrypt.failure) } - return .failure(PubNubError( - .decryptionFailure, - additional: ["Cannot create resulting InputStream at \(outputPath)"] - )) + return .success(stream) } catch { - return .failure(PubNubError( - .decryptionFailure, - underlying: error - )) + return .failure(CBCDecrypt.failure) } } diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift index 19a8e64b..64adf887 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift @@ -80,6 +80,10 @@ public struct LegacyCryptor: Cryptor { do { if withRandomIV { + // A random-IV payload must carry at least one block of IV before any ciphertext. + guard data.data.count >= kCCBlockSizeAES128 else { + return .failure(CBCDecrypt.failure) + } iv = data.data.prefix(kCCBlockSizeAES128) cipherText = data.data.suffix(from: kCCBlockSizeAES128) } else { @@ -87,15 +91,12 @@ public struct LegacyCryptor: Cryptor { cipherText = data.data } - if cipherText.isEmpty { - return .failure(PubNubError( - .decryptionFailure, - additional: ["Cannot decrypt empty Data in \(String(describing: self))"]) - ) + guard CBCDecrypt.isValidInput(iv: iv, cipherText: cipherText, blockSize: kCCBlockSizeAES128) else { + return .failure(CBCDecrypt.failure) } return .success( - try data.data.crypt( + try cipherText.crypt( operation: CCOperation(kCCDecrypt), algorithm: CCAlgorithm(kCCAlgorithmAES128), options: CCOptions(kCCOptionPKCS7Padding), @@ -106,10 +107,7 @@ public struct LegacyCryptor: Cryptor { ) ) } catch { - return .failure(PubNubError( - .decryptionFailure, - underlying: error - )) + return .failure(CBCDecrypt.failure) } } @@ -171,18 +169,12 @@ public struct LegacyCryptor: Cryptor { try cryptoInputStream.writeEncodedData( to: outputPath ) - if let inputStream = InputStream(url: outputPath) { - return .success(inputStream) + guard let inputStream = InputStream(url: outputPath) else { + return .failure(CBCDecrypt.failure) } - return .failure(PubNubError( - .decryptionFailure, - additional: ["Cannot create resulting InputStream at \(outputPath)"] - )) + return .success(inputStream) } catch { - return .failure(PubNubError( - .decryptionFailure, - underlying: error - )) + return .failure(CBCDecrypt.failure) } } diff --git a/Sources/PubNub/Helpers/Crypto/Miscellaneous/CryptoInputStream.swift b/Sources/PubNub/Helpers/Crypto/Miscellaneous/CryptoInputStream.swift index b5ba1796..b66c564e 100644 --- a/Sources/PubNub/Helpers/Crypto/Miscellaneous/CryptoInputStream.swift +++ b/Sources/PubNub/Helpers/Crypto/Miscellaneous/CryptoInputStream.swift @@ -109,17 +109,18 @@ public class CryptoInputStream: InputStream { var initializationVectorBuffer = [UInt8](repeating: 0, count: crypto.cipher.blockSize) if includeInitializationVectorInContent { - switch input.read(&initializationVectorBuffer, maxLength: crypto.cipher.blockSize) { - case let bytesRead where bytesRead < 0: - // -1 means that the operation failed; more information about the error can be obtained with `streamError`. + // The IV must be exactly one block; anything shorter is rejected so that an attacker + // cannot probe decrypt failures by truncating the leading IV. + if input.read(&initializationVectorBuffer, maxLength: crypto.cipher.blockSize) != crypto.cipher.blockSize { _streamStatus = .error - _streamError = input.streamError - default: - // 0 represents end of the current buffer - break + _streamError = input.streamError ?? PubNubError(.decryptionFailure) } - } else { + } else if crypto.iv.count == crypto.cipher.blockSize { initializationVectorBuffer = crypto.iv.map { $0 } + } else { + // A wrong-length IV is rejected for the same reason as above. + _streamStatus = .error + _streamError = PubNubError(.decryptionFailure) } // Init the Crypto Stream diff --git a/Sources/PubNub/Helpers/Crypto/Miscellaneous/Data+CommonCrypto.swift b/Sources/PubNub/Helpers/Crypto/Miscellaneous/Data+CommonCrypto.swift index d5d2a0cb..85316026 100644 --- a/Sources/PubNub/Helpers/Crypto/Miscellaneous/Data+CommonCrypto.swift +++ b/Sources/PubNub/Helpers/Crypto/Miscellaneous/Data+CommonCrypto.swift @@ -11,6 +11,22 @@ import Foundation import CommonCrypto +/// Helpers shared by the AES-CBC decrypt paths +enum CBCDecrypt { + /// A single, uniform decryption failure that never exposes an underlying decryption error. + static var failure: PubNubError { + PubNubError(.decryptionFailure, additional: ["Decryption failed"]) + } + + /// Returns `true` when the IV and ciphertext are valid inputs for an AES-CBC decrypt. + /// + /// Rejects a wrong-length IV, empty ciphertext, and ciphertext that is not a whole number of + /// blocks — preventing invalid input from reaching `CCCrypt` where the status would differ. + static func isValidInput(iv: Data, cipherText: Data, blockSize: Int) -> Bool { + iv.count == blockSize && !cipherText.isEmpty && cipherText.count % blockSize == 0 + } +} + extension Data { func crypt( operation: CCOperation, diff --git a/Tests/PubNubUnitTests/Helpers/CryptoTests.swift b/Tests/PubNubUnitTests/Helpers/CryptoTests.swift index 115df975..96d82978 100644 --- a/Tests/PubNubUnitTests/Helpers/CryptoTests.swift +++ b/Tests/PubNubUnitTests/Helpers/CryptoTests.swift @@ -9,6 +9,7 @@ // import CommonCrypto +import Foundation import XCTest @testable import PubNubSDK @@ -342,4 +343,115 @@ class CryptoTests: XCTestCase { CryptoError(rawValue: CCCryptorStatus(1_240_124)) ) } + + func testDecrypt_RejectsWrongSizeIV() { + let cryptor = AESCBCCryptor(key: "enigma") + let wrongSizeIV = Data(repeating: 0x01, count: kCCBlockSizeAES128 - 1) + + let result = cryptor.decrypt( + data: EncryptedData( + metadata: wrongSizeIV, + data: Data(repeating: 0xAB, count: kCCBlockSizeAES128) + ) + ) + + assertOpaqueDecryptionFailure(result.error) + } + + func testDecrypt_RejectsNonBlockAlignedContent() { + let cryptor = AESCBCCryptor(key: "enigma") + let wrongSizeData = Data(repeating: 0xAB, count: kCCBlockSizeAES128 + 1) + + let result = cryptor.decrypt( + data: EncryptedData( + metadata: Data(repeating: 0x01, count: kCCBlockSizeAES128), + data: wrongSizeData + ) + ) + + assertOpaqueDecryptionFailure(result.error) + } + + func testDecrypt_RejectsEmptyContent() { + let cryptor = AESCBCCryptor(key: "enigma") + let emptyData = Data() + + let result = cryptor.decrypt( + data: EncryptedData( + metadata: Data(repeating: 0x01, count: kCCBlockSizeAES128), + data: emptyData + ) + ) + + assertOpaqueDecryptionFailure(result.error) + } + + func testDecryptStream_RejectsWrongSizeIV() { + let cryptor = AESCBCCryptor(key: "enigma") + let wrongSizeIV = Data(repeating: 0x01, count: kCCBlockSizeAES128 - 1) + let testData = Data(repeating: 0xAB, count: kCCBlockSizeAES128 * 2) + + let outputPath = URL.randomTempPath + let encryptedStreamData = EncryptedStreamData(stream: .init(data: testData), contentLength: testData.count, metadata: wrongSizeIV) + let decryptionResult = cryptor.decrypt(data: encryptedStreamData, outputPath: outputPath) + + assertOpaqueDecryptionFailure(decryptionResult.error) + } + + func testDecryptStream_RejectsNonBlockAlignedContent() { + let cryptor = AESCBCCryptor(key: "enigma") + let iv = Data(repeating: 0x01, count: kCCBlockSizeAES128) + let wrongSizeData = Data(repeating: 0xAB, count: kCCBlockSizeAES128 + 1) + + let outputPath = URL.randomTempPath + let encryptedStreamData = EncryptedStreamData(stream: .init(data: wrongSizeData), contentLength: wrongSizeData.count, metadata: iv) + let decryptionResult = cryptor.decrypt(data: encryptedStreamData, outputPath: outputPath) + + assertOpaqueDecryptionFailure(decryptionResult.error) + } + + func testDecryptStream_RejectsEmptyContent() { + let cryptor = AESCBCCryptor(key: "enigma") + let iv = Data(repeating: 0x01, count: kCCBlockSizeAES128) + let emptyData = Data() + + let outputPath = URL.randomTempPath + let encryptedStreamData = EncryptedStreamData(stream: .init(data: emptyData), contentLength: emptyData.count, metadata: iv) + let decryptionResult = cryptor.decrypt(data: encryptedStreamData, outputPath: outputPath) + + assertOpaqueDecryptionFailure(decryptionResult.error) + } +} + +private extension CryptoTests { + func assertOpaqueDecryptionFailure( + _ error: Error?, + file: StaticString = #filePath, + line: UInt = #line + ) { + guard let pubNubError = error as? PubNubError else { + return XCTFail("Expected a PubNubError, got \(String(describing: error))", file: file, line: line) + } + + XCTAssertEqual(pubNubError.reason, .decryptionFailure, file: file, line: line) + XCTAssertNil(pubNubError.underlying, "Decrypt failures must not expose an underlying status", file: file, line: line) + XCTAssertEqual(pubNubError.details, ["Decryption failed"], file: file, line: line) + } +} + +private extension Result { + var error: Error? { + switch self { + case .success: + return nil + case let .failure(error): + return error + } + } +} + +private extension URL { + static var randomTempPath: URL { + URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + } } From 1c21eccfb5e365b2a7b28022f2b5e69f377d46d4 Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Mon, 29 Jun 2026 15:03:40 +0200 Subject: [PATCH 2/7] Changes --- Snippets/Configuration/configuration.swift | 4 +- Snippets/Misc/misc.swift | 4 +- Sources/PubNub/Extensions/String+PubNub.swift | 18 +++++++ .../PubNub/Helpers/Crypto/CryptoModule.swift | 8 +-- .../Crypto/Cryptors/AESCBCCryptor.swift | 5 ++ Sources/PubNub/Logging/LogWriter.swift | 6 +-- Sources/PubNub/Logging/PubNubLogger.swift | 7 ++- Sources/PubNub/PubNub.swift | 4 +- .../FilesEndpointIntegrationTests.swift | 2 +- .../HistoryEndpointIntegrationTests.swift | 2 +- .../PublishEndpointIntegrationTests.swift | 4 +- .../PubNubUnitTests/Helpers/CryptoTests.swift | 53 +++++++++++-------- .../PubNubConfigurationTests.swift | 4 +- 13 files changed, 78 insertions(+), 43 deletions(-) diff --git a/Snippets/Configuration/configuration.swift b/Snippets/Configuration/configuration.swift index 529b78f3..38d50c9b 100644 --- a/Snippets/Configuration/configuration.swift +++ b/Snippets/Configuration/configuration.swift @@ -38,7 +38,7 @@ func aesCbcCryptoModuleExample() { publishKey: "demo", subscribeKey: "demo", userId: "myUniqueUserId", - cryptoModule: CryptoModule.aesCbcCryptoModule(with: "pubnubenigma") + cryptoModule: CryptoModule.aesCbcCryptoModule(with: "Be7HHAmdGinTHPulC0aLIUHx7Bna10y2") ) ) // snippet.end @@ -52,7 +52,7 @@ func legacyCryptoModuleExample() { publishKey: "demo", subscribeKey: "demo", userId: "myUniqueUserId", - cryptoModule: CryptoModule.legacyCryptoModule(with: "pubnubenigma") + cryptoModule: CryptoModule.legacyCryptoModule(with: "Be7HHAmdGinTHPulC0aLIUHx7Bna10y2") ) ) // snippet.end diff --git a/Snippets/Misc/misc.swift b/Snippets/Misc/misc.swift index 48cb228f..0c967431 100644 --- a/Snippets/Misc/misc.swift +++ b/Snippets/Misc/misc.swift @@ -29,7 +29,7 @@ let pubnub = PubNub( // snippet.encrypt-data func encryptDataExample() throws { // Initialize the crypto module with a cipher key - let cryptoModule = CryptoModule.aesCbcCryptoModule(with: "pubnubenigma") + let cryptoModule = CryptoModule.aesCbcCryptoModule(with: "Be7HHAmdGinTHPulC0aLIUHx7Bna10y2") // The message to encrypt let messageToEncrypt = Data("this is message".utf8) // Encrypt the message @@ -41,7 +41,7 @@ func encryptDataExample() throws { // snippet.decrypt-data func decryptDataExample() throws { // Initialize the crypto module with a cipher key - let cryptoModule = CryptoModule.aesCbcCryptoModule(with: "pubnubenigma") + let cryptoModule = CryptoModule.aesCbcCryptoModule(with: "Be7HHAmdGinTHPulC0aLIUHx7Bna10y2") // Encrypt a message to demonstrate its decryption later let messageToEncrypt = Data("this is message".utf8) let encryptedMessage = try cryptoModule.encrypt(data: messageToEncrypt).get() diff --git a/Sources/PubNub/Extensions/String+PubNub.swift b/Sources/PubNub/Extensions/String+PubNub.swift index a7f56cd6..38843030 100644 --- a/Sources/PubNub/Extensions/String+PubNub.swift +++ b/Sources/PubNub/Extensions/String+PubNub.swift @@ -71,6 +71,24 @@ extension String { } } +extension String { + /// A fully masked representation of a sensitive value for logging + var fullyRedacted: String { + "***" + } + + /// A redacted representation of a sensitive value for logging that keeps a short prefix + /// and the total length, preserving debugging context without exposing the full + /// credential (e.g. `p0F2AkF0… (len=184)`). Values too short to keep a partial prefix + /// without revealing the whole secret are fully masked. + func redacted(prefixLength: Int = 8) -> String { + guard count > prefixLength else { + return "\(fullyRedacted) (len=\(count))" + } + return "\(prefix(prefixLength))… (len=\(count))" + } +} + extension String { /// Creates a structured log description for an object with optional arguments /// - Parameters: diff --git a/Sources/PubNub/Helpers/Crypto/CryptoModule.swift b/Sources/PubNub/Helpers/Crypto/CryptoModule.swift index d4ca2b82..6cd7d264 100644 --- a/Sources/PubNub/Helpers/Crypto/CryptoModule.swift +++ b/Sources/PubNub/Helpers/Crypto/CryptoModule.swift @@ -585,6 +585,8 @@ public extension CryptoModule { /// Encrypts new payloads with ``AESCBCCryptor`` and registers ``LegacyCryptor`` /// as a secondary decryptor, so payloads produced in the older format remain readable. /// + /// Use a long, random, high-entropy key; short or guessable keys remain weak. + /// /// - Parameters: /// - key: Key used for encryption/decryption. /// - withRandomIV: Whether the bundled ``LegacyCryptor`` should expect a random IV when decrypting older payloads. Does not affect new encryption. @@ -641,8 +643,7 @@ extension CryptoModule { .customObject( .init( operation: "encrypt-string", - details: "Encrypting String", - arguments: [("string", string)] + details: "Encrypting String" ) ), category: .crypto ) @@ -681,8 +682,7 @@ extension CryptoModule { .customObject( .init( operation: "decrypt-data", - details: "Decrypting Data", - arguments: [("data", data.utf8String)] + details: "Decrypting Data to String" ) ), category: .crypto diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift index c1119aa3..593e61f1 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift @@ -16,6 +16,11 @@ public struct AESCBCCryptor: Cryptor { private let key: Data private let logger: PubNubLogger? + /// Creates an ``AESCBCCryptor`` from the given cipher key. + /// + /// Use a long, random, high-entropy key; short or guessable keys remain weak. + /// + /// - Parameter key: Secret used to derive the AES key. public init(key: String) { self.key = CryptorUtils.SHA256.hash(from: key.data(using: .utf8) ?? Data()) self.logger = nil diff --git a/Sources/PubNub/Logging/LogWriter.swift b/Sources/PubNub/Logging/LogWriter.swift index 6d95cbc4..dda83774 100644 --- a/Sources/PubNub/Logging/LogWriter.swift +++ b/Sources/PubNub/Logging/LogWriter.swift @@ -45,9 +45,9 @@ public protocol LogWriter { /// /// - Note: This method is called only if the log message's ``LogMessage/logLevel`` is not lower than the parent ``PubNubLogger`` instance's log level. /// - /// - Warning: Debug-level logging, if enabled, is verbose and may include sensitive information, such as API responses, user data, or internal system details. - /// It is **your responsibility** to ensure that sensitive data is properly handled and that logs are not exposed in production environments. For example, - /// our in-house ``OSLogWriter`` implementation safely writes logs using `os.Logger`, ensuring optimal performance and security while adhering to this contract. + /// - Warning: Debug- and trace-level logging, if enabled, is verbose and may include sensitive information, such as API responses, user data, request URLs, + /// authentication tokens, or internal system details. When you provide your own `LogWriter` implementation, it is **your responsibility** to ensure that sensitive data + /// is properly handled and that logs are not exposed in production environments. /// /// - Parameters: /// - message: A closure that returns the log message. This uses `@autoclosure` to defer evaluation until needed. diff --git a/Sources/PubNub/Logging/PubNubLogger.swift b/Sources/PubNub/Logging/PubNubLogger.swift index 530f9a67..6c205b3e 100644 --- a/Sources/PubNub/Logging/PubNubLogger.swift +++ b/Sources/PubNub/Logging/PubNubLogger.swift @@ -86,10 +86,13 @@ public final class PubNubLogger { /// Initializes a new `PubNubLogger` instance with the specified log levels and writers. /// + /// - Warning: Enabling verbose levels such as `.debug`, `.trace`, or `.all` produces logs that may include sensitive information, + /// such as API responses, user data, request URLs, authentication tokens, or internal system details. Avoid these levels in production builds. + /// /// - Parameters: - /// - levels: The log levels to be included in the logger. Defaults to `.all`. + /// - levels: The log levels to be included in the logger. Defaults to `.none`. /// - writers: The writers to be used for logging. Defaults to the default log writers. - public init(levels: LogLevel = .all, writers: [LogWriter] = PubNubLogger.defaultLogWriters()) { + public init(levels: LogLevel = .none, writers: [LogWriter] = PubNubLogger.defaultLogWriters()) { self.writers = writers self.levelsContainer = Atomic(levels) self.pubNubInstanceId = nil diff --git a/Sources/PubNub/PubNub.swift b/Sources/PubNub/PubNub.swift index f28f79a1..9fbf2bb6 100644 --- a/Sources/PubNub/PubNub.swift +++ b/Sources/PubNub/PubNub.swift @@ -76,8 +76,8 @@ public class PubNub { ("configuration.subscribeKey", self.configuration.subscribeKey), ("configuration.userId", self.configuration.userId), ("configuration.cryptoModule", self.configuration.cryptoModule), - ("configuration.authKey", self.configuration.authKey), - ("configuration.authToken", self.configuration.authToken), + ("configuration.authKey", self.configuration.authKey?.fullyRedacted ?? "not set"), + ("configuration.authToken", self.configuration.authToken?.redacted() ?? "not set"), ("configuration.useSecureConnections", self.configuration.useSecureConnections), ("configuration.origin", self.configuration.origin), ("configuration.useInstanceId", self.configuration.useInstanceId), diff --git a/Tests/PubNubIntegrationTests/FilesEndpointIntegrationTests.swift b/Tests/PubNubIntegrationTests/FilesEndpointIntegrationTests.swift index 62bba0a9..1c584c45 100644 --- a/Tests/PubNubIntegrationTests/FilesEndpointIntegrationTests.swift +++ b/Tests/PubNubIntegrationTests/FilesEndpointIntegrationTests.swift @@ -223,7 +223,7 @@ class FilesEndpointIntegrationTests: XCTestCase { // Reuse the same config var configuration = config // Set the system wide crypto module - configuration.cryptoModule = CryptoModule.aesCbcCryptoModule(with: "someKey") + configuration.cryptoModule = CryptoModule.aesCbcCryptoModule(with: "bVw5yU2DAiMLOJjJn33s8UDmuFsCP0lf") let client = PubNub(configuration: configuration, fileSession: fileSession) let downloadFileExpect = expectation(description: "Download Encrypted File Expect") diff --git a/Tests/PubNubIntegrationTests/HistoryEndpointIntegrationTests.swift b/Tests/PubNubIntegrationTests/HistoryEndpointIntegrationTests.swift index a56ff875..f04daebb 100644 --- a/Tests/PubNubIntegrationTests/HistoryEndpointIntegrationTests.swift +++ b/Tests/PubNubIntegrationTests/HistoryEndpointIntegrationTests.swift @@ -253,7 +253,7 @@ class HistoryEndpointIntegrationTests: XCTestCase { // Configure a client with CryptoModule enabled var cryptoConfig = config - cryptoConfig.cryptoModule = CryptoModule.aesCbcCryptoModule(with: "testCipherKey") + cryptoConfig.cryptoModule = CryptoModule.aesCbcCryptoModule(with: "bVw5yU2DAiMLOJjJn33s8UDmuFsCP0lf") let client = PubNub(configuration: cryptoConfig) let historyExpect = expectation(description: "History with Actions") diff --git a/Tests/PubNubIntegrationTests/PublishEndpointIntegrationTests.swift b/Tests/PubNubIntegrationTests/PublishEndpointIntegrationTests.swift index ecb3155c..162a7f36 100644 --- a/Tests/PubNubIntegrationTests/PublishEndpointIntegrationTests.swift +++ b/Tests/PubNubIntegrationTests/PublishEndpointIntegrationTests.swift @@ -211,13 +211,13 @@ class PublishEndpointIntegrationTests: XCTestCase { publishKey: PubNubConfiguration(bundle: testsBundle).publishKey, subscribeKey: PubNubConfiguration(bundle: testsBundle).subscribeKey, userId: PubNubConfiguration(bundle: testsBundle).userId, - cryptoModule: CryptoModule.aesCbcCryptoModule(with: "someKey") + cryptoModule: CryptoModule.aesCbcCryptoModule(with: "bVw5yU2DAiMLOJjJn33s8UDmuFsCP0lf") )) let secondClient = PubNub(configuration: PubNubConfiguration( publishKey: PubNubConfiguration(bundle: testsBundle).publishKey, subscribeKey: PubNubConfiguration(bundle: testsBundle).subscribeKey, userId: PubNubConfiguration(bundle: testsBundle).userId, - cryptoModule: CryptoModule.aesCbcCryptoModule(with: "anotherKey") + cryptoModule: CryptoModule.aesCbcCryptoModule(with: "qVgwzFS3G5aCN6lvp3hV6GSqZYfKa") )) let channelForFistClient = randomString() diff --git a/Tests/PubNubUnitTests/Helpers/CryptoTests.swift b/Tests/PubNubUnitTests/Helpers/CryptoTests.swift index 8bb9fe2d..b048f0d3 100644 --- a/Tests/PubNubUnitTests/Helpers/CryptoTests.swift +++ b/Tests/PubNubUnitTests/Helpers/CryptoTests.swift @@ -74,6 +74,7 @@ class CryptoTests: XCTestCase { let messageData = try XCTUnwrap(message.data(using: .utf8)) let encryptedMessage = try cryptoModule.encrypt(data: messageData).get() let decrypted = try cryptoModule.decrypt(data: encryptedMessage).get() + XCTAssertEqual(message, String(bytes: decrypted, encoding: .utf8)) } @@ -103,7 +104,6 @@ class CryptoTests: XCTestCase { XCTAssertEqual(finalString?.isEmpty, false) let outputPath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("testFile-\(UUID().uuidString)") - try? FileManager.default.removeItem(at: outputPath) cryptoModule.decrypt( stream: InputStream(data: ecrypted), @@ -111,9 +111,9 @@ class CryptoTests: XCTestCase { to: outputPath ) - let decrypted = try Data(contentsOf: outputPath) + let decryptedFile = try Data(contentsOf: outputPath) - XCTAssertEqual(finalString, String(data: decrypted, encoding: .utf8)) + XCTAssertEqual(finalString, String(data: decryptedFile, encoding: .utf8)) } func test_EncryptThenDecryptStream_ReturnsOriginal() throws { @@ -143,7 +143,6 @@ class CryptoTests: XCTestCase { let encryptedStreamResult = try cryptoModule.encrypt(stream: inputStream, contentLength: data.count).get() let decryptedURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("decryptedStream-\(UUID().uuidString)") - try? FileManager.default.removeItem(at: decryptedURL) cryptoModule.decrypt( stream: encryptedStreamResult.stream, @@ -179,8 +178,6 @@ class CryptoTests: XCTestCase { let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory()) let outputPath = temporaryDirectory.appendingPathComponent("decryptedStream-\(UUID().uuidString)") - try? FileManager.default.removeItem(at: outputPath) - cryptoModule.decryptStream( from: encryptedTextURL, to: outputPath @@ -290,14 +287,14 @@ class CryptoTests: XCTestCase { func testDecrypt_RejectsWrongSizeIV() { let cryptor = AESCBCCryptor(key: "enigma") let wrongSizeIV = Data(repeating: 0x01, count: kCCBlockSizeAES128 - 1) - + let result = cryptor.decrypt( data: EncryptedData( metadata: wrongSizeIV, data: Data(repeating: 0xAB, count: kCCBlockSizeAES128) ) ) - + assertOpaqueDecryptionFailure(result.error) } @@ -311,21 +308,21 @@ class CryptoTests: XCTestCase { data: wrongSizeData ) ) - + assertOpaqueDecryptionFailure(result.error) } func testDecrypt_RejectsEmptyContent() { let cryptor = AESCBCCryptor(key: "enigma") let emptyData = Data() - + let result = cryptor.decrypt( data: EncryptedData( metadata: Data(repeating: 0x01, count: kCCBlockSizeAES128), data: emptyData ) ) - + assertOpaqueDecryptionFailure(result.error) } @@ -333,11 +330,17 @@ class CryptoTests: XCTestCase { let cryptor = AESCBCCryptor(key: "enigma") let wrongSizeIV = Data(repeating: 0x01, count: kCCBlockSizeAES128 - 1) let testData = Data(repeating: 0xAB, count: kCCBlockSizeAES128 * 2) - let outputPath = URL.randomTempPath - let encryptedStreamData = EncryptedStreamData(stream: .init(data: testData), contentLength: testData.count, metadata: wrongSizeIV) - let decryptionResult = cryptor.decrypt(data: encryptedStreamData, outputPath: outputPath) - + + let encryptedStreamData = EncryptedStreamData( + stream: .init(data: testData), + contentLength: testData.count, metadata: wrongSizeIV + ) + let decryptionResult = cryptor.decrypt( + data: encryptedStreamData, + outputPath: outputPath + ) + assertOpaqueDecryptionFailure(decryptionResult.error) } @@ -345,11 +348,17 @@ class CryptoTests: XCTestCase { let cryptor = AESCBCCryptor(key: "enigma") let iv = Data(repeating: 0x01, count: kCCBlockSizeAES128) let wrongSizeData = Data(repeating: 0xAB, count: kCCBlockSizeAES128 + 1) - let outputPath = URL.randomTempPath - let encryptedStreamData = EncryptedStreamData(stream: .init(data: wrongSizeData), contentLength: wrongSizeData.count, metadata: iv) - let decryptionResult = cryptor.decrypt(data: encryptedStreamData, outputPath: outputPath) - + + let encryptedStreamData = EncryptedStreamData( + stream: .init(data: wrongSizeData), + contentLength: wrongSizeData.count, metadata: iv + ) + let decryptionResult = cryptor.decrypt( + data: encryptedStreamData, + outputPath: outputPath + ) + assertOpaqueDecryptionFailure(decryptionResult.error) } @@ -357,11 +366,11 @@ class CryptoTests: XCTestCase { let cryptor = AESCBCCryptor(key: "enigma") let iv = Data(repeating: 0x01, count: kCCBlockSizeAES128) let emptyData = Data() - + let outputPath = URL.randomTempPath let encryptedStreamData = EncryptedStreamData(stream: .init(data: emptyData), contentLength: emptyData.count, metadata: iv) let decryptionResult = cryptor.decrypt(data: encryptedStreamData, outputPath: outputPath) - + assertOpaqueDecryptionFailure(decryptionResult.error) } } @@ -375,7 +384,7 @@ private extension CryptoTests { guard let pubNubError = error as? PubNubError else { return XCTFail("Expected a PubNubError, got \(String(describing: error))", file: file, line: line) } - + XCTAssertEqual(pubNubError.reason, .decryptionFailure, file: file, line: line) XCTAssertNil(pubNubError.underlying, "Decrypt failures must not expose an underlying status", file: file, line: line) XCTAssertEqual(pubNubError.details, ["Decryption failed"], file: file, line: line) diff --git a/Tests/PubNubUnitTests/PubNubConfigurationTests.swift b/Tests/PubNubUnitTests/PubNubConfigurationTests.swift index 848a724a..1d1ec0b1 100644 --- a/Tests/PubNubUnitTests/PubNubConfigurationTests.swift +++ b/Tests/PubNubUnitTests/PubNubConfigurationTests.swift @@ -66,13 +66,13 @@ class PubNubConfigurationTests: XCTestCase { publishKey: PubNubConfiguration(bundle: testsBundle).publishKey, subscribeKey: PubNubConfiguration(bundle: testsBundle).subscribeKey, userId: PubNubConfiguration(bundle: testsBundle).userId, - cryptoModule: CryptoModule.aesCbcCryptoModule(with: "someKey") + cryptoModule: CryptoModule.aesCbcCryptoModule(with: "bVw5yU2DAiMLOJjJn33s8UDmuFsCP0lf") ) let secondConfig = PubNubConfiguration( publishKey: PubNubConfiguration(bundle: testsBundle).publishKey, subscribeKey: PubNubConfiguration(bundle: testsBundle).subscribeKey, userId: PubNubConfiguration(bundle: testsBundle).userId, - cryptoModule: CryptoModule.aesCbcCryptoModule(with: "anotherKey") + cryptoModule: CryptoModule.aesCbcCryptoModule(with: "qVgwzFS3G5aCN6lvp3hV6GSqZYfKa") ) XCTAssertNotEqual(firstConfig.hashValue, secondConfig.hashValue) From a3c48363ecb1429b3ea5998a4a88300d7375ef07 Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Mon, 29 Jun 2026 15:21:09 +0200 Subject: [PATCH 3/7] Map encrypt-path errors to .encryptionFailure (CLEN-3530) The catch blocks in AESCBCCryptor.encrypt(data:) and LegacyCryptor.encrypt(data:) returned .decryptionFailure even though the call site is encryption, misdirecting incident triage. Both now return .encryptionFailure, aligning with the stream-encrypt paths. Co-Authored-By: Claude Opus 4.7 --- Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift | 2 +- Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift index 593e61f1..c339f3d5 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift @@ -56,7 +56,7 @@ public struct AESCBCCryptor: Cryptor { )) } catch { return .failure(PubNubError( - .decryptionFailure, + .encryptionFailure, underlying: error )) } diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift index 64adf887..280fc94e 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift @@ -68,7 +68,7 @@ public struct LegacyCryptor: Cryptor { )) } catch { return .failure(PubNubError( - .decryptionFailure, + .encryptionFailure, underlying: error )) } From 79d8e571ab18a480027f69d12f343dba7650e04c Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Mon, 29 Jun 2026 15:35:53 +0200 Subject: [PATCH 4/7] CryptoTests.swift --- .../PubNubUnitTests/Helpers/CryptoTests.swift | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/Tests/PubNubUnitTests/Helpers/CryptoTests.swift b/Tests/PubNubUnitTests/Helpers/CryptoTests.swift index b048f0d3..2d8bb249 100644 --- a/Tests/PubNubUnitTests/Helpers/CryptoTests.swift +++ b/Tests/PubNubUnitTests/Helpers/CryptoTests.swift @@ -22,7 +22,7 @@ class CryptoTests: XCTestCase { let testData = try XCTUnwrap(testMessage.data(using: .utf16)) let encryptedData = try cryptoModule.encrypt(data: testData).get() let decryptedData = try cryptoModule.decrypt(data: encryptedData).get() - let decryptedString = String(bytes: decryptedData, encoding: .utf16) + let decryptedString = try XCTUnwrap(String(bytes: decryptedData, encoding: .utf16)) XCTAssertEqual(testMessage, decryptedString) } @@ -46,7 +46,7 @@ class CryptoTests: XCTestCase { let testData = try XCTUnwrap(jsonMessage.data(using: .utf8)) let encryptedData = try cryptoModule.encrypt(data: testData).get() let decryptedData = try cryptoModule.decrypt(data: encryptedData).get() - let decryptedString = String(bytes: decryptedData, encoding: .utf8)?.reverseJSONDescription + let decryptedString = try XCTUnwrap(String(bytes: decryptedData, encoding: .utf8)).reverseJSONDescription XCTAssertEqual(testMessage, decryptedString) } @@ -75,24 +75,29 @@ class CryptoTests: XCTestCase { let encryptedMessage = try cryptoModule.encrypt(data: messageData).get() let decrypted = try cryptoModule.decrypt(data: encryptedMessage).get() - XCTAssertEqual(message, String(bytes: decrypted, encoding: .utf8)) + XCTAssertEqual(message, try XCTUnwrap(String(bytes: decrypted, encoding: .utf8))) } - func test_DecryptOtherSDKRandomIVPayload_ReturnsOriginal() throws { + func test_EncryptThenDecryptStringWithRandomIV_ReturnsOriginal() throws { let cryptoModule = CryptoModule.legacyCryptoModule(with: "enigma", withRandomIV: true) let plainText = "yay!" - let otherSDKBase64 = "MTIzNDU2Nzg5MDEyMzQ1NjdnONoCgo0wbuMGGMmfMX0=" let swiftEncryptedString = try cryptoModule.encrypt(string: plainText).get() let swiftEncryptedStringAsData = try XCTUnwrap(Data(base64Encoded: swiftEncryptedString)) let swiftDecryptedString = try cryptoModule.decryptedString(from: swiftEncryptedStringAsData).get() XCTAssertEqual(plainText, swiftDecryptedString) + } + + func test_DecryptOtherSDKRandomIVPayload_ReturnsOriginal() throws { + let cryptoModule = CryptoModule.legacyCryptoModule(with: "enigma", withRandomIV: true) + let plainText = "yay!" + let otherSDKBase64 = "MTIzNDU2Nzg5MDEyMzQ1NjdnONoCgo0wbuMGGMmfMX0=" let otherData = try XCTUnwrap(Data(base64Encoded: otherSDKBase64)) let otherDecrypted = try cryptoModule.decrypt(data: otherData).get() - XCTAssertEqual(plainText, String(data: otherDecrypted, encoding: .utf8)) + XCTAssertEqual(plainText, try XCTUnwrap(String(data: otherDecrypted, encoding: .utf8))) } func test_DecryptStreamFromOtherSDK_MatchesPlaintext() throws { @@ -105,15 +110,15 @@ class CryptoTests: XCTestCase { let outputPath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("testFile-\(UUID().uuidString)") - cryptoModule.decrypt( + _ = try cryptoModule.decrypt( stream: InputStream(data: ecrypted), contentLength: ecrypted.count, to: outputPath - ) + ).get() let decryptedFile = try Data(contentsOf: outputPath) - XCTAssertEqual(finalString, String(data: decryptedFile, encoding: .utf8)) + XCTAssertEqual(finalString, try XCTUnwrap(String(data: decryptedFile, encoding: .utf8))) } func test_EncryptThenDecryptStream_ReturnsOriginal() throws { @@ -144,16 +149,16 @@ class CryptoTests: XCTestCase { let decryptedURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("decryptedStream-\(UUID().uuidString)") - cryptoModule.decrypt( + _ = try cryptoModule.decrypt( stream: encryptedStreamResult.stream, contentLength: encryptedStreamResult.contentLength, to: decryptedURL - ) + ).get() - let decryptedString = String( + let decryptedString = try XCTUnwrap(String( data: try Data(contentsOf: decryptedURL), encoding: .utf8 - ) + )) XCTAssertEqual(plainTextString, decryptedString) } @@ -178,15 +183,15 @@ class CryptoTests: XCTestCase { let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory()) let outputPath = temporaryDirectory.appendingPathComponent("decryptedStream-\(UUID().uuidString)") - cryptoModule.decryptStream( + _ = try cryptoModule.decryptStream( from: encryptedTextURL, to: outputPath - ) + ).get() - let actualDecryptedContent = String( + let actualDecryptedContent = try XCTUnwrap(String( data: try Data(contentsOf: outputPath), encoding: .utf8 - ) + )) XCTAssertEqual( expectedDecryptedContent, @@ -284,7 +289,7 @@ class CryptoTests: XCTestCase { ) } - func testDecrypt_RejectsWrongSizeIV() { + func test_Decrypt_RejectsWrongSizeIV() { let cryptor = AESCBCCryptor(key: "enigma") let wrongSizeIV = Data(repeating: 0x01, count: kCCBlockSizeAES128 - 1) @@ -298,7 +303,7 @@ class CryptoTests: XCTestCase { assertOpaqueDecryptionFailure(result.error) } - func testDecrypt_RejectsNonBlockAlignedContent() { + func test_Decrypt_RejectsNonBlockAlignedContent() { let cryptor = AESCBCCryptor(key: "enigma") let wrongSizeData = Data(repeating: 0xAB, count: kCCBlockSizeAES128 + 1) @@ -312,7 +317,7 @@ class CryptoTests: XCTestCase { assertOpaqueDecryptionFailure(result.error) } - func testDecrypt_RejectsEmptyContent() { + func test_Decrypt_RejectsEmptyContent() { let cryptor = AESCBCCryptor(key: "enigma") let emptyData = Data() @@ -326,7 +331,7 @@ class CryptoTests: XCTestCase { assertOpaqueDecryptionFailure(result.error) } - func testDecryptStream_RejectsWrongSizeIV() { + func test_DecryptStream_RejectsWrongSizeIV() { let cryptor = AESCBCCryptor(key: "enigma") let wrongSizeIV = Data(repeating: 0x01, count: kCCBlockSizeAES128 - 1) let testData = Data(repeating: 0xAB, count: kCCBlockSizeAES128 * 2) @@ -344,7 +349,7 @@ class CryptoTests: XCTestCase { assertOpaqueDecryptionFailure(decryptionResult.error) } - func testDecryptStream_RejectsNonBlockAlignedContent() { + func test_DecryptStream_RejectsNonBlockAlignedContent() { let cryptor = AESCBCCryptor(key: "enigma") let iv = Data(repeating: 0x01, count: kCCBlockSizeAES128) let wrongSizeData = Data(repeating: 0xAB, count: kCCBlockSizeAES128 + 1) @@ -362,7 +367,7 @@ class CryptoTests: XCTestCase { assertOpaqueDecryptionFailure(decryptionResult.error) } - func testDecryptStream_RejectsEmptyContent() { + func test_DecryptStream_RejectsEmptyContent() { let cryptor = AESCBCCryptor(key: "enigma") let iv = Data(repeating: 0x01, count: kCCBlockSizeAES128) let emptyData = Data() From beca16ee09afd760c1b70a9a0026d70d990e92ab Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Mon, 29 Jun 2026 16:14:17 +0200 Subject: [PATCH 5/7] Fixes --- Sources/PubNub/Errors/PubNubError.swift | 5 +++-- Sources/PubNub/Helpers/Crypto/CryptoModule.swift | 10 +++++----- .../PubNub/Helpers/Crypto/Header/CryptorHeader.swift | 6 +++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Sources/PubNub/Errors/PubNubError.swift b/Sources/PubNub/Errors/PubNubError.swift index b56da578..57cf8fe6 100644 --- a/Sources/PubNub/Errors/PubNubError.swift +++ b/Sources/PubNub/Errors/PubNubError.swift @@ -19,14 +19,15 @@ public struct PubNubError: Error { public let details: [String] /// The underlying `Error` that caused this `Error` to happen public let underlying: Error? - - let coorelation: [CorrelationIdentifier] + /// The values that were affected by the error public let affected: [AffectedValue] + let coorelation: [CorrelationIdentifier] let router: HTTPRouter? /// The domain of the `Error` public let domain = "PubNub" + /// The subdomain that this error can be categorized with public var subdomain: Domain { return reason.domain diff --git a/Sources/PubNub/Helpers/Crypto/CryptoModule.swift b/Sources/PubNub/Helpers/Crypto/CryptoModule.swift index 6cd7d264..ad9eadd0 100644 --- a/Sources/PubNub/Helpers/Crypto/CryptoModule.swift +++ b/Sources/PubNub/Helpers/Crypto/CryptoModule.swift @@ -182,7 +182,7 @@ public struct CryptoModule { guard !data.isEmpty else { return .failure(PubNubError( .decryptionFailure, - additional: ["Cannot decrypt empty Data in \(String(describing: self))"] + additional: ["Cannot decrypt empty Data"] )) } do { @@ -235,7 +235,7 @@ public struct CryptoModule { if $0.isEmpty { return .failure(PubNubError( .decryptionFailure, - additional: ["Decrypting resulted with empty Data"] + additional: ["Decryption resulted in empty Data"] )) } return .success($0) @@ -249,7 +249,7 @@ public struct CryptoModule { return .failure(PubNubError( .decryptionFailure, underlying: error, - additional: ["Cannot decrypt InputStream"] + additional: ["Could not decrypt Data"] )) } } @@ -475,7 +475,7 @@ public struct CryptoModule { ) return .failure(PubNubError( .decryptionFailure, - additional: ["File doesn't exist at \(localFileURL) path"] + additional: ["File doesn't exist at \(localFileURL)"] )) } @@ -552,7 +552,7 @@ public struct CryptoModule { if outputPath.sizeOf == 0 { return .failure(PubNubError( .decryptionFailure, - additional: ["Decrypting resulted with an empty File"] + additional: ["Decryption resulted in an empty File"] )) } return .success($0) diff --git a/Sources/PubNub/Helpers/Crypto/Header/CryptorHeader.swift b/Sources/PubNub/Helpers/Crypto/Header/CryptorHeader.swift index 428c216b..7518772a 100644 --- a/Sources/PubNub/Helpers/Crypto/Header/CryptorHeader.swift +++ b/Sources/PubNub/Helpers/Crypto/Header/CryptorHeader.swift @@ -107,10 +107,10 @@ struct CryptorHeaderParser { throw PubNubError(.decryptionFailure, additional: ["Could not find Cryptor identifier"]) } guard let cryptorDataSizeByte = scanner.nextByte() else { - throw PubNubError(.decryptionFailure, additional: ["Could not find Cryptor data size byte"]) + throw PubNubError(.decryptionFailure, additional: ["Could not find Cryptor data-size byte"]) } guard let finalCryptorDataSize = try? computeCryptorDataSize(with: Int(cryptorDataSizeByte)) else { - throw PubNubError(.decryptionFailure, additional: ["Could not retrieve Cryptor defined data size"]) + throw PubNubError(.decryptionFailure, additional: ["Could not retrieve Cryptor-defined data size"]) } return .v1( cryptorId: cryptorId.map { $0 }, @@ -123,7 +123,7 @@ struct CryptorHeaderParser { return UInt16(sizeIndicator) } guard let nextBytes = scanner.nextBytes(2) else { - throw PubNubError(.unknownCryptorFailure, additional: ["Could not find next Cryptor data size bytes"]) + throw PubNubError(.unknownCryptorFailure, additional: ["Could not find next Cryptor data-size bytes"]) } return nextBytes.withUnsafeBytes { $0.load(as: UInt16.self) From 3f32883d7fa3de237be7c420d562b0a98ac9839d Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Thu, 2 Jul 2026 11:53:28 +0200 Subject: [PATCH 6/7] Read IV block fully across partial stream reads Loop until a full IV block is read so legitimate file/network streams that return short reads are not wrongly rejected, while truncated payloads still fail. --- .../Miscellaneous/CryptoInputStream.swift | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Sources/PubNub/Helpers/Crypto/Miscellaneous/CryptoInputStream.swift b/Sources/PubNub/Helpers/Crypto/Miscellaneous/CryptoInputStream.swift index 6166b12e..200dac20 100644 --- a/Sources/PubNub/Helpers/Crypto/Miscellaneous/CryptoInputStream.swift +++ b/Sources/PubNub/Helpers/Crypto/Miscellaneous/CryptoInputStream.swift @@ -108,16 +108,13 @@ public class CryptoInputStream: InputStream { var initializationVectorBuffer = [UInt8](repeating: 0, count: crypto.cipher.blockSize) if includeInitializationVectorInContent { - // The IV must be exactly one block; anything shorter is rejected so that an attacker - // cannot probe decrypt failures by truncating the leading IV. - if input.read(&initializationVectorBuffer, maxLength: crypto.cipher.blockSize) != crypto.cipher.blockSize { + if !Self.readExactly(from: input, into: &initializationVectorBuffer, length: crypto.cipher.blockSize) { _streamStatus = .error _streamError = input.streamError ?? PubNubError(.decryptionFailure) } } else if crypto.iv.count == crypto.cipher.blockSize { initializationVectorBuffer = crypto.iv.map { $0 } } else { - // A wrong-length IV is rejected for the same reason as above. _streamStatus = .error _streamError = PubNubError(.decryptionFailure) } @@ -238,6 +235,24 @@ public class CryptoInputStream: InputStream { // MARK: - Helpers + private static func readExactly(from stream: InputStream, into buffer: inout [UInt8], length: Int) -> Bool { + var totalRead = 0 + + while totalRead < length { + let bytesRead = buffer.withUnsafeMutableBufferPointer { ptr -> Int in + guard let baseAddress = ptr.baseAddress else { + return -1 + } + return stream.read(baseAddress + totalRead, maxLength: length - totalRead) + } + if bytesRead <= 0 { + return false + } + totalRead += bytesRead + } + return true + } + private func fillRawDataBuffer(with bytes: Int) { guard _streamStatus == .open, cipherStream.streamStatus == .open else { return From 32dbcfde71b377c474a5ed9207e16cd04fb4bcfe Mon Sep 17 00:00:00 2001 From: Jakub Guz Date: Wed, 8 Jul 2026 13:15:48 +0000 Subject: [PATCH 7/7] PubNub SDK 10.1.8 release. --- .pubnub.yml | 25 +++++++++++++++++++++++-- PubNub.xcodeproj/project.pbxproj | 16 ++++++++-------- PubNubSwift.podspec | 2 +- Sources/PubNub/Helpers/Constants.swift | 2 +- Sources/PubNub/Logging/LogWriter.swift | 2 +- 5 files changed, 34 insertions(+), 13 deletions(-) diff --git a/.pubnub.yml b/.pubnub.yml index bb47f7a8..a09c8006 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,9 +1,30 @@ --- name: swift scm: github.com/pubnub/swift -version: "10.1.7" +version: "10.1.8" schema: 1 changelog: + - date: 2026-07-08 + version: 10.1.8 + changes: + - type: bug + text: "Validate IV length and ciphertext block-alignment before decrypting." + - type: bug + text: "Return a uniform .decryptionFailure that never exposes the underlying CommonCrypto status." + - type: bug + text: "Guard LegacyCryptor random-IV path against payloads too short to contain an IV." + - type: bug + text: "Map encrypt-path errors to .encryptionFailure instead of .decryptionFailure ." + - type: bug + text: "Redact authKey and authToken in instance logs instead of emitting them in cleartext." + - type: bug + text: "Use .none as the default log level for the PubNubLogger public initializer to avoid an insecure default." + - type: bug + text: "Make message action add/remove fire completion only once on error." + - type: improvement + text: "Warn that verbose log levels may expose sensitive data and recommend long, high-entropy cipher keys." + - type: improvement + text: "Make every PubNub instance manage its own presence-state container." - date: 2026-06-15 version: 10.1.7 changes: @@ -797,7 +818,7 @@ sdks: - distribution-type: source distribution-repository: GitHub release package-name: PubNub - location: https://github.com/pubnub/swift/archive/refs/tags/10.1.7.zip + location: https://github.com/pubnub/swift/archive/refs/tags/10.1.8.zip supported-platforms: supported-operating-systems: macOS: diff --git a/PubNub.xcodeproj/project.pbxproj b/PubNub.xcodeproj/project.pbxproj index b6f09601..9b9241f8 100644 --- a/PubNub.xcodeproj/project.pbxproj +++ b/PubNub.xcodeproj/project.pbxproj @@ -4398,7 +4398,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 10.1.7; + MARKETING_VERSION = 10.1.8; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -4449,7 +4449,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 10.1.7; + MARKETING_VERSION = 10.1.8; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -4557,7 +4557,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 10.1.7; + MARKETING_VERSION = 10.1.8; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -4610,7 +4610,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 10.1.7; + MARKETING_VERSION = 10.1.8; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -4731,7 +4731,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 10.1.7; + MARKETING_VERSION = 10.1.8; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -4783,7 +4783,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 10.1.7; + MARKETING_VERSION = 10.1.8; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -5263,7 +5263,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 10.1.7; + MARKETING_VERSION = 10.1.8; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; @@ -5306,7 +5306,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 10.1.7; + MARKETING_VERSION = 10.1.8; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; diff --git a/PubNubSwift.podspec b/PubNubSwift.podspec index 58d250ce..76dd1bf6 100644 --- a/PubNubSwift.podspec +++ b/PubNubSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'PubNubSwift' - s.version = '10.1.7' + s.version = '10.1.8' s.homepage = 'https://github.com/pubnub/swift' s.documentation_url = 'https://www.pubnub.com/docs/swift-native/pubnub-swift-sdk' s.authors = { 'PubNub, Inc.' => 'support@pubnub.com' } diff --git a/Sources/PubNub/Helpers/Constants.swift b/Sources/PubNub/Helpers/Constants.swift index b3c8fa6d..0ab2e267 100644 --- a/Sources/PubNub/Helpers/Constants.swift +++ b/Sources/PubNub/Helpers/Constants.swift @@ -57,7 +57,7 @@ public enum Constant { static let pubnubSwiftSDKName: String = "PubNubSwift" - static let pubnubSwiftSDKVersion: String = "10.1.7" + static let pubnubSwiftSDKVersion: String = "10.1.8" static let appBundleId: String = { if let info = Bundle.main.infoDictionary, diff --git a/Sources/PubNub/Logging/LogWriter.swift b/Sources/PubNub/Logging/LogWriter.swift index dda83774..c15e5e2f 100644 --- a/Sources/PubNub/Logging/LogWriter.swift +++ b/Sources/PubNub/Logging/LogWriter.swift @@ -43,7 +43,7 @@ public protocol LogWriter { /// Logs a given message /// - /// - Note: This method is called only if the log message's ``LogMessage/logLevel`` is not lower than the parent ``PubNubLogger`` instance's log level. + /// - Note: This method is called only if the log message's ``LogMessage/logLevel`` is not lower than the parent ``PubNubLogger`` instance's log level. /// /// - Warning: Debug- and trace-level logging, if enabled, is verbose and may include sensitive information, such as API responses, user data, request URLs, /// authentication tokens, or internal system details. When you provide your own `LogWriter` implementation, it is **your responsibility** to ensure that sensitive data