diff --git a/.pubnub.yml b/.pubnub.yml index 3d1620b0d..d0158c947 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,10 +1,19 @@ name: java -version: 4.33.2 +version: 4.33.3 schema: 1 scm: github.com/pubnub/java files: - - build/libs/pubnub-gson-4.33.2-all.jar + - build/libs/pubnub-gson-4.33.3-all.jar changelog: + - version: v4.33.3 + date: 2020-10-21 + changes: + - type: bug + text: "Improved handling of random initialization vector for encrypting messages." + - type: bug + text: "Restore Android compatibility for Gradle 3.X by removing Stringjoin()." + - type: bug + text: "Return appropriate error information when payload is too large." - version: v4.33.2 date: 2020-10-08 changes: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e6a663d1..365f3a36a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [v4.33.3](https://github.com/pubnub/java/releases/tag/v4.33.3) +October-21-2020 + +[Full Changelog](https://github.com/pubnub/java/compare/v4.33.2...v4.33.3) + +- 🐛 Improved handling of random initialization vector for encrypting messages. +- 🐛 Restore Android compatibility for Gradle 3.X by removing Stringjoin(). +- 🐛 Return appropriate error information when payload is too large. + ## [v4.33.2](https://github.com/pubnub/java/releases/tag/v4.33.2) October-08-2020 diff --git a/build.gradle b/build.gradle index 4794778bc..2262f2f96 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ plugins { } group = 'com.pubnub' -version = '4.33.2' +version = '4.33.3' description = """""" diff --git a/src/main/java/com/pubnub/api/PubNub.java b/src/main/java/com/pubnub/api/PubNub.java index 931f36fea..3548a6c13 100644 --- a/src/main/java/com/pubnub/api/PubNub.java +++ b/src/main/java/com/pubnub/api/PubNub.java @@ -92,7 +92,7 @@ public class PubNub { private static final int TIMESTAMP_DIVIDER = 1000; private static final int MAX_SEQUENCE = 65535; - private static final String SDK_VERSION = "4.33.2"; + private static final String SDK_VERSION = "4.33.3"; public PubNub(@NotNull PNConfiguration initialConfig) { this.configuration = initialConfig; diff --git a/src/main/java/com/pubnub/api/builder/PubNubErrorBuilder.java b/src/main/java/com/pubnub/api/builder/PubNubErrorBuilder.java index 8b2f9bf4a..c244d1a0c 100644 --- a/src/main/java/com/pubnub/api/builder/PubNubErrorBuilder.java +++ b/src/main/java/com/pubnub/api/builder/PubNubErrorBuilder.java @@ -341,6 +341,11 @@ public final class PubNubErrorBuilder { */ public static final int PNERR_PAGINATION_PREV_OUT_OF_BOUNDS = 166; + /** + * Payload too large + */ + public static final int PNERR_PAYLOAD_TOO_LARGE = 167; + // Error Objects public static final PubNubError PNERROBJ_TIMEOUT = PubNubError.builder() .errorCode(PNERR_TIMEOUT) @@ -680,6 +685,11 @@ public final class PubNubErrorBuilder { .message("No pages to load before first one.") .build(); + public static final PubNubError PNERROBJ_PAYLOAD_TOO_LARGE = PubNubError.builder() + .errorCode(PNERR_PAYLOAD_TOO_LARGE) + .message("Payload too large.") + .build(); + private PubNubErrorBuilder() { } diff --git a/src/main/java/com/pubnub/api/endpoints/Endpoint.java b/src/main/java/com/pubnub/api/endpoints/Endpoint.java index 67bd2c692..c3acf1a75 100644 --- a/src/main/java/com/pubnub/api/endpoints/Endpoint.java +++ b/src/main/java/com/pubnub/api/endpoints/Endpoint.java @@ -109,13 +109,7 @@ public Output sync() throws PubNubException { responseBody = null; } - throw PubNubException.builder() - .pubnubError(PubNubErrorBuilder.PNERROBJ_HTTP_ERROR) - .errormsg(responseBodyText) - .jso(responseBody) - .statusCode(serverResponse.code()) - .affectedCall(call) - .build(); + throw createPubNubException(serverResponse, responseBodyText, responseBody); } storeRequestLatency(serverResponse, getOperationType()); @@ -170,12 +164,7 @@ public void onResponse(Call performedCall, Response response) { } PNStatusCategory pnStatusCategory = PNStatusCategory.PNUnknownCategory; - PubNubException ex = PubNubException.builder() - .pubnubError(PubNubErrorBuilder.PNERROBJ_HTTP_ERROR) - .errormsg(responseBodyText) - .jso(responseBody) - .statusCode(response.code()) - .build(); + final PubNubException ex = createPubNubException(response, responseBodyText, responseBody); if (response.code() == HttpURLConnection.HTTP_FORBIDDEN) { pnStatusCategory = PNStatusCategory.PNAccessDeniedCategory; @@ -266,6 +255,29 @@ public void onFailure(Call performedCall, Throwable throwable) { }); } + private PubNubException createPubNubException(Response response, + String responseBodyText, + JsonElement responseBody) { + if (response.code() == HttpURLConnection.HTTP_ENTITY_TOO_LARGE + || response.code() == HttpURLConnection.HTTP_REQ_TOO_LONG) { + return PubNubException.builder() + .pubnubError(PubNubErrorBuilder.PNERROBJ_PAYLOAD_TOO_LARGE) + .affectedCall(call) + .statusCode(response.code()) + .jso(responseBody) + .errormsg(PubNubErrorBuilder.PNERROBJ_PAYLOAD_TOO_LARGE.getMessage()) + .build(); + } + + return PubNubException.builder() + .pubnubError(PubNubErrorBuilder.PNERROBJ_HTTP_ERROR) + .errormsg(responseBodyText) + .jso(responseBody) + .statusCode(response.code()) + .affectedCall(call) + .build(); + } + @Override public void retry() { silenceFailures = false; diff --git a/src/main/java/com/pubnub/api/endpoints/objects_api/utils/Include.java b/src/main/java/com/pubnub/api/endpoints/objects_api/utils/Include.java index 49eb3b455..2c7104193 100644 --- a/src/main/java/com/pubnub/api/endpoints/objects_api/utils/Include.java +++ b/src/main/java/com/pubnub/api/endpoints/objects_api/utils/Include.java @@ -3,7 +3,9 @@ import com.pubnub.api.endpoints.Endpoint; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -90,8 +92,21 @@ public void addInclusionFlag(final String inclusionFlag) { public Map enrichParameters(Map baseParams) { final Map enrichedMap = new HashMap<>(baseParams); if (!inclusionFlags.isEmpty()) { - enrichedMap.put(INCLUDE_PARAM_NAME, String.join(",", inclusionFlags)); + enrichedMap.put(INCLUDE_PARAM_NAME, join(inclusionFlags)); } return enrichedMap; } + + private String join(Collection values) { + final StringBuilder builder = new StringBuilder(); + Iterator flagsIterator = values.iterator(); + + while (flagsIterator.hasNext()) { + builder.append(flagsIterator.next()); + if (flagsIterator.hasNext()) { + builder.append(","); + } + } + return builder.toString(); + } } diff --git a/src/main/java/com/pubnub/api/vendor/Crypto.java b/src/main/java/com/pubnub/api/vendor/Crypto.java index 14d3f587d..ef73c234a 100644 --- a/src/main/java/com/pubnub/api/vendor/Crypto.java +++ b/src/main/java/com/pubnub/api/vendor/Crypto.java @@ -12,7 +12,6 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.io.UnsupportedEncodingException; -import java.nio.Buffer; import java.nio.charset.Charset; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -97,12 +96,12 @@ public String encrypt(String input) throws PubNubException { Cipher cipher = null; cipher = Cipher.getInstance(CIPHER_TRANSFORMATION); cipher.init(Cipher.ENCRYPT_MODE, newKey, ivSpec); - if (dynamicIV){ - byte[] outputBytes = input.getBytes(ENCODING_UTF_8); - byte[] newOutputBytes = new byte[ivBytes.length+ outputBytes.length]; - System.arraycopy(ivBytes, 0, newOutputBytes, 0, ivBytes.length); - System.arraycopy(outputBytes, 0, newOutputBytes, ivBytes.length, outputBytes.length); - return new String(Base64.encode(cipher.doFinal(newOutputBytes), 0), Charset.forName(ENCODING_UTF_8)); + if (dynamicIV) { + byte[] encrypted = cipher.doFinal(input.getBytes(ENCODING_UTF_8)); + byte[] encryptedWithIV = new byte[ivBytes.length + encrypted.length]; + System.arraycopy(ivBytes, 0, encryptedWithIV, 0, ivBytes.length); + System.arraycopy(encrypted, 0, encryptedWithIV, ivBytes.length, encrypted.length); + return new String(Base64.encode(encryptedWithIV, 0), Charset.forName(ENCODING_UTF_8)); } else { return new String(Base64.encode(cipher.doFinal(input.getBytes(ENCODING_UTF_8)), 0), Charset.forName(ENCODING_UTF_8)); diff --git a/src/test/java/com/pubnub/api/PubNubTest.java b/src/test/java/com/pubnub/api/PubNubTest.java index ba27bcc95..110966284 100644 --- a/src/test/java/com/pubnub/api/PubNubTest.java +++ b/src/test/java/com/pubnub/api/PubNubTest.java @@ -98,7 +98,7 @@ public void getVersionAndTimeStamp() { pubnub = new PubNub(pnConfiguration); String version = pubnub.getVersion(); int timeStamp = pubnub.getTimestamp(); - Assert.assertEquals("4.33.2", version); + Assert.assertEquals("4.33.3", version); Assert.assertTrue(timeStamp > 0); } diff --git a/src/test/java/com/pubnub/api/endpoints/EndpointTest.java b/src/test/java/com/pubnub/api/endpoints/EndpointTest.java index 36643185c..5dc55d1bb 100644 --- a/src/test/java/com/pubnub/api/endpoints/EndpointTest.java +++ b/src/test/java/com/pubnub/api/endpoints/EndpointTest.java @@ -2,8 +2,15 @@ import com.pubnub.api.PubNub; import com.pubnub.api.PubNubException; +import com.pubnub.api.builder.PubNubErrorBuilder; +import com.pubnub.api.callbacks.PNCallback; import com.pubnub.api.enums.PNOperationType; +import com.pubnub.api.models.consumer.PNStatus; +import okhttp3.MediaType; import okhttp3.Request; +import okhttp3.ResponseBody; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -13,8 +20,10 @@ import retrofit2.Response; import java.io.IOException; +import java.net.HttpURLConnection; import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; public class EndpointTest extends TestHarness { @@ -68,53 +77,129 @@ protected boolean isAuthRequired() { @Override protected Call doWork(Map baseParams) throws PubNubException { - Call fakeCall = new Call() { + Call fakeCall = successfulCall(); - @Override - public Response execute() throws IOException { - Response newResponse = Response.success(null); - return newResponse; - } + Assert.assertEquals("myUUID", baseParams.get("uuid")); + Assert.assertEquals("PubNubRequestId", baseParams.get("requestid")); + Assert.assertEquals("PubNubInstanceId", baseParams.get("instanceid")); + return fakeCall; + } + }; - @Override - public void enqueue(Callback callback) { + endpoint.sync(); + } - } + @Test + public void payloadTooLargeTest_Sync() { + Endpoint endpoint = testEndpoint(call(Response.error(HttpURLConnection.HTTP_ENTITY_TOO_LARGE, + ResponseBody.create(MediaType.get("application/json"), "{}")))); + + try { + endpoint.sync(); + Assert.fail("Exception expected"); + } catch (PubNubException e) { + Assert.assertEquals(PubNubErrorBuilder.PNERR_PAYLOAD_TOO_LARGE, e.getPubnubError().getErrorCode()); + } + } - @Override - public boolean isExecuted() { - return false; - } + @Test + public void payloadTooLargeTest_Async() { + Endpoint endpoint = testEndpoint(call(Response.error(HttpURLConnection.HTTP_ENTITY_TOO_LARGE, + ResponseBody.create(MediaType.get("application/json"), "{}")))); + + endpoint.async((result, status) -> { + if (status.isError()) { + Assert.assertEquals(PubNubErrorBuilder.PNERR_PAYLOAD_TOO_LARGE, status.getStatusCode()); + } else { + Assert.fail("Error expected"); + } + }); + } - @Override - public void cancel() { + private Endpoint testEndpoint(Call call) { + return new Endpoint(pubnub, null, null) { - } + @Override + protected List getAffectedChannels() { + return null; + } - @Override - public boolean isCanceled() { - return false; - } + @Override + protected List getAffectedChannelGroups() { + return null; + } - @Override - public Call clone() { - return this; - } + @Override + protected void validateParams() throws PubNubException { + } - @Override - public Request request() { - return new Request.Builder().build(); - } - }; + @Override + protected Object createResponse(Response input) throws PubNubException { + return null; + } - Assert.assertEquals("myUUID", baseParams.get("uuid")); - Assert.assertEquals("PubNubRequestId", baseParams.get("requestid")); - Assert.assertEquals("PubNubInstanceId", baseParams.get("instanceid")); + @Override + protected PNOperationType getOperationType() { + return null; + } + + @Override + protected boolean isAuthRequired() { + return true; + } + + @Override + protected Call doWork(Map baseParams) throws PubNubException { + + Call fakeCall = call; return fakeCall; } }; + } - endpoint.sync(); + private Call call(Response response) { + return new Call() { + + @Override + public Response execute() throws IOException { + return response; + } + + @Override + public void enqueue(Callback callback) { + Call that = this; + Executors.newSingleThreadExecutor().execute( + () -> callback.onResponse(that, response) + ); + } + + @Override + public boolean isExecuted() { + return false; + } + + @Override + public void cancel() { + } + + @Override + public boolean isCanceled() { + return false; + } + + @Override + public Call clone() { + return this; + } + + @Override + public Request request() { + return new Request.Builder().build(); + } + }; } + private Call successfulCall() { + return call(Response.success(null)); + } } diff --git a/src/test/java/com/pubnub/api/vendor/EncryptDecryptTest.java b/src/test/java/com/pubnub/api/vendor/EncryptDecryptTest.java index cf427220a..cab85f579 100644 --- a/src/test/java/com/pubnub/api/vendor/EncryptDecryptTest.java +++ b/src/test/java/com/pubnub/api/vendor/EncryptDecryptTest.java @@ -47,4 +47,36 @@ public void canDecryptTextWhatIsEncryptedWithRandomIV() throws IOException, PubN Assert.assertEquals(msgToEncrypt, decryptedMsg); } + @Test + public void encryptingWithRandomIVTwoTimesTheSameMessageProducesDifferentOutput() throws PubNubException { + //given + final String cipherKey = "enigma"; + final String msgToEncrypt = "Hello world"; + + //when + Crypto crypto = new Crypto(cipherKey, true); + final String encrypted1 = crypto.encrypt(msgToEncrypt); + String encrypted2 = crypto.encrypt(msgToEncrypt); + + //then + Assert.assertNotEquals(encrypted1, encrypted2); + } + + @Test + public void encryptingWithRandomIVTwoTimesDecryptedMsgIsTheSame() throws PubNubException { + //given + final String cipherKey = "enigma"; + final String msgToEncrypt = "Hello world"; + + //when + Crypto crypto = new Crypto(cipherKey, true); + final String encrypted1 = crypto.encrypt(msgToEncrypt); + String encrypted2 = crypto.encrypt(msgToEncrypt); + + //then + Assert.assertEquals(msgToEncrypt, crypto.decrypt(encrypted1)); + Assert.assertEquals(msgToEncrypt, crypto.decrypt(encrypted2)); + } + + }