Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Snippets/Configuration/configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func aesCbcCryptoModuleExample() {
publishKey: "demo",
subscribeKey: "demo",
userId: "myUniqueUserId",
cryptoModule: CryptoModule.aesCbcCryptoModule(with: "pubnubenigma")
cryptoModule: CryptoModule.aesCbcCryptoModule(with: "Be7HHAmdGinTHPulC0aLIUHx7Bna10y2")
)
)
// snippet.end
Expand All @@ -52,7 +52,7 @@ func legacyCryptoModuleExample() {
publishKey: "demo",
subscribeKey: "demo",
userId: "myUniqueUserId",
cryptoModule: CryptoModule.legacyCryptoModule(with: "pubnubenigma")
cryptoModule: CryptoModule.legacyCryptoModule(with: "Be7HHAmdGinTHPulC0aLIUHx7Bna10y2")
)
)
// snippet.end
Expand Down
4 changes: 2 additions & 2 deletions Snippets/Misc/misc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
18 changes: 18 additions & 0 deletions Sources/PubNub/Extensions/String+PubNub.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions Sources/PubNub/Helpers/Crypto/CryptoModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -641,8 +643,7 @@ extension CryptoModule {
.customObject(
.init(
operation: "encrypt-string",
details: "Encrypting String",
arguments: [("string", string)]
details: "Encrypting String"
)
), category: .crypto
)
Expand Down Expand Up @@ -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
Expand Down
48 changes: 27 additions & 21 deletions Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,20 +56,21 @@ public struct AESCBCCryptor: Cryptor {
))
} catch {
return .failure(PubNubError(
.decryptionFailure,
.encryptionFailure,
underlying: error
))
}
}

public func decrypt(data: EncryptedData) -> Result<Data, Error> {
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),
Expand All @@ -77,10 +83,7 @@ public struct AESCBCCryptor: Cryptor {
)
)
} catch {
return .failure(PubNubError(
.decryptionFailure,
underlying: error
))
return .failure(CBCDecrypt.failure)
}
}

Expand Down Expand Up @@ -120,6 +123,15 @@ public struct AESCBCCryptor: Cryptor {
}

public func decrypt(data: EncryptedStreamData, outputPath: URL) -> Result<InputStream, Error> {
// 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),
Expand All @@ -140,18 +152,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)
}
}

Expand Down
34 changes: 13 additions & 21 deletions Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public struct LegacyCryptor: Cryptor {
))
} catch {
return .failure(PubNubError(
.decryptionFailure,
.encryptionFailure,
underlying: error
))
}
Expand All @@ -80,22 +80,23 @@ 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 {
iv = try CryptorVector.fixed.data()
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),
Expand All @@ -106,10 +107,7 @@ public struct LegacyCryptor: Cryptor {
)
)
} catch {
return .failure(PubNubError(
.decryptionFailure,
underlying: error
))
return .failure(CBCDecrypt.failure)
}
}

Expand Down Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions Sources/PubNub/Logging/LogWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions Sources/PubNub/Logging/PubNubLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SDK always creates a PubNubLogger internally with the log level disabled. This change removes the insecure default from the public initializer.

self.writers = writers
self.levelsContainer = Atomic(levels)
self.pubNubInstanceId = nil
Expand Down
4 changes: 2 additions & 2 deletions Sources/PubNub/PubNub.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading
Loading