diff --git a/tanker-bindings/src/androidTest/kotlin/io/tanker/api/EncryptionSession.kt b/tanker-bindings/src/androidTest/kotlin/io/tanker/api/EncryptionSession.kt index 11ec27c..e6a4678 100644 --- a/tanker-bindings/src/androidTest/kotlin/io/tanker/api/EncryptionSession.kt +++ b/tanker-bindings/src/androidTest/kotlin/io/tanker/api/EncryptionSession.kt @@ -116,4 +116,53 @@ class EncryptionSessionTests : TankerSpec() { val e = shouldThrow { tankerBob.decrypt(private).get() } assertThat((e.cause is TankerException)).isEqualTo(true) } + + private val encryptionSessionOverhead = 57 + private val encryptionSessionPaddedOverhead = encryptionSessionOverhead + 1 + + @Test + fun can_encrypt_with_auto_padding_by_default() { + val plaintext = "my clear data is clear!" + val lengthWithPadme = 24 + val sess = tankerAlice.createEncryptionSession(null).get() + val encrypted = sess.encrypt(plaintext.toByteArray()).get() + + assertThat(encrypted.size - encryptionSessionPaddedOverhead).isEqualTo(lengthWithPadme) + assertThat(String(tankerAlice.decrypt(encrypted).get())).isEqualTo(plaintext) + } + + @Test + fun can_encrypt_with_auto_padding() { + val plaintext = "my clear data is clear!" + val lengthWithPadme = 24 + val encryptOptions = EncryptionOptions().paddingStep(Padding.auto) + val sess = tankerAlice.createEncryptionSession(encryptOptions).get() + val encrypted = sess.encrypt(plaintext.toByteArray()).get() + + assertThat(encrypted.size - encryptionSessionPaddedOverhead).isEqualTo(lengthWithPadme) + assertThat(String(tankerAlice.decrypt(encrypted).get())).isEqualTo(plaintext) + } + + @Test + fun can_encrypt_with_no_padding() { + val plaintext = "L'assommoir" + val encryptOptions = EncryptionOptions().paddingStep(Padding.off) + val sess = tankerAlice.createEncryptionSession(encryptOptions).get() + val encrypted = sess.encrypt(plaintext.toByteArray()).get() + + assertThat(encrypted.size - encryptionSessionOverhead).isEqualTo(plaintext.length) + assertThat(String(tankerAlice.decrypt(encrypted).get())).isEqualTo(plaintext) + } + + @Test + fun can_encrypt_with_a_padding_step() { + val plaintext = "Au Bonheur des Dames" + val step = 13 + val encryptOptions = EncryptionOptions().paddingStep(Padding.step(step)) + val sess = tankerAlice.createEncryptionSession(encryptOptions).get() + val encrypted = sess.encrypt(plaintext.toByteArray()).get() + + assertThat((encrypted.size - encryptionSessionPaddedOverhead) % step).isEqualTo(0) + assertThat(String(tankerAlice.decrypt(encrypted).get())).isEqualTo(plaintext) + } } diff --git a/tanker-bindings/src/androidTest/kotlin/io/tanker/api/Tanker.kt b/tanker-bindings/src/androidTest/kotlin/io/tanker/api/Tanker.kt index d86dcb8..f165711 100644 --- a/tanker-bindings/src/androidTest/kotlin/io/tanker/api/Tanker.kt +++ b/tanker-bindings/src/androidTest/kotlin/io/tanker/api/Tanker.kt @@ -7,6 +7,7 @@ import org.junit.Before import org.junit.Test import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit +import kotlin.IllegalArgumentException class TankerTests : TankerSpec() { @Before @@ -134,6 +135,89 @@ class TankerTests : TankerSpec() { tanker.stop().get() } + private val simpleEncryptionOverhead = 17 + private val simplePaddedEncryptionOverhead = simpleEncryptionOverhead + 1 + + @Test + fun auto_padding_by_default() { + val tanker = Tanker(options) + val identity = tc.createIdentity() + tanker.start(identity).get() + tanker.registerIdentity(PassphraseVerification("pass")).get() + + val plaintext = "my clear data is clear!" + val lengthWithPadme = 24 + val encrypted = tanker.encrypt(plaintext.toByteArray()).get() + assertThat(encrypted.size - simplePaddedEncryptionOverhead).isEqualTo(lengthWithPadme) + + val decrypted = tanker.decrypt(encrypted).get() + assertThat(String(decrypted)).isEqualTo(plaintext) + + tanker.stop().get() + } + + @Test + fun can_set_padding_auto() { + val tanker = Tanker(options) + val identity = tc.createIdentity() + tanker.start(identity).get() + tanker.registerIdentity(PassphraseVerification("pass")).get() + + val plaintext = "my clear data is clear!" + val lengthWithPadme = 24 + val encryptOptions = EncryptionOptions().paddingStep(Padding.auto) + val encrypted = tanker.encrypt(plaintext.toByteArray(), encryptOptions).get() + assertThat(encrypted.size - simplePaddedEncryptionOverhead).isEqualTo(lengthWithPadme) + + val decrypted = tanker.decrypt(encrypted).get() + assertThat(String(decrypted)).isEqualTo(plaintext) + + tanker.stop().get() + } + + @Test + fun can_disable_padding() { + val tanker = Tanker(options) + val identity = tc.createIdentity() + tanker.start(identity).get() + tanker.registerIdentity(PassphraseVerification("pass")).get() + + val plaintext = "plain text" + val encryptOptions = EncryptionOptions().paddingStep(Padding.off) + val encrypted = tanker.encrypt(plaintext.toByteArray(), encryptOptions).get() + assertThat(encrypted.size - simpleEncryptionOverhead).isEqualTo(plaintext.length) + + val decrypted = tanker.decrypt(encrypted).get() + assertThat(String(decrypted)).isEqualTo(plaintext) + + tanker.stop().get() + } + + @Test + fun can_set_the_padding_step() { + val tanker = Tanker(options) + val identity = tc.createIdentity() + tanker.start(identity).get() + tanker.registerIdentity(PassphraseVerification("pass")).get() + + val plaintext = "plain text" + val encryptOptions = EncryptionOptions().paddingStep(Padding.step(13)) + val encrypted = tanker.encrypt(plaintext.toByteArray(), encryptOptions).get() + assertThat((encrypted.size - simplePaddedEncryptionOverhead) % 13).isEqualTo(0) + + val decrypted = tanker.decrypt(encrypted).get() + assertThat(String(decrypted)).isEqualTo(plaintext) + + tanker.stop().get() + } + + @Test + fun cannot_set_a_bad_padding_step() { + listOf(-1, 0, 1).forEach { + shouldThrow { Padding.step(it) } + } + } + @Test fun can_stop_tanker_while_a_call_is_in_flight() { val tanker = Tanker(options) @@ -165,6 +249,26 @@ class TankerTests : TankerSpec() { tanker.stop().get() } + @Test + fun can_stream_encrypt_with_padding() { + val tanker = Tanker(options) + val identity = tc.createIdentity() + tanker.start(identity).get() + tanker.registerIdentity(PassphraseVerification("pass")).get() + + val plaintext = ByteArray(3 * 1024 * 1024 + 2) + val clear = InputStreamWrapper(plaintext.inputStream()) + + val encryptor = tanker.encrypt(clear).get() + val encrypted = TankerInputStream(encryptor).readBytes() + assertThat(encrypted).hasSize(3211512) + val decryptor = tanker.decrypt(InputStreamWrapper(encrypted.inputStream())).get() + + val decrypted = TankerInputStream(decryptor).readBytes() + assertThat(decrypted).isEqualTo(plaintext) + tanker.stop().get() + } + @Test fun can_encrypt_share_and_decrypt_between_two_users() { val aliceId = tc.createIdentity() diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/EncryptionOptions.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/EncryptionOptions.kt index 6fa786f..73e67ad 100644 --- a/tanker-bindings/src/main/kotlin/io/tanker/api/EncryptionOptions.kt +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/EncryptionOptions.kt @@ -13,12 +13,13 @@ import com.sun.jna.Structure */ open class EncryptionOptions : Structure() { // NOTE: Remember to keep the version in sync w/ the c++! - @JvmField var version: Byte = 3 + @JvmField var version: Byte = 4 @JvmField var shareWithUsers = Pointer(0) @JvmField var nbUsers = 0 @JvmField var shareWithGroups = Pointer(0) @JvmField var nbGroups = 0 @JvmField var shareWithSelf = 1.toByte() + @JvmField var paddingStep = Padding.auto.native_value /** * JNA does not support having a StringArray directly in a struct, @@ -58,7 +59,16 @@ open class EncryptionOptions : Structure() { return this } + /** + * Sets the padding step. + * @param paddingStep A Padding object + */ + fun paddingStep(paddingStep: Padding): EncryptionOptions { + this.paddingStep = paddingStep.native_value + return this + } + override fun getFieldOrder(): List { - return listOf("version", "shareWithUsers", "nbUsers", "shareWithGroups", "nbGroups", "shareWithSelf") + return listOf("version", "shareWithUsers", "nbUsers", "shareWithGroups", "nbGroups", "shareWithSelf", "paddingStep") } } diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/EncryptionSession.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/EncryptionSession.kt index a9e647b..0773bfb 100644 --- a/tanker-bindings/src/main/kotlin/io/tanker/api/EncryptionSession.kt +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/EncryptionSession.kt @@ -21,7 +21,7 @@ class EncryptionSession(private val csess: Pointer) { val inBuf = Memory(data.size.toLong().coerceAtLeast(1)) inBuf.write(0, data, 0, data.size) - val encryptedSize = lib.tanker_encryption_session_encrypted_size(data.size.toLong()) + val encryptedSize = lib.tanker_encryption_session_encrypted_size(csess, data.size.toLong()) val outBuf = Memory(encryptedSize) val futurePtr = lib.tanker_encryption_session_encrypt(csess, outBuf, inBuf, data.size.toLong()) diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/Padding.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/Padding.kt new file mode 100644 index 0000000..c798681 --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/Padding.kt @@ -0,0 +1,38 @@ +package io.tanker.api + +/** + * Padding control for data encryption. + */ +sealed class Padding(internal val native_value: Int) { + /** + * Allowed values for Padding variables + */ + companion object { + /** + * This is the default option. + */ + @JvmStatic val auto : Padding = Auto + + /** + * Disables padding. + */ + @JvmStatic val off : Padding = Off + + /** + * Pads the data up to a multiple of value before encryption. + * To disable padding, use Off(). + * @param value A >= 2 integer. + */ + @JvmStatic fun step(value: Int): Padding = Step(value) + } + + private object Auto : Padding(0) + private object Off : Padding(1) + + private class Step(value: Int) : Padding(value) { + init { + if (value <= 1) + throw IllegalArgumentException("Invalid padding step, the value must be >= 2.") + } + } +} diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/Tanker.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/Tanker.kt index 57322f5..9dcb36f 100644 --- a/tanker-bindings/src/main/kotlin/io/tanker/api/Tanker.kt +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/Tanker.kt @@ -5,6 +5,7 @@ import com.sun.jna.* import io.tanker.bindings.* import io.tanker.datastore.* import io.tanker.jni.KVMx86Bug +import kotlin.IllegalArgumentException /** * Main entry point for the Tanker SDK. Can open a TankerSession. @@ -335,7 +336,8 @@ class Tanker(tankerOptions: TankerOptions) { val inBuf = Memory(data.size.toLong().coerceAtLeast(1)) inBuf.write(0, data, 0, data.size) - val encryptedSize = lib.tanker_encrypted_size(data.size.toLong()) + val paddingStep = options?.paddingStep ?: Padding.auto.native_value + val encryptedSize = lib.tanker_encrypted_size(data.size.toLong(), paddingStep) val outBuf = Memory(encryptedSize) val futurePtr = lib.tanker_encrypt(tanker, outBuf, inBuf, data.size.toLong(), options) diff --git a/tanker-bindings/src/main/kotlin/io/tanker/bindings/TankerLib.kt b/tanker-bindings/src/main/kotlin/io/tanker/bindings/TankerLib.kt index e45c75c..9229605 100644 --- a/tanker-bindings/src/main/kotlin/io/tanker/bindings/TankerLib.kt +++ b/tanker-bindings/src/main/kotlin/io/tanker/bindings/TankerLib.kt @@ -105,7 +105,7 @@ interface TankerLib : AsyncLib, DatastoreLib, Library { fun tanker_set_log_handler(handler: LogHandlerCallback): Void - fun tanker_encrypted_size(clear_size: Long): Long + fun tanker_encrypted_size(clear_size: Long, padding_step: Int): Long fun tanker_decrypted_size(encrypted_data: Pointer, encrypted_size: Long): ExpectedPointer fun tanker_get_resource_id(encrypted_data: Pointer, encrypted_size: Long): ExpectedPointer @@ -128,7 +128,7 @@ interface TankerLib : AsyncLib, DatastoreLib, Library { fun tanker_encryption_session_open(session: SessionPointer, options: EncryptionOptions): FuturePointer fun tanker_encryption_session_close(encSess: EncryptionSessionPointer): FuturePointer - fun tanker_encryption_session_encrypted_size(clear_size: Long): Long + fun tanker_encryption_session_encrypted_size(encSess: EncryptionSessionPointer, clear_size: Long): Long fun tanker_encryption_session_get_resource_id(encSess: EncryptionSessionPointer): ExpectedPointer fun tanker_encryption_session_encrypt(encSess: EncryptionSessionPointer, encrypted_data: Pointer, data: Pointer, data_size: Long): FuturePointer