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 + } +}