Skip to content

Commit

Permalink
fix: Handle unencrypted message while getting messages with crypto (#120
Browse files Browse the repository at this point in the history
)

* subscription: handle unencrypted message when crypto is configured

* history: handle unencrypted message when crypto is configured

* test: handle unencrypted message when crypto is configured

* fix: report PubNubException when message content is not valid for subscribe and histroy api with crypto configuration.

* fix: comment description

* fix: error type for message decryption failure

* fix: catch crypto specific exception

* PubNub SDK v4.3.1 release.

---------

Co-authored-by: PubNub Release Bot <[email protected]>
  • Loading branch information
mohitpubnub and pubnub-release-bot authored Nov 27, 2023
1 parent c2670c9 commit 4287eaf
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 48 deletions.
7 changes: 6 additions & 1 deletion .pubnub.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions pubnub/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion pubnub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion pubnub/lib/src/core/core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions pubnub/lib/src/core/message/base_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,5 +30,6 @@ class BaseMessage {
required this.publishedAt,
required this.content,
required this.originalMessage,
this.error,
});
}
41 changes: 32 additions & 9 deletions pubnub/lib/src/dx/_endpoints/history.dart
Original file line number Diff line number Diff line change
Expand Up @@ -154,25 +154,48 @@ class BatchHistoryResultEntry {
/// Otherwise, it will be `null`.
Map<String, dynamic>? 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<String, dynamic> 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);
}
}

Expand Down
66 changes: 44 additions & 22 deletions pubnub/lib/src/dx/channel/channel_history.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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;
Expand Down
15 changes: 10 additions & 5 deletions pubnub/lib/src/subscribe/envelope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class Envelope extends BaseMessage {

final dynamic userMeta;

@override
PubNubException? error;

dynamic get payload => content;

Envelope._(
Expand All @@ -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) {
Expand All @@ -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'],
);
}
}
Expand Down
11 changes: 8 additions & 3 deletions pubnub/lib/src/subscribe/subscribe_loop/subscribe_loop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,21 @@ 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
.decrypt(base64.decode(object['d'] as String).toList())))
: 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);
Expand Down
2 changes: 1 addition & 1 deletion pubnub/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
21 changes: 16 additions & 5 deletions pubnub/test/integration/subscribe/_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,21 @@ class Subscriber {
return subscription?.cancel();
}

Future<void> expectMessage(String channel, String message) {
Future<void> 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) =>
Expand All @@ -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;
}
}
30 changes: 30 additions & 0 deletions pubnub/test/integration/subscribe/subscribe_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
21 changes: 21 additions & 0 deletions pubnub/test/unit/dx/channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand Down Expand Up @@ -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', () {
Expand Down

0 comments on commit 4287eaf

Please sign in to comment.