diff --git a/.pubnub.yml b/.pubnub.yml index e5ed3bb3..5a898846 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,5 +1,10 @@ --- changelog: + - date: 2023-11-27 + version: v4.3.1 + changes: + - type: bug + text: "Handle unencrypted message while getting messages with cryptoModule configured." - date: 2023-10-16 version: v4.3.0 changes: @@ -432,7 +437,7 @@ supported-platforms: platforms: - "Dart SDK >=2.6.0 <3.0.0" version: "PubNub Dart SDK" -version: "4.3.0" +version: "4.3.1" sdks: - full-name: PubNub Dart SDK diff --git a/pubnub/CHANGELOG.md b/pubnub/CHANGELOG.md index f297c4e0..f16f60e4 100644 --- a/pubnub/CHANGELOG.md +++ b/pubnub/CHANGELOG.md @@ -1,3 +1,9 @@ +## v4.3.1 +November 27 2023 + +#### Fixed +- Handle unencrypted message while getting messages with cryptoModule configured. + ## v4.3.0 October 16 2023 diff --git a/pubnub/README.md b/pubnub/README.md index 5034dd8c..3f108c7a 100644 --- a/pubnub/README.md +++ b/pubnub/README.md @@ -14,7 +14,7 @@ To add the package to your Dart or Flutter project, add `pubnub` as a dependency ```yaml dependencies: - pubnub: ^4.3.0 + pubnub: ^4.3.1 ``` After adding the dependency to `pubspec.yaml`, run the `dart pub get` command in the root directory of your project (the same that the `pubspec.yaml` is in). diff --git a/pubnub/lib/src/core/core.dart b/pubnub/lib/src/core/core.dart index ef58886b..e142b422 100644 --- a/pubnub/lib/src/core/core.dart +++ b/pubnub/lib/src/core/core.dart @@ -21,7 +21,7 @@ class Core { /// Internal module responsible for supervising. SupervisorModule supervisor = SupervisorModule(); - static String version = '4.3.0'; + static String version = '4.3.1'; Core( {Keyset? defaultKeyset, diff --git a/pubnub/lib/src/core/message/base_message.dart b/pubnub/lib/src/core/message/base_message.dart index d392b1b1..362b566a 100644 --- a/pubnub/lib/src/core/message/base_message.dart +++ b/pubnub/lib/src/core/message/base_message.dart @@ -14,6 +14,10 @@ class BaseMessage { /// Original JSON message received from the server. final dynamic originalMessage; + /// If message decryption failed then [error] + /// field contains PubNubExcpeption + final PubNubException? error; + /// Alias for `publishedAt`. @deprecated Timetoken get timetoken => publishedAt; @@ -26,5 +30,6 @@ class BaseMessage { required this.publishedAt, required this.content, required this.originalMessage, + this.error, }); } diff --git a/pubnub/lib/src/dx/_endpoints/history.dart b/pubnub/lib/src/dx/_endpoints/history.dart index e0589e6f..6cfb2299 100644 --- a/pubnub/lib/src/dx/_endpoints/history.dart +++ b/pubnub/lib/src/dx/_endpoints/history.dart @@ -154,25 +154,48 @@ class BatchHistoryResultEntry { /// Otherwise, it will be `null`. Map? meta; + /// This field will contain PubNubException if message decryption is failed + /// for given `message`. + PubNubException? error; + BatchHistoryResultEntry._(this.message, this.timetoken, this.uuid, - this.messageType, this.actions, this.meta); + this.messageType, this.actions, this.meta, this.error); /// @nodoc factory BatchHistoryResultEntry.fromJson(Map object, {CipherKey? cipherKey, Function? decryptFunction}) { + var message; + PubNubException? error; + if (cipherKey == null && decryptFunction is decryptWithKey) { + message = object['message']; + } else { + try { + if (!(object['message'] is String)) { + throw FormatException('not a base64 string.'); + } + message = decryptFunction is decryptWithKey + ? json.decode(utf8.decode(decryptFunction(cipherKey!, + base64.decode(object['message'] as String).toList()))) + : json.decode(utf8.decode(decryptFunction!( + base64.decode(object['message'] as String).toList()))); + } on CryptoException catch (e) { + message = object['message']; + error = e; + } on FormatException catch (e) { + message = object['message']; + error = PubNubException( + 'Can not decrypt the message payload. Please check keyset or crypto configuration. ${e.message}'); + } + } + return BatchHistoryResultEntry._( - (cipherKey == null && decryptFunction is decryptWithKey) - ? object['message'] - : (decryptFunction is decryptWithKey - ? json.decode(utf8.decode(decryptFunction(cipherKey!, - base64.decode(object['message'] as String).toList()))) - : json.decode(utf8.decode(decryptFunction!( - base64.decode(object['message'] as String).toList())))), + message, Timetoken(BigInt.parse('${object['timetoken']}')), object['uuid'], MessageTypeExtension.fromInt(object['message_type']), object['actions'], - object['meta'] == '' ? null : object['meta']); + object['meta'] == '' ? null : object['meta'], + error); } } diff --git a/pubnub/lib/src/dx/channel/channel_history.dart b/pubnub/lib/src/dx/channel/channel_history.dart index a9d260c5..5314c397 100644 --- a/pubnub/lib/src/dx/channel/channel_history.dart +++ b/pubnub/lib/src/dx/channel/channel_history.dart @@ -106,20 +106,31 @@ class ChannelHistory { _cursor = result.endTimetoken; _messages.addAll(await Future.wait(result.messages.map((message) async { + PubNubException? error; if (_keyset.cipherKey != null || _core.crypto is CryptoModule) { - message['message'] = _keyset.cipherKey == - _core.keysets.defaultKeyset.cipherKey - ? await _core.parser.decode(utf8.decode(_core.crypto.decrypt( - base64.decode(message['message'] as String).toList()))) - : await _core.parser.decode(utf8.decode(_core.crypto - .decryptWithKey(_keyset.cipherKey!, - base64.decode(message['message'] as String).toList()))); + try { + if (!(message['message'] is String)) { + throw FormatException('not a base64 string.'); + } + message['message'] = _keyset.cipherKey == + _core.keysets.defaultKeyset.cipherKey + ? await _core.parser.decode(utf8.decode(_core.crypto.decrypt( + base64.decode(message['message'] as String).toList()))) + : await _core.parser.decode(utf8.decode(_core.crypto + .decryptWithKey(_keyset.cipherKey!, + base64.decode(message['message'] as String).toList()))); + } on CryptoException catch (e) { + error = e; + } on FormatException catch (e) { + error = PubNubException( + 'Can not decrypt the message payload. Please check keyset or crypto configuration. ${e.message}'); + } } return BaseMessage( - publishedAt: Timetoken(BigInt.from(message['timetoken'])), - content: message['message'], - originalMessage: message, - ); + publishedAt: Timetoken(BigInt.from(message['timetoken'])), + content: message['message'], + originalMessage: message, + error: error); }))); } while (_cursor.value != BigInt.from(0)); } @@ -209,20 +220,31 @@ class PaginatedChannelHistory { } _messages.addAll(await Future.wait(result.messages.map((message) async { + PubNubException? error; if (_keyset.cipherKey != null || _core.crypto is CryptoModule) { - message['message'] = _keyset.cipherKey == - _core.keysets.defaultKeyset.cipherKey - ? await _core.parser.decode(utf8.decode(_core.crypto - .decrypt(base64.decode(message['message'] as String)))) - : await _core.parser.decode(utf8.decode(_core.crypto.encryptWithKey( - _keyset.cipherKey!, - base64.decode(message['message'] as String).toList()))); + try { + if (!(message['message'] is String)) { + throw FormatException('not a base64 string.'); + } + message['message'] = _keyset.cipherKey == + _core.keysets.defaultKeyset.cipherKey + ? await _core.parser.decode(utf8.decode(_core.crypto + .decrypt(base64.decode(message['message'] as String)))) + : await _core.parser.decode(utf8.decode(_core.crypto + .encryptWithKey(_keyset.cipherKey!, + base64.decode(message['message'] as String).toList()))); + } on CryptoException catch (e) { + error = e; + } on FormatException catch (e) { + error = PubNubException( + 'Can not decrypt the message payload. Please check keyset or crypto configuration. ${e.message}'); + } } return BaseMessage( - originalMessage: message, - publishedAt: Timetoken(BigInt.from(message['timetoken'])), - content: message['message'], - ); + originalMessage: message, + publishedAt: Timetoken(BigInt.from(message['timetoken'])), + content: message['message'], + error: error); }))); return result; diff --git a/pubnub/lib/src/subscribe/envelope.dart b/pubnub/lib/src/subscribe/envelope.dart index 738d7ed3..a21f1523 100644 --- a/pubnub/lib/src/subscribe/envelope.dart +++ b/pubnub/lib/src/subscribe/envelope.dart @@ -18,6 +18,9 @@ class Envelope extends BaseMessage { final dynamic userMeta; + @override + PubNubException? error; + dynamic get payload => content; Envelope._( @@ -33,12 +36,13 @@ class Envelope extends BaseMessage { required this.originalTimetoken, required this.originalRegion, required this.region, - required this.userMeta}) + required this.userMeta, + this.error}) : super( - content: content, - originalMessage: originalMessage, - publishedAt: publishedAt, - ); + content: content, + originalMessage: originalMessage, + publishedAt: publishedAt, + error: error); /// @nodoc factory Envelope.fromJson(dynamic object) { @@ -58,6 +62,7 @@ class Envelope extends BaseMessage { publishedAt: Timetoken(BigInt.parse('${object['p']['t']}')), region: object['p']['r'], userMeta: object['u'], + error: object['error'], ); } } diff --git a/pubnub/lib/src/subscribe/subscribe_loop/subscribe_loop.dart b/pubnub/lib/src/subscribe/subscribe_loop/subscribe_loop.dart index 68a6214d..ffde0e7a 100644 --- a/pubnub/lib/src/subscribe/subscribe_loop/subscribe_loop.dart +++ b/pubnub/lib/src/subscribe/subscribe_loop/subscribe_loop.dart @@ -133,6 +133,9 @@ class SubscribeLoop { !object['c'].endsWith('-pnpres')) { try { _logger.info('Decrypting message...'); + if (!(object['d'] is String)) { + throw FormatException('not a base64 String'); + } object['d'] = state.keyset.cipherKey == core.keysets.defaultKeyset.cipherKey ? await core.parser.decode(utf8.decode(core.crypto @@ -140,9 +143,11 @@ class SubscribeLoop { : await core.parser.decode(utf8.decode(core.crypto .decryptWithKey(state.keyset.cipherKey!, base64.decode(object['d'] as String).toList()))); - } catch (e) { - throw PubNubException( - 'Can not decrypt the message payload. Please check keyset or crypto configuration'); + } on PubNubException catch (e) { + object['error'] = e; + } on FormatException catch (e) { + object['error'] = PubNubException( + 'Can not decrypt the message payload. Please check keyset or crypto configuration. ${e.message}'); } } return Envelope.fromJson(object); diff --git a/pubnub/pubspec.yaml b/pubnub/pubspec.yaml index 11e219ec..8c730b00 100644 --- a/pubnub/pubspec.yaml +++ b/pubnub/pubspec.yaml @@ -1,6 +1,6 @@ name: pubnub description: PubNub SDK v5 for Dart lang (with Flutter support) that allows you to create real-time applications -version: 4.3.0 +version: 4.3.1 homepage: https://www.pubnub.com/docs/sdks/dart environment: diff --git a/pubnub/test/integration/subscribe/_utils.dart b/pubnub/test/integration/subscribe/_utils.dart index 874c1582..fae8525a 100644 --- a/pubnub/test/integration/subscribe/_utils.dart +++ b/pubnub/test/integration/subscribe/_utils.dart @@ -36,19 +36,21 @@ class Subscriber { return subscription?.cancel(); } - Future expectMessage(String channel, String message) { + Future expectMessage(String channel, String message, + [PubNubException? error]) { var actual = queue?.next; - return expectLater( - actual, completion(SubscriptionMessageMatcher(channel, message))); + return expectLater(actual, + completion(SubscriptionMessageMatcher(channel, message, error))); } } class SubscriptionMessageMatcher extends Matcher { final String expectedMessage; final String channel; + PubNubException? error; - SubscriptionMessageMatcher(this.channel, this.expectedMessage); + SubscriptionMessageMatcher(this.channel, this.expectedMessage, this.error); @override Description describe(Description description) => @@ -64,5 +66,14 @@ class SubscriptionMessageMatcher extends Matcher { @override bool matches(item, Map matchState) => - item.channel == channel && item.payload == expectedMessage; + item.channel == channel && + item.payload == expectedMessage && + errorMatch(item); + + bool errorMatch(envelope) { + if (error != null) { + return error is PubNubException; + } + return true; + } } diff --git a/pubnub/test/integration/subscribe/subscribe_test.dart b/pubnub/test/integration/subscribe/subscribe_test.dart index fe22283b..3ef53d93 100644 --- a/pubnub/test/integration/subscribe/subscribe_test.dart +++ b/pubnub/test/integration/subscribe/subscribe_test.dart @@ -90,6 +90,36 @@ void main() { await subscriber.expectMessage(channel, message); }); + test('with crypto configuration and plain message', () async { + var channel = 'test-${DateTime.now().millisecondsSinceEpoch}'; + var message = 'hello pubnub!'; + pubnub = PubNub( + defaultKeyset: Keyset( + subscribeKey: SUBSCRIBE_KEY, + publishKey: PUBLISH_KEY, + userId: UserId('dart-test')), + ); + var pubnubWithCrypto = PubNub( + crypto: + CryptoModule.aesCbcCryptoModule(CipherKey.fromUtf8('cipherKey')), + defaultKeyset: Keyset( + subscribeKey: SUBSCRIBE_KEY, + publishKey: PUBLISH_KEY, + userId: UserId('dart-test'), + ), + ); + subscriber = Subscriber.init(pubnubWithCrypto, SUBSCRIBE_KEY); + subscriber.subscribe(channel); + await Future.delayed(Duration(seconds: 2)); + await pubnub.publish(channel, message); + + await subscriber.expectMessage( + channel, + message, + PubNubException( + 'Can not decrypt the message payload. Please check keyset or crypto configuration.')); + }); + tearDown(() async { await subscriber.cleanup(); await pubnub.unsubscribeAll(); diff --git a/pubnub/test/unit/dx/channel_test.dart b/pubnub/test/unit/dx/channel_test.dart index 845c6343..dd607a70 100644 --- a/pubnub/test/unit/dx/channel_test.dart +++ b/pubnub/test/unit/dx/channel_test.dart @@ -7,12 +7,18 @@ part './fixtures/channel.dart'; void main() { PubNub? pubnub; + PubNub? pubnubWithCrypto; group('DX [channel]', () { setUp(() { pubnub = PubNub( defaultKeyset: Keyset( subscribeKey: 'test', publishKey: 'test', uuid: UUID('test')), networking: FakeNetworkingModule()); + pubnubWithCrypto = PubNub( + crypto: CryptoModule.aesCbcCryptoModule(CipherKey.fromUtf8('enigma')), + defaultKeyset: Keyset( + subscribeKey: 'test', publishKey: 'test', uuid: UUID('test')), + networking: FakeNetworkingModule()); }); test('#channel should return an instance of Channel', () { @@ -108,6 +114,21 @@ void main() { expect(history.messages.length, equals(1)); }); + + test('#fetch with crypto configured', () async { + channel = pubnubWithCrypto!.channel('test'); + var history = channel.messages(); + when( + method: 'GET', + path: + 'v2/history/sub-key/test/channel/test?count=100&reverse=true&include_token=true&uuid=test&pnsdk=PubNub-Dart%2F${PubNub.version}', + ).then(status: 200, body: _historyMessagesFetchResponse); + + await history.fetch(); + + expect(history.messages.length, equals(1)); + expect(history.messages[0].error, isException); + }); }); test('#history should return an instance of PaginatedChannelHistory', () {