Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Handle unencrypted message while getting messages with crypto #120

Merged
merged 8 commits into from
Nov 27, 2023
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, what's the logic behind it?

Copy link

@jguz-pubnub jguz-pubnub Nov 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, now I see that you could pass decrypt(...) or decryptWithKey(...) function from ICryptoModule. Perhaps there's no way to split deserialization and decryption into two separate concerns, this would require significant changes. What I mean by this is parsing the objects as they came without introducing decryption there. This could be a separate layer that takes BatchHistoryResultEntry or whatever lightweight DAO object created in the previous parsing step, decrypts its payload, and returns the decrypted result back to the caller. It's not possible to do it right now so let's leave it as it is, but anyway I wanted to mention about other possibilities :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed!
Need to deal with this till the time we completely stop supporting explicit cipherKey passing mechanism.

? 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
Loading