Skip to content

Commit 415c246

Browse files
Fixed upload/download encrypted file API (#383)
fix: Fixed upload/download encrypted file API. Fixed upload/download encrypted file API. When file is encrypted application/octet-stream data format is enforced regardless of original file type (image/jpeg, video/mp4, text/plain) or server's suggested Content-Type from generateUploadUrl. fix: Removed redundant buffering when parsing encrypted data. * PubNub SDK v12.0.1 release. --------- Co-authored-by: PubNub Release Bot <120067856+pubnub-release-bot@users.noreply.github.com>
1 parent ca83e11 commit 415c246

File tree

10 files changed

+360
-23
lines changed

10 files changed

+360
-23
lines changed

.pubnub.yml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
name: kotlin
2-
version: 12.0.0
2+
version: 12.0.1
33
schema: 1
44
scm: github.com/pubnub/kotlin
55
files:
6-
- build/libs/pubnub-kotlin-12.0.0-all.jar
6+
- build/libs/pubnub-kotlin-12.0.1-all.jar
77
sdks:
88
-
99
type: library
@@ -23,8 +23,8 @@ sdks:
2323
-
2424
distribution-type: library
2525
distribution-repository: maven
26-
package-name: pubnub-kotlin-12.0.0
27-
location: https://repo.maven.apache.org/maven2/com/pubnub/pubnub-kotlin/12.0.0/pubnub-kotlin-12.0.0.jar
26+
package-name: pubnub-kotlin-12.0.1
27+
location: https://repo.maven.apache.org/maven2/com/pubnub/pubnub-kotlin/12.0.1/pubnub-kotlin-12.0.1.jar
2828
supported-platforms:
2929
supported-operating-systems:
3030
Android:
@@ -121,6 +121,13 @@ sdks:
121121
license-url: https://www.apache.org/licenses/LICENSE-2.0.txt
122122
is-required: Required
123123
changelog:
124+
- date: 2025-11-19
125+
version: v12.0.1
126+
changes:
127+
- type: bug
128+
text: "Fixed upload/download encrypted file API. When file is encrypted application/octet-stream data format is enforced regardless of original file type (image/jpeg, video/mp4, text/plain) or server's suggested Content-Type from generateUploadUrl."
129+
- type: bug
130+
text: "Removed redundant buffering when parsing encrypted data."
124131
- date: 2025-11-10
125132
version: v12.0.0
126133
changes:

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## v12.0.1
2+
November 19 2025
3+
4+
#### Fixed
5+
- Fixed upload/download encrypted file API. When file is encrypted application/octet-stream data format is enforced regardless of original file type (image/jpeg, video/mp4, text/plain) or server's suggested Content-Type from generateUploadUrl.
6+
- Removed redundant buffering when parsing encrypted data.
7+
18
## v12.0.0
29
November 10 2025
310

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ You will need the publish and subscribe keys to authenticate your app. Get your
2020
<dependency>
2121
<groupId>com.pubnub</groupId>
2222
<artifactId>pubnub-kotlin</artifactId>
23-
<version>12.0.0</version>
23+
<version>12.0.1</version>
2424
</dependency>
2525
```
2626

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ RELEASE_SIGNING_ENABLED=true
1818
SONATYPE_HOST=DEFAULT
1919
SONATYPE_AUTOMATIC_RELEASE=false
2020
GROUP=com.pubnub
21-
VERSION_NAME=12.0.0
21+
VERSION_NAME=12.0.1
2222
POM_PACKAGING=jar
2323

2424
POM_NAME=PubNub SDK

pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/FilesIntegrationTest.kt

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,111 @@ class FilesIntegrationTest : BaseIntegrationTest() {
3131
uploadListDownloadDelete(false)
3232
}
3333

34+
@Test
35+
fun legacyEncryptedFileTransfer() {
36+
uploadListDownloadDeleteFileWithCipher(true)
37+
}
38+
39+
@Test
40+
fun aesCbcEncryptedFileTransfer() {
41+
uploadListDownloadDeleteFileWithCipher(false)
42+
}
43+
44+
fun uploadListDownloadDeleteFileWithCipher(withLegacyCrypto: Boolean) {
45+
if (withLegacyCrypto) {
46+
clientConfig = {
47+
cryptoModule = CryptoModule.createLegacyCryptoModule("enigma")
48+
}
49+
} else {
50+
clientConfig = {
51+
cryptoModule = CryptoModule.createAesCbcCryptoModule("enigma")
52+
}
53+
}
54+
55+
val channel: String = randomChannel()
56+
val fileName = "logback.xml"
57+
val message = "This is message"
58+
val meta = "This is meta"
59+
val customMessageType = "myCustomType"
60+
61+
// Read the logback.xml file from resources
62+
val logbackResource = this.javaClass.classLoader.getResourceAsStream("logback.xml")
63+
?: throw IllegalStateException("logback.xml not found in resources")
64+
val originalContent = logbackResource.readBytes()
65+
val originalContentString = String(originalContent, StandardCharsets.UTF_8)
66+
67+
val connectedLatch = CountDownLatch(1)
68+
val fileEventReceived = CountDownLatch(1)
69+
pubnub.addListener(
70+
object : SubscribeCallback() {
71+
override fun status(
72+
pubnub: PubNub,
73+
status: PNStatus,
74+
) {
75+
if (status.category == PNStatusCategory.PNConnectedCategory) {
76+
connectedLatch.countDown()
77+
}
78+
}
79+
80+
override fun file(
81+
pubnub: PubNub,
82+
result: PNFileEventResult,
83+
) {
84+
if (result.file.name == fileName && result.customMessageType == customMessageType) {
85+
fileEventReceived.countDown()
86+
}
87+
}
88+
},
89+
)
90+
pubnub.subscribe(channels = listOf(channel))
91+
connectedLatch.await(10, TimeUnit.SECONDS)
92+
93+
val sendResult: PNFileUploadResult? =
94+
ByteArrayInputStream(originalContent).use {
95+
pubnub.sendFile(
96+
channel = channel,
97+
fileName = fileName,
98+
inputStream = it,
99+
message = message,
100+
meta = meta,
101+
customMessageType = customMessageType
102+
).sync()
103+
}
104+
105+
if (sendResult == null) {
106+
Assert.fail()
107+
return
108+
}
109+
fileEventReceived.await(10, TimeUnit.SECONDS)
110+
111+
val (_, _, _, data) = pubnub.listFiles(channel = channel).sync()
112+
val fileFoundOnList = data.find { it.id == sendResult.file.id } != null
113+
Assert.assertTrue(fileFoundOnList)
114+
115+
val (_, byteStream) =
116+
pubnub.downloadFile(
117+
channel = channel,
118+
fileName = fileName,
119+
fileId = sendResult.file.id,
120+
).sync()
121+
122+
byteStream?.use {
123+
val downloadedContent = it.readBytes()
124+
val downloadedString = String(downloadedContent, StandardCharsets.UTF_8)
125+
Assert.assertEquals(
126+
"Downloaded content should match original logback.xml",
127+
originalContentString,
128+
downloadedString
129+
)
130+
}
131+
132+
pubnub.deleteFile(
133+
channel = channel,
134+
fileName = fileName,
135+
fileId = sendResult.file.id,
136+
).sync()
137+
}
138+
34139
@Test
35140
fun testSendFileAndDeleteFileOnChannelEntity() {
36141
val sendFileResultReference: AtomicReference<PNFileUploadResult> = AtomicReference()
@@ -205,6 +310,126 @@ class FilesIntegrationTest : BaseIntegrationTest() {
205310
).sync()
206311
}
207312

313+
@Test
314+
fun uploadLargeEncryptedFileWithLegacyCryptoModule() {
315+
uploadLargeEncryptedFileWithCryptoModule(withLegacyCrypto = true)
316+
}
317+
318+
@Test
319+
fun uploadLargeEncryptedFileWithAesCbcCryptoModule() {
320+
uploadLargeEncryptedFileWithCryptoModule(withLegacyCrypto = false)
321+
}
322+
323+
fun uploadLargeEncryptedFileWithCryptoModule(withLegacyCrypto: Boolean) {
324+
clientConfig = {
325+
cryptoModule = CryptoModule.createLegacyCryptoModule("enigma")
326+
}
327+
val channel: String = randomChannel()
328+
val fileName = "large_file_${System.currentTimeMillis()}.bin"
329+
330+
// Create a large binary file (1MB) to test encryption
331+
val largeContent = ByteArray(1024 * 1024) { it.toByte() }
332+
333+
val sendResult: PNFileUploadResult? =
334+
ByteArrayInputStream(largeContent).use {
335+
pubnub.sendFile(
336+
channel = channel,
337+
fileName = fileName,
338+
inputStream = it,
339+
message = "Large encrypted file test",
340+
).sync()
341+
}
342+
343+
if (sendResult == null) {
344+
Assert.fail("Failed to upload large encrypted file")
345+
return
346+
}
347+
348+
// Download and verify
349+
val (_, byteStream) =
350+
pubnub.downloadFile(
351+
channel = channel,
352+
fileName = fileName,
353+
fileId = sendResult.file.id,
354+
).sync()
355+
356+
byteStream?.use {
357+
val downloadedContent = it.readBytes()
358+
Assert.assertArrayEquals(
359+
"Downloaded encrypted content should match original",
360+
largeContent,
361+
downloadedContent
362+
)
363+
}
364+
365+
// Cleanup
366+
pubnub.deleteFile(
367+
channel = channel,
368+
fileName = fileName,
369+
fileId = sendResult.file.id,
370+
).sync()
371+
}
372+
373+
@Test
374+
fun uploadMultipleSizesWithEncryption() {
375+
clientConfig = {
376+
cryptoModule = CryptoModule.createLegacyCryptoModule("enigma")
377+
}
378+
val channel: String = randomChannel()
379+
380+
val testSizes = listOf(
381+
100, // Small file
382+
1024, // 1KB
383+
10240, // 10KB
384+
102400, // 100KB
385+
524288 // 512KB
386+
)
387+
388+
for (size in testSizes) {
389+
val fileName = "test_${size}_${System.currentTimeMillis()}.bin"
390+
val content = ByteArray(size) { (it % 256).toByte() }
391+
392+
val sendResult: PNFileUploadResult? =
393+
ByteArrayInputStream(content).use {
394+
pubnub.sendFile(
395+
channel = channel,
396+
fileName = fileName,
397+
inputStream = it,
398+
message = "Test file size: $size",
399+
).sync()
400+
}
401+
402+
if (sendResult == null) {
403+
Assert.fail("Failed to upload file of size $size")
404+
return
405+
}
406+
407+
// Download and verify
408+
val (_, byteStream) =
409+
pubnub.downloadFile(
410+
channel = channel,
411+
fileName = fileName,
412+
fileId = sendResult.file.id,
413+
).sync()
414+
415+
byteStream?.use {
416+
val downloadedContent = it.readBytes()
417+
Assert.assertArrayEquals(
418+
"Downloaded content should match original for size $size",
419+
content,
420+
downloadedContent
421+
)
422+
}
423+
424+
// Cleanup
425+
pubnub.deleteFile(
426+
channel = channel,
427+
fileName = fileName,
428+
fileId = sendResult.file.id,
429+
).sync()
430+
}
431+
}
432+
208433
private fun readToString(inputStream: InputStream): String {
209434
Scanner(inputStream).useDelimiter("\\A").use { s ->
210435
return if (s.hasNext()) {

pubnub-kotlin/pubnub-kotlin-impl/src/main/kotlin/com/pubnub/internal/crypto/cryptor/HeaderParser.kt

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,11 @@ internal class HeaderParser(val logConfig: LogConfig?) {
3636
)
3737

3838
fun parseDataWithHeader(stream: BufferedInputStream): ParseResult<out InputStream> {
39-
val bufferedInputStream = stream.buffered()
40-
bufferedInputStream.mark(Int.MAX_VALUE) // TODO Can be calculated from spec
39+
stream.mark(Int.MAX_VALUE) // TODO Can be calculated from spec
4140
val possibleInitialHeader = ByteArray(MINIMAL_SIZE_OF_CRYPTO_HEADER)
42-
val initiallyRead = bufferedInputStream.read(possibleInitialHeader)
41+
val initiallyRead = stream.read(possibleInitialHeader)
4342
if (!possibleInitialHeader.sliceArray(SENTINEL_STARTING_INDEX..SENTINEL_ENDING_INDEX).contentEquals(SENTINEL)) {
44-
bufferedInputStream.reset()
43+
stream.reset()
4544
return ParseResult.NoHeader
4645
}
4746

@@ -58,17 +57,17 @@ internal class HeaderParser(val logConfig: LogConfig?) {
5857

5958
val cryptorData: ByteArray =
6059
if (cryptorDataSizeFirstByte == THREE_BYTES_SIZE_CRYPTOR_DATA_INDICATOR) {
61-
val cryptorDataSizeBytes = readExactlyNBytez(bufferedInputStream, 2)
60+
val cryptorDataSizeBytes = readExactlyNBytez(stream, 2)
6261
val cryptorDataSize = convertTwoBytesToIntBigEndian(cryptorDataSizeBytes[0], cryptorDataSizeBytes[1])
63-
readExactlyNBytez(bufferedInputStream, cryptorDataSize)
62+
readExactlyNBytez(stream, cryptorDataSize)
6463
} else {
6564
if (cryptorDataSizeFirstByte == UByte.MIN_VALUE) {
6665
byteArrayOf()
6766
} else {
68-
readExactlyNBytez(bufferedInputStream, cryptorDataSizeFirstByte.toInt())
67+
readExactlyNBytez(stream, cryptorDataSizeFirstByte.toInt())
6968
}
7069
}
71-
return ParseResult.Success(cryptorId, cryptorData, bufferedInputStream)
70+
return ParseResult.Success(cryptorId, cryptorData, stream)
7271
}
7372

7473
private fun readExactlyNBytez(
@@ -130,9 +129,9 @@ internal class HeaderParser(val logConfig: LogConfig?) {
130129
val finalCryptorDataSize: ByteArray =
131130
if (cryptorDataSize < THREE_BYTES_SIZE_CRYPTOR_DATA_INDICATOR.toInt()) {
132131
byteArrayOf(cryptorDataSize.toByte()) // cryptorDataSize will be stored on 1 byte
133-
} else if (cryptorDataSize < MAX_VALUE_THAT_CAN_BE_STORED_ON_TWO_BYTES) {
134-
// cryptorDataSize will be stored on 3 byte
135-
byteArrayOf(cryptorDataSize.toByte()) + writeNumberOnTwoBytes(cryptorDataSize)
132+
} else if (cryptorDataSize <= MAX_VALUE_THAT_CAN_BE_STORED_ON_TWO_BYTES) {
133+
// cryptorDataSize will be stored on 3 bytes: indicator (255) + 2 bytes for actual size
134+
byteArrayOf(THREE_BYTES_SIZE_CRYPTOR_DATA_INDICATOR.toByte()) + writeNumberOnTwoBytes(cryptorDataSize)
136135
} else {
137136
throw PubNubException(
138137
errorMessage = "Cryptor Data Size is: $cryptorDataSize whereas max cryptor data size is: $MAX_VALUE_THAT_CAN_BE_STORED_ON_TWO_BYTES",

pubnub-kotlin/pubnub-kotlin-impl/src/main/kotlin/com/pubnub/internal/endpoints/files/SendFileEndpoint.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,15 @@ class SendFileEndpoint internal constructor(
8686
): ExtendedRemoteAction<PNFileUploadResult> {
8787
val result = AtomicReference<FileUploadRequestDetails>()
8888

89+
val isEncrypted = cryptoModule != null
8990
val content =
9091
cryptoModule?.encryptStream(InputStreamSeparator(inputStream))?.use {
9192
it.readBytes()
9293
} ?: inputStream.readBytes()
9394
return ComposableRemoteAction.firstDo(generateUploadUrlFactory.create(channel, fileName)) // 1. generateUrl
9495
.then { res ->
9596
result.set(res)
96-
sendFileToS3Factory.create(fileName, content, res) // 2. upload to s3
97+
sendFileToS3Factory.create(fileName, content, res, isEncrypted) // 2. upload to s3
9798
}.checkpoint().then {
9899
val details = result.get()
99100
publishFileMessageFactory.create( // 3. PublishFileMessage

0 commit comments

Comments
 (0)