Skip to content

Commit

Permalink
Can read unencrypted history message when crypto is configured.
Browse files Browse the repository at this point in the history
  • Loading branch information
marcin-cebo committed Nov 7, 2023
1 parent bbe06c1 commit 8987e71
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import com.google.gson.JsonPrimitive
import com.pubnub.api.CommonUtils
import com.pubnub.api.CommonUtils.emoji
import com.pubnub.api.CommonUtils.randomChannel
import com.pubnub.api.PubNub
import com.pubnub.api.crypto.CryptoModule
import com.pubnub.api.models.consumer.history.Action
import com.pubnub.api.models.consumer.history.HistoryMessageType
import com.pubnub.api.models.consumer.history.PNFetchMessageItem
Expand Down Expand Up @@ -61,6 +63,56 @@ class HistoryIntegrationTest : BaseIntegrationTest() {
assertEquals(expectedHistoryResultChannels, historyResult?.messages)
}

@Test
fun `when reading unencrypted message from history using pubNub with configured encryption should log error and return error in response and return unencrypted message`() {
val pnConfigurationWithCrypto = getBasicPnConfiguration()
val cipherKey = "enigma"
pnConfigurationWithCrypto.cryptoModule =
CryptoModule.createAesCbcCryptoModule(cipherKey = cipherKey, randomIv = false)
val pubNubWithCrypto = PubNub(pnConfigurationWithCrypto)

val channel = randomChannel()
val expectedMeta = JsonObject().also { it.add("thisIsMeta", JsonPrimitive("thisIsMetaValue")) }
val expectedMessage = "this is not encrypted message"

val result = pubnub.publish(
channel = channel,
message = expectedMessage,
meta = expectedMeta,
shouldStore = true,
ttl = 60
).sync()!!

var historyResult: PNHistoryResult? = null

await
.pollInterval(Duration.ofMillis(1_000))
.pollDelay(Duration.ofMillis(1_000))
.atMost(Duration.ofMillis(10_000))
.untilAsserted {
historyResult = pubNubWithCrypto.history(
channel = channel,
includeMeta = true,
includeTimetoken = true,
).sync()

assertNotNull(historyResult)
assertThat(historyResult?.messages, hasSize(not(0)))
}

val expectedHistoryResultChannels =
listOf(
PNHistoryItemResult(
entry = JsonPrimitive(expectedMessage),
timetoken = result.timetoken,
meta = expectedMeta,
error = "Crypto is configured but message is not encrypted."
)
)

assertEquals(expectedHistoryResultChannels, historyResult?.messages)
}

@Test
fun fetchMessagesSingleScenario() {
val channel = randomChannel()
Expand Down Expand Up @@ -150,6 +202,65 @@ class HistoryIntegrationTest : BaseIntegrationTest() {
)
}

@Test
fun `when fetching unencrypted message using pubNub with configured encryption should log error and return error in respone and return unencrypted message`() {
val pnConfigurationWithCrypto = getBasicPnConfiguration()
val cipherKey = "enigma"
pnConfigurationWithCrypto.cryptoModule =
CryptoModule.createLegacyCryptoModule(cipherKey = cipherKey, randomIv = true)
val pubNubWithCrypto = PubNub(pnConfigurationWithCrypto)

val channel = randomChannel()
val expectedMeta = JsonObject().also { it.add("thisIsMeta", JsonPrimitive("thisIsMetaValue")) }
val expectedMessage = CommonUtils.generatePayload()

val result = pubnub.publish(
channel = channel,
message = expectedMessage,
meta = expectedMeta,
shouldStore = true,
ttl = 60
).sync()!!

var fetchResult: PNFetchMessagesResult? = null

await
.pollInterval(Duration.ofMillis(1_000))
.pollDelay(Duration.ofMillis(1_000))
.atMost(Duration.ofMillis(10_000))
.untilAsserted {
fetchResult = pubNubWithCrypto.fetchMessages(
includeMeta = true,
includeMessageActions = true,
includeMessageType = true,
includeUUID = true,
channels = listOf(channel)
).sync()
assertNotNull(fetchResult)
assertThat(fetchResult?.channels, aMapWithSize(not(0)))
}

val expectedItem = PNFetchMessageItem(
uuid = pubnub.configuration.userId.value,
message = expectedMessage,
timetoken = result.timetoken,
meta = expectedMeta,
messageType = HistoryMessageType.Message,
actions = emptyMap<String, Map<String, List<Action>>>(),
error = "Crypto is configured but message is not encrypted."
)

val expectedChannelsResponse: Map<String, List<PNFetchMessageItem>> =
mapOf(
channel to listOf(
expectedItem
)
)

val fetchMessageItem = fetchResult!!
assertEquals(expectedChannelsResponse, fetchMessageItem.channels)
}

@Test
fun always() {
assertEquals(
Expand Down
6 changes: 5 additions & 1 deletion src/main/kotlin/com/pubnub/api/endpoints/FetchMessages.kt
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ class FetchMessages internal constructor(
val body = input.body()!!
val channelsMap = body.channels.mapValues { (_, value) ->
value.map { serverMessageItem ->
val newMessage = serverMessageItem.message.processHistoryMessage(pubnub.cryptoModule, pubnub.mapper)
val (newMessage, error) = serverMessageItem.message.processHistoryMessage(
pubnub.cryptoModule,
pubnub.mapper
)
val newActions =
if (includeMessageActions) serverMessageItem.actions ?: mapOf() else serverMessageItem.actions
PNFetchMessageItem(
Expand All @@ -92,6 +95,7 @@ class FetchMessages internal constructor(
timetoken = serverMessageItem.timetoken,
actions = newActions,
messageType = if (includeMessageType) HistoryMessageType.of(serverMessageItem.messageType) else null,
error = error
)
}
}.toMap()
Expand Down
12 changes: 8 additions & 4 deletions src/main/kotlin/com/pubnub/api/endpoints/History.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,26 +71,30 @@ class History internal constructor(

val historyEntry = iterator.next()

var message: JsonElement
var timetoken: Long? = null
var meta: JsonElement? = null
val historyMessageWithError: Pair<JsonElement, String?>

if (includeTimetoken || includeMeta) {
message = pubnub.mapper.getField(historyEntry, "message")!!.processHistoryMessage(pubnub.cryptoModule, pubnub.mapper)
historyMessageWithError = pubnub.mapper.getField(historyEntry, "message")!!.processHistoryMessage(pubnub.cryptoModule, pubnub.mapper)
if (includeTimetoken) {
timetoken = pubnub.mapper.elementToLong(historyEntry, "timetoken")
}
if (includeMeta) {
meta = pubnub.mapper.getField(historyEntry, "meta")
}
} else {
message = historyEntry.processHistoryMessage(pubnub.cryptoModule, pubnub.mapper)
historyMessageWithError = historyEntry.processHistoryMessage(pubnub.cryptoModule, pubnub.mapper)
}

val message: JsonElement = historyMessageWithError.first
val error: String? = historyMessageWithError.second

val historyItem = PNHistoryItemResult(
entry = message,
timetoken = timetoken,
meta = meta
meta = meta,
error = error
)

messages.add(historyItem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ data class PNFetchMessageItem(
val meta: JsonElement?,
val timetoken: Long,
val actions: Map<String, Map<String, List<Action>>>? = null,
val messageType: HistoryMessageType?
val messageType: HistoryMessageType?,
val error: String? = null
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ class PNHistoryResult internal constructor(
data class PNHistoryItemResult(
val entry: JsonElement,
val timetoken: Long? = null,
val meta: JsonElement? = null
val meta: JsonElement? = null,
val error: String? = null
)
53 changes: 39 additions & 14 deletions src/main/kotlin/com/pubnub/extension/JsonElement.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,55 @@ import com.google.gson.JsonElement
import com.pubnub.api.crypto.CryptoModule
import com.pubnub.api.crypto.decryptString
import com.pubnub.api.managers.MapperManager
import org.slf4j.LoggerFactory

internal fun JsonElement.processHistoryMessage(cryptoModule: CryptoModule?, mapper: MapperManager): JsonElement {
cryptoModule ?: return this
private val log = LoggerFactory.getLogger("JsonElement")

val inputText =
if (mapper.isJsonObject(this) && mapper.hasField(
this,
"pn_other"
)
) {
mapper.elementToString(this, "pn_other")
private const val PN_OTHER = "pn_other"

internal fun JsonElement.processHistoryMessage(
cryptoModule: CryptoModule?,
mapper: MapperManager
): Pair<JsonElement, String?> {

cryptoModule ?: return Pair(this, null)
val error: String?

val inputText = if (mapper.isJsonObject(this)) {
// property pn_other is used when we want to send encrypted Push Notification, not whole JSON object is encrypted but only value of pn_other property
if (mapper.hasField(this, PN_OTHER)) {
// JSON with pn_other property indicates that this is encrypted Push Notification
mapper.elementToString(this, PN_OTHER)
} else {
mapper.elementToString(this)
// plain JSON object indicates that this is not encrypted message
error = logAndReturnDecryptionError()
return Pair(this, error)
}
} else {
// String may represent not encrypted string or encrypted data. We will check this when decrypting.
mapper.elementToString(this)
}

val outputText = cryptoModule.decryptString(inputText!!)
val outputText = try {
cryptoModule.decryptString(inputText!!)
} catch (e: Exception) {
error = logAndReturnDecryptionError()
return Pair(this, error)
}

var outputObject = mapper.fromJson(outputText, JsonElement::class.java)

mapper.getField(this, "pn_other")?.let {
mapper.getField(this, PN_OTHER)?.let {
val objectNode = mapper.getAsObject(this)
mapper.putOnObject(objectNode, "pn_other", outputObject)
mapper.putOnObject(objectNode, PN_OTHER, outputObject)
outputObject = objectNode
}

return outputObject
return Pair(outputObject, null)
}

private fun logAndReturnDecryptionError(): String {
val errorMessage = "Crypto is configured but message is not encrypted."
log.warn(errorMessage)
return errorMessage
}
Original file line number Diff line number Diff line change
Expand Up @@ -474,55 +474,4 @@ class HistoryEndpointTest : BaseTest() {
assertEquals(44, messages[1].entry.asJsonObject["b"].asInt)
}
}

@Test
fun testSyncProcessMessageError() {
val historyItem1 = mapOf(
"a" to 11,
"b" to 22
)
val historyEnvelope1 = mapOf(
"timetoken" to 1111,
"message" to historyItem1
)

val historyItem2 = mapOf(
"a" to 33,
"b" to 44
)
val historyEnvelope2 = mapOf(
"timetoken" to 2222,
"message" to historyItem2
)

val historyItems = listOf(
historyEnvelope1,
historyEnvelope2
)
val testArray = listOf(
historyItems,
1234,
4321
)

stubFor(
get(urlPathEqualTo("/v2/history/sub-key/mySubscribeKey/channel/niceChannel"))
.willReturn(aResponse().withBody(pubnub.mapper.toJson(testArray)))
)

pubnub.configuration.cipherKey = "Test"

try {
pubnub.history(
channel = "niceChannel",
count = 5,
reverse = true,
start = 1L,
end = 2L,
includeTimetoken = true
).sync()!!
failTest()
} catch (e: UnsupportedOperationException) {
}
}
}
Loading

0 comments on commit 8987e71

Please sign in to comment.