Skip to content

Commit 56b8f5e

Browse files
authored
Enhance file encryption with stream metadata (#206)
feat(crypto-module): return stream metadata when encrypting a file feat(crypto-module): add a convenience `decryptStream(from: to:)` method in `CryptoModule` for file decryption
1 parent 2f50dfe commit 56b8f5e

File tree

12 files changed

+143
-47
lines changed

12 files changed

+143
-47
lines changed

.pubnub.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
---
22
name: swift
33
scm: github.com/pubnub/swift
4-
version: "9.0.1"
4+
version: "9.1.0"
55
schema: 1
66
changelog:
7+
- date: 2025-05-13
8+
version: 9.1.0
9+
changes:
10+
- type: feature
11+
text: "The `encrypt(stream:contentLength:)` method in `CryptoModule` now returns an `EncryptedStreamResult` containing both the encrypted stream and its total content length."
12+
- type: feature
13+
text: "The new `decryptStream(from:to:)` method in CryptoModule simplifies file decryption by automatically handling low-level details, in contrast to `decrypt(stream:contentLength:to:)`. Instead of manually creating an InputStream and specifying the content length, you now only need to provide the source and destination URLs."
714
- date: 2025-03-20
815
version: 9.0.1
916
changes:
@@ -100,7 +107,7 @@ changelog:
100107
- type: feature
101108
text: "Replace module name with `PubNubSDK` due to compiler error when a public type shares the same name as the module."
102109
- type: feature
103-
text: " add new `subscriptionChanged(channels, groups)` connection status and remove previously deprecated `connecting` and `reconnecting` cases."
110+
text: "Add new `subscriptionChanged(channels, groups)` connection status and remove previously deprecated `connecting` and `reconnecting` cases."
104111
- type: feature
105112
text: "Remove previously deprecated `.legacyExponential(base, scale, maxDelay)` reconnection policy."
106113
- type: feature
@@ -676,7 +683,7 @@ sdks:
676683
- distribution-type: source
677684
distribution-repository: GitHub release
678685
package-name: PubNub
679-
location: https://github.com/pubnub/swift/archive/refs/tags/9.0.1.zip
686+
location: https://github.com/pubnub/swift/archive/refs/tags/9.1.0.zip
680687
supported-platforms:
681688
supported-operating-systems:
682689
macOS:

PubNub.xcodeproj/project.pbxproj

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2238,12 +2238,12 @@
22382238
path = Strategy;
22392239
sourceTree = "<group>";
22402240
};
2241-
3D6265D22ABC8E6900FDD5E6 /* CryptorModule */ = {
2241+
3D6265D22ABC8E6900FDD5E6 /* CryptoModule */ = {
22422242
isa = PBXGroup;
22432243
children = (
22442244
3DBB2C202ABD8053008A100E /* PubNubCryptoModuleContractTestSteps.swift */,
22452245
);
2246-
path = CryptorModule;
2246+
path = CryptoModule;
22472247
sourceTree = "<group>";
22482248
};
22492249
3D7411A12C171F20002267B8 /* Helpers */ = {
@@ -2463,7 +2463,7 @@
24632463
isa = PBXGroup;
24642464
children = (
24652465
3D38A0192B35AFBE006928E7 /* EventEngine */,
2466-
3D6265D22ABC8E6900FDD5E6 /* CryptorModule */,
2466+
3D6265D22ABC8E6900FDD5E6 /* CryptoModule */,
24672467
A5F88ECF2906A9DE00F49D5C /* Objects */,
24682468
79407BC2271D4CFA0032076C /* Access */,
24692469
79407BC4271D4CFA0032076C /* Message Actions */,
@@ -4011,7 +4011,7 @@
40114011
"@loader_path/Frameworks",
40124012
);
40134013
MACOSX_DEPLOYMENT_TARGET = 10.15;
4014-
MARKETING_VERSION = 9.0.1;
4014+
MARKETING_VERSION = 9.1.0;
40154015
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
40164016
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
40174017
MTL_FAST_MATH = YES;
@@ -4062,7 +4062,7 @@
40624062
"@loader_path/Frameworks",
40634063
);
40644064
MACOSX_DEPLOYMENT_TARGET = 10.15;
4065-
MARKETING_VERSION = 9.0.1;
4065+
MARKETING_VERSION = 9.1.0;
40664066
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
40674067
MTL_ENABLE_DEBUG_INFO = NO;
40684068
MTL_FAST_MATH = YES;
@@ -4170,7 +4170,7 @@
41704170
"@loader_path/Frameworks",
41714171
);
41724172
MACOSX_DEPLOYMENT_TARGET = 10.15;
4173-
MARKETING_VERSION = 9.0.1;
4173+
MARKETING_VERSION = 9.1.0;
41744174
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
41754175
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
41764176
MTL_FAST_MATH = YES;
@@ -4223,7 +4223,7 @@
42234223
"@loader_path/Frameworks",
42244224
);
42254225
MACOSX_DEPLOYMENT_TARGET = 10.15;
4226-
MARKETING_VERSION = 9.0.1;
4226+
MARKETING_VERSION = 9.1.0;
42274227
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
42284228
MTL_ENABLE_DEBUG_INFO = NO;
42294229
MTL_FAST_MATH = YES;
@@ -4344,7 +4344,7 @@
43444344
"@loader_path/Frameworks",
43454345
);
43464346
MACOSX_DEPLOYMENT_TARGET = 10.15;
4347-
MARKETING_VERSION = 9.0.1;
4347+
MARKETING_VERSION = 9.1.0;
43484348
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
43494349
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
43504350
MTL_FAST_MATH = YES;
@@ -4396,7 +4396,7 @@
43964396
"@loader_path/Frameworks",
43974397
);
43984398
MACOSX_DEPLOYMENT_TARGET = 10.15;
4399-
MARKETING_VERSION = 9.0.1;
4399+
MARKETING_VERSION = 9.1.0;
44004400
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
44014401
MTL_ENABLE_DEBUG_INFO = NO;
44024402
MTL_FAST_MATH = YES;
@@ -4876,7 +4876,7 @@
48764876
"$(TOOLCHAIN_DIR)/usr/lib/swift/macosx",
48774877
);
48784878
MACOSX_DEPLOYMENT_TARGET = 10.15;
4879-
MARKETING_VERSION = 9.0.1;
4879+
MARKETING_VERSION = 9.1.0;
48804880
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14";
48814881
OTHER_CFLAGS = "$(inherited)";
48824882
OTHER_LDFLAGS = "$(inherited)";
@@ -4919,7 +4919,7 @@
49194919
"$(TOOLCHAIN_DIR)/usr/lib/swift/macosx",
49204920
);
49214921
MACOSX_DEPLOYMENT_TARGET = 10.15;
4922-
MARKETING_VERSION = 9.0.1;
4922+
MARKETING_VERSION = 9.1.0;
49234923
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14";
49244924
OTHER_CFLAGS = "$(inherited)";
49254925
OTHER_LDFLAGS = "$(inherited)";

PubNubSwift.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'PubNubSwift'
3-
s.version = '9.0.1'
3+
s.version = '9.1.0'
44
s.homepage = 'https://github.com/pubnub/swift'
55
s.documentation_url = 'https://www.pubnub.com/docs/swift-native/pubnub-swift-sdk'
66
s.authors = { 'PubNub, Inc.' => '[email protected]' }

Sources/PubNub/Errors/ErrorDescription.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,9 @@ extension PubNubError.Reason: CustomStringConvertible, LocalizedError {
263263
case .outputStreamFailure:
264264
return "An `OutputStream` failed due to the contained underlying error"
265265
case .fileMissingAtPath:
266-
return "A File did not exist at the specified path. Ensure the URL is not nil, and it paths to a file"
266+
return "A File did not exist at the specified path. Ensure the URL is not nil, and it paths to a file"
267+
case .fileExistsAtPath:
268+
return "A File exists at the specified path"
267269
case .backgroundUpdatesDisabled:
268270
return "System canceled the background task because background tasks are disabled."
269271
case .backgroundInsufficientResources:

Sources/PubNub/Errors/PubNubError.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ public struct PubNubError: Error {
149149

150150
// File Management
151151
case fileMissingAtPath
152+
case fileExistsAtPath
152153
case fileTooLarge
153154
case fileAccessDenied
154155
case fileContentLength
@@ -215,7 +216,7 @@ public struct PubNubError: Error {
215216
return .uncategorized
216217
case .streamCouldNotBeInitialized, .inputStreamFailure, .outputStreamFailure:
217218
return .streamFailure
218-
case .fileTooLarge, .fileMissingAtPath, .fileAccessDenied, .fileContentLength:
219+
case .fileTooLarge, .fileMissingAtPath, .fileExistsAtPath, .fileAccessDenied, .fileContentLength:
219220
return .fileManagement
220221
case .encryptionFailure, .decryptionFailure, .unknownCryptorFailure:
221222
return .crypto

Sources/PubNub/Extensions/InputStream+PubNub.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Foundation
1313
extension InputStream {
1414
public func writeEncodedData(to fileURL: URL) throws {
1515
if FileManager.default.fileExists(atPath: fileURL.path) {
16-
throw PubNubError(.fileMissingAtPath, additional: [fileURL.path])
16+
throw PubNubError(.fileExistsAtPath, additional: [fileURL.path])
1717
}
1818

1919
guard let outputStream = OutputStream(url: fileURL, append: false) else {

Sources/PubNub/Extensions/URLRequest+PubNub.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ public extension URLRequest {
5959
if let cryptoModule = cryptoModule {
6060
switch cryptoModule.encrypt(stream: contentStream, contentLength: content.contentLength) {
6161
case .success(let encryptingResult):
62-
finalStream = encryptingResult
63-
contentLength = prefixData.count + ((encryptingResult as? MultipartInputStream)?.length ?? 0) + postfixData.count
62+
finalStream = encryptingResult.stream
63+
contentLength = prefixData.count + encryptingResult.contentLength + postfixData.count
6464
case .failure(let encryptionError):
6565
throw encryptionError
6666
}

Sources/PubNub/Helpers/Constants.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public enum Constant {
5757

5858
static let pubnubSwiftSDKName: String = "PubNubSwift"
5959

60-
static let pubnubSwiftSDKVersion: String = "9.0.1"
60+
static let pubnubSwiftSDKVersion: String = "9.1.0"
6161

6262
static let appBundleId: String = {
6363
if let info = Bundle.main.infoDictionary,

Sources/PubNub/Helpers/Crypto/CryptoModule.swift

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,6 @@
1010

1111
import Foundation
1212

13-
@available(*, unavailable, renamed: "CryptoModule")
14-
public class CryptorModule {
15-
public static func aesCbcCryptoModule(with key: String, withRandomIV: Bool = true) -> CryptoModule {
16-
preconditionFailure("This method is no longer available")
17-
}
18-
19-
public static func legacyCryptoModule(with key: String, withRandomIV: Bool = true) -> CryptoModule {
20-
preconditionFailure("This method is no longer available")
21-
}
22-
}
23-
2413
/// Object capable of encryption/decryption
2514
public struct CryptoModule {
2615
private let defaultCryptor: any Cryptor
@@ -29,6 +18,14 @@ public struct CryptoModule {
2918

3019
typealias Base64EncodedString = String
3120

21+
/// Represents an encrypted stream with its total content length
22+
public struct EncryptedStreamResult {
23+
/// The encrypted input stream
24+
public let stream: InputStream
25+
/// Total length of the encrypted content
26+
public let contentLength: Int
27+
}
28+
3229
/// Initializes `CryptoModule` with custom ``Cryptor`` objects capable of encryption and decryption
3330
///
3431
/// Use this constructor if you would like to provide **custom** objects for decryption and encryption
@@ -176,15 +173,15 @@ public struct CryptoModule {
176173
}
177174
}
178175

179-
/// Encrypts the given `InputStream` object
176+
/// Creates an encrypted stream from the given stream
180177
///
181178
/// - Parameters:
182179
/// - stream: Stream to encrypt
183-
/// - contentLength: Content length of encoded stream
180+
/// - contentLength: Content length of the stream to encrypt
184181
/// - Returns:
185-
/// - **Success**: An `InputStream` value
182+
/// - **Success**: An `EncryptedStreamResult` containing the encrypted input stream and its total content length
186183
/// - **Failure**: `PubNubError` describing the reason of failure
187-
public func encrypt(stream: InputStream, contentLength: Int) -> Result<InputStream, PubNubError> {
184+
public func encrypt(stream: InputStream, contentLength: Int) -> Result<EncryptedStreamResult, PubNubError> {
188185
PubNub.log.debug(
189186
"Encrypting file",
190187
category: .crypto
@@ -202,10 +199,15 @@ public struct CryptoModule {
202199
PubNub.log.debug("Encryption of file failed due to \(error)")
203200
}
204201

205-
return streamEncryptionResult
202+
return streamEncryptionResult.map { multipartStream in
203+
EncryptedStreamResult(
204+
stream: multipartStream,
205+
contentLength: multipartStream.length
206+
)
207+
}
206208
}
207209

208-
private func performStreamEncryption(stream: InputStream, contentLength: Int) -> Result<InputStream, PubNubError> {
210+
private func performStreamEncryption(stream: InputStream, contentLength: Int) -> Result<MultipartInputStream, PubNubError> {
209211
guard contentLength > 0 else {
210212
return .failure(PubNubError(
211213
.encryptionFailure,
@@ -242,7 +244,7 @@ public struct CryptoModule {
242244
///
243245
/// - Parameters:
244246
/// - stream: Stream to decrypt
245-
/// - contentLength: Content length of encrypted stream
247+
/// - contentLength: Content length of the encrypted stream
246248
/// - to: URL where the stream should be decrypted to
247249
/// - Returns:
248250
/// - **Success**: A decrypted `InputStream` object
@@ -273,6 +275,48 @@ public struct CryptoModule {
273275
return streamDecryptionResult
274276
}
275277

278+
/// Decrypts the stream from the given URL and writes it to the output path
279+
///
280+
/// - Parameters:
281+
/// - url: URL of the encrypted stream
282+
/// - outputPath: Path to write the decrypted stream
283+
/// - Returns:
284+
/// - **Success**: A decrypted `InputStream` object
285+
/// - **Failure**: `PubNubError` describing the reason of failure
286+
@discardableResult
287+
public func decryptStream(from url: URL, to outputPath: URL) -> Result<InputStream, PubNubError> {
288+
PubNub.log.debug(
289+
"Decrypting file",
290+
category: .crypto
291+
)
292+
293+
guard let inputStream = InputStream(url: url) else {
294+
PubNub.log.debug(
295+
"Cannot create InputStream from \(url). Ensure that the file exists at the specified path",
296+
category: .crypto
297+
)
298+
return .failure(PubNubError(
299+
.decryptionFailure,
300+
additional: ["File doesn't exist at \(url) path"]
301+
))
302+
}
303+
304+
let streamDecryptionResult = performStreamDecryption(
305+
stream: inputStream,
306+
contentLength: url.sizeOf,
307+
to: outputPath
308+
)
309+
310+
switch streamDecryptionResult {
311+
case .success:
312+
PubNub.log.debug("File decrypted successfully", category: .crypto)
313+
case let .failure(error):
314+
PubNub.log.debug("Decryption of file failed due to \(error)", category: .crypto)
315+
}
316+
317+
return streamDecryptionResult
318+
}
319+
276320
@discardableResult
277321
public func performStreamDecryption(
278322
stream: InputStream,

Sources/PubNub/KMP/Wrappers/KMPFileChangeEvent.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public class KMPFile: NSObject {
8787
}
8888

8989
init(from: PubNubFile, url: URL?) {
90-
self.id = from.channel
90+
self.id = from.fileId
9191
self.name = from.filename
9292
self.size = from.size
9393
self.contentType = from.contentType

Tests/PubNubContractTest/Steps/CryptorModule/PubNubCryptoModuleContractTestSteps.swift renamed to Tests/PubNubContractTest/Steps/CryptoModule/PubNubCryptoModuleContractTestSteps.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class PubNubCryptoModuleContractTestSteps: PubNubContractTestCase {
2424

2525
var encryptDataRes: Result<Data, PubNubError>!
2626
var decryptDataRes: Result<Data, PubNubError>!
27-
var encryptStreamRes: Result<InputStream, PubNubError>!
27+
var encryptStreamRes: Result<CryptoModule.EncryptedStreamResult, PubNubError>!
2828
var decryptStreamRes: Result<InputStream, PubNubError>!
2929

3030
var givenFileUrl: URL!
@@ -150,9 +150,9 @@ public class PubNubCryptoModuleContractTestSteps: PubNubContractTestCase {
150150
let decryptedData = try! self.cryptoModule.decrypt(data: encryptedData).get()
151151
XCTAssertEqual(expectedData, decryptedData)
152152
} else {
153-
let encryptedStream = try! self.encryptStreamRes.get()
154-
let length = (encryptedStream as! MultipartInputStream).length
155-
self.cryptoModule.decrypt(stream: encryptedStream, contentLength: length, to: self.outputPath)
153+
let encryptedStreamMetadata = try! self.encryptStreamRes.get()
154+
let length = encryptedStreamMetadata.contentLength
155+
self.cryptoModule.decrypt(stream: encryptedStreamMetadata.stream, contentLength: length, to: self.outputPath)
156156
let decryptedData = try! Data(contentsOf: self.outputPath)
157157
XCTAssertEqual(expectedData, decryptedData)
158158
}

0 commit comments

Comments
 (0)