Skip to content

Commit c64ed30

Browse files
committed
Added Base64Sink and CipherSink.
1 parent d00029f commit c64ed30

File tree

6 files changed

+188
-0
lines changed

6 files changed

+188
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package nl.nl2312.okio.base64
2+
3+
import okio.Buffer
4+
import okio.ForwardingSink
5+
import okio.Sink
6+
7+
/**
8+
* Encodes writes to a [Sink] on the fly using Base64.
9+
*
10+
* <p>Chunked writing is unsupported; the written Base64 output is always finalised. However, partial writing (not
11+
* draining the full source) is supported.
12+
*/
13+
class Base64Sink(delegate: Sink) : ForwardingSink(delegate) {
14+
15+
override fun write(source: Buffer, byteCount: Long) {
16+
// Read the requested number of bytes (or all available) from source
17+
val bytesToRead = byteCount.coerceAtMost(source.size())
18+
val decoded = source.readByteString(bytesToRead)
19+
20+
// Base64-encode
21+
val encoded = decoded.base64()
22+
23+
val encodedSink = Buffer()
24+
encodedSink.writeUtf8(encoded)
25+
super.write(encodedSink, encodedSink.size())
26+
}
27+
28+
}

lib/src/main/java/nl/nl2312/okio/base64/Base64Source.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import okio.Source
77

88
/**
99
* Accepts any Base64-encoded data [Source] and decodes it on the fly.
10+
*
11+
* <p>Chunked and partial reading is supported. Requests for a specific number of bytes are honored (until the source
12+
* is drained). Due to the nature of Base64 encoding, this means that for every 3 requested bytes, 4 bytes are read from
13+
* the source.
1014
*/
1115
class Base64Source(source: Source) : ForwardingSource(source) {
1216

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package nl.nl2312.okio.cipher
2+
3+
import okio.Buffer
4+
import okio.ForwardingSink
5+
import okio.Sink
6+
import javax.crypto.Cipher
7+
8+
/**
9+
* Encrypts writes to a [Sink] on the fly given a pre-set [Cipher].
10+
*
11+
* <p>Chunked writing is unsupported; the written ciphered output is always finalised. However, partial writing (not
12+
* draining the full source) is supported.
13+
*/
14+
class CipherSink(
15+
delegate: Sink,
16+
private val cipher: Cipher) : ForwardingSink(delegate) {
17+
18+
override fun write(source: Buffer, byteCount: Long) {
19+
// Read the requested number of bytes (or all available) from source
20+
val bytesToRead = byteCount.coerceAtMost(source.size())
21+
val decrypted = source.readByteArray(bytesToRead)
22+
23+
// Encrypt
24+
val encrypted = cipher.doFinal(decrypted)
25+
26+
val encryptedSink = Buffer()
27+
encryptedSink.write(encrypted)
28+
super.write(encryptedSink, encryptedSink.size())
29+
}
30+
31+
}

lib/src/main/java/nl/nl2312/okio/cipher/CipherSource.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import javax.crypto.Cipher
77

88
/**
99
* Accepts an encrypted [Source] and deciphers it on the fly.
10+
*
11+
* <p>Chunked and partial reading is supported. However, the deciphered output text is not complete until the [Source]
12+
* is drained. Do not attempt to (re-)use the supplied [cipher] until the full source stream is completed.
1013
*/
1114
class CipherSource(
1215
source: Source,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package nl.nl2312.okio.base64
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import okio.Buffer
5+
import org.junit.Test
6+
7+
class Base64SinkTest {
8+
9+
private val utf8String = "okio oh my¿¡"
10+
private val utf8Sink = Buffer().writeUtf8(utf8String)
11+
12+
@Test
13+
fun write_fromFixedString() {
14+
val output = Buffer()
15+
val sink = Base64Sink(output)
16+
17+
sink.write(utf8Sink, Long.MAX_VALUE)
18+
19+
assertThat(output.readUtf8()).isEqualTo("b2tpbyBvaCBtecK/wqE=")
20+
}
21+
22+
@Test
23+
fun write_partialWrite() {
24+
val output = Buffer()
25+
val sink = Base64Sink(output)
26+
27+
// Request only to write the first 5 (decoded) characters
28+
sink.write(utf8Sink, 5)
29+
30+
assertThat(output.readUtf8()).isEqualTo("b2tpbyA=")
31+
}
32+
33+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package nl.nl2312.okio.cipher
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import okio.Buffer
5+
import org.junit.Test
6+
import java.security.SecureRandom
7+
import javax.crypto.BadPaddingException
8+
import javax.crypto.Cipher
9+
import javax.crypto.spec.IvParameterSpec
10+
import javax.crypto.spec.SecretKeySpec
11+
12+
13+
class CipherSinkTest {
14+
15+
private val encodeCipher: Cipher
16+
private val decodeCipher: Cipher
17+
18+
init {
19+
// Generate random secret key for the scope of these tests
20+
val secureRandom = SecureRandom()
21+
val key = ByteArray(16)
22+
secureRandom.nextBytes(key)
23+
val secretKeySpec = SecretKeySpec(key, "AES")
24+
val ivBytes = ByteArray(16)
25+
secureRandom.nextBytes(ivBytes)
26+
val iv = IvParameterSpec(ivBytes)
27+
28+
// Encode some data to test with
29+
encodeCipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
30+
encodeCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv)
31+
32+
// Prepare decoding decodeCipher for our tests
33+
decodeCipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
34+
decodeCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, iv)
35+
}
36+
37+
@Test
38+
fun write_fromFixedString() {
39+
val output = Buffer()
40+
val cipheredSink = CipherSink(output, encodeCipher)
41+
42+
val utf8Sink = Buffer().writeUtf8("okio oh my¿¡")
43+
cipheredSink.write(utf8Sink, Long.MAX_VALUE)
44+
45+
val cipheredBytes = output.readByteArray()
46+
assertThat(String(cipheredBytes)).isNotEqualTo("okio oh my¿¡")
47+
val decipheredOutput = decodeCipher.doFinal(cipheredBytes)
48+
assertThat(String(decipheredOutput)).isEqualTo("okio oh my¿¡")
49+
}
50+
51+
@Test
52+
fun write_partialWrite() {
53+
val output = Buffer()
54+
val cipheredSink = CipherSink(output, encodeCipher)
55+
56+
val utf8Sink = Buffer().writeUtf8("okio oh my¿¡")
57+
58+
// Encrypt first 5 bytes
59+
cipheredSink.write(utf8Sink, 5)
60+
val firstOutput = decodeCipher.doFinal(output.readByteArray())
61+
assertThat(String(firstOutput)).isEqualTo("okio ")
62+
63+
// Encrypt another 5 bytes
64+
cipheredSink.write(utf8Sink, 5)
65+
val secondOutput = decodeCipher.doFinal(output.readByteArray())
66+
assertThat(String(secondOutput)).isEqualTo("oh my")
67+
68+
// Asking to encrypt another 5 bytes will drain the sink
69+
cipheredSink.write(utf8Sink, 5)
70+
val finalOutput = decodeCipher.doFinal(output.readByteArray())
71+
assertThat(String(finalOutput)).isEqualTo("¿¡")
72+
}
73+
74+
@Test(expected = BadPaddingException::class)
75+
fun write_chunkedWrite() {
76+
val output = Buffer()
77+
val cipheredSink = CipherSink(output, encodeCipher)
78+
79+
val utf8Sink = Buffer().writeUtf8("okio oh my¿¡")
80+
81+
// Encrypt first 5 bytes and then another 5 bytes
82+
cipheredSink.write(utf8Sink, 5)
83+
cipheredSink.write(utf8Sink, 5)
84+
85+
// Throws BadPaddingException as the output is not a single cipher text but two individual ones
86+
decodeCipher.doFinal(output.readByteArray())
87+
}
88+
89+
}

0 commit comments

Comments
 (0)