Skip to content

Commit

Permalink
Allow reading unencrypted messages from history when crypto is set
Browse files Browse the repository at this point in the history
  • Loading branch information
wkal-pubnub authored Nov 9, 2023
1 parent d88281c commit 0ea6eac
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.pubnub.api.integration;

import com.pubnub.api.PNConfiguration;
import com.pubnub.api.PubNub;
import com.pubnub.api.PubNubException;
import com.pubnub.api.builder.PubNubErrorBuilder;
import com.pubnub.api.crypto.CryptoModule;
import com.pubnub.api.integration.util.BaseIntegrationTest;
import com.pubnub.api.integration.util.RandomGenerator;
Expand Down Expand Up @@ -339,6 +341,65 @@ public void testHistorySingleChannel_IncludeAll_Crypto() throws PubNubException
}
}

@Test
public void testReadUnencryptedMessage_FromHistory_WithCrypto() throws PubNubException {
final String expectedCipherKey = random();

final PNConfiguration config = getBasicPnConfiguration();
config.setCryptoModule(CryptoModule.createLegacyCryptoModule(expectedCipherKey, true));;
final PubNub observer = getPubNub(config);

final String expectedChannelName = random();
final int expectedMessageCount = 10;

assertEquals(expectedMessageCount,
publishMixed(pubNub, expectedMessageCount, expectedChannelName).size());

final PNHistoryResult historyResult = observer.history()
.channel(expectedChannelName)
.includeTimetoken(true)
.includeMeta(true)
.sync();

assert historyResult != null;
for (PNHistoryItemResult message : historyResult.getMessages()) {
assertNotNull(message.getEntry());
assertNotNull(message.getTimetoken());
assertNotNull(message.getMeta());
assertTrue(message.getEntry().toString().contains("_msg"));
assertEquals(message.getError(), PubNubErrorBuilder.PNERROBJ_PNERR_CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED);
}
}

@Test
public void testReadUnencryptedMessage_FetchMessages_WithCrypto() throws PubNubException {
final String expectedCipherKey = random();

final PubNub observer = getPubNub();
observer.getConfiguration().setCryptoModule(CryptoModule.createLegacyCryptoModule(expectedCipherKey, true));

final String expectedChannelName = random();
final int expectedMessageCount = 10;

assertEquals(expectedMessageCount,
publishMixed(pubNub, expectedMessageCount, expectedChannelName).size());

final PNFetchMessagesResult fetchMessagesResult = observer.fetchMessages()
.channels(Collections.singletonList(expectedChannelName))
.maximumPerChannel(25)
.includeMeta(true)
.sync();

assert fetchMessagesResult != null;
for (PNFetchMessageItem messageItem : fetchMessagesResult.getChannels().get(expectedChannelName)) {
assertNotNull(messageItem.getMessage());
assertNotNull(messageItem.getTimetoken());
assertNotNull(messageItem.getMeta());
assertTrue(messageItem.getMessage().toString().contains("_msg"));
assertEquals(messageItem.getError(), PubNubErrorBuilder.PNERROBJ_PNERR_CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED);
}
}

@Test
public void testFetchSingleChannel_IncludeAll_Crypto() throws PubNubException {
final String expectedCipherKey = random();
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/pubnub/api/builder/PubNubErrorBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,11 @@ public final class PubNubErrorBuilder {
*/
public static final int PNERR_USERID_CAN_NOT_BE_DIFFERENT = 173;

/**
* Used when crypto is configured but the message was not encrypted.
*/
public static final int PNERR_CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED = 174;

// Error Objects
public static final PubNubError PNERROBJ_TIMEOUT = PubNubError.builder()
.errorCode(PNERR_TIMEOUT)
Expand Down Expand Up @@ -750,6 +755,11 @@ public final class PubNubErrorBuilder {
.message("UserId can't be different from UserId in configuration when flag withHeartbeat is set to true.")
.build();

public static final PubNubError PNERROBJ_PNERR_CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED = PubNubError.builder()
.errorCode(PNERR_CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED)
.message("Crypto is configured but message is not encrypted.")
.build();

private PubNubErrorBuilder() {

}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/pubnub/api/endpoints/Endpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,10 @@ public void retry() {
*/
@Override
public void silentCancel() {
if (call == null) {
System.out.println("CALL IS NULL!");
System.exit(-1);
}
if (call != null && !call.isCanceled()) {
this.silenceFailures = true;
call.cancel();
Expand Down
43 changes: 36 additions & 7 deletions src/main/java/com/pubnub/api/endpoints/FetchMessages.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.pubnub.api.PubNub;
import com.pubnub.api.PubNubError;
import com.pubnub.api.PubNubException;
import com.pubnub.api.PubNubUtil;
import com.pubnub.api.builder.PubNubErrorBuilder;
Expand All @@ -20,6 +21,7 @@
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.VisibleForTesting;
import retrofit2.Call;
import retrofit2.Response;

Expand All @@ -39,6 +41,7 @@ public class FetchMessages extends Endpoint<FetchMessagesEnvelope, PNFetchMessag
private static final int MULTIPLE_CHANNEL_MAX_MESSAGES = 25;
private static final int DEFAULT_MESSAGES_WITH_ACTIONS = 25;
private static final int MAX_MESSAGES_WITH_ACTIONS = 25;
static final String PN_OTHER = "pn_other";

@Setter
private List<String> channels;
Expand Down Expand Up @@ -166,7 +169,16 @@ protected PNFetchMessagesResult createResponse(Response<FetchMessagesEnvelope> i
for (PNFetchMessageItem item : entry.getValue()) {
PNFetchMessageItem.PNFetchMessageItemBuilder messageItemBuilder = item.toBuilder();

messageItemBuilder.message(processMessage(item.getMessage()));
try {
messageItemBuilder.message(processMessage(item.getMessage()));
} catch (PubNubException e) {
if (e.getPubnubError() == PubNubErrorBuilder.PNERROBJ_PNERR_CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED) {
messageItemBuilder.message(item.getMessage());
messageItemBuilder.error(e.getPubnubError());
} else {
throw e;
}
}
if (includeMessageActions) {
if (item.getActions() != null) {
messageItemBuilder.actions(item.getActions());
Expand Down Expand Up @@ -204,7 +216,8 @@ protected boolean isAuthRequired() {
return true;
}

private JsonElement processMessage(JsonElement message) throws PubNubException {
@VisibleForTesting
JsonElement processMessage(JsonElement message) throws PubNubException {
// if we do not have a crypto module, there is no way to process the node; let's return.
CryptoModule cryptoModule = this.getPubnub().getCryptoModule();
if (cryptoModule == null) {
Expand All @@ -216,22 +229,38 @@ private JsonElement processMessage(JsonElement message) throws PubNubException {
String outputText;
JsonElement outputObject;

if (mapper.isJsonObject(message) && mapper.hasField(message, "pn_other")) {
inputText = mapper.elementToString(message, "pn_other");
if (mapper.isJsonObject(message)) {
if (mapper.hasField(message, PN_OTHER)) {
inputText = mapper.elementToString(message, PN_OTHER);
} else {
PubNubError error = logAndReturnDecryptionError();
throw new PubNubException(error.getMessage(), error, null, null, 0, null, null);
}
} else {
inputText = mapper.elementToString(message);
}

outputText = CryptoModuleKt.decryptString(cryptoModule, inputText);
try {
outputText = CryptoModuleKt.decryptString(cryptoModule, inputText);
} catch (Exception e) {
PubNubError error = logAndReturnDecryptionError();
throw new PubNubException(error.getMessage(), error, null, null, 0, null, null);
}
outputObject = mapper.fromJson(outputText, JsonElement.class);

// inject the decoded response into the payload
if (mapper.isJsonObject(message) && mapper.hasField(message, "pn_other")) {
if (mapper.isJsonObject(message) && mapper.hasField(message, PN_OTHER)) {
JsonObject objectNode = mapper.getAsObject(message);
mapper.putOnObject(objectNode, "pn_other", outputObject);
mapper.putOnObject(objectNode, PN_OTHER, outputObject);
outputObject = objectNode;
}

return outputObject;
}

private PubNubError logAndReturnDecryptionError() {
PubNubError error = PubNubErrorBuilder.PNERROBJ_PNERR_CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED;
log.warn(error.getMessage());
return error;
}
}
59 changes: 49 additions & 10 deletions src/main/java/com/pubnub/api/endpoints/History.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.pubnub.api.PubNub;
import com.pubnub.api.PubNubError;
import com.pubnub.api.PubNubException;
import com.pubnub.api.builder.PubNubErrorBuilder;
import com.pubnub.api.crypto.CryptoModule;
Expand All @@ -16,6 +17,8 @@
import com.pubnub.api.models.consumer.history.PNHistoryResult;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.VisibleForTesting;
import retrofit2.Call;
import retrofit2.Response;

Expand All @@ -25,6 +28,9 @@
import java.util.List;
import java.util.Map;

import static com.pubnub.api.endpoints.FetchMessages.PN_OTHER;

@Slf4j
@Accessors(chain = true, fluent = true)
public class History extends Endpoint<JsonElement, PNHistoryResult> {
private static final int MAX_COUNT = 100;
Expand Down Expand Up @@ -131,15 +137,34 @@ protected PNHistoryResult createResponse(Response<JsonElement> input) throws Pub
JsonElement message;

if (includeTimetoken || includeMeta) {
message = processMessage(mapper.getField(historyEntry, "message"));
JsonElement messageElement = mapper.getField(historyEntry, "message");
try {
message = processMessage(messageElement);
} catch (PubNubException e) {
if (e.getPubnubError() == PubNubErrorBuilder.PNERROBJ_PNERR_CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED) {
message = messageElement;
historyItem.error(e.getPubnubError());
} else {
throw e;
}
}
if (includeTimetoken) {
historyItem.timetoken(mapper.elementToLong(historyEntry, "timetoken"));
}
if (includeMeta) {
historyItem.meta(mapper.getField(historyEntry, "meta"));
}
} else {
message = processMessage(historyEntry);
try {
message = processMessage(historyEntry);
} catch (PubNubException e) {
if (e.getPubnubError() == PubNubErrorBuilder.PNERROBJ_PNERR_CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED) {
message = historyEntry;
historyItem.error(e.getPubnubError());
} else {
throw e;
}
}
}

historyItem.entry(message);
Expand All @@ -153,7 +178,6 @@ protected PNHistoryResult createResponse(Response<JsonElement> input) throws Pub
.build();
}


historyData.messages(messages);
}

Expand All @@ -170,7 +194,8 @@ protected boolean isAuthRequired() {
return true;
}

private JsonElement processMessage(JsonElement message) throws PubNubException {
@VisibleForTesting
JsonElement processMessage(JsonElement message) throws PubNubException {
// if we do not have a crypto module, there is no way to process the node; let's return.
CryptoModule cryptoModule = this.getPubnub().getCryptoModule();
if (cryptoModule == null) {
Expand All @@ -182,23 +207,37 @@ private JsonElement processMessage(JsonElement message) throws PubNubException {
String outputText;
JsonElement outputObject;

if (mapper.isJsonObject(message) && mapper.hasField(message, "pn_other")) {
inputText = mapper.elementToString(message, "pn_other");
if (mapper.isJsonObject(message)) {
if (mapper.hasField(message, PN_OTHER)) {
inputText = mapper.elementToString(message, PN_OTHER);
} else {
PubNubError error = logAndReturnDecryptionError();
throw new PubNubException(error.getMessage(), error, null, null, 0, null, null);
}
} else {
inputText = mapper.elementToString(message);
}

outputText = CryptoModuleKt.decryptString(cryptoModule, inputText);
try {
outputText = CryptoModuleKt.decryptString(cryptoModule, inputText);
} catch (Exception e) {
PubNubError error = logAndReturnDecryptionError();
throw new PubNubException(error.getMessage(), error, null, null, 0, null, null);
}
outputObject = this.getPubnub().getMapper().fromJson(outputText, JsonElement.class);

// inject the decoded response into the payload
if (mapper.isJsonObject(message) && mapper.hasField(message, "pn_other")) {
if (mapper.isJsonObject(message) && mapper.hasField(message, PN_OTHER)) {
JsonObject objectNode = mapper.getAsObject(message);
mapper.putOnObject(objectNode, "pn_other", outputObject);
mapper.putOnObject(objectNode, PN_OTHER, outputObject);
outputObject = objectNode;
}

return outputObject;
}

private PubNubError logAndReturnDecryptionError() {
PubNubError error = PubNubErrorBuilder.PNERROBJ_PNERR_CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED;
log.warn(error.getMessage());
return error;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.gson.JsonElement;
import com.google.gson.annotations.SerializedName;
import com.pubnub.api.PubNubError;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
Expand All @@ -19,6 +20,14 @@ public class PNFetchMessageItem {
private final Long timetoken;
private final HashMap<String, HashMap<String, List<Action>>> actions;
private final String uuid;
/**
* The error associated with message retrieval, if any. Can be null.
* Currently, the only possible error is {@link com.pubnub.api.builder.PubNubErrorBuilder#PNERROBJ_PNERR_CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED}
* when the message was unencrypted, but PubNub instance is configured with a crypto module. In that case,
* the unencrypted message content will still be available in {@code message}.
*/
private final PubNubError error;

@SerializedName("message_type")
@Getter(AccessLevel.NONE)
private final String messageType;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.pubnub.api.models.consumer.history;

import com.google.gson.JsonElement;
import com.pubnub.api.PubNubError;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
Expand All @@ -13,4 +14,11 @@ public class PNHistoryItemResult {
private Long timetoken;
private JsonElement entry;
private JsonElement meta;
/**
* The error associated with message retrieval, if any. Can be null.
* Currently, the only possible error is {@link com.pubnub.api.builder.PubNubErrorBuilder#PNERROBJ_PNERR_CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED}
* when the message was unencrypted, but PubNub instance is configured with a crypto module. In that case,
* the unencrypted message content will still be available in {@code entry}.
*/
private PubNubError error;
}
Loading

0 comments on commit 0ea6eac

Please sign in to comment.