diff --git a/.pubnub.yml b/.pubnub.yml
index 158b04836..c484d55fe 100644
--- a/.pubnub.yml
+++ b/.pubnub.yml
@@ -1,9 +1,9 @@
name: java
-version: 6.3.6
+version: 6.4.0
schema: 1
scm: github.com/pubnub/java
files:
- - build/libs/pubnub-gson-6.3.6-all.jar
+ - build/libs/pubnub-gson-6.4.0-all.jar
sdks:
-
type: library
@@ -23,8 +23,8 @@ sdks:
-
distribution-type: library
distribution-repository: GitHub
- package-name: pubnub-gson-6.3.6
- location: https://github.com/pubnub/java/releases/download/v6.3.6/pubnub-gson-6.3.6-all.jar
+ package-name: pubnub-gson-6.4.0
+ location: https://github.com/pubnub/java/releases/download/v6.4.0/pubnub-gson-6.4.0-all.jar
supported-platforms:
supported-operating-systems:
Android:
@@ -135,8 +135,8 @@ sdks:
-
distribution-type: library
distribution-repository: maven
- package-name: pubnub-gson-6.3.6
- location: https://repo.maven.apache.org/maven2/com/pubnub/pubnub-gson/6.3.6/pubnub-gson-6.3.6.jar
+ package-name: pubnub-gson-6.4.0
+ location: https://repo.maven.apache.org/maven2/com/pubnub/pubnub-gson/6.4.0/pubnub-gson-6.4.0.jar
supported-platforms:
supported-operating-systems:
Android:
@@ -234,6 +234,13 @@ sdks:
is-required: Required
changelog:
+ - date: 2023-10-16
+ version: v6.4.0
+ changes:
+ - type: feature
+ text: "Add crypto module that allows configure SDK to encrypt and decrypt messages."
+ - type: bug
+ text: "Improved security of crypto implementation by adding enhanced AES-CBC cryptor."
- date: 2023-06-19
version: v6.3.6
changes:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d50ca36f..c82312dce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+## v6.4.0
+October 16 2023
+
+#### Added
+- Add crypto module that allows configure SDK to encrypt and decrypt messages.
+
+#### Fixed
+- Improved security of crypto implementation by adding enhanced AES-CBC cryptor.
+
## v6.3.6
June 19 2023
diff --git a/LICENSE b/LICENSE
index 3efa3922e..5e1ef1880 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,27 +1,29 @@
-PubNub Real-time Cloud-Hosted Push API and Push Notification Client Frameworks
-Copyright (c) 2013 PubNub Inc.
-http://www.pubnub.com/
-http://www.pubnub.com/terms
+PubNub Software Development Kit License Agreement
+Copyright © 2023 PubNub Inc. All rights reserved.
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Subject to the terms and conditions of the license, you are hereby granted
+a non-exclusive, worldwide, royalty-free license to (a) copy and modify
+the software in source code or binary form for use with the software services
+and interfaces provided by PubNub, and (b) redistribute unmodified copies
+of the software to third parties. The software may not be incorporated in
+or used to provide any product or service competitive with the products
+and services of PubNub.
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+The above copyright notice and this license shall be included
+in or with all copies or substantial portions of the software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+This license does not grant you permission to use the trade names, trademarks,
+service marks, or product names of PubNub, except as required for reasonable
+and customary use in describing the origin of the software and reproducing
+the content of this license.
-PubNub Real-time Cloud-Hosted Push API and Push Notification Client Frameworks
-Copyright (c) 2013 PubNub Inc.
-http://www.pubnub.com/
-http://www.pubnub.com/terms
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
+EVENT SHALL PUBNUB OR THE AUTHORS OR COPYRIGHT HOLDERS OF THE SOFTWARE BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+https://www.pubnub.com/
+https://www.pubnub.com/terms
diff --git a/README.md b/README.md
index dea3681b2..352245b22 100644
--- a/README.md
+++ b/README.md
@@ -22,13 +22,13 @@ You will need the publish and subscribe keys to authenticate your app. Get your
com.pubnub
pubnub-gson
- 6.3.6
+ 6.4.0
```
* for Gradle, add the following dependency in your `gradle.build`:
```groovy
- implementation 'com.pubnub:pubnub-gson:6.3.6'
+ implementation 'com.pubnub:pubnub-gson:6.4.0'
```
2. Configure your keys:
diff --git a/build.gradle b/build.gradle
index 8bb6f7634..4872f9a38 100644
--- a/build.gradle
+++ b/build.gradle
@@ -10,7 +10,7 @@ plugins {
}
group = 'com.pubnub'
-version = '6.3.6'
+version = '6.4.0'
description = """"""
diff --git a/gradle.properties b/gradle.properties
index d79d94d5f..8de554250 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,7 +3,7 @@ SONATYPE_HOST=DEFAULT
SONATYPE_AUTOMATIC_RELEASE=true
GROUP=com.pubnub
POM_ARTIFACT_ID=pubnub-gson
-VERSION_NAME=6.3.6
+VERSION_NAME=6.4.0
POM_PACKAGING=jar
POM_NAME=PubNub Java SDK
diff --git a/src/integrationTest/java/com/pubnub/api/integration/HistoryIntegrationTest.java b/src/integrationTest/java/com/pubnub/api/integration/HistoryIntegrationTest.java
index f845d9c17..82f68eebd 100644
--- a/src/integrationTest/java/com/pubnub/api/integration/HistoryIntegrationTest.java
+++ b/src/integrationTest/java/com/pubnub/api/integration/HistoryIntegrationTest.java
@@ -2,6 +2,7 @@
import com.pubnub.api.PubNub;
import com.pubnub.api.PubNubException;
+import com.pubnub.api.crypto.CryptoModule;
import com.pubnub.api.integration.util.BaseIntegrationTest;
import com.pubnub.api.integration.util.RandomGenerator;
import com.pubnub.api.models.consumer.PNPublishResult;
@@ -312,12 +313,10 @@ public void testFetchSingleChannel_OverflowLimit() throws PubNubException {
@Test
public void testHistorySingleChannel_IncludeAll_Crypto() throws PubNubException {
final String expectedCipherKey = random();
- pubNub.getConfiguration().setCipherKey(expectedCipherKey);
+ pubNub.getConfiguration().setCryptoModule(CryptoModule.createLegacyCryptoModule(expectedCipherKey, true));
final PubNub observer = getPubNub();
- observer.getConfiguration().setCipherKey(expectedCipherKey);
-
- assertEquals(pubNub.getConfiguration().getCipherKey(), observer.getConfiguration().getCipherKey());
+ observer.getConfiguration().setCryptoModule(CryptoModule.createLegacyCryptoModule(expectedCipherKey, true));
final String expectedChannelName = random();
final int expectedMessageCount = 10;
@@ -343,12 +342,11 @@ public void testHistorySingleChannel_IncludeAll_Crypto() throws PubNubException
@Test
public void testFetchSingleChannel_IncludeAll_Crypto() throws PubNubException {
final String expectedCipherKey = random();
- pubNub.getConfiguration().setCipherKey(expectedCipherKey);
+ pubNub.getConfiguration().setCryptoModule(CryptoModule.createLegacyCryptoModule(expectedCipherKey, false));
final PubNub observer = getPubNub();
- observer.getConfiguration().setCipherKey(expectedCipherKey);
+ observer.getConfiguration().setCryptoModule(CryptoModule.createLegacyCryptoModule(expectedCipherKey, false));
- assertEquals(pubNub.getConfiguration().getCipherKey(), observer.getConfiguration().getCipherKey());
final String expectedChannelName = random();
final int expectedMessageCount = 10;
@@ -379,7 +377,7 @@ public void testFetchSingleChannel_WithActions_IncludeAll_Crypto() throws PubNub
final PubNub observer = getPubNub();
observer.getConfiguration().setCipherKey(expectedCipherKey);
- assertEquals(pubNub.getConfiguration().getCipherKey(), observer.getConfiguration().getCipherKey());
+ assertEquals(pubNub.getConfiguration().getCipherKey(), observer.getConfiguration().getCipherKey()); //todo
final String expectedChannelName = random();
final int expectedMessageCount = 10;
diff --git a/src/main/java/com/pubnub/api/PNConfiguration.java b/src/main/java/com/pubnub/api/PNConfiguration.java
index 825de0a4d..e646c07d6 100644
--- a/src/main/java/com/pubnub/api/PNConfiguration.java
+++ b/src/main/java/com/pubnub/api/PNConfiguration.java
@@ -1,6 +1,7 @@
package com.pubnub.api;
+import com.pubnub.api.crypto.CryptoModule;
import com.pubnub.api.enums.PNHeartbeatNotificationOptions;
import com.pubnub.api.enums.PNLogVerbosity;
import com.pubnub.api.enums.PNReconnectionPolicy;
@@ -94,9 +95,40 @@ public class PNConfiguration {
*/
private String publishKey;
private String secretKey;
- private String cipherKey;
private String authKey;
+
+ /**
+ * @deprecated Use {@link #cryptoModule} instead.
+ */
+ @Deprecated
+ private String cipherKey;
+
+ /**
+ * @deprecated Use {@link #cryptoModule} instead.
+ */
+ @Deprecated
+ private boolean useRandomInitializationVector;
+
+ /**
+ * CryptoModule is responsible for handling encryption and decryption.
+ * If set, all communications to and from PubNub will be encrypted.
+ */
+ private CryptoModule cryptoModule;
+
+ public CryptoModule getCryptoModule() {
+ if (cryptoModule != null) {
+ return cryptoModule;
+ } else {
+ if (cipherKey != null && !cipherKey.isEmpty()) {
+ log.warning("cipherKey is deprecated. Use CryptoModule instead");
+ return CryptoModule.createLegacyCryptoModule(cipherKey, useRandomInitializationVector);
+ } else {
+ return null;
+ }
+ }
+ }
+
/**
* @deprecated Use {@link #getUserId()} instead.
*/
@@ -110,7 +142,7 @@ public void setUuid(@NotNull String uuid) {
this.uuid = uuid;
}
- public UserId getUserId() {
+ public UserId getUserId() {
return new UserId(this.uuid);
}
@@ -210,9 +242,6 @@ public void setUserId(@NotNull UserId userId) {
private boolean dedupOnSubscribe;
@Setter
private Integer maximumMessagesCacheSize;
- @Setter
- private boolean useRandomInitializationVector;
-
@Setter
private int fileMessagePublishRetryLimit;
diff --git a/src/main/java/com/pubnub/api/PubNub.java b/src/main/java/com/pubnub/api/PubNub.java
index af74b7939..da8c689b7 100644
--- a/src/main/java/com/pubnub/api/PubNub.java
+++ b/src/main/java/com/pubnub/api/PubNub.java
@@ -5,6 +5,8 @@
import com.pubnub.api.builder.SubscribeBuilder;
import com.pubnub.api.builder.UnsubscribeBuilder;
import com.pubnub.api.callbacks.SubscribeCallback;
+import com.pubnub.api.crypto.CryptoModule;
+import com.pubnub.api.crypto.CryptoModuleKt;
import com.pubnub.api.endpoints.DeleteMessages;
import com.pubnub.api.endpoints.FetchMessages;
import com.pubnub.api.endpoints.History;
@@ -68,8 +70,6 @@
import com.pubnub.api.managers.token_manager.TokenManager;
import com.pubnub.api.managers.token_manager.TokenParser;
import com.pubnub.api.models.consumer.access_manager.v3.PNToken;
-import com.pubnub.api.vendor.Crypto;
-import com.pubnub.api.vendor.FileEncryptionUtil;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -105,12 +105,16 @@ public class PubNub {
private static final int TIMESTAMP_DIVIDER = 1000;
private static final int MAX_SEQUENCE = 65535;
- private static final String SDK_VERSION = "6.3.6";
+ private static final String SDK_VERSION = "6.4.0";
private final ListenerManager listenerManager;
private final StateManager stateManager;
private final TokenManager tokenManager;
+ public CryptoModule getCryptoModule() {
+ return configuration.getCryptoModule();
+ }
+
public PubNub(@NotNull PNConfiguration initialConfig) {
this.configuration = initialConfig;
this.mapper = new MapperManager();
@@ -456,8 +460,7 @@ public String decrypt(String inputString) throws PubNubException {
if (inputString == null) {
throw PubNubException.builder().pubnubError(PubNubErrorBuilder.PNERROBJ_INVALID_ARGUMENTS).build();
}
-
- return decrypt(inputString, this.getConfiguration().getCipherKey());
+ return decrypt(inputString, null);
}
/**
@@ -473,16 +476,33 @@ public String decrypt(String inputString, String cipherKey) throws PubNubExcepti
if (inputString == null) {
throw PubNubException.builder().pubnubError(PubNubErrorBuilder.PNERROBJ_INVALID_ARGUMENTS).build();
}
- boolean dynamicIV = this.getConfiguration().isUseRandomInitializationVector();
- return new Crypto(cipherKey, dynamicIV).decrypt(inputString);
+ CryptoModule cryptoModule = getCryptoModuleOrThrow(cipherKey);
+
+ return CryptoModuleKt.decryptString(cryptoModule, inputString);
+ }
+
+ private CryptoModule getCryptoModuleOrThrow(String cipherKey) throws PubNubException {
+ CryptoModule effectiveCryptoModule;
+ if (cipherKey != null) {
+ effectiveCryptoModule = CryptoModule.createLegacyCryptoModule(cipherKey, this.getConfiguration().isUseRandomInitializationVector());
+ } else {
+ CryptoModule cryptoModule = getCryptoModule();
+ if (cryptoModule != null) {
+ effectiveCryptoModule = cryptoModule;
+ } else {
+ throw PubNubException.builder().errormsg("Crypto module is not initialized").build();
+ }
+ }
+ return effectiveCryptoModule;
}
public InputStream decryptInputStream(InputStream inputStream) throws PubNubException {
- return decryptInputStream(inputStream, this.getConfiguration().getCipherKey());
+ return decryptInputStream(inputStream, null);
}
public InputStream decryptInputStream(InputStream inputStream, String cipherKey) throws PubNubException {
- return FileEncryptionUtil.decrypt(cipherKey, inputStream);
+ CryptoModule cryptoModule = getCryptoModuleOrThrow(cipherKey);
+ return cryptoModule.decryptStream(inputStream);
}
/**
@@ -497,7 +517,7 @@ public String encrypt(String inputString) throws PubNubException {
throw PubNubException.builder().pubnubError(PubNubErrorBuilder.PNERROBJ_INVALID_ARGUMENTS).build();
}
- return encrypt(inputString, this.getConfiguration().getCipherKey());
+ return encrypt(inputString, null);
}
/**
@@ -514,16 +534,17 @@ public String encrypt(String inputString, String cipherKey) throws PubNubExcepti
throw PubNubException.builder().pubnubError(PubNubErrorBuilder.PNERROBJ_INVALID_ARGUMENTS).build();
}
- boolean dynamicIV = this.getConfiguration().isUseRandomInitializationVector();
- return new Crypto(cipherKey, dynamicIV).encrypt(inputString);
+ CryptoModule cryptoModule = getCryptoModuleOrThrow(cipherKey);
+ return CryptoModuleKt.encryptString(cryptoModule, inputString);
}
public InputStream encryptInputStream(InputStream inputStream) throws PubNubException {
- return encryptInputStream(inputStream, this.getConfiguration().getCipherKey());
+ return encryptInputStream(inputStream, null);
}
public InputStream encryptInputStream(InputStream inputStream, String cipherKey) throws PubNubException {
- return FileEncryptionUtil.encrypt(cipherKey, inputStream);
+ CryptoModule cryptoModule = getCryptoModuleOrThrow(cipherKey);
+ return cryptoModule.encryptStream(inputStream);
}
public int getTimestamp() {
diff --git a/src/main/java/com/pubnub/api/crypto/CryptoModule.kt b/src/main/java/com/pubnub/api/crypto/CryptoModule.kt
new file mode 100644
index 000000000..df2f6c834
--- /dev/null
+++ b/src/main/java/com/pubnub/api/crypto/CryptoModule.kt
@@ -0,0 +1,201 @@
+package com.pubnub.api.crypto
+
+import com.pubnub.api.crypto.cryptor.AesCbcCryptor
+import com.pubnub.api.crypto.cryptor.Cryptor
+import com.pubnub.api.crypto.cryptor.HeaderParser
+import com.pubnub.api.crypto.cryptor.LEGACY_CRYPTOR_ID
+import com.pubnub.api.crypto.cryptor.LegacyCryptor
+import com.pubnub.api.crypto.cryptor.ParseResult
+import com.pubnub.api.crypto.data.EncryptedData
+import com.pubnub.api.crypto.data.EncryptedStreamData
+import com.pubnub.api.crypto.exception.PubNubError
+import com.pubnub.api.crypto.exception.PubNubException
+import com.pubnub.api.vendor.Base64
+import java.io.BufferedInputStream
+import java.io.InputStream
+import java.io.SequenceInputStream
+import java.lang.Integer.min
+
+class CryptoModule internal constructor(
+ internal val primaryCryptor: Cryptor,
+ internal val cryptorsForDecryptionOnly: List = listOf(),
+ internal val headerParser: HeaderParser = HeaderParser()
+) {
+
+ companion object {
+ @JvmStatic
+ fun createLegacyCryptoModule(cipherKey: String, randomIv: Boolean = true): CryptoModule {
+ return CryptoModule(
+ primaryCryptor = LegacyCryptor(cipherKey, randomIv),
+ cryptorsForDecryptionOnly = listOf(LegacyCryptor(cipherKey, randomIv), AesCbcCryptor(cipherKey))
+ )
+ }
+
+ @JvmStatic
+ fun createAesCbcCryptoModule(cipherKey: String, randomIv: Boolean = true): CryptoModule {
+ return CryptoModule(
+ primaryCryptor = AesCbcCryptor(cipherKey),
+ cryptorsForDecryptionOnly = listOf(AesCbcCryptor(cipherKey), LegacyCryptor(cipherKey, randomIv))
+ )
+ }
+
+ @JvmStatic
+ fun createNewCryptoModule(
+ defaultCryptor: Cryptor,
+ cryptorsForDecryptionOnly: List = listOf()
+ ): CryptoModule {
+ return CryptoModule(
+ primaryCryptor = defaultCryptor,
+ cryptorsForDecryptionOnly = listOf(defaultCryptor) + cryptorsForDecryptionOnly
+ )
+ }
+ }
+
+ fun encrypt(data: ByteArray): ByteArray {
+ validateData(data)
+ val (metadata, encryptedData) = primaryCryptor.encrypt(data)
+
+ return if (primaryCryptor.id().contentEquals(LEGACY_CRYPTOR_ID)) {
+ encryptedData
+ } else {
+ val cryptorHeader = headerParser.createCryptorHeader(primaryCryptor.id(), metadata)
+ cryptorHeader + encryptedData
+ }
+ }
+
+ fun decrypt(encryptedData: ByteArray): ByteArray {
+ validateData(encryptedData)
+ val parsedData: ParseResult = headerParser.parseDataWithHeader(encryptedData)
+ val decryptedData: ByteArray = when (parsedData) {
+ is ParseResult.NoHeader -> {
+ getDecryptedDataForLegacyCryptor(encryptedData)
+ }
+ is ParseResult.Success -> {
+ getDecryptedDataForCryptorWithHeader(parsedData)
+ }
+ }
+ return decryptedData
+ }
+
+ fun encryptStream(stream: InputStream): InputStream {
+ val bufferedInputStream = validateStreamAndReturnBuffered(stream)
+ val (metadata, encryptedData) = primaryCryptor.encryptStream(bufferedInputStream)
+ return if (primaryCryptor.id().contentEquals(LEGACY_CRYPTOR_ID)) {
+ encryptedData
+ } else {
+ val cryptorHeader: ByteArray = headerParser.createCryptorHeader(primaryCryptor.id(), metadata)
+ SequenceInputStream(cryptorHeader.inputStream(), encryptedData)
+ }
+ }
+
+ fun decryptStream(encryptedData: InputStream): InputStream {
+ val bufferedInputStream = validateStreamAndReturnBuffered(encryptedData)
+ return when (val parsedHeader = headerParser.parseDataWithHeader(bufferedInputStream)) {
+ ParseResult.NoHeader -> {
+ val decryptor = cryptorsForDecryptionOnly.firstOrNull { it.id().contentEquals(LEGACY_CRYPTOR_ID) }
+ decryptor?.decryptStream(EncryptedStreamData(stream = bufferedInputStream)) ?: throw PubNubException(
+ errorMessage = "LegacyCryptor not registered",
+ pubnubError = PubNubError.UNKNOWN_CRYPTOR
+ )
+ }
+
+ is ParseResult.Success -> {
+ val decryptor = cryptorsForDecryptionOnly.first {
+ it.id().contentEquals(parsedHeader.cryptoId)
+ }
+ decryptor.decryptStream(
+ EncryptedStreamData(
+ metadata = parsedHeader.cryptorData,
+ stream = parsedHeader.encryptedData
+ )
+ )
+ }
+ }
+ }
+
+ private fun getDecryptedDataForLegacyCryptor(encryptedData: ByteArray): ByteArray {
+ return getLegacyCryptor()?.decrypt(EncryptedData(data = encryptedData)) ?: throw PubNubException(
+ errorMessage = "LegacyCryptor not available",
+ pubnubError = PubNubError.UNKNOWN_CRYPTOR
+ )
+ }
+
+ private fun getDecryptedDataForCryptorWithHeader(parsedHeader: ParseResult.Success): ByteArray {
+ val decryptedData: ByteArray
+ val cryptorId = parsedHeader.cryptoId
+ val cryptorData = parsedHeader.cryptorData
+ val pureEncryptedData = parsedHeader.encryptedData
+ val cryptor = getCryptorById(cryptorId)
+ decryptedData =
+ cryptor?.decrypt(EncryptedData(cryptorData, pureEncryptedData))
+ ?: throw PubNubException(errorMessage = "No cryptor found", pubnubError = PubNubError.UNKNOWN_CRYPTOR)
+ return decryptedData
+ }
+
+ private fun getLegacyCryptor(): Cryptor? {
+ val idOfLegacyCryptor = ByteArray(4) { 0.toByte() }
+ return getCryptorById(idOfLegacyCryptor)
+ }
+
+ private fun getCryptorById(cryptorId: ByteArray): Cryptor? {
+ return cryptorsForDecryptionOnly.firstOrNull { it.id().contentEquals(cryptorId) }
+ }
+
+ private fun validateData(data: ByteArray) {
+ if (data.isEmpty()) {
+ throw PubNubException(
+ errorMessage = "Encryption/Decryption of empty data not allowed.",
+ pubnubError = PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED
+ )
+ }
+ }
+
+ private fun validateStreamAndReturnBuffered(stream: InputStream): BufferedInputStream {
+ val bufferedInputStream = stream.buffered()
+ bufferedInputStream.checkMinSize(1) {
+ throw PubNubException(
+ errorMessage = "Encryption/Decryption of empty data not allowed.",
+ pubnubError = PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED
+ )
+ }
+ return bufferedInputStream
+ }
+}
+
+internal fun CryptoModule.encryptString(inputString: String): String =
+ String(Base64.encode(encrypt(inputString.toByteArray()), Base64.NO_WRAP))
+
+internal fun CryptoModule.decryptString(inputString: String): String =
+ decrypt(Base64.decode(inputString, Base64.NO_WRAP)).toString(Charsets.UTF_8)
+
+// this method read data from stream and allows to read them again in subsequent reads without manual reset or repositioning
+internal fun BufferedInputStream.checkMinSize(size: Int, exceptionBlock: (Int) -> Unit) {
+ mark(size + 1)
+
+ val readBytes = readNBytez(size)
+ reset()
+ if (readBytes.size < size) {
+ exceptionBlock(size)
+ }
+}
+
+internal fun BufferedInputStream.readExactlyNBytez(size: Int, exceptionBlock: (Int) -> Unit): ByteArray {
+ val readBytes = readNBytez(size)
+ if (readBytes.size < size) {
+ exceptionBlock(size)
+ }
+ return readBytes
+}
+
+internal fun InputStream.readNBytez(len: Int): ByteArray {
+ var remaining: Int = len
+ var n: Int
+ val originalArray = ByteArray(remaining)
+ var nread = 0
+
+ while (read(originalArray, nread, min(originalArray.size - nread, remaining)).also { n = it } > 0) {
+ nread += n
+ remaining -= n
+ }
+ return originalArray.copyOf(nread)
+}
diff --git a/src/main/java/com/pubnub/api/crypto/cryptor/AesCbcCryptor.kt b/src/main/java/com/pubnub/api/crypto/cryptor/AesCbcCryptor.kt
new file mode 100644
index 000000000..d9f38b907
--- /dev/null
+++ b/src/main/java/com/pubnub/api/crypto/cryptor/AesCbcCryptor.kt
@@ -0,0 +1,126 @@
+package com.pubnub.api.crypto.cryptor
+
+import com.pubnub.api.crypto.checkMinSize
+import com.pubnub.api.crypto.data.EncryptedData
+import com.pubnub.api.crypto.data.EncryptedStreamData
+import com.pubnub.api.crypto.exception.PubNubError
+import com.pubnub.api.crypto.exception.PubNubException
+import java.io.BufferedInputStream
+import java.io.InputStream
+import java.security.MessageDigest
+import java.security.SecureRandom
+import javax.crypto.Cipher
+import javax.crypto.CipherInputStream
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+private const val CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding"
+private const val RANDOM_IV_SIZE = 16
+
+class AesCbcCryptor(val cipherKey: String) : Cryptor {
+ private val newKey: SecretKeySpec = createNewKey()
+
+ override fun id(): ByteArray {
+ return byteArrayOf('A'.code.toByte(), 'C'.code.toByte(), 'R'.code.toByte(), 'H'.code.toByte())
+ }
+
+ override fun encrypt(data: ByteArray): EncryptedData {
+ validateData(data)
+ return try {
+ val ivBytes: ByteArray = createRandomIv()
+ val cipher = createInitializedCipher(ivBytes, Cipher.ENCRYPT_MODE)
+ val encryptedData: ByteArray = cipher.doFinal(data)
+ EncryptedData(metadata = ivBytes, data = encryptedData)
+ } catch (e: Exception) {
+ throw PubNubException(errorMessage = e.message, pubnubError = PubNubError.CRYPTO_ERROR)
+ }
+ }
+
+ override fun decrypt(encryptedData: EncryptedData): ByteArray {
+ validateData(encryptedData.data)
+ return try {
+ val ivBytes: ByteArray = encryptedData.metadata?.takeIf { it.size == RANDOM_IV_SIZE }
+ ?: throw PubNubException(errorMessage = "Invalid random IV", pubnubError = PubNubError.CRYPTO_ERROR)
+ val cipher = createInitializedCipher(ivBytes, Cipher.DECRYPT_MODE)
+ val decryptedData = cipher.doFinal(encryptedData.data)
+ decryptedData
+ } catch (e: Exception) {
+ throw PubNubException(errorMessage = e.message, pubnubError = PubNubError.CRYPTO_ERROR)
+ }
+ }
+
+ override fun encryptStream(stream: InputStream): EncryptedStreamData {
+ val bufferedInputStream = validateInputStreamAndReturnBuffered(stream)
+ try {
+ val ivBytes: ByteArray = createRandomIv()
+ val cipher = createInitializedCipher(ivBytes, Cipher.ENCRYPT_MODE)
+ val cipheredStream = CipherInputStream(bufferedInputStream, cipher)
+
+ return EncryptedStreamData(
+ metadata = ivBytes,
+ stream = cipheredStream
+ )
+ } catch (e: Exception) {
+ throw PubNubException(e.message, PubNubError.CRYPTO_ERROR)
+ }
+ }
+
+ override fun decryptStream(encryptedData: EncryptedStreamData): InputStream {
+ val bufferedInputStream = validateInputStreamAndReturnBuffered(encryptedData.stream)
+ try {
+ val ivBytes: ByteArray = encryptedData.metadata?.takeIf { it.size == RANDOM_IV_SIZE }
+ ?: throw PubNubException(errorMessage = "Invalid random IV", pubnubError = PubNubError.CRYPTO_ERROR)
+ val cipher = createInitializedCipher(ivBytes, Cipher.DECRYPT_MODE)
+ return CipherInputStream(bufferedInputStream, cipher)
+ } catch (e: Exception) {
+ throw PubNubException(errorMessage = e.message, pubnubError = PubNubError.CRYPTO_ERROR)
+ }
+ }
+
+ private fun validateData(data: ByteArray) {
+ if (data.isEmpty()) {
+ throw PubNubException(
+ errorMessage = "Encryption/Decryption of empty data not allowed.",
+ pubnubError = PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED
+ )
+ }
+ }
+
+ private fun createInitializedCipher(iv: ByteArray, mode: Int): Cipher {
+ return Cipher.getInstance(CIPHER_TRANSFORMATION).also {
+ it.init(mode, newKey, IvParameterSpec(iv))
+ }
+ }
+
+ private fun createNewKey(): SecretKeySpec {
+ val keyBytes = sha256(cipherKey.toByteArray(Charsets.UTF_8))
+ return SecretKeySpec(keyBytes, "AES")
+ }
+
+ private fun createRandomIv(): ByteArray {
+ val ivBytes = ByteArray(RANDOM_IV_SIZE)
+ SecureRandom().nextBytes(ivBytes)
+ return ivBytes
+ }
+
+ private fun sha256(input: ByteArray): ByteArray {
+ val digest: MessageDigest
+ return try {
+ digest = MessageDigest.getInstance("SHA-256")
+ digest.digest(input)
+ } catch (e: java.lang.Exception) {
+ throw PubNubException(errorMessage = e.message, pubnubError = PubNubError.CRYPTO_ERROR)
+ }
+ }
+
+ private fun validateInputStreamAndReturnBuffered(stream: InputStream): BufferedInputStream {
+ val bufferedInputStream = stream.buffered()
+ bufferedInputStream.checkMinSize(1) {
+ throw PubNubException(
+ errorMessage = "Encryption/Decryption of empty data not allowed.",
+ pubnubError = PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED
+ )
+ }
+ return bufferedInputStream
+ }
+}
diff --git a/src/main/java/com/pubnub/api/crypto/cryptor/Cryptor.kt b/src/main/java/com/pubnub/api/crypto/cryptor/Cryptor.kt
new file mode 100644
index 000000000..f21ea51cd
--- /dev/null
+++ b/src/main/java/com/pubnub/api/crypto/cryptor/Cryptor.kt
@@ -0,0 +1,13 @@
+package com.pubnub.api.crypto.cryptor
+
+import com.pubnub.api.crypto.data.EncryptedData
+import com.pubnub.api.crypto.data.EncryptedStreamData
+import java.io.InputStream
+
+interface Cryptor {
+ fun id(): ByteArray // Assuming 4 bytes,
+ fun encrypt(data: ByteArray): EncryptedData
+ fun decrypt(encryptedData: EncryptedData): ByteArray
+ fun encryptStream(stream: InputStream): EncryptedStreamData
+ fun decryptStream(encryptedData: EncryptedStreamData): InputStream
+}
diff --git a/src/main/java/com/pubnub/api/crypto/cryptor/CryptorHeader.kt b/src/main/java/com/pubnub/api/crypto/cryptor/CryptorHeader.kt
new file mode 100644
index 000000000..99e3d02d3
--- /dev/null
+++ b/src/main/java/com/pubnub/api/crypto/cryptor/CryptorHeader.kt
@@ -0,0 +1,38 @@
+package com.pubnub.api.crypto.cryptor
+
+class CryptorHeader(
+ val sentinel: ByteArray, // 4 bytes
+ val version: Byte, // 1 byte
+ val cryptorId: ByteArray, // 4 bytes
+ val cryptorDataSize: ByteArray, // 1 or 3 bytes
+ val cryptorData: ByteArray // 0-65535 bytes
+) {
+
+ fun toByteArray(): ByteArray {
+ return sentinel + version + cryptorId + cryptorDataSize + cryptorData
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as CryptorHeader
+
+ if (!sentinel.contentEquals(other.sentinel)) return false
+ if (version != other.version) return false
+ if (!cryptorId.contentEquals(other.cryptorId)) return false
+ if (!cryptorDataSize.contentEquals(other.cryptorDataSize)) return false
+ if (!cryptorData.contentEquals(other.cryptorData)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = sentinel.contentHashCode()
+ result = 31 * result + version
+ result = 31 * result + cryptorId.contentHashCode()
+ result = 31 * result + cryptorDataSize.contentHashCode()
+ result = 31 * result + cryptorData.contentHashCode()
+ return result
+ }
+}
diff --git a/src/main/java/com/pubnub/api/crypto/cryptor/CryptorHeaderVersion.kt b/src/main/java/com/pubnub/api/crypto/cryptor/CryptorHeaderVersion.kt
new file mode 100644
index 000000000..d7e0d8d6d
--- /dev/null
+++ b/src/main/java/com/pubnub/api/crypto/cryptor/CryptorHeaderVersion.kt
@@ -0,0 +1,11 @@
+package com.pubnub.api.crypto.cryptor
+
+enum class CryptorHeaderVersion(val value: Int) {
+ One(1);
+
+ companion object {
+ fun fromValue(value: Int): CryptorHeaderVersion? {
+ return values().find { it.value == value }
+ }
+ }
+}
diff --git a/src/main/java/com/pubnub/api/crypto/cryptor/HeaderParser.kt b/src/main/java/com/pubnub/api/crypto/cryptor/HeaderParser.kt
new file mode 100644
index 000000000..2166ec2f5
--- /dev/null
+++ b/src/main/java/com/pubnub/api/crypto/cryptor/HeaderParser.kt
@@ -0,0 +1,189 @@
+package com.pubnub.api.crypto.cryptor
+
+import com.pubnub.api.crypto.exception.PubNubError
+import com.pubnub.api.crypto.exception.PubNubException
+import com.pubnub.api.crypto.readExactlyNBytez
+import org.slf4j.LoggerFactory
+import java.io.BufferedInputStream
+import java.io.InputStream
+
+private val SENTINEL = "PNED".toByteArray()
+private const val STARTING_INDEX_OF_ONE_BYTE_CRYPTOR_DATA_SIZE = 10
+private const val STARTING_INDEX_OF_THREE_BYTES_CRYPTOR_DATA_SIZE = 12
+private const val MINIMAL_SIZE_OF_DATA_HAVING_CRYPTOR_HEADER = 10
+private const val THREE_BYTES_SIZE_CRYPTOR_DATA_INDICATOR: UByte = 255U
+
+private const val SENTINEL_STARTING_INDEX = 0
+private const val SENTINEL_ENDING_INDEX = 3
+private const val VERSION_INDEX = 4
+private const val CRYPTOR_ID_STARTING_INDEX = 5
+private const val CRYPTOR_ID_ENDING_INDEX = 8
+private const val CRYPTOR_DATA_SIZE_STARTING_INDEX = 9
+private const val THREE_BYTES_CRYPTOR_DATA_SIZE_STARTING_INDEX = 10
+private const val THREE_BYTES_CRYPTOR_DATA_SIZE_ENDING_INDEX = 11
+private const val MAX_VALUE_THAT_CAN_BE_STORED_ON_TWO_BYTES = 65535
+private const val MINIMAL_SIZE_OF_CRYPTO_HEADER = 10
+
+class HeaderParser {
+ private val log = LoggerFactory.getLogger(HeaderParser::class.java)
+
+ fun parseDataWithHeader(stream: BufferedInputStream): ParseResult {
+ val bufferedInputStream = stream.buffered()
+ bufferedInputStream.mark(Int.MAX_VALUE) // TODO Can be calculated from spec
+ val possibleInitialHeader = ByteArray(MINIMAL_SIZE_OF_CRYPTO_HEADER)
+ val initiallyRead = bufferedInputStream.read(possibleInitialHeader)
+ if (!possibleInitialHeader.sliceArray(SENTINEL_STARTING_INDEX..SENTINEL_ENDING_INDEX).contentEquals(SENTINEL)) {
+ bufferedInputStream.reset()
+ return ParseResult.NoHeader
+ }
+
+ if (initiallyRead < MINIMAL_SIZE_OF_DATA_HAVING_CRYPTOR_HEADER) {
+ throw PubNubException(
+ errorMessage = "Minimal size of Cryptor Data Header is: $MINIMAL_SIZE_OF_DATA_HAVING_CRYPTOR_HEADER",
+ pubnubError = PubNubError.CRYPTOR_HEADER_PARSE_ERROR
+ )
+ }
+
+ validateCryptorHeaderVersion(possibleInitialHeader)
+ val cryptorId = possibleInitialHeader.sliceArray(CRYPTOR_ID_STARTING_INDEX..CRYPTOR_ID_ENDING_INDEX)
+ val cryptorDataSizeFirstByte = possibleInitialHeader[CRYPTOR_DATA_SIZE_STARTING_INDEX].toUByte()
+
+ val cryptorData: ByteArray = if (cryptorDataSizeFirstByte == THREE_BYTES_SIZE_CRYPTOR_DATA_INDICATOR) {
+ val cryptorDataSizeBytes = readExactlyNBytez(bufferedInputStream, 2)
+ val cryptorDataSize = convertTwoBytesToIntBigEndian(cryptorDataSizeBytes[0], cryptorDataSizeBytes[1])
+ readExactlyNBytez(bufferedInputStream, cryptorDataSize)
+ } else {
+ if (cryptorDataSizeFirstByte == UByte.MIN_VALUE) {
+ byteArrayOf()
+ } else {
+ readExactlyNBytez(bufferedInputStream, cryptorDataSizeFirstByte.toInt())
+ }
+ }
+ return ParseResult.Success(cryptorId, cryptorData, bufferedInputStream)
+ }
+
+ private fun readExactlyNBytez(
+ bufferedInputStream: BufferedInputStream,
+ numberOfBytesToRead: Int
+ ) = bufferedInputStream.readExactlyNBytez(numberOfBytesToRead) { n ->
+ throw PubNubException(errorMessage = "Couldn't read $n bytes")
+ }
+
+ fun parseDataWithHeader(data: ByteArray): ParseResult {
+ if (data.size < SENTINEL.size) {
+ return ParseResult.NoHeader
+ }
+ val sentinel = data.sliceArray(SENTINEL_STARTING_INDEX..SENTINEL_ENDING_INDEX)
+ if (!SENTINEL.contentEquals(sentinel)) {
+ return ParseResult.NoHeader
+ }
+
+ if (data.size < MINIMAL_SIZE_OF_DATA_HAVING_CRYPTOR_HEADER) {
+ throw PubNubException(
+ errorMessage =
+ "Minimal size of encrypted data having Cryptor Data Header is: $MINIMAL_SIZE_OF_DATA_HAVING_CRYPTOR_HEADER",
+ pubnubError = PubNubError.CRYPTOR_DATA_HEADER_SIZE_TO_SMALL
+ )
+ }
+
+ validateCryptorHeaderVersion(data)
+
+ val cryptorId = data.sliceArray(CRYPTOR_ID_STARTING_INDEX..CRYPTOR_ID_ENDING_INDEX)
+ log.trace("CryptoId: ${String(cryptorId, Charsets.UTF_8)}")
+
+ val cryptorDataSizeFirstByte: Byte = data[CRYPTOR_DATA_SIZE_STARTING_INDEX]
+ val (startingIndexOfCryptorData, cryptorDataSize) = getCryptorDataSizeAndStartingIndex(
+ data,
+ cryptorDataSizeFirstByte
+ )
+
+ if (startingIndexOfCryptorData + cryptorDataSize > data.size) {
+ throw PubNubException(
+ errorMessage = "Input data size: ${data.size} is to small to fit header of size $startingIndexOfCryptorData and cryptorData of size: $cryptorDataSize",
+ pubnubError = PubNubError.CRYPTOR_HEADER_PARSE_ERROR
+ )
+ }
+ val cryptorData =
+ data.sliceArray(startingIndexOfCryptorData until (startingIndexOfCryptorData + cryptorDataSize))
+ val sizeOfCryptorHeader = startingIndexOfCryptorData + cryptorDataSize
+ val encryptedData = data.sliceArray(sizeOfCryptorHeader until data.size)
+
+ return ParseResult.Success(cryptorId, cryptorData, encryptedData)
+ }
+
+ fun createCryptorHeader(cryptorId: ByteArray, cryptorData: ByteArray?): ByteArray {
+ val sentinel: ByteArray = SENTINEL
+ val cryptorHeaderVersion: Byte = getCurrentCryptoHeaderVersion()
+ val cryptorDataSize: Int = cryptorData?.size ?: 0
+ val finalCryptorDataSize: ByteArray =
+ if (cryptorDataSize < THREE_BYTES_SIZE_CRYPTOR_DATA_INDICATOR.toInt()) {
+ byteArrayOf(cryptorDataSize.toByte()) // cryptorDataSize will be stored on 1 byte
+ } else if (cryptorDataSize < MAX_VALUE_THAT_CAN_BE_STORED_ON_TWO_BYTES) {
+ byteArrayOf(cryptorDataSize.toByte()) + writeNumberOnTwoBytes(cryptorDataSize) // cryptorDataSize will be stored on 3 byte
+ } else {
+ throw PubNubException(
+ errorMessage = "Cryptor Data Size is: $cryptorDataSize whereas max cryptor data size is: $MAX_VALUE_THAT_CAN_BE_STORED_ON_TWO_BYTES",
+ pubnubError = PubNubError.CRYPTOR_HEADER_PARSE_ERROR
+ )
+ }
+
+ val cryptorHeader =
+ CryptorHeader(sentinel, cryptorHeaderVersion, cryptorId, finalCryptorDataSize, cryptorData ?: byteArrayOf())
+ return cryptorHeader.toByteArray()
+ }
+
+ private fun getCurrentCryptoHeaderVersion(): Byte {
+ return CryptorHeaderVersion.One.value.toByte()
+ }
+
+ private fun getCryptorDataSizeAndStartingIndex(data: ByteArray, cryptorDataSizeFirstByte: Byte): Pair {
+ val startingIndexOfCryptorData: Int
+ val cryptorDataSize: Int
+ val cryptoDataFirstByteAsUByte: UByte = cryptorDataSizeFirstByte.toUByte()
+
+ if (cryptoDataFirstByteAsUByte == THREE_BYTES_SIZE_CRYPTOR_DATA_INDICATOR) {
+ startingIndexOfCryptorData = STARTING_INDEX_OF_THREE_BYTES_CRYPTOR_DATA_SIZE
+ log.trace("\"Cryptor data size\" first byte's value is 255 that mean that size is stored on two next bytes")
+ val cryptorDataSizeSecondByte = data[THREE_BYTES_CRYPTOR_DATA_SIZE_STARTING_INDEX]
+ val cryptorDataSizeThirdByte = data[THREE_BYTES_CRYPTOR_DATA_SIZE_ENDING_INDEX]
+ cryptorDataSize = convertTwoBytesToIntBigEndian(cryptorDataSizeSecondByte, cryptorDataSizeThirdByte)
+ } else {
+ startingIndexOfCryptorData = STARTING_INDEX_OF_ONE_BYTE_CRYPTOR_DATA_SIZE
+ cryptorDataSize = cryptoDataFirstByteAsUByte.toInt()
+ log.trace("\"Cryptor data size\" is 1 byte long and its value is: $cryptorDataSize")
+ }
+ return Pair(startingIndexOfCryptorData, cryptorDataSize)
+ }
+
+ private fun validateCryptorHeaderVersion(data: ByteArray) {
+ val version: UByte = data[VERSION_INDEX].toUByte() // 5th byte
+ val versionAsInt = version.toInt()
+ log.trace("Cryptor header version is: $versionAsInt")
+ // check if version exist in this SDK version
+ CryptorHeaderVersion.fromValue(versionAsInt)
+ ?: throw PubNubException(
+ errorMessage = "Cryptor header version unknown. Please, update SDK",
+ pubnubError = PubNubError.CRYPTOR_HEADER_VERSION_UNKNOWN
+ )
+ }
+
+ private fun convertTwoBytesToIntBigEndian(byte1: Byte, byte2: Byte): Int {
+ return ((byte1.toInt() and 0xFF) shl 8) or (byte2.toInt() and 0xFF)
+ }
+
+ private fun writeNumberOnTwoBytes(number: Int): ByteArray {
+ val result = ByteArray(2)
+
+ result[0] = (number shr 8).toByte()
+ result[1] = number.toByte()
+
+ return result
+ }
+}
+
+sealed class ParseResult {
+ data class Success(val cryptoId: ByteArray, val cryptorData: ByteArray, val encryptedData: T) :
+ ParseResult()
+
+ object NoHeader : ParseResult()
+}
diff --git a/src/main/java/com/pubnub/api/crypto/cryptor/InputStreamSeparator.kt b/src/main/java/com/pubnub/api/crypto/cryptor/InputStreamSeparator.kt
new file mode 100644
index 000000000..9a7a6f678
--- /dev/null
+++ b/src/main/java/com/pubnub/api/crypto/cryptor/InputStreamSeparator.kt
@@ -0,0 +1,41 @@
+package com.pubnub.api.crypto.cryptor
+
+import java.io.InputStream
+
+/** This class is used to separate the inputStream from the CipherInputStream.
+ * We might want to separate the inputStream from the CipherInputStream because we want to be able to close the
+ * CipherInputStream without closing the inputStream.
+ * */
+internal class InputStreamSeparator(private val inputStream: InputStream) : InputStream() {
+ override fun read(): Int {
+ return inputStream.read()
+ }
+
+ override fun read(b: ByteArray): Int {
+ return inputStream.read(b)
+ }
+
+ override fun read(b: ByteArray, off: Int, len: Int): Int {
+ return inputStream.read(b, off, len)
+ }
+
+ override fun skip(n: Long): Long {
+ return inputStream.skip(n)
+ }
+
+ override fun available(): Int {
+ return inputStream.available()
+ }
+
+ override fun mark(readlimit: Int) {
+ inputStream.mark(readlimit)
+ }
+
+ override fun reset() {
+ inputStream.reset()
+ }
+
+ override fun markSupported(): Boolean {
+ return inputStream.markSupported()
+ }
+}
diff --git a/src/main/java/com/pubnub/api/crypto/cryptor/LegacyCryptor.kt b/src/main/java/com/pubnub/api/crypto/cryptor/LegacyCryptor.kt
new file mode 100644
index 000000000..401d58c5b
--- /dev/null
+++ b/src/main/java/com/pubnub/api/crypto/cryptor/LegacyCryptor.kt
@@ -0,0 +1,216 @@
+package com.pubnub.api.crypto.cryptor
+
+import com.pubnub.api.crypto.checkMinSize
+import com.pubnub.api.crypto.data.EncryptedData
+import com.pubnub.api.crypto.data.EncryptedStreamData
+import com.pubnub.api.crypto.exception.PubNubError
+import com.pubnub.api.crypto.exception.PubNubException
+import java.io.BufferedInputStream
+import java.io.InputStream
+import java.io.SequenceInputStream
+import java.io.UnsupportedEncodingException
+import java.security.MessageDigest
+import java.security.SecureRandom
+import java.util.*
+import javax.crypto.Cipher
+import javax.crypto.CipherInputStream
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+private const val STATIC_IV = "0123456789012345"
+private const val CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding"
+internal val LEGACY_CRYPTOR_ID = ByteArray(4) { 0.toByte() }
+
+private const val IV_SIZE = 16
+private const val SIZE_OF_ONE_BLOCK_OF_ENCRYPTED_DATA = 16
+private const val RANDOM_IV_STARTING_INDEX = 0
+private const val RANDOM_IV_ENDING_INDEX = 15
+private const val ENCRYPTED_DATA_STARTING_INDEX = 16 // this is when useRandomIv = true
+
+class LegacyCryptor(val cipherKey: String, val useRandomIv: Boolean = true) : Cryptor {
+ private val newKey: SecretKeySpec = createNewKey()
+
+ override fun id(): ByteArray {
+ return LEGACY_CRYPTOR_ID // it was agreed that legacy PN Cryptor will have 0 as ID
+ }
+
+ override fun encrypt(data: ByteArray): EncryptedData {
+ validateData(data)
+ return try {
+ val ivBytes: ByteArray = getIvBytesForEncryption()
+ val cipher = createInitializedCipher(ivBytes, Cipher.ENCRYPT_MODE)
+ val encrypted: ByteArray = cipher.doFinal(data)
+ if (useRandomIv) {
+ EncryptedData(
+ data = ivBytes + encrypted
+ )
+ } else {
+ EncryptedData(
+ data = encrypted
+ )
+ }
+ } catch (e: Exception) {
+ throw PubNubException(errorMessage = e.message, pubnubError = PubNubError.CRYPTO_ERROR)
+ }
+ }
+
+ override fun decrypt(encryptedData: EncryptedData): ByteArray {
+ validateData(encryptedData)
+ return try {
+ val ivBytes: ByteArray = getIvBytesForDecryption(encryptedData)
+ val cipher = createInitializedCipher(ivBytes, Cipher.DECRYPT_MODE)
+ val encryptedDataForProcessing = getEncryptedDataForProcessing(encryptedData)
+ val decryptedData = cipher.doFinal(encryptedDataForProcessing)
+ decryptedData
+ } catch (e: Exception) {
+ throw PubNubException(errorMessage = e.message, pubnubError = PubNubError.CRYPTO_ERROR)
+ }
+ }
+
+ override fun encryptStream(stream: InputStream): EncryptedStreamData {
+ val bufferedInputStream = validateStreamAndReturnBuffered(stream)
+ try {
+ val ivBytes: ByteArray = createRandomIv()
+ val cipher = createInitializedCipher(ivBytes, Cipher.ENCRYPT_MODE)
+ val cipheredStream = CipherInputStream(bufferedInputStream, cipher)
+ return EncryptedStreamData(stream = SequenceInputStream(ivBytes.inputStream(), cipheredStream))
+ } catch (e: Exception) {
+ throw PubNubException(errorMessage = e.message, pubnubError = PubNubError.CRYPTO_ERROR)
+ }
+ }
+
+ override fun decryptStream(encryptedData: EncryptedStreamData): InputStream {
+ val bufferedInputStream = validateEncryptedInputStreamAndReturnBuffered(encryptedData.stream)
+ try {
+ val ivBytes = ByteArray(IV_SIZE)
+ val numberOfReadBytes = bufferedInputStream.read(ivBytes)
+ if (numberOfReadBytes != IV_SIZE) {
+ throw PubNubException(
+ errorMessage = "Could not read IV from encrypted stream",
+ pubnubError = PubNubError.CRYPTO_ERROR
+ )
+ }
+ val cipher = createInitializedCipher(ivBytes, Cipher.DECRYPT_MODE)
+ return CipherInputStream(bufferedInputStream, cipher)
+ } catch (e: Exception) {
+ throw PubNubException(errorMessage = e.message, pubnubError = PubNubError.CRYPTO_ERROR)
+ }
+ }
+
+ private fun validateEncryptedInputStreamAndReturnBuffered(stream: InputStream): BufferedInputStream {
+ val bufferedInputStream = stream.buffered()
+ bufferedInputStream.checkMinSize(IV_SIZE + SIZE_OF_ONE_BLOCK_OF_ENCRYPTED_DATA) {
+ throw PubNubException(
+ errorMessage = "Encryption/Decryption of empty data not allowed.",
+ pubnubError = PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED
+ )
+ }
+ return bufferedInputStream
+ }
+
+ private fun validateStreamAndReturnBuffered(stream: InputStream): BufferedInputStream {
+ val bufferedInputStream = stream.buffered()
+ bufferedInputStream.checkMinSize(1) {
+ throw PubNubException(
+ errorMessage = "Encryption/Decryption of empty data not allowed.",
+ pubnubError = PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED
+ )
+ }
+ return bufferedInputStream
+ }
+
+ private fun createNewKey(): SecretKeySpec {
+ val keyBytes = String(hexEncode(sha256(cipherKey.toByteArray())), Charsets.UTF_8)
+ .substring(0, 32)
+ .lowercase(Locale.getDefault()).toByteArray()
+ return SecretKeySpec(keyBytes, "AES")
+ }
+
+ private fun sha256(input: ByteArray): ByteArray {
+ val digest: MessageDigest
+ return try {
+ digest = MessageDigest.getInstance("SHA-256")
+ digest.digest(input)
+ } catch (e: java.lang.Exception) {
+ throw PubNubException(errorMessage = e.message, pubnubError = PubNubError.CRYPTO_ERROR)
+ }
+ }
+
+ private fun hexEncode(input: ByteArray): ByteArray {
+ val result = StringBuilder()
+ for (byt in input) {
+ result.append(Integer.toString((byt.toInt() and 0xff) + 0x100, 16).substring(1))
+ }
+ try {
+ return result.toString().toByteArray()
+ } catch (e: UnsupportedEncodingException) {
+ throw PubNubException(errorMessage = e.message, pubnubError = PubNubError.CRYPTO_ERROR)
+ }
+ }
+
+ private fun validateData(data: ByteArray) {
+ if (data.isEmpty()) {
+ throw PubNubException(
+ errorMessage = "Encryption/Decryption of empty data not allowed.",
+ pubnubError = PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED
+ )
+ }
+ }
+
+ private fun getIvBytesForEncryption(): ByteArray {
+ return if (useRandomIv) {
+ createRandomIv()
+ } else {
+ STATIC_IV.toByteArray()
+ }
+ }
+
+ private fun createRandomIv(): ByteArray {
+ val ivBytes = ByteArray(IV_SIZE)
+ SecureRandom().nextBytes(ivBytes)
+ return ivBytes
+ }
+
+ private fun validateData(encryptedData: EncryptedData) {
+ val encryptedDatSize = encryptedData.data.size
+ if (useRandomIv) {
+ if (encryptedDatSize <= IV_SIZE) {
+ throw PubNubException(
+ errorMessage = "Encryption/Decryption of empty data not allowed.",
+ pubnubError = PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED
+ )
+ }
+ } else {
+ if (encryptedDatSize == 0) {
+ throw PubNubException(
+ errorMessage = "Encryption/Decryption of empty data not allowed.",
+ pubnubError = PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED
+ )
+ }
+ }
+ }
+
+ private fun getIvBytesForDecryption(encryptedData: EncryptedData): ByteArray {
+ return if (useRandomIv) {
+ encryptedData.data.sliceArray(RANDOM_IV_STARTING_INDEX..RANDOM_IV_ENDING_INDEX)
+ } else {
+ STATIC_IV.toByteArray()
+ }
+ }
+
+ private fun createInitializedCipher(iv: ByteArray, mode: Int): Cipher {
+ return Cipher.getInstance(CIPHER_TRANSFORMATION).also {
+ it.init(mode, newKey, IvParameterSpec(iv))
+ }
+ }
+
+ private fun getEncryptedDataForProcessing(encryptedData: EncryptedData): ByteArray {
+ val encryptedDataForProcessing: ByteArray = if (useRandomIv) {
+ encryptedData.data.sliceArray(ENCRYPTED_DATA_STARTING_INDEX until encryptedData.data.size)
+ } else {
+ // when there is useRandomIv = false then there is no IV in message
+ encryptedData.data
+ }
+ return encryptedDataForProcessing
+ }
+}
diff --git a/src/main/java/com/pubnub/api/crypto/data/EncryptedData.kt b/src/main/java/com/pubnub/api/crypto/data/EncryptedData.kt
new file mode 100644
index 000000000..06cbdf3c8
--- /dev/null
+++ b/src/main/java/com/pubnub/api/crypto/data/EncryptedData.kt
@@ -0,0 +1,6 @@
+package com.pubnub.api.crypto.data
+
+data class EncryptedData(
+ val metadata: ByteArray? = null,
+ val data: ByteArray
+)
diff --git a/src/main/java/com/pubnub/api/crypto/data/EncryptedStreamData.kt b/src/main/java/com/pubnub/api/crypto/data/EncryptedStreamData.kt
new file mode 100644
index 000000000..237992689
--- /dev/null
+++ b/src/main/java/com/pubnub/api/crypto/data/EncryptedStreamData.kt
@@ -0,0 +1,8 @@
+package com.pubnub.api.crypto.data
+
+import java.io.InputStream
+
+data class EncryptedStreamData(
+ val metadata: ByteArray? = null,
+ val stream: InputStream
+)
diff --git a/src/main/java/com/pubnub/api/crypto/exception/PubNubError.kt b/src/main/java/com/pubnub/api/crypto/exception/PubNubError.kt
new file mode 100644
index 000000000..04c1df005
--- /dev/null
+++ b/src/main/java/com/pubnub/api/crypto/exception/PubNubError.kt
@@ -0,0 +1,234 @@
+package com.pubnub.api.crypto.exception
+
+import com.pubnub.api.models.consumer.PNStatus
+
+/**
+ * List of known PubNub errors. Observe them in [PubNubException.pubnubError] in [PNStatus.exception].
+ *
+ * @property code The error code.
+ * @property message The error message.
+ */
+enum class PubNubError(private val code: Int, val message: String) {
+
+ TIMEOUT(
+ 100,
+ "Timeout Occurred"
+ ),
+
+ CONNECT_EXCEPTION(
+ 102,
+ "Connect Exception. Please verify if network is reachable"
+ ),
+
+ SECRET_KEY_MISSING(
+ 114,
+ "ULS configuration failed. Secret Key not configured"
+ ),
+
+ JSON_ERROR(
+ 121,
+ "JSON Error while processing API response"
+ ),
+ INTERNAL_ERROR(
+ 125,
+ "Internal Error"
+ ),
+ PARSING_ERROR(
+ 126,
+ "Parsing Error"
+ ),
+ INVALID_ARGUMENTS(
+ 131,
+ "Invalid arguments"
+ ),
+ CONNECTION_NOT_SET(
+ 133,
+ "PubNub Connection not set"
+ ),
+
+ GROUP_MISSING(
+ 136,
+ "Group Missing"
+ ),
+
+ SUBSCRIBE_KEY_MISSING(
+ 138,
+ "ULS configuration failed. Subscribe Key not configured."
+ ),
+
+ PUBLISH_KEY_MISSING(
+ 139,
+ "ULS configuration failed. Publish Key not configured."
+ ),
+
+ SUBSCRIBE_TIMEOUT(
+ 130,
+ "Subscribe Timeout"
+ ),
+
+ HTTP_ERROR(
+ 103,
+ "HTTP Error. Please check network connectivity. Please contact support with error details if the issue persists."
+ ),
+
+ MESSAGE_MISSING(
+ 142,
+ "Message Missing"
+ ),
+
+ CHANNEL_MISSING(
+ 132,
+ "Channel Missing"
+ ),
+
+ CRYPTO_ERROR(
+ 135,
+ "Error while encrypting/decrypting message. Please contact support with error details."
+ ),
+
+ STATE_MISSING(
+ 140,
+ "State Missing."
+ ),
+
+ CHANNEL_AND_GROUP_MISSING(
+ 141,
+ "Channel and Group Missing."
+ ),
+
+ PUSH_TYPE_MISSING(
+ 143,
+ "Push Type Missing."
+ ),
+
+ DEVICE_ID_MISSING(
+ 144,
+ "Device ID Missing"
+ ),
+
+ TIMETOKEN_MISSING(
+ 145,
+ "Timetoken Missing."
+ ),
+
+ CHANNELS_TIMETOKEN_MISMATCH(
+ 146,
+ "Channels and timetokens are not equal in size."
+ ),
+
+ USER_MISSING(
+ 147,
+ "User is missing"
+ ),
+
+ USER_ID_MISSING(
+ 148,
+ "User ID is missing"
+ ),
+
+ USER_NAME_MISSING(
+ 149,
+ "User name is missing"
+ ),
+
+ RESOURCES_MISSING(
+ 153,
+ "Resources missing"
+ ),
+
+ PERMISSION_MISSING(
+ 156,
+ "Permission missing"
+ ),
+
+ INVALID_ACCESS_TOKEN(
+ 157,
+ "Invalid access token"
+ ),
+
+ MESSAGE_ACTION_MISSING(
+ 158,
+ "Message action is missing."
+ ),
+
+ MESSAGE_ACTION_TYPE_MISSING(
+ 159,
+ "Message action type is missing."
+ ),
+
+ MESSAGE_ACTION_VALUE_MISSING(
+ 160,
+ "Message action value is missing."
+ ),
+
+ MESSAGE_TIMETOKEN_MISSING(
+ 161,
+ "Message timetoken is missing."
+ ),
+
+ MESSAGE_ACTION_TIMETOKEN_MISSING(
+ 162,
+ "Message action timetoken is missing."
+ ),
+
+ HISTORY_MESSAGE_ACTIONS_MULTIPLE_CHANNELS(
+ 163,
+ "History can return message action data for a single channel only. Either pass a single channel or disable the includeMessageActions flag."
+ ),
+
+ PUSH_TOPIC_MISSING(
+ 164,
+ "Push notification topic is missing. Required only if push type is APNS2."
+ ),
+
+ TOKEN_MISSING(
+ 168,
+ "Token missing"
+ ),
+
+ UUID_NULL_OR_EMPTY(
+ 169,
+ "Uuid can't be null nor empty"
+ ),
+
+ USERID_NULL_OR_EMPTY(
+ 170,
+ "UserId can't have empty value"
+ ),
+
+ CHANNEL_OR_CHANNEL_GROUP_MISSING(
+ 171,
+ "Please, provide channel or channelGroup"
+ ),
+
+ UNKNOWN_CRYPTOR(
+ 172,
+ "Cryptor not found."
+ ),
+
+ CRYPTOR_DATA_HEADER_SIZE_TO_SMALL(
+ 173,
+ "Cryptor data size is to small."
+ ),
+
+ CRYPTOR_HEADER_VERSION_UNKNOWN(
+ 174,
+ "Cryptor header version unknown. Please, update SDK."
+ ),
+
+ CRYPTOR_HEADER_PARSE_ERROR(
+ 175,
+ "Cryptor header parse error."
+ ),
+
+ ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED(
+ 176,
+ "Encryption of empty data not allowed."
+ ),
+
+ ;
+
+ override fun toString(): String {
+ return "PubNubError(name=$name, code=$code, message='$message')"
+ }
+}
diff --git a/src/main/java/com/pubnub/api/crypto/exception/PubNubException.kt b/src/main/java/com/pubnub/api/crypto/exception/PubNubException.kt
new file mode 100644
index 000000000..0bc6e320b
--- /dev/null
+++ b/src/main/java/com/pubnub/api/crypto/exception/PubNubException.kt
@@ -0,0 +1,26 @@
+package com.pubnub.api.crypto.exception
+
+import retrofit2.Call
+
+/**
+ * Custom exception wrapper for errors occurred during execution or processing of a PubNub API operation.
+ *
+ * @property errorMessage The error message received from the server, if any.
+ * @property pubnubError The appropriate matching PubNub error.
+ * @property jso The error json received from the server, if any.
+ * @property statusCode HTTP status code.
+ * @property affectedCall A reference to the affected call. Useful for calling [retry][Endpoint.retry].
+ */
+data class PubNubException(
+ val errorMessage: String? = null,
+ val pubnubError: PubNubError? = null,
+ val jso: String? = null,
+ val statusCode: Int = 0,
+ val affectedCall: Call<*>? = null
+) : Exception(errorMessage) {
+
+ internal constructor(pubnubError: PubNubError) : this(
+ errorMessage = pubnubError.message,
+ pubnubError = pubnubError
+ )
+}
diff --git a/src/main/java/com/pubnub/api/crypto/util/FileEncryptionUtilKT.kt b/src/main/java/com/pubnub/api/crypto/util/FileEncryptionUtilKT.kt
new file mode 100644
index 000000000..a203423f8
--- /dev/null
+++ b/src/main/java/com/pubnub/api/crypto/util/FileEncryptionUtilKT.kt
@@ -0,0 +1,148 @@
+package com.pubnub.api.vendor
+
+import com.pubnub.api.PubNub
+import com.pubnub.api.crypto.exception.PubNubException
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.UnsupportedEncodingException
+import java.security.InvalidAlgorithmParameterException
+import java.security.InvalidKeyException
+import java.security.NoSuchAlgorithmException
+import java.security.SecureRandom
+import java.security.spec.AlgorithmParameterSpec
+import java.util.*
+import javax.crypto.BadPaddingException
+import javax.crypto.Cipher
+import javax.crypto.IllegalBlockSizeException
+import javax.crypto.NoSuchPaddingException
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+object FileEncryptionUtilKT {
+ private const val IV_SIZE_BYTES = 16
+ const val ENCODING_UTF_8 = "UTF-8"
+ const val CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding"
+
+ /**
+ * @see [PubNub.encryptInputStream]
+ */
+ @Throws(PubNubException::class)
+ fun encrypt(inputStream: InputStream, cipherKey: String): InputStream {
+ return encryptToBytes(inputStream.readBytes(), cipherKey).inputStream()
+ }
+
+ /**
+ * @see [PubNub.decryptInputStream]
+ */
+ @Throws(PubNubException::class)
+ fun decrypt(inputStream: InputStream, cipherKey: String): InputStream {
+ return try {
+ val keyBytes = keyBytes(cipherKey)
+ val (ivBytes, dataToDecrypt) = loadIvAndDataFromInputStream(inputStream)
+ val decryptionCipher = decryptionCipher(keyBytes, ivBytes)
+ val decryptedBytes = decryptionCipher.doFinal(dataToDecrypt)
+ ByteArrayInputStream(decryptedBytes)
+ } catch (e: Exception) {
+ when (e) {
+ is NoSuchAlgorithmException,
+ is InvalidAlgorithmParameterException,
+ is NoSuchPaddingException,
+ is InvalidKeyException,
+ is IOException,
+ is IllegalBlockSizeException,
+ is BadPaddingException -> {
+ throw PubNubException(errorMessage = e.message)
+ }
+ else -> throw e
+ }
+ }
+ }
+
+ @Throws(PubNubException::class)
+ internal fun encryptToBytes(bytesToEncrypt: ByteArray, cipherKey: String): ByteArray {
+ try {
+ ByteArrayOutputStream().use { byteArrayOutputStream ->
+ val randomIvBytes = randomIv()
+ byteArrayOutputStream.write(randomIvBytes)
+
+ val keyBytes = keyBytes(cipherKey)
+ val encryptionCipher = encryptionCipher(keyBytes, randomIvBytes)
+ byteArrayOutputStream.write(encryptionCipher.doFinal(bytesToEncrypt))
+ return byteArrayOutputStream.toByteArray()
+ }
+ } catch (e: Exception) {
+ when (e) {
+ is NoSuchAlgorithmException,
+ is InvalidAlgorithmParameterException,
+ is NoSuchPaddingException,
+ is InvalidKeyException,
+ is IOException,
+ is BadPaddingException,
+ is IllegalBlockSizeException -> {
+ throw PubNubException(errorMessage = e.message)
+ }
+ else -> throw e
+ }
+ }
+ }
+
+ @Throws(IOException::class)
+ private fun loadIvAndDataFromInputStream(inputStreamToEncrypt: InputStream): Pair {
+ val ivBytes = ByteArray(IV_SIZE_BYTES)
+ inputStreamToEncrypt.read(ivBytes, 0, IV_SIZE_BYTES)
+ return ivBytes to inputStreamToEncrypt.readBytes()
+ }
+
+ @Throws(
+ NoSuchAlgorithmException::class,
+ NoSuchPaddingException::class,
+ InvalidKeyException::class,
+ InvalidAlgorithmParameterException::class
+ )
+ private fun encryptionCipher(keyBytes: ByteArray, ivBytes: ByteArray): Cipher {
+ return cipher(keyBytes, ivBytes, Cipher.ENCRYPT_MODE)
+ }
+
+ @Throws(
+ NoSuchAlgorithmException::class,
+ NoSuchPaddingException::class,
+ InvalidKeyException::class,
+ InvalidAlgorithmParameterException::class
+ )
+ private fun decryptionCipher(keyBytes: ByteArray, ivBytes: ByteArray): Cipher {
+ return cipher(keyBytes, ivBytes, Cipher.DECRYPT_MODE)
+ }
+
+ @Throws(
+ NoSuchAlgorithmException::class,
+ NoSuchPaddingException::class,
+ InvalidKeyException::class,
+ InvalidAlgorithmParameterException::class
+ )
+ private fun cipher(keyBytes: ByteArray, ivBytes: ByteArray, mode: Int): Cipher {
+ val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
+ val iv: AlgorithmParameterSpec = IvParameterSpec(ivBytes)
+ val key = SecretKeySpec(keyBytes, "AES")
+ cipher.init(mode, key, iv)
+ return cipher
+ }
+
+ @Throws(UnsupportedEncodingException::class, PubNubException::class)
+ private fun keyBytes(cipherKey: String): ByteArray {
+ return String(
+ Crypto.hexEncode(Crypto.sha256(cipherKey.toByteArray(charset(ENCODING_UTF_8)))),
+ charset(ENCODING_UTF_8)
+ )
+ .substring(0, 32)
+ .lowercase(Locale.getDefault()).toByteArray(charset(ENCODING_UTF_8))
+ }
+
+ @Throws(NoSuchAlgorithmException::class)
+ private fun randomIv(): ByteArray {
+ val randomIv = ByteArray(IV_SIZE_BYTES)
+ SecureRandom.getInstance("SHA1PRNG").nextBytes(randomIv)
+ return randomIv
+ }
+}
diff --git a/src/main/java/com/pubnub/api/endpoints/FetchMessages.java b/src/main/java/com/pubnub/api/endpoints/FetchMessages.java
index 2a6f5ef74..a6e1dd7eb 100644
--- a/src/main/java/com/pubnub/api/endpoints/FetchMessages.java
+++ b/src/main/java/com/pubnub/api/endpoints/FetchMessages.java
@@ -6,6 +6,8 @@
import com.pubnub.api.PubNubException;
import com.pubnub.api.PubNubUtil;
import com.pubnub.api.builder.PubNubErrorBuilder;
+import com.pubnub.api.crypto.CryptoModule;
+import com.pubnub.api.crypto.CryptoModuleKt;
import com.pubnub.api.enums.PNOperationType;
import com.pubnub.api.managers.MapperManager;
import com.pubnub.api.managers.RetrofitManager;
@@ -15,7 +17,6 @@
import com.pubnub.api.models.consumer.history.PNFetchMessageItem;
import com.pubnub.api.models.consumer.history.PNFetchMessagesResult;
import com.pubnub.api.models.server.FetchMessagesEnvelope;
-import com.pubnub.api.vendor.Crypto;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
@@ -204,13 +205,12 @@ protected boolean isAuthRequired() {
}
private JsonElement processMessage(JsonElement message) throws PubNubException {
- // if we do not have a crypto key, there is no way to process the node; let's return.
- if (this.getPubnub().getConfiguration().getCipherKey() == null) {
+ // 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) {
return message;
}
- Crypto crypto = new Crypto(this.getPubnub().getConfiguration().getCipherKey(),
- this.getPubnub().getConfiguration().isUseRandomInitializationVector());
MapperManager mapper = this.getPubnub().getMapper();
String inputText;
String outputText;
@@ -222,7 +222,7 @@ private JsonElement processMessage(JsonElement message) throws PubNubException {
inputText = mapper.elementToString(message);
}
- outputText = crypto.decrypt(inputText);
+ outputText = CryptoModuleKt.decryptString(cryptoModule, inputText);
outputObject = mapper.fromJson(outputText, JsonElement.class);
// inject the decoded response into the payload
diff --git a/src/main/java/com/pubnub/api/endpoints/History.java b/src/main/java/com/pubnub/api/endpoints/History.java
index 2a8df8438..de8bf2ce4 100644
--- a/src/main/java/com/pubnub/api/endpoints/History.java
+++ b/src/main/java/com/pubnub/api/endpoints/History.java
@@ -5,6 +5,8 @@
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.crypto.CryptoModuleKt;
import com.pubnub.api.enums.PNOperationType;
import com.pubnub.api.managers.MapperManager;
import com.pubnub.api.managers.RetrofitManager;
@@ -12,7 +14,6 @@
import com.pubnub.api.managers.token_manager.TokenManager;
import com.pubnub.api.models.consumer.history.PNHistoryItemResult;
import com.pubnub.api.models.consumer.history.PNHistoryResult;
-import com.pubnub.api.vendor.Crypto;
import lombok.Setter;
import lombok.experimental.Accessors;
import retrofit2.Call;
@@ -170,12 +171,12 @@ protected boolean isAuthRequired() {
}
private JsonElement processMessage(JsonElement message) throws PubNubException {
- // if we do not have a crypto key, there is no way to process the node; let's return.
- if (this.getPubnub().getConfiguration().getCipherKey() == null) {
+ // 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) {
return message;
}
- Crypto crypto = new Crypto(this.getPubnub().getConfiguration().getCipherKey(), this.getPubnub().getConfiguration().isUseRandomInitializationVector());
MapperManager mapper = getPubnub().getMapper();
String inputText;
String outputText;
@@ -187,7 +188,7 @@ private JsonElement processMessage(JsonElement message) throws PubNubException {
inputText = mapper.elementToString(message);
}
- outputText = crypto.decrypt(inputText);
+ outputText = CryptoModuleKt.decryptString(cryptoModule, inputText);
outputObject = this.getPubnub().getMapper().fromJson(outputText, JsonElement.class);
// inject the decoded response into the payload
diff --git a/src/main/java/com/pubnub/api/endpoints/files/DownloadFile.java b/src/main/java/com/pubnub/api/endpoints/files/DownloadFile.java
index ae8411f7e..21e8eacc0 100644
--- a/src/main/java/com/pubnub/api/endpoints/files/DownloadFile.java
+++ b/src/main/java/com/pubnub/api/endpoints/files/DownloadFile.java
@@ -3,9 +3,10 @@
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.endpoints.BuilderSteps.ChannelStep;
import com.pubnub.api.endpoints.Endpoint;
import com.pubnub.api.endpoints.files.requiredparambuilder.ChannelFileNameFileIdBuilder;
-import com.pubnub.api.endpoints.BuilderSteps.ChannelStep;
import com.pubnub.api.endpoints.files.requiredparambuilder.FilesBuilderSteps.FileIdStep;
import com.pubnub.api.endpoints.files.requiredparambuilder.FilesBuilderSteps.FileNameStep;
import com.pubnub.api.enums.PNOperationType;
@@ -13,7 +14,6 @@
import com.pubnub.api.managers.TelemetryManager;
import com.pubnub.api.managers.token_manager.TokenManager;
import com.pubnub.api.models.consumer.files.PNDownloadFileResult;
-import com.pubnub.api.vendor.FileEncryptionUtil;
import lombok.Setter;
import lombok.experimental.Accessors;
import okhttp3.ResponseBody;
@@ -25,7 +25,7 @@
import java.util.List;
import java.util.Map;
-import static com.pubnub.api.vendor.FileEncryptionUtil.effectiveCipherKey;
+import static com.pubnub.api.vendor.FileEncryptionUtil.effectiveCryptoModule;
@Accessors(chain = true, fluent = true)
public class DownloadFile extends Endpoint {
@@ -87,11 +87,12 @@ protected PNDownloadFileResult createResponse(Response input) thro
.pubnubError(PubNubErrorBuilder.PNERROBJ_INTERNAL_ERROR)
.build();
}
- String effectiveCipherKey = effectiveCipherKey(getPubnub(), cipherKey);
- if (effectiveCipherKey == null) {
- return new PNDownloadFileResult(fileName, input.body().byteStream());
+ CryptoModule cryptoModule = effectiveCryptoModule(getPubnub(), cipherKey);
+ InputStream byteStream = input.body().byteStream();
+ if (cryptoModule == null) {
+ return new PNDownloadFileResult(fileName, byteStream);
} else {
- InputStream decryptedByteStream = FileEncryptionUtil.decrypt(effectiveCipherKey, input.body().byteStream());
+ InputStream decryptedByteStream = cryptoModule.decryptStream(byteStream);
return new PNDownloadFileResult(fileName, decryptedByteStream);
}
}
diff --git a/src/main/java/com/pubnub/api/endpoints/files/PublishFileMessage.java b/src/main/java/com/pubnub/api/endpoints/files/PublishFileMessage.java
index 583d3f7aa..63c290817 100644
--- a/src/main/java/com/pubnub/api/endpoints/files/PublishFileMessage.java
+++ b/src/main/java/com/pubnub/api/endpoints/files/PublishFileMessage.java
@@ -5,11 +5,13 @@
import com.pubnub.api.PubNubException;
import com.pubnub.api.PubNubUtil;
import com.pubnub.api.builder.PubNubErrorBuilder;
-import com.pubnub.api.endpoints.Endpoint;
+import com.pubnub.api.crypto.CryptoModule;
+import com.pubnub.api.crypto.CryptoModuleKt;
import com.pubnub.api.endpoints.BuilderSteps.ChannelStep;
+import com.pubnub.api.endpoints.Endpoint;
+import com.pubnub.api.endpoints.files.requiredparambuilder.ChannelFileNameFileIdBuilder;
import com.pubnub.api.endpoints.files.requiredparambuilder.FilesBuilderSteps.FileIdStep;
import com.pubnub.api.endpoints.files.requiredparambuilder.FilesBuilderSteps.FileNameStep;
-import com.pubnub.api.endpoints.files.requiredparambuilder.ChannelFileNameFileIdBuilder;
import com.pubnub.api.enums.PNOperationType;
import com.pubnub.api.managers.MapperManager;
import com.pubnub.api.managers.RetrofitManager;
@@ -19,7 +21,6 @@
import com.pubnub.api.models.consumer.files.PNPublishFileMessageResult;
import com.pubnub.api.models.server.files.FileUploadNotification;
import com.pubnub.api.services.FilesService;
-import com.pubnub.api.vendor.Crypto;
import lombok.Setter;
import lombok.experimental.Accessors;
import retrofit2.Call;
@@ -87,9 +88,10 @@ protected void validateParams() throws PubNubException {
protected Call> doWork(Map baseParams) throws PubNubException {
String stringifiedMessage = mapper.toJsonUsinJackson(new FileUploadNotification(this.message, pnFile));
String messageAsString;
- if (getPubnub().getConfiguration().getCipherKey() != null) {
- Crypto crypto = new Crypto(getPubnub().getConfiguration().getCipherKey(), getPubnub().getConfiguration().isUseRandomInitializationVector());
- messageAsString = "\"".concat(crypto.encrypt(stringifiedMessage)).concat("\"");
+ CryptoModule cryptoModule = getPubnub().getCryptoModule();
+ if (cryptoModule != null) {
+ String encryptString = CryptoModuleKt.encryptString(cryptoModule, stringifiedMessage);
+ messageAsString = "\"".concat(encryptString).concat("\"");
} else {
messageAsString = PubNubUtil.urlEncode(stringifiedMessage);
}
diff --git a/src/main/java/com/pubnub/api/endpoints/files/SendFile.java b/src/main/java/com/pubnub/api/endpoints/files/SendFile.java
index ab4012698..69bcc8101 100644
--- a/src/main/java/com/pubnub/api/endpoints/files/SendFile.java
+++ b/src/main/java/com/pubnub/api/endpoints/files/SendFile.java
@@ -4,6 +4,7 @@
import com.pubnub.api.PubNubException;
import com.pubnub.api.builder.PubNubErrorBuilder;
import com.pubnub.api.callbacks.PNCallback;
+import com.pubnub.api.crypto.CryptoModule;
import com.pubnub.api.endpoints.BuilderSteps.ChannelStep;
import com.pubnub.api.endpoints.files.requiredparambuilder.FilesBuilderSteps.FileIdStep;
import com.pubnub.api.endpoints.files.requiredparambuilder.FilesBuilderSteps.FileNameStep;
@@ -22,6 +23,7 @@
import com.pubnub.api.models.consumer.files.PNFileUploadResult;
import com.pubnub.api.models.consumer.files.PNPublishFileMessageResult;
import com.pubnub.api.models.server.files.FileUploadRequestDetails;
+import com.pubnub.api.vendor.FileEncryptionUtil;
import lombok.Data;
import lombok.Setter;
import lombok.experimental.Accessors;
@@ -55,13 +57,16 @@ public class SendFile implements RemoteAction {
private Boolean shouldStore;
@Setter
private String cipherKey;
+ private CryptoModule cryptoModule;
SendFile(Builder.SendFileRequiredParams requiredParams,
GenerateUploadUrl.Factory generateUploadUrlFactory,
ChannelStep>> publishFileMessageBuilder,
UploadFile.Factory sendFileToS3Factory,
ExecutorService executorService,
- int fileMessagePublishRetryLimit) {
+ int fileMessagePublishRetryLimit,
+ CryptoModule cryptoModule
+ ) {
this.channel = requiredParams.channel();
this.fileName = requiredParams.fileName();
this.content = requiredParams.content();
@@ -72,6 +77,7 @@ public class SendFile implements RemoteAction {
generateUploadUrlFactory,
publishFileMessageBuilder,
sendFileToS3Factory);
+ this.cryptoModule = FileEncryptionUtil.effectiveCryptoModule(cryptoModule, cipherKey);
}
public PNFileUploadResult sync() throws PubNubException {
@@ -172,7 +178,7 @@ public void silentCancel() {
private RemoteAction sendToS3(FileUploadRequestDetails result,
UploadFile.Factory sendFileToS3Factory) {
- return sendFileToS3Factory.create(fileName, content, cipherKey, result);
+ return sendFileToS3Factory.create(fileName, content, cryptoModule, result);
}
public static Builder builder(PubNub pubnub,
@@ -251,7 +257,8 @@ public SendFile inputStream(InputStream inputStream) {
publishFileMessageBuilder,
uploadFileFactory,
retrofit.getTransactionClientExecutorService(),
- pubnub.getConfiguration().getFileMessagePublishRetryLimit());
+ pubnub.getConfiguration().getFileMessagePublishRetryLimit(),
+ pubnub.getCryptoModule());
} catch (IOException e) {
return new SendFile(new SendFileRequiredParams(channelValue,
@@ -262,7 +269,9 @@ public SendFile inputStream(InputStream inputStream) {
publishFileMessageBuilder,
uploadFileFactory,
retrofit.getTransactionClientExecutorService(),
- pubnub.getConfiguration().getFileMessagePublishRetryLimit());
+ pubnub.getConfiguration().getFileMessagePublishRetryLimit(),
+ pubnub.getCryptoModule()
+ );
}
}
}
diff --git a/src/main/java/com/pubnub/api/endpoints/files/UploadFile.java b/src/main/java/com/pubnub/api/endpoints/files/UploadFile.java
index 60a27e83c..58902868e 100644
--- a/src/main/java/com/pubnub/api/endpoints/files/UploadFile.java
+++ b/src/main/java/com/pubnub/api/endpoints/files/UploadFile.java
@@ -4,6 +4,7 @@
import com.pubnub.api.PubNubException;
import com.pubnub.api.builder.PubNubErrorBuilder;
import com.pubnub.api.callbacks.PNCallback;
+import com.pubnub.api.crypto.CryptoModule;
import com.pubnub.api.endpoints.remoteaction.RemoteAction;
import com.pubnub.api.enums.PNOperationType;
import com.pubnub.api.enums.PNStatusCategory;
@@ -13,7 +14,6 @@
import com.pubnub.api.models.server.files.FileUploadRequestDetails;
import com.pubnub.api.models.server.files.FormField;
import com.pubnub.api.services.S3Service;
-import com.pubnub.api.vendor.FileEncryptionUtil;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
@@ -37,8 +37,6 @@
import java.net.UnknownHostException;
import java.util.List;
-import static com.pubnub.api.vendor.FileEncryptionUtil.effectiveCipherKey;
-
@Slf4j
class UploadFile implements RemoteAction {
private static final MediaType APPLICATION_OCTET_STREAM = MediaType.get("application/octet-stream");
@@ -47,7 +45,7 @@ class UploadFile implements RemoteAction {
private final S3Service s3Service;
private final String fileName;
private final byte[] content;
- private final String cipherKey;
+ private final CryptoModule cryptoModule;
private final FormField key;
private final List formParams;
private final String baseUrl;
@@ -56,14 +54,14 @@ class UploadFile implements RemoteAction {
UploadFile(S3Service s3Service,
String fileName,
byte[] content,
- String cipherKey,
+ CryptoModule cryptoModule,
FormField key,
List formParams,
String baseUrl) {
this.s3Service = s3Service;
this.fileName = fileName;
this.content = content;
- this.cipherKey = cipherKey;
+ this.cryptoModule = cryptoModule;
this.key = key;
this.formParams = formParams;
this.baseUrl = baseUrl;
@@ -86,10 +84,10 @@ private Call prepareCall() throws PubNubException, IOException {
MediaType mediaType = getMediaType(getContentType(formParams));
RequestBody requestBody;
- if (cipherKey == null) {
+ if (cryptoModule == null) {
requestBody = RequestBody.create(content, mediaType);
} else {
- requestBody = RequestBody.create(FileEncryptionUtil.encryptToBytes(cipherKey, content), mediaType);
+ requestBody = RequestBody.create(cryptoModule.encrypt(content), mediaType);
}
builder.addFormDataPart(FILE_PART_MULTIPART, fileName, requestBody);
@@ -115,7 +113,7 @@ private MediaType getMediaType(@Nullable String contentType) {
try {
return MediaType.get(contentType);
- } catch (Throwable t) {
+ } catch (Throwable t) {
log.warn("Content-Type: " + contentType + " was not recognized by MediaType.get", t);
return APPLICATION_OCTET_STREAM;
}
@@ -164,7 +162,7 @@ public void onResponse(@NotNull Call performedCall, @NotNull Response create(String fileName,
byte[] content,
- String cipherKey,
+ CryptoModule cryptoModule,
FileUploadRequestDetails fileUploadRequestDetails) {
- String effectiveCipherKey = effectiveCipherKey(pubNub, cipherKey);
+
return new UploadFile(retrofitManager.getS3Service(),
fileName,
content,
- effectiveCipherKey,
+ cryptoModule,
fileUploadRequestDetails.getKeyFormField(), fileUploadRequestDetails.getFormFields(),
fileUploadRequestDetails.getUrl());
}
diff --git a/src/main/java/com/pubnub/api/endpoints/pubsub/Publish.java b/src/main/java/com/pubnub/api/endpoints/pubsub/Publish.java
index b37110f32..ebcd4d478 100644
--- a/src/main/java/com/pubnub/api/endpoints/pubsub/Publish.java
+++ b/src/main/java/com/pubnub/api/endpoints/pubsub/Publish.java
@@ -4,6 +4,8 @@
import com.pubnub.api.PubNubException;
import com.pubnub.api.PubNubUtil;
import com.pubnub.api.builder.PubNubErrorBuilder;
+import com.pubnub.api.crypto.CryptoModule;
+import com.pubnub.api.crypto.CryptoModuleKt;
import com.pubnub.api.endpoints.Endpoint;
import com.pubnub.api.enums.PNOperationType;
import com.pubnub.api.managers.MapperManager;
@@ -12,7 +14,6 @@
import com.pubnub.api.managers.TelemetryManager;
import com.pubnub.api.managers.token_manager.TokenManager;
import com.pubnub.api.models.consumer.PNPublishResult;
-import com.pubnub.api.vendor.Crypto;
import lombok.Setter;
import lombok.experimental.Accessors;
import retrofit2.Call;
@@ -109,9 +110,9 @@ protected Call> doWork(Map params) throws PubNubExc
params.put("norep", "true");
}
- if (this.getPubnub().getConfiguration().getCipherKey() != null) {
- Crypto crypto = new Crypto(this.getPubnub().getConfiguration().getCipherKey(), this.getPubnub().getConfiguration().isUseRandomInitializationVector());
- stringifiedMessage = crypto.encrypt(stringifiedMessage).replace("\n", "");
+ CryptoModule cryptoModule = this.getPubnub().getCryptoModule();
+ if (cryptoModule != null) {
+ stringifiedMessage = CryptoModuleKt.encryptString(cryptoModule, stringifiedMessage).replace("\n", "");
}
params.putAll(encodeParams(params));
@@ -119,7 +120,7 @@ protected Call> doWork(Map params) throws PubNubExc
if (usePOST != null && usePOST) {
Object payloadToSend;
- if (this.getPubnub().getConfiguration().getCipherKey() != null) {
+ if (cryptoModule != null) {
payloadToSend = stringifiedMessage;
} else {
payloadToSend = message;
@@ -130,7 +131,7 @@ protected Call> doWork(Map params) throws PubNubExc
channel, payloadToSend, params);
} else {
- if (this.getPubnub().getConfiguration().getCipherKey() != null) {
+ if (cryptoModule != null) {
stringifiedMessage = "\"".concat(stringifiedMessage).concat("\"");
}
diff --git a/src/main/java/com/pubnub/api/vendor/Crypto.java b/src/main/java/com/pubnub/api/vendor/Crypto.java
index accbeb1ef..ae15e3b81 100644
--- a/src/main/java/com/pubnub/api/vendor/Crypto.java
+++ b/src/main/java/com/pubnub/api/vendor/Crypto.java
@@ -169,35 +169,6 @@ public String decrypt(String cipher_text) throws PubNubException {
}
}
- public static byte[] hexStringToByteArray(String s) {
- int len = s.length();
- byte[] data = new byte[len / 2];
- for (int i = 0; i < len; i += 2) {
- data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
- }
- return data;
- }
-
- /**
- * Get MD5
- *
- * @param input
- * @return byte[]
- * @throws PubNubException
- */
- public static byte[] md5(String input) throws PubNubException {
- MessageDigest digest;
- try {
- digest = MessageDigest.getInstance("MD5");
- byte[] hashedBytes = digest.digest(input.getBytes(ENCODING_UTF_8));
- return hashedBytes;
- } catch (NoSuchAlgorithmException e) {
- throw PubNubException.builder().pubnubError(newCryptoError(118, e.toString())).errormsg(e.getMessage()).cause(e).build();
- } catch (UnsupportedEncodingException e) {
- throw PubNubException.builder().pubnubError(newCryptoError(119, e.toString())).errormsg(e.getMessage()).cause(e).build();
- }
- }
-
/**
* Get SHA256
*
diff --git a/src/main/java/com/pubnub/api/vendor/FileEncryptionUtil.java b/src/main/java/com/pubnub/api/vendor/FileEncryptionUtil.java
index 1c4ff005f..e418242e6 100644
--- a/src/main/java/com/pubnub/api/vendor/FileEncryptionUtil.java
+++ b/src/main/java/com/pubnub/api/vendor/FileEncryptionUtil.java
@@ -1,26 +1,9 @@
package com.pubnub.api.vendor;
import com.pubnub.api.PubNub;
-import com.pubnub.api.PubNubException;
+import com.pubnub.api.crypto.CryptoModule;
import lombok.Data;
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-import java.io.*;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.security.spec.AlgorithmParameterSpec;
-
-import static com.pubnub.api.PubNubUtil.readBytes;
-import static com.pubnub.api.vendor.Crypto.hexEncode;
-import static com.pubnub.api.vendor.Crypto.sha256;
-
public final class FileEncryptionUtil {
private static final int IV_SIZE_BYTES = 16;
public static final int BUFFER_SIZE_BYTES = 8192;
@@ -33,111 +16,15 @@ private static class IvAndData {
final byte[] dataToDecrypt;
}
- private FileEncryptionUtil() {}
+ public static CryptoModule effectiveCryptoModule(PubNub pubNub, String cipherKey) {
+ return effectiveCryptoModule(pubNub.getCryptoModule(), cipherKey);
+ }
- public static String effectiveCipherKey(PubNub pubNub, String cipherKey) {
+ public static CryptoModule effectiveCryptoModule(CryptoModule cryptoModule, String cipherKey) {
if (cipherKey != null) {
- return cipherKey;
- } else if (pubNub.getConfiguration().getCipherKey() != null) {
- return pubNub.getConfiguration().getCipherKey();
+ return CryptoModule.createLegacyCryptoModule(cipherKey, true);
} else {
- return null;
- }
- }
-
- public static byte[] encryptToBytes(final String cipherKey, final byte[] bytesToEncrypt)
- throws PubNubException {
- try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
- final byte[] keyBytes = keyBytes(cipherKey);
- final byte[] randomIvBytes = randomIv();
- final Cipher encryptionCipher = encryptionCipher(keyBytes, randomIvBytes);
-
- byteArrayOutputStream.write(randomIvBytes);
- byteArrayOutputStream.write(encryptionCipher.doFinal(bytesToEncrypt));
- return byteArrayOutputStream.toByteArray();
- } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException |
- InvalidKeyException | IOException | BadPaddingException | IllegalBlockSizeException e) {
- throw PubNubException.builder().errormsg(e.toString()).build();
- }
- }
-
- public static InputStream encrypt(final String cipherKey, final InputStream inputStreamToEncrypt)
- throws PubNubException {
-
- try {
- return new ByteArrayInputStream(encryptToBytes(cipherKey, readBytes(inputStreamToEncrypt)));
- } catch (IOException e) {
- throw PubNubException.builder()
- .errormsg(e.getMessage())
- .cause(e)
- .build();
- }
- }
-
- public static InputStream decrypt(final String cipherKey, final InputStream encryptedInputStream)
- throws PubNubException {
- try {
- final byte[] keyBytes = keyBytes(cipherKey);
- final IvAndData ivAndData = loadIvAndDataFromInputStream(encryptedInputStream);
- final Cipher decryptionCipher = decryptionCipher(keyBytes, ivAndData.ivBytes);
- byte[] decryptedBytes = decryptionCipher.doFinal(ivAndData.dataToDecrypt);
- return new ByteArrayInputStream(decryptedBytes);
- } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException
- | InvalidKeyException | IOException | IllegalBlockSizeException | BadPaddingException e) {
- throw PubNubException.builder().errormsg(e.toString()).cause(e).build();
+ return cryptoModule;
}
}
-
- private static IvAndData loadIvAndDataFromInputStream(final InputStream inputStreamToEncrypt) throws IOException {
- final byte[] ivBytes = new byte[IV_SIZE_BYTES];
- {
- int read;
- int readSoFar = 0;
- do {
- read = inputStreamToEncrypt.read(ivBytes, readSoFar, IV_SIZE_BYTES - readSoFar);
- if (read != -1) {
- readSoFar += read;
- }
- } while (read != -1 && readSoFar < IV_SIZE_BYTES);
- if (read == -1) {
- throw new IOException("EOF before IV fully read");
- }
- }
-
- return new IvAndData(ivBytes, readBytes(inputStreamToEncrypt));
- }
-
- private static Cipher encryptionCipher(final byte[] keyBytes, final byte[] ivBytes)
- throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
- InvalidAlgorithmParameterException {
- return cipher(keyBytes, ivBytes, Cipher.ENCRYPT_MODE);
- }
-
- private static Cipher decryptionCipher(final byte[] keyBytes, final byte[] ivBytes)
- throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
- InvalidAlgorithmParameterException {
- return cipher(keyBytes, ivBytes, Cipher.DECRYPT_MODE);
- }
-
- private static Cipher cipher(final byte[] keyBytes, final byte[] ivBytes, final int mode)
- throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
- InvalidAlgorithmParameterException {
- Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
- AlgorithmParameterSpec iv = new IvParameterSpec(ivBytes);
- SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
- cipher.init(mode, key, iv);
- return cipher;
- }
-
- private static byte[] keyBytes(final String cipherKey) throws UnsupportedEncodingException, PubNubException {
- return new String(hexEncode(sha256(cipherKey.getBytes(ENCODING_UTF_8))), ENCODING_UTF_8)
- .substring(0, 32)
- .toLowerCase().getBytes(ENCODING_UTF_8);
- }
-
- private static byte[] randomIv() throws NoSuchAlgorithmException {
- byte[] randomIv = new byte[IV_SIZE_BYTES];
- SecureRandom.getInstance("SHA1PRNG").nextBytes(randomIv);
- return randomIv;
- }
}
diff --git a/src/main/java/com/pubnub/api/workers/SubscribeMessageProcessor.java b/src/main/java/com/pubnub/api/workers/SubscribeMessageProcessor.java
index 2cc823653..135f3d1b3 100644
--- a/src/main/java/com/pubnub/api/workers/SubscribeMessageProcessor.java
+++ b/src/main/java/com/pubnub/api/workers/SubscribeMessageProcessor.java
@@ -8,6 +8,8 @@
import com.pubnub.api.PubNub;
import com.pubnub.api.PubNubException;
import com.pubnub.api.PubNubUtil;
+import com.pubnub.api.crypto.CryptoModule;
+import com.pubnub.api.crypto.CryptoModuleKt;
import com.pubnub.api.managers.DuplicationManager;
import com.pubnub.api.managers.MapperManager;
import com.pubnub.api.models.consumer.files.PNDownloadableFile;
@@ -18,7 +20,11 @@
import com.pubnub.api.models.consumer.objects_api.membership.PNMembershipResult;
import com.pubnub.api.models.consumer.objects_api.uuid.PNUUIDMetadata;
import com.pubnub.api.models.consumer.objects_api.uuid.PNUUIDMetadataResult;
-import com.pubnub.api.models.consumer.pubsub.*;
+import com.pubnub.api.models.consumer.pubsub.BasePubSubResult;
+import com.pubnub.api.models.consumer.pubsub.PNEvent;
+import com.pubnub.api.models.consumer.pubsub.PNMessageResult;
+import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult;
+import com.pubnub.api.models.consumer.pubsub.PNSignalResult;
import com.pubnub.api.models.consumer.pubsub.files.PNFileEventResult;
import com.pubnub.api.models.consumer.pubsub.message_actions.PNMessageActionResult;
import com.pubnub.api.models.consumer.pubsub.objects.ObjectPayload;
@@ -27,7 +33,6 @@
import com.pubnub.api.models.server.SubscribeMessage;
import com.pubnub.api.models.server.files.FileUploadNotification;
import com.pubnub.api.services.FilesService;
-import com.pubnub.api.vendor.Crypto;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -191,8 +196,9 @@ PNEvent processIncomingPayload(SubscribeMessage message) throws PubNubException
private JsonElement processMessage(SubscribeMessage subscribeMessage) throws PubNubException {
JsonElement input = subscribeMessage.getPayload();
- // if we do not have a crypto key, there is no way to process the node; let's return.
- if (pubnub.getConfiguration().getCipherKey() == null) {
+ // if we do not have a crypto module, there is no way to process the node; let's return.
+ CryptoModule cryptoModule = pubnub.getCryptoModule();
+ if (cryptoModule == null) {
return input;
}
@@ -202,8 +208,6 @@ private JsonElement processMessage(SubscribeMessage subscribeMessage) throws Pub
return input;
}
- Crypto crypto = new Crypto(pubnub.getConfiguration().getCipherKey(),
- pubnub.getConfiguration().isUseRandomInitializationVector());
MapperManager mapper = this.pubnub.getMapper();
String inputText;
String outputText;
@@ -215,7 +219,7 @@ private JsonElement processMessage(SubscribeMessage subscribeMessage) throws Pub
inputText = mapper.elementToString(input);
}
- outputText = crypto.decrypt(inputText);
+ outputText = CryptoModuleKt.decryptString(cryptoModule, inputText);
outputObject = mapper.fromJson(outputText, JsonElement.class);
diff --git a/src/test/java/com/pubnub/api/PubNubTest.java b/src/test/java/com/pubnub/api/PubNubTest.java
index d68d0a089..109256c2d 100644
--- a/src/test/java/com/pubnub/api/PubNubTest.java
+++ b/src/test/java/com/pubnub/api/PubNubTest.java
@@ -100,7 +100,7 @@ public void getVersionAndTimeStamp() {
pubnub = new PubNub(pnConfiguration);
String version = pubnub.getVersion();
int timeStamp = pubnub.getTimestamp();
- Assert.assertEquals("6.3.6", version);
+ Assert.assertEquals("6.4.0", version);
Assert.assertTrue(timeStamp > 0);
}
diff --git a/src/test/java/com/pubnub/api/endpoints/files/SendFileTest.java b/src/test/java/com/pubnub/api/endpoints/files/SendFileTest.java
index ff187137c..82e473e73 100644
--- a/src/test/java/com/pubnub/api/endpoints/files/SendFileTest.java
+++ b/src/test/java/com/pubnub/api/endpoints/files/SendFileTest.java
@@ -3,6 +3,7 @@
import com.pubnub.api.PubNub;
import com.pubnub.api.PubNubException;
import com.pubnub.api.callbacks.PNCallback;
+import com.pubnub.api.crypto.CryptoModule;
import com.pubnub.api.endpoints.remoteaction.TestRemoteAction;
import com.pubnub.api.managers.RetrofitManager;
import com.pubnub.api.managers.token_manager.TokenManager;
@@ -190,7 +191,8 @@ private SendFile sendFile(String channel, String fileName, InputStream inputStre
publishFileMessageBuilder,
sendFileToS3Factory,
Executors.newSingleThreadExecutor(),
- numberOfRetries
+ numberOfRetries,
+ CryptoModule.createLegacyCryptoModule("enigma", true)
);
}
diff --git a/src/test/java/com/pubnub/api/vendor/CryptoTest.java b/src/test/java/com/pubnub/api/vendor/CryptoTest.java
deleted file mode 100644
index 56d7395c9..000000000
--- a/src/test/java/com/pubnub/api/vendor/CryptoTest.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.pubnub.api.vendor;
-
-import com.pubnub.api.PubNubException;
-import org.apache.commons.io.IOUtils;
-import org.junit.Test;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Random;
-
-import static org.hamcrest.Matchers.*;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-public class CryptoTest {
- private static final int MAX_FILE_SIZE_IN_BYTES = 1024 * 1024 * 5;
-
- @Test
- public void canDecryptWhatIsEncrypted() throws IOException, PubNubException {
- //given
- final String cipherKey = "enigma";
- final byte[] byteArrayToEncrypt = byteArrayToEncrypt();
- byte[] decryptedByteArray;
-
- //when
- final byte[] encryptedByteArray = FileEncryptionUtil.encryptToBytes(cipherKey,
- byteArrayToEncrypt);
- try (InputStream decryptedInputStream = FileEncryptionUtil.decrypt(cipherKey,
- new ByteArrayInputStream(encryptedByteArray))) {
- try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
- IOUtils.copy(decryptedInputStream, byteArrayOutputStream);
- decryptedByteArray = byteArrayOutputStream.toByteArray();
- }
- }
-
- //then
- assertThat(decryptedByteArray, allOf(
- equalTo(byteArrayToEncrypt),
- not(equalTo(encryptedByteArray))));
- }
-
- private byte[] byteArrayToEncrypt() {
- final Random random = new Random();
- final int fileSize = random.nextInt(MAX_FILE_SIZE_IN_BYTES);
- byte[] fileContents = new byte[fileSize];
- random.nextBytes(fileContents);
- return fileContents;
- }
-}
\ No newline at end of file
diff --git a/src/test/java/com/pubnub/contract/ContractTestConfig.kt b/src/test/java/com/pubnub/contract/ContractTestConfig.kt
index 96a26dde7..b5404ecae 100644
--- a/src/test/java/com/pubnub/contract/ContractTestConfig.kt
+++ b/src/test/java/com/pubnub/contract/ContractTestConfig.kt
@@ -30,6 +30,10 @@ interface ContractTestConfig : Config {
@Config.Key("dataFileLocation")
@Config.DefaultValue("src/test/resources/sdk-specifications/features/data")
fun dataFileLocation(): String
+
+ @Config.Key("cryptoFilesLocation")
+ @Config.DefaultValue("src/test/resources/sdk-specifications/features/encryption/assets")
+ fun cryptoFilesLocation(): String
}
val CONTRACT_TEST_CONFIG: ContractTestConfig = ConfigFactory.create(ContractTestConfig::class.java, System.getenv())
diff --git a/src/test/java/com/pubnub/contract/Utils.kt b/src/test/java/com/pubnub/contract/Utils.kt
new file mode 100644
index 000000000..fd1cf767b
--- /dev/null
+++ b/src/test/java/com/pubnub/contract/Utils.kt
@@ -0,0 +1,9 @@
+package com.pubnub.contract
+
+import java.nio.file.Files
+import java.nio.file.Paths
+
+fun getFileContentAsByteArray(fileName: String): ByteArray {
+ val cryptoFileLocation = CONTRACT_TEST_CONFIG.cryptoFilesLocation()
+ return Files.readAllBytes(Paths.get(cryptoFileLocation, fileName))
+}
diff --git a/src/test/java/com/pubnub/contract/crypto/CryptoModuleState.kt b/src/test/java/com/pubnub/contract/crypto/CryptoModuleState.kt
new file mode 100644
index 000000000..da8cb0f14
--- /dev/null
+++ b/src/test/java/com/pubnub/contract/crypto/CryptoModuleState.kt
@@ -0,0 +1,16 @@
+package com.pubnub.contract.crypto
+
+import com.pubnub.api.crypto.exception.PubNubError
+
+class CryptoModuleState {
+ var defaultCryptorType: String? = null
+ var decryptionOnlyCryptorType: String? = null
+ var cryptorCipherKey: String? = null
+ var initializationVectorType: String? = null
+ var decryptionError: PubNubError? = null
+ var encryptionError: PubNubError? = null
+ var encryptedData: ByteArray? = null
+ var decryptedData: ByteArray? = null
+ var fileContent: ByteArray? = null
+ var encryptionType: String? = null
+}
diff --git a/src/test/java/com/pubnub/contract/crypto/CryptoModuleSteps.kt b/src/test/java/com/pubnub/contract/crypto/CryptoModuleSteps.kt
new file mode 100644
index 000000000..ae277175b
--- /dev/null
+++ b/src/test/java/com/pubnub/contract/crypto/CryptoModuleSteps.kt
@@ -0,0 +1,212 @@
+package com.pubnub.contract.crypto
+
+import com.pubnub.api.crypto.CryptoModule
+import com.pubnub.api.crypto.cryptor.AesCbcCryptor
+import com.pubnub.api.crypto.cryptor.Cryptor
+import com.pubnub.api.crypto.cryptor.LegacyCryptor
+import com.pubnub.api.crypto.exception.PubNubError
+import com.pubnub.api.crypto.exception.PubNubException
+import com.pubnub.api.vendor.Base64
+import com.pubnub.api.vendor.Crypto
+import com.pubnub.api.vendor.FileEncryptionUtilKT
+import com.pubnub.contract.getFileContentAsByteArray
+import io.cucumber.java.en.Given
+import io.cucumber.java.en.Then
+import io.cucumber.java.en.When
+import org.junit.jupiter.api.Assertions.assertArrayEquals
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.Assertions.assertTrue
+import java.io.ByteArrayInputStream
+
+private const val LEGACY_NEW = "legacy"
+private const val AES_CBC = "acrh"
+private const val RANDOM_IV = "random"
+private const val CRYPTION_TYPE_BINARY = "binary" // not a stream
+private const val CRYPTION_TYPE_STREAM = "stream"
+
+class CryptoModuleSteps(
+ private val cryptoModuleState: CryptoModuleState,
+) {
+
+ @Given("Crypto module with {string} cryptor")
+ fun crypto_module_with_cryptor(cryptorType: String) {
+ cryptoModuleState.defaultCryptorType = cryptorType
+ }
+
+ @Given("with {string} cipher key")
+ fun cryptor_with_cipher_key(cipherKey: String) {
+ cryptoModuleState.cryptorCipherKey = cipherKey
+ }
+
+ @Given("with {string} vector")
+ fun cryptor_with_initialization_vector(initializationVectorType: String) {
+ cryptoModuleState.initializationVectorType = initializationVectorType
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ @Given("Legacy code with {string} cipher key and {string} vector")
+ fun legacy_code_with_cipher_key_and_vector(cipherKey: String, initializationVectorType: String) {
+ // this is fine, nothing here
+ }
+
+ @Given("Crypto module with default {string} and additional {string} cryptors")
+ fun crypto_module_with_default_cryptor_and_additional_cryptor(
+ defaultCryptorType: String,
+ decryptionCryptorType: String
+ ) {
+ cryptoModuleState.defaultCryptorType = defaultCryptorType
+ cryptoModuleState.decryptionOnlyCryptorType = decryptionCryptorType
+ }
+
+ @When("I decrypt {string} file")
+ fun I_decrypt_file(fileName: String) {
+ val encryptedFileContent = getFileContentAsByteArray(fileName)
+ var cryptoModule: CryptoModule? = null
+ if (cryptoModuleState.defaultCryptorType == AES_CBC) {
+ cryptoModule = CryptoModule.createNewCryptoModule(AesCbcCryptor(cryptoModuleState.cryptorCipherKey!!))
+ } else if (cryptoModuleState.defaultCryptorType == LEGACY_NEW) {
+ cryptoModule = CryptoModule.createNewCryptoModule(LegacyCryptor(cryptoModuleState.cryptorCipherKey!!))
+ }
+
+ try {
+ cryptoModule?.decrypt(encryptedData = encryptedFileContent)
+ } catch (e: PubNubException) {
+ cryptoModuleState.decryptionError = e.pubnubError
+ }
+ }
+
+ @When("I encrypt {string} file as {string}")
+ fun I_encrypt_file(fileName: String, encryptionType: String) {
+ val notEncryptedFileContent = getFileContentAsByteArray(fileName)
+ cryptoModuleState.fileContent = notEncryptedFileContent
+ cryptoModuleState.encryptionType = encryptionType
+ val cryptoModule = createCryptoModuleForEncryption()
+ var encryptedData: ByteArray = byteArrayOf()
+ try {
+ encryptedData = when (encryptionType) {
+ CRYPTION_TYPE_BINARY -> cryptoModule.encrypt(notEncryptedFileContent)
+ CRYPTION_TYPE_STREAM -> cryptoModule.encryptStream(notEncryptedFileContent.inputStream()).readBytes()
+ else -> throw PubNubException("Invalid encryptionType type. Should be binary or stream")
+ }
+ } catch (e: PubNubException) {
+ cryptoModuleState.encryptionError = e.pubnubError
+ }
+ cryptoModuleState.encryptedData = encryptedData
+ }
+
+ private fun createCryptoModuleForEncryption(): CryptoModule {
+ val randoIv: Boolean = cryptoModuleState.initializationVectorType == RANDOM_IV
+
+ val defaultCryptorType = cryptoModuleState.defaultCryptorType
+ val cryptor = createCryptor(defaultCryptorType!!, cryptoModuleState.cryptorCipherKey!!, randoIv)
+ val cryptoModule = CryptoModule.createNewCryptoModule(cryptor)
+ return cryptoModule
+ }
+
+ @When("I decrypt {string} file as {string}")
+ fun I_decrypt_file_as_binary(encryptedFile: String, decryptionType: String) {
+ val cryptoModule: CryptoModule = createCryptoModuleForDecryption()
+
+ val encryptedFileContent = getFileContentAsByteArray(encryptedFile)
+ var decryptedData = ByteArray(0)
+ try {
+ decryptedData = when (decryptionType) {
+ CRYPTION_TYPE_BINARY -> cryptoModule.decrypt(encryptedFileContent)
+ CRYPTION_TYPE_STREAM -> cryptoModule.decryptStream(encryptedFileContent.inputStream()).readBytes()
+ else -> throw PubNubException("Invalid decryptionType type. Should be binary or stream")
+ }
+ } catch (e: PubNubException) {
+ cryptoModuleState.decryptionError = e.pubnubError
+ }
+ cryptoModuleState.decryptedData = decryptedData
+ }
+
+ private fun createCryptoModuleForDecryption(): CryptoModule {
+ val defaultCryptorType = cryptoModuleState.defaultCryptorType
+ val cipherKey = cryptoModuleState.cryptorCipherKey!!
+ val randoIv: Boolean = cryptoModuleState.initializationVectorType == RANDOM_IV
+ val cryptoModule: CryptoModule
+ if (cryptoModuleState.decryptionOnlyCryptorType == null) {
+ cryptoModule = when (defaultCryptorType) {
+ LEGACY_NEW -> {
+ CryptoModule.createNewCryptoModule(LegacyCryptor(cipherKey, randoIv))
+ }
+ AES_CBC -> {
+ CryptoModule.createNewCryptoModule(AesCbcCryptor(cipherKey))
+ }
+ else -> throw PubNubException("Invalid cryptor type")
+ }
+ } else {
+ val decryptionOnlyCryptorType = cryptoModuleState.decryptionOnlyCryptorType
+ val defaultCryptor = createCryptor(defaultCryptorType!!, cipherKey, randoIv)
+ val decryptionOnlyCryptor = createCryptor(decryptionOnlyCryptorType!!, cipherKey, randoIv)
+ cryptoModule = CryptoModule.createNewCryptoModule(defaultCryptor, listOf(decryptionOnlyCryptor))
+ }
+ return cryptoModule
+ }
+
+ private fun createCryptor(cryptorType: String, cipherKey: String, useRandomIv: Boolean): Cryptor {
+ return when (cryptorType) {
+ LEGACY_NEW -> {
+ LegacyCryptor(cipherKey, useRandomIv)
+ }
+ AES_CBC -> {
+ AesCbcCryptor(cipherKey)
+ }
+ else -> {
+ throw PubNubException("Invalid cryptor type")
+ }
+ }
+ }
+
+ @Then("I receive {string}")
+ fun I_receive_outcome(outcome: String) {
+ when (outcome) {
+ "unknown cryptor error" -> {
+ assertTrue(cryptoModuleState.decryptionError == PubNubError.UNKNOWN_CRYPTOR || cryptoModuleState.decryptionError == PubNubError.CRYPTOR_HEADER_VERSION_UNKNOWN)
+ }
+ "decryption error" -> {
+ val isDecryptionError01 = PubNubError.CRYPTOR_DATA_HEADER_SIZE_TO_SMALL == cryptoModuleState.decryptionError
+ val isDecryptionError02 = PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED == cryptoModuleState.decryptionError
+ val isDecryptionError03 = PubNubError.UNKNOWN_CRYPTOR == cryptoModuleState.decryptionError
+ assertTrue(isDecryptionError01 || isDecryptionError02 || isDecryptionError03)
+ }
+ "success" -> assertNull(cryptoModuleState.decryptionError)
+ "encryption error" -> assertEquals(
+ PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED,
+ cryptoModuleState.encryptionError
+ )
+ }
+ }
+
+ @Then("Successfully decrypt an encrypted file with legacy code")
+ fun successfully_decrypt_an_encrypted_file_with_legacy_code() {
+ val encryptedData = cryptoModuleState.encryptedData
+ val encryptedDataAsStringBase64 = String(Base64.encode(encryptedData, Base64.NO_WRAP))
+ val randoIv: Boolean = cryptoModuleState.initializationVectorType == RANDOM_IV
+ val cipherKey = cryptoModuleState.cryptorCipherKey
+
+ val encryptionType = cryptoModuleState.encryptionType
+ val decryptedDataAsString: String = when (encryptionType) {
+ CRYPTION_TYPE_BINARY -> {
+ val crypto = Crypto(cipherKey, randoIv)
+ crypto.decrypt(encryptedDataAsStringBase64)
+ }
+ CRYPTION_TYPE_STREAM -> {
+ val byteArrayInputStream = ByteArrayInputStream(encryptedData)
+ val decryptedStreamAsByteArray = FileEncryptionUtilKT.decrypt(byteArrayInputStream, cipherKey!!).readBytes()
+ String(decryptedStreamAsByteArray)
+ }
+ else -> { throw PubNubException("Invalid cryptor type") }
+ }
+
+ assertEquals(String(cryptoModuleState.fileContent!!), decryptedDataAsString)
+ }
+
+ @Then("Decrypted file content equal to the {string} file content")
+ fun decrypted_file_content_equal_to_the_source_file_content(sourceFileName: String) {
+ val sourceFileContent = getFileContentAsByteArray(sourceFileName)
+ assertArrayEquals(sourceFileContent, cryptoModuleState.decryptedData)
+ }
+}
diff --git a/src/test/kotlin/com/pubnub/api/crypto/CryptoModuleTest.kt b/src/test/kotlin/com/pubnub/api/crypto/CryptoModuleTest.kt
new file mode 100644
index 000000000..5b3c26db6
--- /dev/null
+++ b/src/test/kotlin/com/pubnub/api/crypto/CryptoModuleTest.kt
@@ -0,0 +1,355 @@
+package com.pubnub.api.crypto
+
+import com.pubnub.api.crypto.cryptor.AesCbcCryptor
+import com.pubnub.api.crypto.cryptor.Cryptor
+import com.pubnub.api.crypto.cryptor.LegacyCryptor
+import com.pubnub.api.crypto.data.EncryptedData
+import com.pubnub.api.crypto.data.EncryptedStreamData
+import com.pubnub.api.crypto.exception.PubNubError
+import com.pubnub.api.crypto.exception.PubNubException
+import org.hamcrest.CoreMatchers
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.hasSize
+import org.junit.jupiter.api.Assertions.assertArrayEquals
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertThrows
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+import java.io.ByteArrayInputStream
+import java.io.InputStream
+import java.util.*
+import org.hamcrest.Matchers.`is` as iz
+
+class CryptoModuleTest {
+
+ @Test
+ fun `can createLegacyCryptoModule`() {
+ // given
+ val cipherKey = "enigma"
+
+ // when
+ val legacyCryptoModule = CryptoModule.createLegacyCryptoModule(cipherKey)
+
+ // then
+ assertTrue(legacyCryptoModule.primaryCryptor is LegacyCryptor)
+ assertThat(legacyCryptoModule.cryptorsForDecryptionOnly, hasSize(2))
+ assertThat(
+ legacyCryptoModule.cryptorsForDecryptionOnly,
+ containsInAnyOrder(
+ listOf(
+ iz(CoreMatchers.instanceOf(AesCbcCryptor::class.java)),
+ iz(CoreMatchers.instanceOf(LegacyCryptor::class.java))
+ )
+ )
+ )
+ }
+
+ @Test
+ fun `can createAesCbcCryptoModule`() {
+ // given
+ val cipherKey = "enigma"
+
+ // when
+ val aesCbcCryptoModule = CryptoModule.createAesCbcCryptoModule(cipherKey)
+
+ // then
+ assertTrue(aesCbcCryptoModule.primaryCryptor is AesCbcCryptor)
+ assertThat(aesCbcCryptoModule.cryptorsForDecryptionOnly, hasSize(2))
+ assertThat(
+ aesCbcCryptoModule.cryptorsForDecryptionOnly,
+ containsInAnyOrder(
+ listOf(
+ iz(CoreMatchers.instanceOf(AesCbcCryptor::class.java)),
+ iz(CoreMatchers.instanceOf(LegacyCryptor::class.java))
+ )
+ )
+ )
+ }
+
+ @Test
+ fun `can createNewCryptoModule`() {
+ // given
+ val cipherKey = "enigma"
+
+ // when
+ val newCryptoModule = CryptoModule.createNewCryptoModule(defaultCryptor = AesCbcCryptor(cipherKey))
+
+ // then
+ assertTrue(newCryptoModule.primaryCryptor is AesCbcCryptor)
+ assertThat(newCryptoModule.cryptorsForDecryptionOnly, hasSize(1))
+ assertThat(
+ newCryptoModule.cryptorsForDecryptionOnly.first(),
+ iz(CoreMatchers.instanceOf(AesCbcCryptor::class.java))
+ )
+ }
+
+ @Test
+ fun `can decrypt encrypted message using LegacyCryptoModule with randomIV`() {
+ // given
+ val cipherKey = "enigma"
+ val legacyCryptoModuleWithRandomIv = CryptoModule.createLegacyCryptoModule(cipherKey)
+ val msgToEncrypt = "Hello world".toByteArray()
+
+ // when
+ val encryptedMsg = legacyCryptoModuleWithRandomIv.encrypt(msgToEncrypt)
+ val decryptedMsg = legacyCryptoModuleWithRandomIv.decrypt(encryptedMsg)
+
+ // then
+ assertArrayEquals(msgToEncrypt, decryptedMsg)
+ }
+
+ @Test
+ fun `can decrypt encrypted message using LegacyCryptoModule with staticIV`() {
+ // given
+ val cipherKey = "enigma"
+ val legacyCryptoModuleWithStaticIv = CryptoModule.createLegacyCryptoModule(cipherKey, false)
+ val msgToEncrypt = "Hello world".toByteArray()
+
+ // when
+ val encryptedMsg = legacyCryptoModuleWithStaticIv.encrypt(msgToEncrypt)
+ val decryptedMsg = legacyCryptoModuleWithStaticIv.decrypt(encryptedMsg)
+
+ // then
+ assertArrayEquals(msgToEncrypt, decryptedMsg)
+ }
+
+ @Test
+ fun `using LegacyCryptoModule can decrypt message that was encrypted with AesCbcCryptor`() {
+ // given
+ val cipherKey = "enigma"
+ val moduleWithAesCbcCryptorOnly = CryptoModule.createNewCryptoModule(defaultCryptor = AesCbcCryptor(cipherKey))
+ val legacyCryptoModule = CryptoModule.createLegacyCryptoModule(cipherKey)
+ val msgToEncrypt = "Hello world".toByteArray()
+
+ // when
+ val encryptedMsg = moduleWithAesCbcCryptorOnly.encrypt(msgToEncrypt)
+ val decryptedMsg = legacyCryptoModule.decrypt(encryptedMsg)
+
+ // then
+ assertArrayEquals(msgToEncrypt, decryptedMsg)
+ }
+
+ @Test
+ fun `using AesCbcCryptoModule can decrypt message that was encrypted with LegacyCryptor with randomIV `() {
+ // given
+ val cipherKey = "enigma"
+ val moduleWithLegacyCryptorOnlyWithRandomIV =
+ CryptoModule.createNewCryptoModule(defaultCryptor = LegacyCryptor(cipherKey))
+ val aesCbcCryptoModule = CryptoModule.createAesCbcCryptoModule(cipherKey)
+ val msgToEncrypt = "Hello world".toByteArray()
+
+ // when
+ val encryptedMsg = moduleWithLegacyCryptorOnlyWithRandomIV.encrypt(msgToEncrypt)
+ val decryptedMsg = aesCbcCryptoModule.decrypt(encryptedMsg)
+
+ // then
+ assertArrayEquals(msgToEncrypt, decryptedMsg)
+ }
+
+ @Test
+ fun `using AesCbcCryptoModule can decrypt message that was encrypted with LegacyCryptor with staticIV `() {
+ // given
+ val cipherKey = "enigma"
+ val moduleWithLegacyCryptorOnlyWithStaticIV =
+ CryptoModule.createNewCryptoModule(defaultCryptor = LegacyCryptor(cipherKey, false))
+ val aesCbcCryptoModule = CryptoModule.createAesCbcCryptoModule(cipherKey, false)
+ val msgToEncrypt = "Hello world".toByteArray()
+
+ // when
+ val encryptedMsg = moduleWithLegacyCryptorOnlyWithStaticIV.encrypt(msgToEncrypt)
+ val decryptedMsg = aesCbcCryptoModule.decrypt(encryptedMsg)
+
+ // then
+ assertArrayEquals(msgToEncrypt, decryptedMsg)
+ }
+
+ @Test
+ fun `can decrypt encrypted message using module with only AesCbcCryptor`() {
+ // given
+ val cipherKey = "enigma"
+ val aesCbcCryptoModule = CryptoModule.createAesCbcCryptoModule(cipherKey)
+ val msgToEncrypt = "Hello world".toByteArray()
+
+ // when
+ val encryptedMsg = aesCbcCryptoModule.encrypt(msgToEncrypt)
+ val decryptedMsg = aesCbcCryptoModule.decrypt(encryptedMsg)
+
+ // then
+ assertArrayEquals(msgToEncrypt, decryptedMsg)
+ }
+
+ @Test
+ fun `can add the same module as a defaultCryptor and cryptorsForDecryptionOnly and have decryption working properly`() {
+ // given
+ val cipherKey = "enigma"
+ val legacyCryptor = LegacyCryptor(cipherKey)
+ val cryptoModule = CryptoModule.createNewCryptoModule(
+ defaultCryptor = legacyCryptor,
+ cryptorsForDecryptionOnly = listOf(legacyCryptor, AesCbcCryptor(cipherKey))
+ )
+ val msgToEncrypt = "Hello world".toByteArray()
+
+ // when
+ val encryptedMsg = cryptoModule.encrypt(msgToEncrypt)
+ val decryptedMsg = cryptoModule.decrypt(encryptedMsg)
+
+ // then
+ assertArrayEquals(msgToEncrypt, decryptedMsg)
+ }
+
+ @Test
+ fun `can decrypt encrypted message using custom cryptor `() {
+ // given
+ val customCryptor = myCustomCryptor()
+ val msgToEncrypt = "Hello world".toByteArray()
+
+ // when
+ val encryptedMsg = customCryptor.encrypt(msgToEncrypt)
+ val decryptedMsg = customCryptor.decrypt(encryptedMsg)
+
+ // then
+ assertArrayEquals(msgToEncrypt, decryptedMsg)
+ }
+
+ @ParameterizedTest
+ @MethodSource("legacyAndAesCbcCryptors")
+ fun `should throw exception when encrypting empty data`(cryptoModule: CryptoModule) {
+ // given
+ val dataToBeEncrypted = ByteArray(0)
+
+ // when
+ val exception = assertThrows(PubNubException::class.java) {
+ cryptoModule.encrypt(dataToBeEncrypted)
+ }
+
+ // then
+ assertEquals("Encryption/Decryption of empty data not allowed.", exception.errorMessage)
+ assertEquals(PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED, exception.pubnubError)
+ }
+
+ @ParameterizedTest
+ @MethodSource("legacyAndAesCbcCryptors")
+ fun `should throw exception when decrypting empty data`(cryptoModule: CryptoModule) {
+ // given
+ val dataToBeDecrypted = ByteArray(0)
+
+ // when
+ val exception = assertThrows(PubNubException::class.java) {
+ cryptoModule.decrypt(dataToBeDecrypted)
+ }
+
+ // then
+ assertEquals("Encryption/Decryption of empty data not allowed.", exception.errorMessage)
+ assertEquals(PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED, exception.pubnubError)
+ }
+
+ @ParameterizedTest
+ @MethodSource("legacyAndAesCbcCryptors")
+ fun `should throw exception when encrypting empty stream`(cryptoModule: CryptoModule) {
+ // given
+ val dataToBeEncrypted = ByteArray(0)
+ val streamToBeEncrypted = ByteArrayInputStream(dataToBeEncrypted)
+
+ // when
+ val exception = assertThrows(PubNubException::class.java) {
+ cryptoModule.encryptStream(streamToBeEncrypted)
+ }
+
+ // then
+ assertEquals("Encryption/Decryption of empty data not allowed.", exception.errorMessage)
+ assertEquals(PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED, exception.pubnubError)
+ }
+
+ @ParameterizedTest
+ @MethodSource("legacyAndAesCbcCryptors")
+ fun `should throw exception when decrypting empty stream`(cryptoModule: CryptoModule) {
+ // given
+ val dataToBeDecrypted = ByteArray(0)
+ val streamToBeDecrypted = ByteArrayInputStream(dataToBeDecrypted)
+
+ // when
+ val exception = assertThrows(PubNubException::class.java) {
+ cryptoModule.decryptStream(streamToBeDecrypted)
+ }
+
+ // then
+ assertEquals("Encryption/Decryption of empty data not allowed.", exception.errorMessage)
+ assertEquals(PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED, exception.pubnubError)
+ }
+
+ private fun myCustomCryptor() = object : Cryptor {
+ override fun id(): ByteArray {
+ return byteArrayOf('C'.code.toByte(), 'U'.code.toByte(), 'S'.code.toByte(), 'T'.code.toByte())
+ }
+
+ override fun encrypt(data: ByteArray): EncryptedData {
+ return EncryptedData(metadata = null, data = data)
+ }
+
+ override fun decrypt(encryptedData: EncryptedData): ByteArray {
+ return encryptedData.data
+ }
+
+ override fun encryptStream(stream: InputStream): EncryptedStreamData {
+ throw NotImplementedError()
+ }
+
+ override fun decryptStream(encryptedData: EncryptedStreamData): InputStream {
+ throw NotImplementedError()
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("decryptStreamSource")
+ fun decryptStreamEncryptedByGo(expected: String, encryptedBase64: String, cipherKey: String) {
+ val crypto = CryptoModule.createLegacyCryptoModule(cipherKey, true)
+ val decrypted = crypto.decryptStream(Base64.getDecoder().decode(encryptedBase64).inputStream())
+ assertEquals(expected, String(decrypted.readBytes()))
+ }
+
+ @ParameterizedTest
+ @MethodSource("encryptStreamDecryptStreamSource")
+ fun encryptStreamDecryptStream(input: String, cryptoModule: CryptoModule) {
+ val encrypted = cryptoModule.encryptStream(input.byteInputStream())
+ val decrypted = cryptoModule.decryptStream(encrypted)
+ assertEquals(input, String(decrypted.readBytes()))
+ }
+
+ companion object {
+ @JvmStatic
+ fun decryptStreamSource(): List = listOf(
+ Arguments.of(
+ "Hello world encrypted with legacyModuleRandomIv",
+ "T3J9iXI87PG9YY/lhuwmGRZsJgA5y8sFLtUpdFmNgrU1IAitgAkVok6YP7lacBiVhBJSJw39lXCHOLxl2d98Bg==",
+ "myCipherKey",
+
+ ),
+ Arguments.of(
+ "Hello world encrypted with aesCbcModule",
+ "UE5FRAFBQ1JIEKzlyoyC/jB1hrjCPY7zm+X2f7skPd0LBocV74cRYdrkRQ2BPKeA22gX/98pMqvcZtFB6TCGp3Zf1M8F730nlfk=",
+ "myCipherKey"
+
+ ),
+ )
+
+ @JvmStatic
+ fun encryptStreamDecryptStreamSource(): List = listOf(
+ Arguments.of("Hello world1", CryptoModule.createLegacyCryptoModule("myCipherKey", true)),
+ Arguments.of("Hello world2", CryptoModule.createLegacyCryptoModule("myCipherKey", false)),
+ Arguments.of("Hello world3", CryptoModule.createAesCbcCryptoModule("myCipherKey", true)),
+ Arguments.of("Hello world4", CryptoModule.createAesCbcCryptoModule("myCipherKey", false)),
+ )
+
+ @JvmStatic
+ fun legacyAndAesCbcCryptors(): List = listOf(
+ Arguments.of(CryptoModule.createLegacyCryptoModule("myCipherKey", true)),
+ Arguments.of(CryptoModule.createLegacyCryptoModule("myCipherKey", false)),
+ Arguments.of(CryptoModule.createAesCbcCryptoModule("myCipherKey", true)),
+ Arguments.of(CryptoModule.createAesCbcCryptoModule("myCipherKey", false)),
+ )
+ }
+}
diff --git a/src/test/kotlin/com/pubnub/api/crypto/algorithm/AesCBCCryptorTest.kt b/src/test/kotlin/com/pubnub/api/crypto/algorithm/AesCBCCryptorTest.kt
new file mode 100644
index 000000000..f79a01220
--- /dev/null
+++ b/src/test/kotlin/com/pubnub/api/crypto/algorithm/AesCBCCryptorTest.kt
@@ -0,0 +1,142 @@
+package com.pubnub.api.crypto.algorithm
+
+import com.pubnub.api.crypto.cryptor.AesCbcCryptor
+import com.pubnub.api.crypto.data.EncryptedData
+import com.pubnub.api.crypto.data.EncryptedStreamData
+import com.pubnub.api.crypto.exception.PubNubError
+import com.pubnub.api.crypto.exception.PubNubException
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Assertions.assertArrayEquals
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+
+class AesCBCCryptorTest {
+ private lateinit var objectUnderTest: AesCbcCryptor
+
+ companion object {
+ @JvmStatic
+ fun messageToBeEncrypted(): List = listOf(
+ Arguments.of("Hello world"),
+ Arguments.of("Zażółć gęślą jaźń"), // Polish
+ Arguments.of("हैलो वर्ल्ड"), // Hindi
+ Arguments.of("こんにちは世界"), // Japan
+ Arguments.of("你好世界"), // Chinese
+ )
+ }
+
+ @BeforeEach
+ fun setUp() {
+ objectUnderTest = AesCbcCryptor("enigma")
+ }
+
+ @ParameterizedTest
+ @MethodSource("messageToBeEncrypted")
+ fun canDecryptTextWhatIsEncrypted(msgToBeEncrypted: String) {
+ // given
+ val msgToEncrypt = msgToBeEncrypted.toByteArray()
+
+ // when
+ val encryptedMsg = objectUnderTest.encrypt(msgToEncrypt)
+ val decryptedMsg = objectUnderTest.decrypt(encryptedMsg)
+
+ // then
+ assertArrayEquals(msgToEncrypt, decryptedMsg)
+ }
+
+ @ParameterizedTest
+ @MethodSource("messageToBeEncrypted")
+ fun encryptingTwoTimesTheSameMessageProducesDifferentOutput(msgToBeEncrypted: String) {
+ // given
+ val msgToEncrypt = msgToBeEncrypted.toByteArray()
+
+ // when
+ val encrypted1: EncryptedData = objectUnderTest.encrypt(msgToEncrypt)
+ val encrypted2: EncryptedData = objectUnderTest.encrypt(msgToEncrypt)
+
+ // then
+ Assertions.assertFalse(encrypted1.data.contentEquals(encrypted2.data))
+ }
+
+ @ParameterizedTest
+ @MethodSource("messageToBeEncrypted")
+ fun encryptingTwoTimesDecryptedMsgIsTheSame(msgToBeEncrypted: String) {
+ // given
+ val msgToEncrypt = msgToBeEncrypted.toByteArray()
+
+ // when
+ val encrypted1 = objectUnderTest.encrypt(msgToEncrypt)
+ val encrypted2 = objectUnderTest.encrypt(msgToEncrypt)
+
+ // then
+ assertArrayEquals(msgToEncrypt, objectUnderTest.decrypt(encrypted1))
+ assertArrayEquals(msgToEncrypt, objectUnderTest.decrypt(encrypted2))
+ }
+
+ @Test
+ fun `should throw exception when encrypting empty data`() {
+ // given
+ val msgToEncrypt = "".toByteArray()
+
+ // when
+ val exception = Assertions.assertThrows(PubNubException::class.java) {
+ objectUnderTest.encrypt(msgToEncrypt)
+ }
+
+ // then
+ assertEquals("Encryption/Decryption of empty data not allowed.", exception.errorMessage)
+ assertEquals(PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED, exception.pubnubError)
+ }
+
+ @Test
+ fun `should throw exception when decrypting empty data`() {
+ // given
+ val msgToDecrypt = "".toByteArray()
+ val encryptedData = EncryptedData(data = msgToDecrypt)
+
+ // when
+ val exception = Assertions.assertThrows(PubNubException::class.java) {
+ objectUnderTest.decrypt(encryptedData)
+ }
+
+ // then
+ assertEquals("Encryption/Decryption of empty data not allowed.", exception.errorMessage)
+ assertEquals(PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED, exception.pubnubError)
+ }
+
+ @Test
+ fun `should throw exception when encrypting empty stream`() {
+ // given
+ val msgToEncrypt = ""
+ val streamToEncrypt = msgToEncrypt.byteInputStream()
+
+ // when
+ val exception = Assertions.assertThrows(PubNubException::class.java) {
+ objectUnderTest.encryptStream(streamToEncrypt)
+ }
+
+ // then
+ assertEquals("Encryption/Decryption of empty data not allowed.", exception.errorMessage)
+ assertEquals(PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED, exception.pubnubError)
+ }
+
+ @Test
+ fun `should throw exception when decrypting empty stream`() {
+ // given
+ val msgToDecrypt = ""
+ val streamToEncrypt = msgToDecrypt.byteInputStream()
+ val encryptedStreamData = EncryptedStreamData(stream = streamToEncrypt)
+
+ // when
+ val exception = Assertions.assertThrows(PubNubException::class.java) {
+ objectUnderTest.decryptStream(encryptedStreamData)
+ }
+
+ // then
+ assertEquals("Encryption/Decryption of empty data not allowed.", exception.errorMessage)
+ assertEquals(PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED, exception.pubnubError)
+ }
+}
diff --git a/src/test/kotlin/com/pubnub/api/crypto/algorithm/LegacyCryptorTest.kt b/src/test/kotlin/com/pubnub/api/crypto/algorithm/LegacyCryptorTest.kt
new file mode 100644
index 000000000..a6e59efdf
--- /dev/null
+++ b/src/test/kotlin/com/pubnub/api/crypto/algorithm/LegacyCryptorTest.kt
@@ -0,0 +1,186 @@
+package com.pubnub.api.crypto.algorithm
+
+import com.pubnub.api.crypto.cryptor.LegacyCryptor
+import com.pubnub.api.crypto.data.EncryptedData
+import com.pubnub.api.crypto.data.EncryptedStreamData
+import com.pubnub.api.crypto.exception.PubNubError
+import com.pubnub.api.crypto.exception.PubNubException
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Assertions.assertArrayEquals
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+import org.junit.jupiter.params.provider.ValueSource
+import java.io.ByteArrayInputStream
+
+class LegacyCryptorTest {
+
+ companion object {
+ @JvmStatic
+ fun messageToBeEncrypted(): List = listOf(
+ Arguments.of("Hello world"),
+ Arguments.of("Zażółć gęślą jaźń"), // Polish
+ Arguments.of("हैलो वर्ल्ड"), // Hindi
+ Arguments.of("こんにちは世界"), // Japan
+ Arguments.of("你好世界"), // Chinese
+ )
+ }
+
+ @ParameterizedTest
+ @MethodSource("messageToBeEncrypted")
+ fun canDecryptTextWhatIsEncryptedWithStaticIV(messageToBeEncrypted: String) {
+ // given
+ val cipherKey = "enigma"
+ val msgToEncrypt = messageToBeEncrypted.toByteArray()
+
+ // when
+ val cryptor = LegacyCryptor(cipherKey = cipherKey, useRandomIv = false)
+ val encryptedMsg = cryptor.encrypt(msgToEncrypt)
+ val decryptedMsg = cryptor.decrypt(encryptedMsg)
+
+ // then
+ assertArrayEquals(msgToEncrypt, decryptedMsg)
+ }
+
+ @ParameterizedTest
+ @MethodSource("messageToBeEncrypted")
+ fun canDecryptTextWhatIsEncryptedWithRandomIV(messageToBeEncrypted: String) {
+ // given
+ val cipherKey = "enigma"
+ val msgToEncrypt = messageToBeEncrypted.toByteArray()
+
+ // when
+ val cryptor = LegacyCryptor(cipherKey = cipherKey)
+ val encryptedMsg = cryptor.encrypt(msgToEncrypt)
+ val decryptedMsg = cryptor.decrypt(encryptedMsg)
+
+ // then
+ assertArrayEquals(msgToEncrypt, decryptedMsg)
+ }
+
+ @ParameterizedTest
+ @MethodSource("messageToBeEncrypted")
+ fun encryptingWithRandomIVTwoTimesTheSameMessageProducesDifferentOutput(messageToBeEncrypted: String) {
+ // given
+ val cipherKey = "enigma"
+ val msgToEncrypt = messageToBeEncrypted.toByteArray()
+
+ // when
+ val cryptor = LegacyCryptor(cipherKey = cipherKey)
+ val encrypted1: EncryptedData = cryptor.encrypt(msgToEncrypt)
+ val encrypted2: EncryptedData = cryptor.encrypt(msgToEncrypt)
+
+ // then
+ assertFalse(encrypted1.data.contentEquals(encrypted2.data))
+ }
+
+ @ParameterizedTest
+ @MethodSource("messageToBeEncrypted")
+ fun encryptingWithRandomIVTwoTimesDecryptedMsgIsTheSame(messageToBeEncrypted: String) {
+ // given
+ val cipherKey = "enigma"
+ val msgToEncrypt = messageToBeEncrypted.toByteArray()
+
+ // when
+ val cryptor = LegacyCryptor(cipherKey = cipherKey)
+ val encrypted1 = cryptor.encrypt(msgToEncrypt)
+ val encrypted2 = cryptor.encrypt(msgToEncrypt)
+
+ // then
+ assertArrayEquals(msgToEncrypt, cryptor.decrypt(encrypted1))
+ assertArrayEquals(msgToEncrypt, cryptor.decrypt(encrypted2))
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = [true, false])
+ fun `should throw exception when encrypting empty data`(useRandomIv: Boolean) {
+ // given
+ val msgToEncrypt = "".toByteArray()
+ val cipherKey = "enigma"
+ val cryptor = LegacyCryptor(cipherKey = cipherKey, useRandomIv = useRandomIv)
+
+ // when
+ val exception = Assertions.assertThrows(PubNubException::class.java) {
+ cryptor.encrypt(msgToEncrypt)
+ }
+
+ // then
+ Assertions.assertEquals("Encryption/Decryption of empty data not allowed.", exception.errorMessage)
+ Assertions.assertEquals(PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED, exception.pubnubError)
+ }
+
+ @Test
+ fun `should throw exception when decrypting data containing only initialization vector and cryptor has randomIv`() {
+ // given
+ val msgToDecrypt = ByteArray(16) { it.toByte() } // IV has 16 bytes
+ val encryptedData = EncryptedData(data = msgToDecrypt)
+ val cipherKey = "enigma"
+ val cryptor = LegacyCryptor(cipherKey = cipherKey, useRandomIv = true)
+
+ // when
+ val exception = Assertions.assertThrows(PubNubException::class.java) {
+ cryptor.decrypt(encryptedData)
+ }
+
+ // then
+ Assertions.assertEquals("Encryption/Decryption of empty data not allowed.", exception.errorMessage)
+ Assertions.assertEquals(PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED, exception.pubnubError)
+ }
+
+ @Test
+ fun `should throw exception when decrypting empty data and cryptor has staticIv`() {
+ // given
+ val msgToDecrypt = "".toByteArray()
+ val encryptedData = EncryptedData(data = msgToDecrypt)
+ val cipherKey = "enigma"
+ val cryptor = LegacyCryptor(cipherKey = cipherKey, useRandomIv = false)
+
+ // when
+ val exception = Assertions.assertThrows(PubNubException::class.java) {
+ cryptor.decrypt(encryptedData)
+ }
+
+ // then
+ Assertions.assertEquals("Encryption/Decryption of empty data not allowed.", exception.errorMessage)
+ Assertions.assertEquals(PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED, exception.pubnubError)
+ }
+
+ @Test
+ fun `should throw exception when encrypting empty stream`() {
+ // given
+ val msgToEncrypt = ""
+ val streamToEncrypt = msgToEncrypt.byteInputStream()
+ val cipherKey = "enigma"
+ val cryptor = LegacyCryptor(cipherKey = cipherKey, useRandomIv = false)
+
+ // when
+ val exception = Assertions.assertThrows(PubNubException::class.java) {
+ cryptor.encryptStream(streamToEncrypt)
+ }
+
+ // then
+ Assertions.assertEquals("Encryption/Decryption of empty data not allowed.", exception.errorMessage)
+ Assertions.assertEquals(PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED, exception.pubnubError)
+ }
+
+ @Test
+ fun `should throw exception when decrypting empty stream`() {
+ // given
+ val msgToDecrypt = ByteArray(16) { it.toByte() } // IV has 16 bytes
+ val streamToEncrypt = ByteArrayInputStream(msgToDecrypt)
+ val encryptedStreamData = EncryptedStreamData(stream = streamToEncrypt)
+ val cipherKey = "enigma"
+ val cryptor = LegacyCryptor(cipherKey = cipherKey, useRandomIv = false)
+
+ // when
+ val exception = Assertions.assertThrows(PubNubException::class.java) {
+ cryptor.decryptStream(encryptedStreamData)
+ }
+
+ // then
+ Assertions.assertEquals("Encryption/Decryption of empty data not allowed.", exception.errorMessage)
+ Assertions.assertEquals(PubNubError.ENCRYPTION_AND_DECRYPTION_OF_EMPTY_DATA_NOT_ALLOWED, exception.pubnubError)
+ }
+}
diff --git a/src/test/kotlin/com/pubnub/api/crypto/cryptor/HeaderParserTest.kt b/src/test/kotlin/com/pubnub/api/crypto/cryptor/HeaderParserTest.kt
new file mode 100644
index 000000000..a491d11b7
--- /dev/null
+++ b/src/test/kotlin/com/pubnub/api/crypto/cryptor/HeaderParserTest.kt
@@ -0,0 +1,98 @@
+package com.pubnub.api.crypto.cryptor
+
+import com.pubnub.api.crypto.CryptoModule
+import com.pubnub.api.crypto.exception.PubNubError
+import com.pubnub.api.crypto.exception.PubNubException
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertThrows
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Assertions.fail
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class HeaderParserTest {
+ private lateinit var objectUnderTest: HeaderParser
+
+ @BeforeEach
+ fun setUp() {
+ objectUnderTest = HeaderParser()
+ }
+
+ @Test
+ fun `can create and parse data with header when cryptorDataSize is 1`() {
+ val cryptorId: ByteArray =
+ byteArrayOf('C'.code.toByte(), 'R'.code.toByte(), 'I'.code.toByte(), 'V'.code.toByte()) // "CRIV"
+
+ val cipherKey = "enigma"
+ val cryptoModule = CryptoModule.createLegacyCryptoModule(cipherKey, false)
+ val cryptorData = byteArrayOf(0x50, 0x56, 0x56, 0x56, 0x56, 0x01, 0x43, 0x52, 0x49, 0x56, 0x10, 0x10, 0x56, 0x56, 0x56, 0x10)
+ val cryptorHeader = objectUnderTest.createCryptorHeader(cryptorId, cryptorData)
+
+ val dataToBeEncrypted = byteArrayOf('D'.code.toByte(), 'A'.code.toByte())
+ val encryptedData = cryptoModule.encrypt(dataToBeEncrypted)
+ val headerWithData: ByteArray = cryptorHeader + encryptedData
+ val parseResult = objectUnderTest.parseDataWithHeader(headerWithData)
+
+ when (parseResult) {
+ is ParseResult.NoHeader -> fail("Expected header")
+ is ParseResult.Success -> {
+ assertTrue(cryptorId.contentEquals(parseResult.cryptoId))
+ assertTrue(cryptorData.contentEquals(parseResult.cryptorData))
+ assertTrue(encryptedData.contentEquals(parseResult.encryptedData))
+ }
+ }
+ }
+
+ @Test
+ fun `can create and parse data with header when cryptorDataSize is 3`() {
+ val cryptorId: ByteArray =
+ byteArrayOf('C'.code.toByte(), 'R'.code.toByte(), 'I'.code.toByte(), 'V'.code.toByte()) // "CRIV"
+ val cryptorData = createByteArrayThatHas255Elements()
+ val cryptorHeader = objectUnderTest.createCryptorHeader(cryptorId, cryptorData)
+
+ val dataToBeEncrypted = byteArrayOf('D'.code.toByte(), 'A'.code.toByte())
+ val headerWithData: ByteArray = cryptorHeader + dataToBeEncrypted
+ val parseResult = objectUnderTest.parseDataWithHeader(headerWithData)
+
+ when (parseResult) {
+ is ParseResult.NoHeader -> fail("Expected header")
+ is ParseResult.Success -> {
+ assertTrue(cryptorId.contentEquals(parseResult.cryptoId))
+ assertTrue(cryptorData.contentEquals(parseResult.cryptorData))
+ assertTrue(dataToBeEncrypted.contentEquals(parseResult.encryptedData))
+ }
+ }
+ }
+
+ @Test
+ fun `should return NoHeader when there is no sentinel`() {
+ val cryptorHeaderWithInvalidSentinel =
+ byteArrayOf(0x56, 0x56, 0x56, 0x56, 0x01, 0x43, 0x52, 0x49, 0x56, 0x10, 0x10, 0x56, 0x56, 0x56, 0x56, 0x01, 0x43, 0x52, 0x49, 0x56, 0x10, 0x10)
+ val parseResult = objectUnderTest.parseDataWithHeader(cryptorHeaderWithInvalidSentinel)
+
+ assertThat(parseResult, `is`(ParseResult.NoHeader))
+ }
+
+ @Test
+ fun `should throw exception when input data are to short`() {
+ val cryptorHeaderWithToShortData =
+ byteArrayOf(80, 78, 69, 68, 1, 43, 52, 49, 56)
+
+ val exception: PubNubException = assertThrows(PubNubException::class.java) {
+ objectUnderTest.parseDataWithHeader(cryptorHeaderWithToShortData)
+ }
+
+ assertEquals("Minimal size of encrypted data having Cryptor Data Header is: 10", exception.errorMessage)
+ assertEquals(PubNubError.CRYPTOR_DATA_HEADER_SIZE_TO_SMALL, exception.pubnubError)
+ }
+
+ private fun createByteArrayThatHas255Elements(): ByteArray {
+ var byteArray: ByteArray = byteArrayOf()
+ for (i in 1..255) {
+ byteArray += byteArrayOf(i.toByte())
+ }
+ return byteArray
+ }
+}