diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/AsynchronousByteChannelWrapper.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/AsynchronousByteChannelWrapper.kt new file mode 100644 index 0000000..f75166f --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/AsynchronousByteChannelWrapper.kt @@ -0,0 +1,29 @@ +package io.tanker.api + +import androidx.annotation.RequiresApi +import java.nio.ByteBuffer +import java.nio.channels.AsynchronousByteChannel +import java.nio.channels.CompletionHandler + +@RequiresApi(26) +class AsynchronousByteChannelWrapper(private val channel: AsynchronousByteChannel) : TankerAsynchronousByteChannel { + override fun close() { + channel.close() + } + + override fun isOpen(): Boolean { + return channel.isOpen + } + + override fun read(dst: ByteBuffer?, attachment: A, handler: TankerCompletionHandler?) { + channel.read(dst, attachment, object : CompletionHandler { + override fun completed(result: Int, attachment: A) { + handler!!.completed(result, attachment) + } + + override fun failed(exc: Throwable, attachment: A) { + handler!!.failed(exc, attachment) + } + }) + } +} \ No newline at end of file diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/ErrorCode.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/ErrorCode.kt index b1507a6..6881700 100644 --- a/tanker-bindings/src/main/kotlin/io/tanker/api/ErrorCode.kt +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/ErrorCode.kt @@ -19,4 +19,5 @@ enum class ErrorCode(val value: Int) { INVALID_VERIFICATION(8), TOO_MANY_ATTEMPTS(9), EXPIRED_VERIFICATION(10), + IO_ERROR(11), } diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/InputStreamWrapper.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/InputStreamWrapper.kt new file mode 100644 index 0000000..67c5218 --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/InputStreamWrapper.kt @@ -0,0 +1,34 @@ +package io.tanker.api + +import java.io.InputStream +import java.nio.ByteBuffer +import java.util.concurrent.Future + +class InputStreamWrapper(private val inputStream: InputStream) : TankerAsynchronousByteChannel { + companion object { + private var isClosed = false + } + + override fun read(dst: ByteBuffer?, attachment: A, handler: TankerCompletionHandler?) { + TankerFuture.threadPool.execute { + try { + val b = ByteArray(dst!!.remaining()) + val nbRead = inputStream.read(b) + if (nbRead != -1) { + dst.put(b, 0, nbRead) + } + handler!!.completed(nbRead, attachment) + } catch (e: Throwable) { + handler!!.failed(e, attachment) + } + } + } + override fun isOpen(): Boolean { + return !isClosed + } + + override fun close() { + inputStream.close() + isClosed = true + } +} 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 c83deb7..ede7050 100644 --- a/tanker-bindings/src/main/kotlin/io/tanker/api/Tanker.kt +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/Tanker.kt @@ -1,11 +1,14 @@ package io.tanker.api +import androidx.annotation.RequiresApi import android.util.Log import com.sun.jna.Memory import com.sun.jna.Pointer import com.sun.jna.StringArray import io.tanker.bindings.* import io.tanker.jni.KVMx86Bug +import java.io.InputStream +import java.nio.channels.AsynchronousByteChannel /** * Main entry point for the Tanker SDK. Can open a TankerSession. @@ -15,7 +18,7 @@ class Tanker(tankerOptions: TankerOptions) { private const val LOG_TAG = "io.tanker.sdk" private const val TANKER_ANDROID_VERSION = "dev" - private val lib = TankerLib.create() + internal val lib = TankerLib.create() @ProguardKeep private var logCallbackLifeSupport: LogHandlerCallback? = null @@ -270,11 +273,66 @@ class Tanker(tankerOptions: TankerOptions) { val outBuf = Memory(encryptedSize) val futurePtr = lib.tanker_encrypt(tanker, outBuf, inBuf, data.size.toLong(), options) - return TankerFuture(futurePtr, Unit::class.java).andThen(TankerCallbackWithKeepAlive(keepAlive = inBuf) { + return TankerFuture(futurePtr, Unit::class.java).andThen(TankerCallbackWithKeepAlive(keepAlive = inBuf) { outBuf.getByteArray(0, encryptedSize.toInt()) }) } + @RequiresApi(26) + fun encrypt(channel: AsynchronousByteChannel): TankerFuture { + return encrypt(channel, null) + } + + @RequiresApi(26) + fun encrypt(channel: AsynchronousByteChannel, options: EncryptOptions?): TankerFuture { + return encrypt(AsynchronousByteChannelWrapper(channel), options).andThen(TankerCallback { + TankerStreamChannelWrapper(it) + }) + } + + @RequiresApi(26) + fun decrypt(channel: AsynchronousByteChannel): TankerFuture { + return decrypt(AsynchronousByteChannelWrapper(channel)).andThen(TankerCallback { + TankerStreamChannelWrapper(it) + }) + } + + fun encrypt(stream: InputStream): TankerFuture { + return encrypt(stream, null) + } + + fun encrypt(stream: InputStream, options: EncryptOptions?): TankerFuture { + return encrypt(InputStreamWrapper(stream), options).andThen(TankerCallback { + TankerInputStream(it) + }) + } + + fun encrypt(channel: TankerAsynchronousByteChannel): TankerFuture { + return encrypt(channel, null) + } + + fun encrypt(channel: TankerAsynchronousByteChannel, options: EncryptOptions?): TankerFuture { + val cb = TankerStreamInputSourceCallback(channel) + val futurePtr = lib.tanker_stream_encrypt(tanker, cb, null, options) + return TankerFuture(futurePtr, Pointer::class.java).andThen(TankerCallback { + TankerStreamChannel(it, cb) + }) + } + + fun decrypt(channel: TankerAsynchronousByteChannel): TankerFuture { + val cb = TankerStreamInputSourceCallback(channel) + val futurePtr = lib.tanker_stream_decrypt(tanker, cb, null) + return TankerFuture(futurePtr, Pointer::class.java).andThen(TankerCallback { + TankerStreamChannel(it, cb) + }) + } + + fun decrypt(stream: InputStream): TankerFuture { + return decrypt(InputStreamWrapper(stream)).andThen(TankerCallback { + TankerInputStream(it) + }) + } + /** * Decrypts {@code data} with options, assuming the data was encrypted and shared beforehand. @@ -314,6 +372,29 @@ class Tanker(tankerOptions: TankerOptions) { return outString } + /** + * Get the resource ID used for sharing encrypted data. + * @param channel Tanker channel returned either by {@code encrypt} or {@code decrypt}. + * @return The resource ID of the encrypted data (base64 encoded). + */ + fun getResourceID(channel: TankerStreamChannel): String { + return channel.resourceID + } + + @RequiresApi(26) + fun getResourceID(channel: TankerStreamChannelWrapper): String { + return channel.streamChannel.resourceID + } + + /** + * Get the resource ID used for sharing encrypted data. + * @param stream Tanker input stream returned either by {@code encrypt} or {@code decrypt}. + * @return The resource ID of the encrypted data (base64 encoded). + */ + fun getResourceID(stream: TankerInputStream): String { + return stream.resourceID + } + /** * Shares the key for an encrypted resource with another Tanker user. * @param resourceIDs The IDs of the encrypted resources to share (base64 encoded each). diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/TankerAsynchronousByteChannel.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerAsynchronousByteChannel.kt new file mode 100644 index 0000000..3df3586 --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerAsynchronousByteChannel.kt @@ -0,0 +1,13 @@ +package io.tanker.api + +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.channels.Channel +import java.util.concurrent.Future + +// nio.channels.AsynchronousByteChannel requires API 26 +// provide our own interface as a replacement + +interface TankerAsynchronousByteChannel : Channel { + public abstract fun read(dst: ByteBuffer?, attachment: A, handler: TankerCompletionHandler?) +} \ No newline at end of file diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/TankerChannels.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerChannels.kt new file mode 100644 index 0000000..3fd5b9e --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerChannels.kt @@ -0,0 +1,13 @@ +package io.tanker.api + +import java.io.InputStream + +class TankerChannels { + + companion object { + @JvmStatic + public fun newInputStream(channel: TankerStreamChannel): InputStream { + return TankerInputStream(channel) + } + } +} \ No newline at end of file diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/TankerCompletionHandler.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerCompletionHandler.kt new file mode 100644 index 0000000..0e292c0 --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerCompletionHandler.kt @@ -0,0 +1,9 @@ +package io.tanker.api + +// nio.channels.CompletionHandler requires API 26 +// provide our own interface as a replacement + +interface TankerCompletionHandler { + public abstract fun completed(result: V, attachment: A): Unit + public abstract fun failed(exc: Throwable, attachment: A): Unit +} \ No newline at end of file diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/TankerInputStream.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerInputStream.kt new file mode 100644 index 0000000..12712c0 --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerInputStream.kt @@ -0,0 +1,68 @@ +package io.tanker.api + +import io.tanker.bindings.TankerError +import io.tanker.bindings.TankerLib +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.channels.ClosedChannelException +import java.util.concurrent.Callable +import java.util.concurrent.FutureTask +import java.util.concurrent.ThreadPoolExecutor + + +class TankerInputStream internal constructor(private val channel: TankerStreamChannel) : InputStream() { + val resourceID = channel.resourceID + + override fun read(): Int { + val buffer = ByteArray(1) + if (read(buffer, 0, 1) == -1) + return -1 + return buffer[0].toInt() + } + + override fun read(b: ByteArray): Int { + return read(b, 0, b.size) + + } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + val fut = FutureTask {} + var nbRead = 0 + var err : Throwable? = null + + val buffer = ByteBuffer.wrap(b, off, len) + channel.read(buffer, Unit, object : TankerCompletionHandler { + override fun completed(result: Int, attachment: Unit) { + nbRead = result + fut.run() + } + + override fun failed(exc: Throwable, attachment: Unit) { + err = exc + fut.run() + } + }) + fut.get() + if (err != null) { + if (err is ClosedChannelException) { + throw IOException("Stream is closed", err) + } + throw err!! + } + return nbRead + } + + override fun markSupported(): Boolean { + return false + } + + override fun available(): Int { + return 0 + } + + override fun close() { + channel.close() + } + +} \ No newline at end of file diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/TankerReadPendingException.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerReadPendingException.kt new file mode 100644 index 0000000..bb34158 --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerReadPendingException.kt @@ -0,0 +1,6 @@ +package io.tanker.api + +import java.lang.IllegalStateException + +open class TankerReadPendingException : IllegalStateException() { +} \ No newline at end of file diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/TankerStreamChannel.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerStreamChannel.kt new file mode 100644 index 0000000..1492062 --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerStreamChannel.kt @@ -0,0 +1,90 @@ +package io.tanker.api + +import com.sun.jna.Memory +import com.sun.jna.Pointer +import io.tanker.bindings.StreamPointer +import io.tanker.bindings.TankerError +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.channels.ClosedChannelException +import java.nio.channels.ReadPendingException +import java.util.concurrent.Future + +// FIXME wrap it in an API 26 class + +class TankerStreamChannel internal constructor(private var cStream: StreamPointer?, private val cb: TankerStreamInputSourceCallback) : TankerAsynchronousByteChannel { + + val resourceID = initResourceID() + var pendingReadOperation = false + + private fun initResourceID(): String { + if (cStream == null) + throw IOException("Stream is closed") + + val future = Tanker.lib.tanker_stream_get_resource_id(cStream!!) + val outStringPtr = TankerFuture(future, Pointer::class.java).get() + val outString = outStringPtr.getString(0, "UTF-8") + Tanker.lib.tanker_free_buffer(outStringPtr) + return outString + } + + override fun isOpen(): Boolean { + return cStream != null + } + + override fun close(): Unit { + if (cStream == null) + return + TankerFuture(Tanker.lib.tanker_stream_close(cStream!!), Unit::class.java).get() + pendingReadOperation = false + cStream = null + } + + override fun read(dst: ByteBuffer?, attachment: A, handler: TankerCompletionHandler?) { + if (pendingReadOperation) + handler!!.failed(TankerReadPendingException(), attachment) + else + readTankerInput(dst!!, attachment, handler!!) + } + + private fun readTankerInput(buffer: ByteBuffer, attachment: A, handler: TankerCompletionHandler) { + val offset = buffer.position() + val size = buffer.remaining() + if (size == 0) + handler.completed(0, attachment) + else if (cStream == null) + handler.failed(ClosedChannelException(), attachment) + else { + val inBuf = Memory(size.toLong()) + + pendingReadOperation = true + TankerFuture(Tanker.lib.tanker_stream_read(cStream!!, inBuf, size.toLong()), Int::class.java).then(TankerVoidCallback { + pendingReadOperation = false + val err = it.getError() + if (err != null) { + if (cb.streamError != null) { + handler.failed(cb.streamError!!, attachment) + } else { + if ((err as TankerException).errorCode == ErrorCode.OPERATION_CANCELED) { + handler.failed(ClosedChannelException(), attachment) + } else { + handler.failed(err, attachment) + } + } + } else { + var nbRead = it.get() + if (buffer.hasArray()) { + inBuf.read(0, buffer.array(), offset, nbRead) + } else { + val b = inBuf.getByteBuffer(0, nbRead.toLong()) + buffer.put(b) + } + if (nbRead == 0) { + nbRead = -1 + } + handler.completed(nbRead, attachment) + } + }) + } + } +} diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/TankerStreamChannelWrapper.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerStreamChannelWrapper.kt new file mode 100644 index 0000000..d80dc93 --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerStreamChannelWrapper.kt @@ -0,0 +1,48 @@ +package io.tanker.api + +import androidx.annotation.RequiresApi +import java.nio.ByteBuffer +import java.nio.channels.AsynchronousByteChannel +import java.nio.channels.CompletionHandler +import java.nio.channels.ReadPendingException +import java.util.concurrent.Future + +@RequiresApi(26) +class TankerStreamChannelWrapper(internal val streamChannel: TankerStreamChannel) : AsynchronousByteChannel { + override fun read(dst: ByteBuffer?): Future { + throw UnsupportedOperationException() + } + + override fun read(dst: ByteBuffer?, attachment: A, handler: CompletionHandler?) { + return streamChannel.read(dst, attachment, object : TankerCompletionHandler { + override fun completed(result: Int, attachment: A) { + handler!!.completed(result, attachment) + } + + override fun failed(exc: Throwable, attachment: A) { + if (exc is TankerReadPendingException) + handler!!.failed(ReadPendingException(), attachment) + else + handler!!.failed(exc, attachment) + } + }) + } + + override fun close() { + return streamChannel.close() + } + + override fun write(src: ByteBuffer?): Future { + throw UnsupportedOperationException() + } + + override fun write(src: ByteBuffer?, attachment: A, handler: CompletionHandler?) { + throw UnsupportedOperationException() + } + + + override fun isOpen(): Boolean { + return streamChannel.isOpen + } + +} diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/TankerStreamInputSourceCallback.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerStreamInputSourceCallback.kt new file mode 100644 index 0000000..f37a095 --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerStreamInputSourceCallback.kt @@ -0,0 +1,29 @@ +package io.tanker.api + +import com.sun.jna.Pointer +import io.tanker.bindings.StreamInputSourceReadOperationPointer +import io.tanker.bindings.TankerLib +import java.nio.ByteBuffer + +class TankerStreamInputSourceCallback(val channel: TankerAsynchronousByteChannel) : TankerLib.StreamInputSourceCallback { + var streamError: Throwable? = null + + override fun callback(buffer: Pointer, buffer_size: Long, op: StreamInputSourceReadOperationPointer, userArg: Pointer?) { + val b = ByteBuffer.allocate(buffer_size.toInt()) + channel.read(b, Unit, object : TankerCompletionHandler { + override fun completed(result: Int, attachment: Unit) { + var nbRead = result + if (result == -1) { + nbRead = 0 + } + buffer.write(0, b.array(), 0, nbRead) + Tanker.lib.tanker_stream_read_operation_finish(op, nbRead.toLong()) + } + + override fun failed(exc: Throwable, attachment: Unit) { + Tanker.lib.tanker_stream_read_operation_finish(op, -1) + streamError = exc + } + }) + } + } \ No newline at end of file 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 8a1b842..43d833a 100644 --- a/tanker-bindings/src/main/kotlin/io/tanker/bindings/TankerLib.kt +++ b/tanker-bindings/src/main/kotlin/io/tanker/bindings/TankerLib.kt @@ -14,6 +14,8 @@ typealias SessionPointer = Pointer typealias ConnectionPointer = Pointer typealias AdminPointer = Pointer typealias TrustchainDescriptorPointer = Pointer +typealias StreamInputSourceReadOperationPointer = Pointer +typealias StreamPointer = Pointer // JNA messes up functions that return bool on x86 // so we return Int instead, but we must not forget to take only the first byte of the result // https://github.com/java-native-access/jna/issues/1076 @@ -37,6 +39,10 @@ interface TankerLib : Library { fun callback(arg: Pointer?) } + interface StreamInputSourceCallback : Callback { + fun callback(buffer: Pointer, buffer_size: Long, op: StreamInputSourceReadOperationPointer, userArg: Pointer?) + } + fun tanker_init(): Void fun tanker_version_string(): String fun tanker_create(options: TankerOptions): FuturePointer @@ -86,6 +92,13 @@ interface TankerLib : Library { recipient_gids: StringArray, nbRecipientGids: Long, resource_ids: StringArray, nbResourceIds: Long): FuturePointer + fun tanker_stream_encrypt(session: SessionPointer, cb: StreamInputSourceCallback, user_data: Pointer?, options: EncryptOptions?): FuturePointer + fun tanker_stream_decrypt(session: SessionPointer, cb: StreamInputSourceCallback, user_data: Pointer?): FuturePointer + fun tanker_stream_read(stream: StreamPointer, buffer: Pointer, buffer_size: Long): FuturePointer + fun tanker_stream_read_operation_finish(op: StreamInputSourceReadOperationPointer, nb_read: Long) + fun tanker_stream_get_resource_id(stream: StreamPointer): ExpectedPointer + fun tanker_stream_close(stream: StreamPointer): FuturePointer + fun tanker_create_group(tanker: SessionPointer, member_uids: StringArray, nbMembers: Long): FuturePointer fun tanker_update_group_members(tanker: SessionPointer, group_id: String, users_to_add: StringArray, nb_users_to_add: Long): FuturePointer diff --git a/tanker-bindings/src/test/kotlin/io/tanker/api/Tanker.kt b/tanker-bindings/src/test/kotlin/io/tanker/api/Tanker.kt index e3888ca..2fdcde9 100644 --- a/tanker-bindings/src/test/kotlin/io/tanker/api/Tanker.kt +++ b/tanker-bindings/src/test/kotlin/io/tanker/api/Tanker.kt @@ -28,7 +28,7 @@ class TankerTests : TankerSpec() { versionString shouldNot haveLength(0) } - "Can open a Tanker session by signin up" { + "Can open a Tanker session by starting" { val tanker = Tanker(options) val identity = tc.createIdentity() val status = tanker.start(identity).get() @@ -66,6 +66,23 @@ class TankerTests : TankerSpec() { tanker.stop().get() } + "Can stream encrypt and stream decrypt back" { + val tanker = Tanker(options) + val identity = tc.createIdentity() + tanker.start(identity).get() + tanker.registerIdentity(PassphraseVerification("pass")).get() + + val plaintext = ByteArray(3 * 1024 * 1024) + val clear = plaintext.inputStream() + + val encryptor = tanker.encrypt(clear).get() + val decryptor = tanker.decrypt(encryptor).get() + + val decrypted = decryptor.readBytes() + decrypted shouldBe plaintext + tanker.stop().get() + } + "Can encrypt, share, and decrypt between two users" { val aliceId = tc.createIdentity() val bobId = tc.createIdentity() @@ -88,6 +105,64 @@ class TankerTests : TankerSpec() { tankerBob.stop().get() } + "Can retrieve the resource ID in both encryption and decryption streams" { + val tanker = Tanker(options) + val identity = tc.createIdentity() + tanker.start(identity).get() + tanker.registerIdentity(PassphraseVerification("pass")).get() + + val clear = ByteArray(0).inputStream() + + val encryptor = tanker.encrypt(clear).get() + val decryptor = tanker.decrypt(encryptor).get() + + tanker.getResourceID(encryptor) shouldBe tanker.getResourceID(decryptor) + tanker.stop().get() + } + + "Can stream encrypt, share, and stream decrypt between two users" { + val aliceId = tc.createIdentity() + val bobId = tc.createIdentity() + + val tankerAlice = Tanker(options) + tankerAlice.start(aliceId).get() + tankerAlice.registerIdentity(PassphraseVerification("pass")).get() + + val tankerBob = Tanker(options) + tankerBob.start(bobId).get() + tankerBob.registerIdentity(PassphraseVerification("pass")).get() + + val plaintext = "plain text" + val encryptor = tankerAlice.encrypt(plaintext.toByteArray().inputStream()).get() + val shareOptions = ShareOptions().shareWithUsers(Identity.getPublicIdentity(bobId)) + tankerAlice.share(arrayOf(tankerAlice.getResourceID(encryptor)), shareOptions).get() + String(tankerBob.decrypt(encryptor).get().readBytes()) shouldBe plaintext + + tankerAlice.stop().get() + tankerBob.stop().get() + } + + "Can stream encrypt and share, then stream decrypt, between two users" { + val aliceId = tc.createIdentity() + val bobId = tc.createIdentity() + + val tankerAlice = Tanker(options) + tankerAlice.start(aliceId).get() + tankerAlice.registerIdentity(PassphraseVerification("pass")).get() + + val tankerBob = Tanker(options) + tankerBob.start(bobId).get() + tankerBob.registerIdentity(PassphraseVerification("pass")).get() + + val plaintext = "There are no mistakes, just happy accidents" + val encryptOptions = EncryptOptions().shareWithUsers(Identity.getPublicIdentity(bobId)) + val encryptor = tankerAlice.encrypt(plaintext.toByteArray().inputStream(), encryptOptions).get() + String(tankerBob.decrypt(encryptor).get().readBytes()) shouldBe plaintext + + tankerAlice.stop().get() + tankerBob.stop().get() + } + "Can encrypt-and-share, then decrypt, between two users" { val aliceId = tc.createIdentity() val bobId = tc.createIdentity() diff --git a/tanker-bindings/src/test/kotlin/io/tanker/api/TankerInputStream.kt b/tanker-bindings/src/test/kotlin/io/tanker/api/TankerInputStream.kt new file mode 100644 index 0000000..0952582 --- /dev/null +++ b/tanker-bindings/src/test/kotlin/io/tanker/api/TankerInputStream.kt @@ -0,0 +1,90 @@ +package io.tanker.api + +import io.kotlintest.Description +import io.kotlintest.shouldBe +import io.kotlintest.shouldThrow +import java.io.IOException +import java.io.InputStream + +class InputStreamTests : TankerSpec() { + lateinit var tanker: Tanker + lateinit var buffer: ByteArray + + override fun beforeTest(description: Description) { + tanker = Tanker(options.setWritablePath(createTmpDir().toString())) + val st = tanker.start(tc.createIdentity()).get() + st shouldBe Status.IDENTITY_REGISTRATION_NEEDED + tanker.registerIdentity(PassphraseVerification("")).get() + buffer = ByteArray(10) + } + + init { + "Attempting to decrypt a non encrypted stream throws" { + val clear = "clear" + shouldThrow { tanker.decrypt(clear.byteInputStream()).get() } + } + + "Attempting to encrypt a closed stream throws" { + val file = createTempFile() + val stream = file.inputStream() + val encryptor = tanker.encrypt(stream).get() + stream.close() + shouldThrow { encryptor.read() } + } + + "Attempting to decrypt a closed stream throws" { + val encryptor = tanker.encrypt(buffer.inputStream()).get() + encryptor.close() + shouldThrow { tanker.decrypt(encryptor).get() } + } + + "Reading 0 bytes from a closed stream does nothing" { + val encryptor = tanker.encrypt(buffer.inputStream()).get() + encryptor.close() + encryptor.read(buffer, 0 , 0) shouldBe 0 + } + + "Reading a byte" { + val decryptor = tanker.decrypt(tanker.encrypt(buffer.inputStream()).get()).get() + decryptor.read() shouldBe 0 + } + + "Reading into a whole ByteArray" { + val decryptor = tanker.decrypt(tanker.encrypt(buffer.inputStream()).get()).get() + val b = ByteArray(10) { 1 } + decryptor.read(b) shouldBe 10 + b shouldBe buffer + decryptor.read() shouldBe -1 + } + + "Reading 0 bytes should do nothing" { + val encryptor = tanker.encrypt(buffer.inputStream()).get() + val b = ByteArray(10) { 1 } + encryptor.read(b, 0, 0) shouldBe 0 + b.all { it == 1.toByte() } shouldBe true + + val empty = ByteArray(0) + encryptor.read(empty) shouldBe 0 + empty.size shouldBe 0 + } + + "Giving negative values to read throws" { + val encryptor = tanker.encrypt(buffer.inputStream()).get() + shouldThrow { encryptor.read(buffer, -1, 1) } + shouldThrow { encryptor.read(buffer, 0, -1)} + } + + "Giving a length larger than buffer size - offset throws" { + val encryptor = tanker.encrypt(buffer.inputStream()).get() + shouldThrow { encryptor.read(buffer, 9, 10) } + } + + "Reading into a ByteArray twice" { + val decryptor = tanker.decrypt(tanker.encrypt(buffer.inputStream()).get()).get() + val b = ByteArray(10) { 1 } + decryptor.read(b, off = 0, len = 5) shouldBe 5 + decryptor.read(b, off = 5, len = 5) shouldBe 5 + b shouldBe buffer + } + } +} diff --git a/tanker-bindings/src/test/kotlin/io/tanker/api/TankerStreamChannel.kt b/tanker-bindings/src/test/kotlin/io/tanker/api/TankerStreamChannel.kt new file mode 100644 index 0000000..4d6261c --- /dev/null +++ b/tanker-bindings/src/test/kotlin/io/tanker/api/TankerStreamChannel.kt @@ -0,0 +1,238 @@ +package io.tanker.api + +import androidx.annotation.RequiresApi +import io.kotlintest.Description +import io.kotlintest.shouldBe +import io.kotlintest.shouldNotBe +import org.eclipse.jgit.merge.ThreeWayMergeStrategy +import java.nio.ByteBuffer +import java.nio.channels.AsynchronousByteChannel +import java.nio.channels.ClosedChannelException +import java.nio.channels.CompletionHandler +import java.nio.channels.ReadPendingException +import java.util.concurrent.Future +import java.util.concurrent.FutureTask + +class DummyChannel : TankerAsynchronousByteChannel { + val clearBuffer = ByteBuffer.allocate(1024 * 1024 * 2)!! + private var isClosed = false + + override fun read(dst: ByteBuffer?, attachment: A, handler: TankerCompletionHandler?) { + try { + if (dst!!.remaining() == 0) { + handler!!.completed(0, attachment) + } else if (clearBuffer.remaining() == 0) { + handler!!.completed(-1, attachment) + } else { + val clearArray = clearBuffer.array() + val currentPos = clearBuffer.arrayOffset() + val finalLength = minOf(dst.remaining(), clearBuffer.remaining()) + dst.put(clearArray, currentPos, finalLength) + clearBuffer.position(clearBuffer.position() + finalLength) + handler!!.completed(finalLength, attachment) + } + } catch (e: Throwable) { + handler!!.failed(e, attachment) + } + } + + override fun isOpen(): Boolean { + return !isClosed + } + + override fun close() { + isClosed = true + } +} + +@RequiresApi(26) +class API26DummyChannel : AsynchronousByteChannel { + override fun write(src: ByteBuffer?): Future { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun write(src: ByteBuffer?, attachment: A, handler: CompletionHandler?) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun read(dst: ByteBuffer?): Future { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + val clearBuffer = ByteBuffer.allocate(1024 * 1024 * 2)!! + private var isClosed = false + + override fun read(dst: ByteBuffer?, attachment: A, handler: CompletionHandler?) { + try { + if (dst!!.remaining() == 0) { + handler!!.completed(0, attachment) + } else if (clearBuffer.remaining() == 0) { + handler!!.completed(-1, attachment) + } else { + val clearArray = clearBuffer.array() + val currentPos = clearBuffer.arrayOffset() + val finalLength = minOf(dst.remaining(), clearBuffer.remaining()) + dst.put(clearArray, currentPos, finalLength) + clearBuffer.position(clearBuffer.position() + finalLength) + handler!!.completed(finalLength, attachment) + } + } catch (e: Throwable) { + handler!!.failed(e, attachment) + } + } + + override fun isOpen(): Boolean { + return !isClosed + } + + override fun close() { + isClosed = true + } +} + +@RequiresApi(26) +class API26StreamChannelTestHelper(tanker: Tanker) { + val clearChannel = API26DummyChannel() + var err: Throwable? = null + var nbRead = 0 + var decryptor: AsynchronousByteChannel + val decryptedBuffer = ByteBuffer.allocate(1024 * 1024 * 2) + val fut = FutureTask {} + + fun callback(): CompletionHandler { + return object : CompletionHandler { + override fun completed(result: Int, attachment: Unit) { + if (result == -1) { + fut.run() + } else { + nbRead += result + decryptor.read(decryptedBuffer, Unit, this) + } + } + + override fun failed(exc: Throwable, attachment: Unit) { + err = exc + fut.run() + } + } + } + + init { + decryptor = tanker.decrypt(tanker.encrypt(clearChannel).get()).get() + } +} + +class StreamChannelTestHelper(tanker: Tanker) { + val clearChannel = DummyChannel() + var err: Throwable? = null + var nbRead = 0 + var decryptor: TankerStreamChannel + val decryptedBuffer = ByteBuffer.allocate(1024 * 1024 * 2) + val fut = FutureTask {} + + fun callback(): TankerCompletionHandler { + return object : TankerCompletionHandler { + override fun completed(result: Int, attachment: Unit) { + if (result == -1) { + fut.run() + } else { + nbRead += result + decryptor.read(decryptedBuffer, Unit, this) + } + } + + override fun failed(exc: Throwable, attachment: Unit) { + err = exc + fut.run() + } + } + } + + init { + decryptor = tanker.decrypt(tanker.encrypt(clearChannel).get()).get() + } +} + + +class StreamChannelTests : TankerSpec() { + lateinit var tanker: Tanker + lateinit var helper: StreamChannelTestHelper + + override fun beforeTest(description: Description) { + tanker = Tanker(options.setWritablePath(createTmpDir().toString())) + val st = tanker.start(tc.createIdentity()).get() + st shouldBe Status.IDENTITY_REGISTRATION_NEEDED + tanker.registerIdentity(PassphraseVerification("")).get() + helper = StreamChannelTestHelper(tanker) + } + + init { + "Reading asynchronously" { + helper.decryptor.read(helper.decryptedBuffer, Unit, helper.callback()) + helper.fut.get() + helper.err shouldBe null + helper.nbRead shouldBe helper.decryptedBuffer.capacity() + helper.clearChannel.clearBuffer.position(0) + helper.decryptedBuffer shouldBe helper.clearChannel.clearBuffer + } + + "Reading a closed channel throws" { + helper.decryptor.read(helper.decryptedBuffer, Unit, helper.callback()) + helper.decryptor.close() + helper.fut.get() + helper.err shouldNotBe null + (helper.err is ClosedChannelException) shouldBe true + } + + "Attempting two read operations simultaneously throws" { + val secondBuffer = ByteBuffer.allocate(helper.decryptedBuffer.capacity()) + helper.decryptor.read(helper.decryptedBuffer, Unit, helper.callback()) + helper.decryptor.read(secondBuffer, Unit, helper.callback()) + helper.fut.get() + helper.err shouldNotBe null + (helper.err is TankerReadPendingException) shouldBe true + } + } +} + +@RequiresApi (26) +class API26StreamChannelTests : TankerSpec() { + lateinit var tanker: Tanker + lateinit var helper: API26StreamChannelTestHelper + + override fun beforeTest(description: Description) { + tanker = Tanker(options.setWritablePath(createTmpDir().toString())) + val st = tanker.start(tc.createIdentity()).get() + st shouldBe Status.IDENTITY_REGISTRATION_NEEDED + tanker.registerIdentity(PassphraseVerification("")).get() + helper = API26StreamChannelTestHelper(tanker) + } + + init { + "Reading asynchronously" { + helper.decryptor.read(helper.decryptedBuffer, Unit, helper.callback()) + helper.fut.get() + helper.err shouldBe null + helper.nbRead shouldBe helper.decryptedBuffer.capacity() + helper.clearChannel.clearBuffer.position(0) + helper.decryptedBuffer shouldBe helper.clearChannel.clearBuffer + } + + "Reading a closed channel throws" { + helper.decryptor.read(helper.decryptedBuffer, Unit, helper.callback()) + helper.decryptor.close() + helper.fut.get() + helper.err shouldNotBe null + (helper.err is ClosedChannelException) shouldBe true + } + + "Attempting two read operations simultaneously throws" { + val secondBuffer = ByteBuffer.allocate(helper.decryptedBuffer.capacity()) + helper.decryptor.read(helper.decryptedBuffer, Unit, helper.callback()) + helper.decryptor.read(secondBuffer, Unit, helper.callback()) + helper.fut.get() + helper.err shouldNotBe null + (helper.err is ReadPendingException) shouldBe true + } + } +}