From 2cb46f93584f3681f91ca0d24b5a1b159d3b2e09 Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Mon, 2 Oct 2023 17:56:57 +0200 Subject: [PATCH] CryptorModule * Prevent from decoding & encoding empty Data & Files * Added inline documentation for public items --- PubNub.xcodeproj/project.pbxproj | 6 +- .../PubNub/Helpers/Crypto/CryptorModule.swift | 105 +++++++++++++++++- .../Crypto/Cryptors/AESCBCCryptor.swift | 10 +- .../Helpers/Crypto/Cryptors/Cryptor.swift | 37 ++++++ .../Crypto/Cryptors/LegacyCryptor.swift | 4 + .../PubNubCryptoModuleContractTestSteps.swift | 68 ++++++++---- 6 files changed, 199 insertions(+), 31 deletions(-) rename Tests/PubNubContractTest/Steps/{Crypto => CryptorModule}/PubNubCryptoModuleContractTestSteps.swift (74%) diff --git a/PubNub.xcodeproj/project.pbxproj b/PubNub.xcodeproj/project.pbxproj index a3483f8f..82c0ac96 100644 --- a/PubNub.xcodeproj/project.pbxproj +++ b/PubNub.xcodeproj/project.pbxproj @@ -1909,12 +1909,12 @@ path = EndpointError; sourceTree = ""; }; - 3D6265D22ABC8E6900FDD5E6 /* Crypto */ = { + 3D6265D22ABC8E6900FDD5E6 /* CryptorModule */ = { isa = PBXGroup; children = ( 3DBB2C202ABD8053008A100E /* PubNubCryptoModuleContractTestSteps.swift */, ); - path = Crypto; + path = CryptorModule; sourceTree = ""; }; 3D758DC42AB06977005D2B36 /* Miscellaneous */ = { @@ -2000,7 +2000,7 @@ 79407BC1271D4CFA0032076C /* Steps */ = { isa = PBXGroup; children = ( - 3D6265D22ABC8E6900FDD5E6 /* Crypto */, + 3D6265D22ABC8E6900FDD5E6 /* CryptorModule */, A5F88ECF2906A9DE00F49D5C /* Objects */, 79407BC2271D4CFA0032076C /* Access */, 79407BC4271D4CFA0032076C /* Message Actions */, diff --git a/Sources/PubNub/Helpers/Crypto/CryptorModule.swift b/Sources/PubNub/Helpers/Crypto/CryptorModule.swift index f8f713d0..1b21f31d 100644 --- a/Sources/PubNub/Helpers/Crypto/CryptorModule.swift +++ b/Sources/PubNub/Helpers/Crypto/CryptorModule.swift @@ -27,8 +27,11 @@ import Foundation +/// Represents the result of stream encryption public struct EncryptedStreamResult { + /// Encoded stream you can read from public let stream: InputStream + /// Content length of encoded stream public let contentLength: Int public init(stream: InputStream, contentLength: Int) { @@ -37,6 +40,9 @@ public struct EncryptedStreamResult { } } +/// Object capable of encryption/decryption. +/// +/// - Important: Replaces obsolete [Crypto](https://github.com/pubnub/swift/blob/master/Sources/PubNub/Helpers/Crypto/Crypto.swift#L32) public struct CryptorModule { private let defaultCryptor: Cryptor private let cryptors: [Cryptor] @@ -45,14 +51,35 @@ public struct CryptorModule { typealias Base64EncodedString = String + /// Initializes `CryptorModule` 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 returns `CryptorModule` 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 + /// - encoding: Default String encoding used when publishing new messages public init(default cryptor: Cryptor, cryptors: [Cryptor] = [], encoding: String.Encoding = .utf8) { self.defaultCryptor = cryptor self.cryptors = cryptors self.defaultStringEncoding = encoding } + /// Encrypts the given `Data` object + /// + /// - Parameters: + /// - data: Data to encrypt + /// - Returns: A success, storing encrypted `Data` if operation succeeds. Otherwise, a failure storing `PubNubError` is returned public func encrypt(data: Data) -> Result { - defaultCryptor.encrypt(data: data).map { + guard !data.isEmpty else { + return .failure(PubNubError( + .encryptionError, + additional: ["Cannot encrypt empty Data"]) + ) + } + return defaultCryptor.encrypt(data: data).map { if defaultCryptor.id == LegacyCryptor.ID { return $0.data } else { @@ -66,7 +93,18 @@ public struct CryptorModule { } } + /// Decrypts the given `Data` object + /// + /// - Parameters: + /// - data: Data to encrypt + /// - Returns: A success, storing decrypted `Data` if operation succeeds. Otherwise, a failure storing `PubNubError` is returned public func decrypt(data: Data) -> Result { + guard !data.isEmpty else { + return .failure(PubNubError( + .decryptionError, + additional: ["Cannot decrypt empty Data"]) + ) + } do { let header = try CryptorHeader.from(data: data) @@ -84,7 +122,17 @@ public struct CryptorModule { metadata: header.metadataIfAny(), data: data.subdata(in: header.length().. Result { + guard contentLength > 0 else { + return .failure(PubNubError( + .encryptionError, + additional: ["Cannot encrypt empty InputStream"]) + ) + } return defaultCryptor.encrypt( stream: stream, contentLength: contentLength @@ -120,12 +180,25 @@ public struct CryptorModule { } } + /// Decrypts the given `InputStream` object + /// + /// - Parameters: + /// - data: A value describing encrypted stream + /// - outputPath: URL where the stream should be decrypted to + /// - Returns: A success, storing a decrypted ``EncryptedStreamResult`` value if operation succeeds. Otherwise, a failure storing `PubNubError` is returned @discardableResult public func decrypt( stream streamData: EncryptedStreamResult, to outputPath: URL ) -> Result { do { + guard streamData.contentLength > 0 else { + return .failure(PubNubError( + .decryptionError, + additional: ["Cannot decrypt empty InputStream"] + )) + } + let finder = CryptorHeaderWithinStreamFinder(stream: streamData.stream) let readHeaderResponse = try finder.findHeader() @@ -145,7 +218,16 @@ public struct CryptorModule { metadata: readHeaderResponse.header.metadataIfAny() ), outputPath: outputPath - ).mapError { + ).flatMap { + if outputPath.sizeOf == 0 { + return .failure(PubNubError( + .decryptionError, + additional: ["Decrypting resulted with an empty File"]) + ) + } + return .success($0) + } + .mapError { PubNubError(.decryptionError, underlying: $0) } } catch let error as PubNubError { @@ -166,10 +248,27 @@ public struct CryptorModule { } } +/// Convenience methods for creating `CryptorModule` public extension CryptorModule { + + /// Returns **recommended** `CryptorModule` for encryption/decryption + /// + /// - Parameters: + /// - key: Key used for encryption/decryption + /// - withRandomIV: A flag describing whether random initialization vector should be used + /// + /// This method sets ``AESCBCCryptor`` as the primary object for decryption and encryption. It also instantiates ``LegacyCryptor`` with `withRandomIV` + /// flag in order to decode messages/files that were encoded in old way. static func aesCbcCryptoModule(with key: String, withRandomIV: Bool = true) -> CryptorModule { CryptorModule(default: AESCBCCryptor(key: key), cryptors: [LegacyCryptor(key: key, withRandomIV: withRandomIV)]) } + + /// Returns legacy `CryptorModule` for encryption/decryption + /// + /// - Parameters: + /// - key: Key used for encryption/decryption + /// - withRandomIV: A flag describing whether random initialization vector should be used + /// - Warning: It's highly recommended to always use ``aesCbcCryptoModule(with:withRandomIV:)`` static func legacyCryptoModule(with key: String, withRandomIV: Bool = true) -> CryptorModule { CryptorModule(default: LegacyCryptor(key: key, withRandomIV: withRandomIV), cryptors: [AESCBCCryptor(key: key)]) } diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift index 549dd1b8..15ebe5bd 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift @@ -28,6 +28,7 @@ import Foundation import CommonCrypto +/// Provides PubNub's **recommended** ``Cryptor`` for encryption/decryption public struct AESCBCCryptor: Cryptor { private let key: Data @@ -144,12 +145,11 @@ public struct AESCBCCryptor: Cryptor { ) if let stream = InputStream(url: outputPath) { return .success(stream) - } else { - return .failure(PubNubError( - .decryptionError, - additional: ["Cannot create final decoded stream"]) - ) } + return .failure(PubNubError( + .decryptionError, + additional: ["Cannot create resulting InputStream at \(outputPath)"] + )) } catch { return .failure(PubNubError( .decryptionError, diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift index 858e34d5..eabe942a 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift @@ -28,24 +28,61 @@ import Foundation import CommonCrypto +/// Represents the result of encrypted `Data` public struct EncryptedData { + /// Metadata (if any) used while encrypting let metadata: Data + /// Resulting encrypted `Data` let data: Data } +/// Represents the result of encrypted `InputStream` public struct EncryptedStreamData { + /// Encrypted stream you can read from let stream: InputStream + /// Content length of encrypted stream let contentLength: Int + /// Metadata (if any) used while encrypting let metadata: Data } +/// Typealias for uniquely identifying applied encryption public typealias CryptorId = [UInt8] +/// Protocol for all types that encapsulate concrete encryption/decryption operations public protocol Cryptor { + /// Unique 4-byte identifier across all `Cryptor` + /// + /// - Important: `[0x41, 0x43, 0x52, 0x48]` and `[0x00, 0x00, 0x00, 0x00]` values are reserved var id: CryptorId { get } + /// Encrypts the given `Data` object + /// + /// - Parameters: + /// - data: Data to encrypt + /// - Returns: A success, storing an ``EncryptedData`` value if operation succeeds. Otherwise, a failure storing `PubNubError` is returned func encrypt(data: Data) -> Result + + /// Decrypts the given `Data` object + /// + /// - Parameters: + /// - data: Data to encrypt + /// - Returns: A success, storing decrypted `Data` if operation succeeds. Otherwise, a failure storing `PubNubError` is returned func decrypt(data: EncryptedData) -> Result + + /// Encrypts the given `InputStream` object + /// + /// - Parameters: + /// - stream: Stream to encrypt + /// - contentLength: Content length of encoded stream + /// - Returns: A success, storing an ``EncryptedStreamData`` value if operation succeeds. Otherwise, a failure storing `PubNubError` is returned func encrypt(stream: InputStream, contentLength: Int) -> Result + + /// Decrypts the given `InputStream` object + /// + /// - Parameters: + /// - data: A value describing encrypted stream + /// - outputPath: URL where the stream should be decrypted to + /// - Returns: A success, storing a decrypted `InputStream` value at the given path if operation succeeds. Otherwise, a failure storing `PubNubError` is returned func decrypt(data: EncryptedStreamData, outputPath: URL) -> Result } diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift index 6a621079..7d23a72a 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift @@ -28,6 +28,10 @@ import Foundation import CommonCrypto +/// Provides backward-compatible way of encryption/decryption that matches +/// deprecated [Crypto](https://github.com/pubnub/swift/blob/master/Sources/PubNub/Helpers/Crypto/Crypto.swift#L32) +/// +/// - Important: Using this `Cryptor` for encoding is strongly discouraged. Use ``AESCBCCryptor`` instead. public struct LegacyCryptor: Cryptor { private let key: Data private let withRandomIV: Bool diff --git a/Tests/PubNubContractTest/Steps/Crypto/PubNubCryptoModuleContractTestSteps.swift b/Tests/PubNubContractTest/Steps/CryptorModule/PubNubCryptoModuleContractTestSteps.swift similarity index 74% rename from Tests/PubNubContractTest/Steps/Crypto/PubNubCryptoModuleContractTestSteps.swift rename to Tests/PubNubContractTest/Steps/CryptorModule/PubNubCryptoModuleContractTestSteps.swift index bf6b8bd7..1994d727 100644 --- a/Tests/PubNubContractTest/Steps/Crypto/PubNubCryptoModuleContractTestSteps.swift +++ b/Tests/PubNubContractTest/Steps/CryptorModule/PubNubCryptoModuleContractTestSteps.swift @@ -37,7 +37,7 @@ public class PubNubCryptoModuleContractTestSteps: PubNubContractTestCase { var cryptorKind: String = "" var cipherKey: String = "" - var withRandomIV: Bool = true + var randomIV: Bool = true var otherCryptors: [String] = [] Given("Crypto module with '(.*)' cryptor") { args, userInfo in @@ -45,7 +45,7 @@ public class PubNubCryptoModuleContractTestSteps: PubNubContractTestCase { } Given("Legacy code with '(.*)' cipher key and '(.*)' vector") { args, userInfo in cipherKey = args?.first as? String ?? "" - withRandomIV = args?.first ?? "" == "random" + randomIV = args?.first ?? "" == "random" } Given("Crypto module with default '(.*)' and additional '(.*)'") { args, userInfo in cryptorKind = args?.first ?? "" @@ -55,7 +55,7 @@ public class PubNubCryptoModuleContractTestSteps: PubNubContractTestCase { cipherKey = args?.first ?? "" } Match(["*"], "with '(.*)' vector") { args, userInfo in - withRandomIV = args?.first ?? "" == "random" + randomIV = args?.first ?? "" == "random" } When("I decrypt '(.*)' file") { args, userInfo in let fileName = args?.first ?? "" @@ -63,7 +63,7 @@ public class PubNubCryptoModuleContractTestSteps: PubNubContractTestCase { let inputStream = InputStream(url: localUrl)! let outputUrl = self.generateTestOutputUrl() - let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: withRandomIV) + let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: randomIV) let encryptedStreamData = EncryptedStreamResult(stream: inputStream, contentLength: localUrl.sizeOf) let decryptingRes = cryptorModule.decrypt(stream: encryptedStreamData, to: outputUrl) @@ -82,57 +82,77 @@ public class PubNubCryptoModuleContractTestSteps: PubNubContractTestCase { } When("I encrypt '(.*)' file as 'binary'") { args, userInfo in let fileName = args?.first ?? "" - let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: withRandomIV) + let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: randomIV) let localFileUrl = self.localUrl(for: fileName) let inputData = try! Data(contentsOf: localFileUrl) - let encryptedData = try! cryptorModule.encrypt(data: inputData).get() + let encryptedDataRes = cryptorModule.encrypt(data: inputData) Then("Successfully decrypt an encrypted file with legacy code") { _, _ in - let decryptedData = try! cryptorModule.decrypt(data: encryptedData).get() + let decryptedData = try! cryptorModule.decrypt(data: try! encryptedDataRes.get()).get() XCTAssertEqual(inputData, decryptedData) } + Then("I receive 'encryption error'") { _, _ in + guard case .failure(let failure) = encryptedDataRes else { + XCTFail("Encryption error is expected"); return; + } + XCTAssertTrue(failure.reason == .encryptionError) + } } When("I encrypt '(.*)' file as 'stream'") { args, userInfo in let fileName = args?.first ?? "" - let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: withRandomIV) + let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: randomIV) let localFileUrl = self.localUrl(for: fileName) let inputStream = InputStream(url: localFileUrl)! - let res = try! cryptorModule.encrypt(stream: inputStream, contentLength: localFileUrl.sizeOf).get() + let encryptRes = cryptorModule.encrypt(stream: inputStream, contentLength: localFileUrl.sizeOf) let outputURL = self.generateTestOutputUrl() Then("Successfully decrypt an encrypted file with legacy code") { _, _ in - cryptorModule.decrypt(stream: res, to: outputURL) + cryptorModule.decrypt(stream: try! encryptRes.get(), to: outputURL) let expectedData = try! Data(contentsOf: localFileUrl) let receivedData = try! Data(contentsOf: outputURL) XCTAssertEqual(expectedData, receivedData) } + + Then("I receive 'encryption error'") { _, _ in + guard case .failure(let failure) = encryptRes else { + XCTFail("Encryption error is expected"); return; + } + XCTAssertTrue(failure.reason == .encryptionError) + } } When("I decrypt '(.*)' file as 'binary'") { args, _ in let fileName = args?.first ?? "" - let cryptorModule = self.createCryptorModule(cryptorKind, others: otherCryptors, key: cipherKey, withRandomIV: withRandomIV) + let cryptorModule = self.createCryptorModule(cryptorKind, additional: otherCryptors, key: cipherKey, withRandomIV: randomIV) let localFileUrl = self.localUrl(for: fileName) let localData = try! Data(contentsOf: localFileUrl) - let result = try! cryptorModule.decrypt(data: localData).get() + let decryptResult = cryptorModule.decrypt(data: localData) Then("Decrypted file content equal to the '(.*)' file content") { thenArgs, _ in let fileNameToCompare = thenArgs?.first ?? "" let fileNameUrlToCompare = self.localUrl(for: fileNameToCompare) let expectedData = try! Data(contentsOf: fileNameUrlToCompare) - let receivedData = result + let receivedData = try! decryptResult.get() XCTAssertEqual(expectedData, receivedData) } + + Then("I receive 'decryption error'") { _, _ in + guard case .failure(let failure) = decryptResult else { + XCTFail("Decryption error is expected"); return; + } + XCTAssertTrue(failure.reason == .decryptionError) + } } When("I decrypt '(.*)' file as 'stream'") { args, _ in let fileName = args?.first ?? "" - let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: withRandomIV) + let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: randomIV) let localFileUrl = self.localUrl(for: fileName) let stream = InputStream(url: localFileUrl)! let outputUrl = self.generateTestOutputUrl() - cryptorModule.decrypt( + let decryptRes = cryptorModule.decrypt( stream: EncryptedStreamResult(stream: stream, contentLength: localFileUrl.sizeOf), to: outputUrl ) @@ -143,6 +163,13 @@ public class PubNubCryptoModuleContractTestSteps: PubNubContractTestCase { let expectedData = try! Data(contentsOf: self.localUrl(for: expectedFileName)) XCTAssertEqual(decodedData, expectedData) } + + Then("I receive 'decryption error'") { _, _ in + guard case .failure(let failure) = decryptRes else { + XCTFail("Decryption error is expected"); return; + } + XCTAssertTrue(failure.reason == .decryptionError) + } } } } @@ -155,12 +182,13 @@ fileprivate extension PubNubCryptoModuleContractTestSteps { return URL(fileURLWithPath: finalPath) } - func createCryptorModule(_ id: String, others: [String] = [], key: String, withRandomIV: Bool) -> CryptorModule { - CryptorModule( + func createCryptorModule(_ id: String, additional cryptors: [String] = [], key: String, withRandomIV: Bool) -> CryptorModule { + let additionalCryptors = cryptors.map { id -> Cryptor in + id == "acrh" ? AESCBCCryptor(key: key) : LegacyCryptor(key: key, withRandomIV: withRandomIV) + } + return CryptorModule( default: id == "acrh" ? AESCBCCryptor(key: key) : LegacyCryptor(key: key, withRandomIV: withRandomIV), - cryptors: others.map { id -> Cryptor in - id == "acrh" ? AESCBCCryptor(key: key) : LegacyCryptor(key: key, withRandomIV: withRandomIV) - } + cryptors: additionalCryptors ) }