Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions Sources/PubNub/Errors/PubNubError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
18 changes: 9 additions & 9 deletions Sources/PubNub/Helpers/Crypto/CryptoModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -249,7 +249,7 @@ public struct CryptoModule {
return .failure(PubNubError(
.decryptionFailure,
underlying: error,
additional: ["Cannot decrypt InputStream"]
additional: ["Could not decrypt Data"]
))
}
}
Expand Down Expand Up @@ -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)"]
))
}

Expand Down Expand Up @@ -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)
Expand Down 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
6 changes: 3 additions & 3 deletions Sources/PubNub/Helpers/Crypto/Header/CryptorHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,15 @@ 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`.
if !Self.readExactly(from: input, into: &initializationVectorBuffer, length: 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 {
_streamStatus = .error
_streamError = PubNubError(.decryptionFailure)
}

// Init the Crypto Stream
Expand Down Expand Up @@ -237,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
Expand Down
Loading
Loading