Skip to content

Commit

Permalink
CryptorModule
Browse files Browse the repository at this point in the history
* Prevent from decoding & encoding empty Data & Files
* Added inline documentation for public items
  • Loading branch information
jguz-pubnub committed Oct 2, 2023
1 parent 83ba71f commit 2cb46f9
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 31 deletions.
6 changes: 3 additions & 3 deletions PubNub.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1909,12 +1909,12 @@
path = EndpointError;
sourceTree = "<group>";
};
3D6265D22ABC8E6900FDD5E6 /* Crypto */ = {
3D6265D22ABC8E6900FDD5E6 /* CryptorModule */ = {
isa = PBXGroup;
children = (
3DBB2C202ABD8053008A100E /* PubNubCryptoModuleContractTestSteps.swift */,
);
path = Crypto;
path = CryptorModule;
sourceTree = "<group>";
};
3D758DC42AB06977005D2B36 /* Miscellaneous */ = {
Expand Down Expand Up @@ -2000,7 +2000,7 @@
79407BC1271D4CFA0032076C /* Steps */ = {
isa = PBXGroup;
children = (
3D6265D22ABC8E6900FDD5E6 /* Crypto */,
3D6265D22ABC8E6900FDD5E6 /* CryptorModule */,
A5F88ECF2906A9DE00F49D5C /* Objects */,
79407BC2271D4CFA0032076C /* Access */,
79407BC4271D4CFA0032076C /* Message Actions */,
Expand Down
105 changes: 102 additions & 3 deletions Sources/PubNub/Helpers/Crypto/CryptorModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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]
Expand All @@ -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<Data, PubNubError> {
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 {
Expand All @@ -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<Data, PubNubError> {
guard !data.isEmpty else {
return .failure(PubNubError(
.decryptionError,
additional: ["Cannot decrypt empty Data"])
)
}
do {
let header = try CryptorHeader.from(data: data)

Expand All @@ -84,7 +122,17 @@ public struct CryptorModule {
metadata: header.metadataIfAny(),
data: data.subdata(in: header.length()..<data.count)
)
).mapError {
)
.flatMap {
if $0.isEmpty {
return .failure(PubNubError(
.decryptionError,
additional: ["Decrypting resulted with empty Data"])
)
}
return .success($0)
}
.mapError {
PubNubError(.decryptionError, underlying: $0)
}
} catch let error as PubNubError {
Expand All @@ -98,7 +146,19 @@ public struct CryptorModule {
}
}

/// Encrypts the given `InputStream` object
///
/// - Parameters:
/// - stream: Stream to encrypt
/// - contentLength: Content length of encoded stream
/// - Returns: A success, storing an ``EncryptedStreamResult`` value if operation succeeds. Otherwise, a failure storing `PubNubError` is returned
public func encrypt(stream: InputStream, contentLength: Int) -> Result<EncryptedStreamResult, PubNubError> {
guard contentLength > 0 else {
return .failure(PubNubError(
.encryptionError,
additional: ["Cannot encrypt empty InputStream"])
)
}
return defaultCryptor.encrypt(
stream: stream,
contentLength: contentLength
Expand All @@ -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<InputStream, PubNubError> {
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()

Expand All @@ -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 {
Expand All @@ -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)])
}
Expand Down
10 changes: 5 additions & 5 deletions Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import Foundation
import CommonCrypto

/// Provides PubNub's **recommended** ``Cryptor`` for encryption/decryption
public struct AESCBCCryptor: Cryptor {
private let key: Data

Expand Down Expand Up @@ -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,
Expand Down
37 changes: 37 additions & 0 deletions Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<EncryptedData, Error>

/// 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<Data, Error>

/// 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<EncryptedStreamData, Error>

/// 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<InputStream, Error>
}
4 changes: 4 additions & 0 deletions Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 2cb46f9

Please sign in to comment.