diff --git a/.gitignore b/.gitignore index 680f4d7..b0446f0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.apk *.ap_ *.dex +*.log # Java build artifacts class files *.class diff --git a/gradle.properties b/gradle.properties index b89d1cc..da5ec13 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,3 +20,5 @@ org.gradle.jvmargs=-Xmx1024m # The Android Gradle Plugin uses deprecated APIs, they have until Gradle 6.0 to fix their crap. # https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/scope/BuildArtifactsHolder.kt org.gradle.warning.mode=none +android.enableJetifier=true +android.useAndroidX=true diff --git a/tanker-bindings/build.gradle b/tanker-bindings/build.gradle index af96275..78f4848 100644 --- a/tanker-bindings/build.gradle +++ b/tanker-bindings/build.gradle @@ -141,11 +141,12 @@ dependencies { api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.2' api 'net.java.dev.jna:jna:4.5.2@aar' - implementation 'com.android.support:support-compat:28.0.0' + implementation 'androidx.core:core:1.0.0' testImplementation 'io.kotlintest:kotlintest-core:3.3.2' testImplementation 'io.kotlintest:kotlintest-runner-junit5:3.3.2' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'androidx.annotation:annotation:1.0.0' + androidTestImplementation 'androidx.test:runner:1.1.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' testImplementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.9.9' testImplementation 'org.slf4j:slf4j-nop:1.7.26' testImplementation files('jna.jar') diff --git a/tanker-bindings/proguard-rules.pro b/tanker-bindings/proguard-rules.pro deleted file mode 100644 index 3787093..0000000 --- a/tanker-bindings/proguard-rules.pro +++ /dev/null @@ -1,3 +0,0 @@ --keepclassmembers class * { - @io.tanker.api.ProguardKeep ; -} diff --git a/tanker-bindings/src/androidTest/java/io/tanker/api/TankerTest.java b/tanker-bindings/src/androidTest/java/io/tanker/api/TankerTest.java index 7321282..bc5c553 100644 --- a/tanker-bindings/src/androidTest/java/io/tanker/api/TankerTest.java +++ b/tanker-bindings/src/androidTest/java/io/tanker/api/TankerTest.java @@ -1,8 +1,8 @@ package io.tanker.api; import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; 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..07e36d3 --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/AsynchronousByteChannelWrapper.kt @@ -0,0 +1,30 @@ +package io.tanker.api + +import androidx.annotation.RequiresApi +import java.nio.ByteBuffer +import java.nio.channels.AsynchronousByteChannel +import java.nio.channels.CompletionHandler + +@RequiresApi(26) +internal 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) { + // CompletionHandler is API 26 only, hence the boilerplate + 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) + } + }) + } +} 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..a8c9e56 --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/InputStreamWrapper.kt @@ -0,0 +1,37 @@ +package io.tanker.api + +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer + +class InputStreamWrapper(private var inputStream: InputStream?) : TankerAsynchronousByteChannel { + override fun read(dst: ByteBuffer, attachment: A, handler: TankerCompletionHandler) { + TankerFuture.threadPool.execute { + if (!isOpen) + handler.failed(IOException("Stream is closed"), attachment) + else { + 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 inputStream != null + } + + override fun close() { + if (inputStream != null) { + inputStream!!.close() + inputStream = null + } + } +} 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..df1f85b 100644 --- a/tanker-bindings/src/main/kotlin/io/tanker/api/Tanker.kt +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/Tanker.kt @@ -1,5 +1,6 @@ package io.tanker.api +import androidx.annotation.RequiresApi import android.util.Log import com.sun.jna.Memory import com.sun.jna.Pointer @@ -15,7 +16,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 +271,30 @@ 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()) }) } + 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 { + TankerResourceChannel(it, cb) + }) + } + + fun encrypt(channel: TankerAsynchronousByteChannel): TankerFuture { + return encrypt(channel, null) + } + + 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 { + TankerResourceChannel(it, cb) + }) + } /** * Decrypts {@code data} with options, assuming the data was encrypted and shared beforehand. @@ -314,6 +334,15 @@ 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: TankerAsynchronousByteChannel): String { + return (channel as TankerResourceChannel).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..3b6420d --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerAsynchronousByteChannel.kt @@ -0,0 +1,11 @@ +package io.tanker.api + +import java.nio.ByteBuffer +import java.nio.channels.Channel + +// nio.channels.AsynchronousByteChannel requires API 26 +// provide our own interface as a replacement + +interface TankerAsynchronousByteChannel : Channel { + 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/TankerAsynchronousByteChannelWrapper.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerAsynchronousByteChannelWrapper.kt new file mode 100644 index 0000000..07b919d --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerAsynchronousByteChannelWrapper.kt @@ -0,0 +1,51 @@ +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) +internal class TankerAsynchronousByteChannelWrapper(internal val streamChannel: TankerAsynchronousByteChannel) : AsynchronousByteChannel { + override fun read(dst: ByteBuffer?): Future { + throw UnsupportedOperationException() + } + + override fun read(dst: ByteBuffer, attachment: A, handler: CompletionHandler) { + try { + 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) { + handler.failed(exc, attachment) + } + }) + } catch (exc: Throwable) { + if (exc is TankerPendingReadException) + throw ReadPendingException() + throw exc + } + } + + 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/TankerChannels.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerChannels.kt new file mode 100644 index 0000000..e90694e --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerChannels.kt @@ -0,0 +1,32 @@ +package io.tanker.api + +import androidx.annotation.RequiresApi +import java.io.InputStream +import java.nio.channels.AsynchronousByteChannel + +class TankerChannels { + + companion object { + @JvmStatic + fun toInputStream(channel: TankerAsynchronousByteChannel): InputStream { + return TankerInputStream(channel) + } + + @JvmStatic + fun fromInputStream(stream: InputStream): TankerAsynchronousByteChannel { + return InputStreamWrapper(stream) + } + + @RequiresApi(26) + @JvmStatic + fun toAsynchronousByteChannel(channel: TankerAsynchronousByteChannel): AsynchronousByteChannel { + return TankerAsynchronousByteChannelWrapper(channel) + } + + @RequiresApi(26) + @JvmStatic + fun fromAsynchronousByteChannel(channel: AsynchronousByteChannel): TankerAsynchronousByteChannel { + return AsynchronousByteChannelWrapper(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..cad597e --- /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 { + fun completed(result: V, attachment: A) + fun failed(exc: Throwable, attachment: A) +} \ No newline at end of file diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/TankerFuture.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerFuture.kt index 70c4ee2..739edd0 100644 --- a/tanker-bindings/src/main/kotlin/io/tanker/api/TankerFuture.kt +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerFuture.kt @@ -6,7 +6,7 @@ import com.sun.jna.Pointer import io.tanker.bindings.TankerLib import java.lang.reflect.Type import java.util.concurrent.Executors -import android.support.annotation.WorkerThread +import androidx.annotation.WorkerThread class TankerFuture(private var cfuture: Pointer, private var valueType: Type) { private sealed class ThenResult { 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..663324e --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerInputStream.kt @@ -0,0 +1,66 @@ +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 + + +internal class TankerInputStream constructor(private val channel: TankerAsynchronousByteChannel) : InputStream() { + + 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/TankerPendingReadException.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerPendingReadException.kt new file mode 100644 index 0000000..ec8c37e --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerPendingReadException.kt @@ -0,0 +1,6 @@ +package io.tanker.api + +import java.lang.IllegalStateException + +open class TankerPendingReadException : IllegalStateException() { +} \ No newline at end of file diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/TankerResourceChannel.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerResourceChannel.kt new file mode 100644 index 0000000..224432a --- /dev/null +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerResourceChannel.kt @@ -0,0 +1,89 @@ +package io.tanker.api + +import com.sun.jna.Memory +import com.sun.jna.Pointer +import io.tanker.bindings.StreamPointer +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.channels.ClosedChannelException + +internal class TankerResourceChannel constructor(private var cStream: StreamPointer?, private val cb: TankerStreamInputSourceCallback) : TankerAsynchronousByteChannel { + + val resourceID = initResourceID() + private 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) + throw TankerPendingReadException() + readTankerInput(dst, attachment, handler) + } + + private fun readTankerInput(buffer: ByteBuffer, attachment: A, handler: TankerCompletionHandler) { + val offset = buffer.position() + val size = buffer.remaining() + if (cStream == null) + handler.failed(ClosedChannelException(), attachment) + else { + var inBuf: Pointer? = null + // handle special 0 case, which will trigger a buffering operation + if (size != 0) + 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 (inBuf == null) { + handler.completed(nbRead, attachment) + } else { + 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/TankerStreamInputSourceCallback.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/TankerStreamInputSourceCallback.kt new file mode 100644 index 0000000..d0efc5e --- /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 + +internal 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 + } + }) + } +} 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..e1551d4 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..c2fa8cc 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 = InputStreamWrapper(plaintext.inputStream()) + + val encryptor = tanker.encrypt(clear).get() + val decryptor = tanker.decrypt(encryptor).get() + + val decrypted = TankerInputStream(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,68 @@ 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 = InputStreamWrapper(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 channel = InputStreamWrapper(plaintext.toByteArray().inputStream()) + val encryptor = tankerAlice.encrypt(channel).get() + val shareOptions = ShareOptions().shareWithUsers(Identity.getPublicIdentity(bobId)) + tankerAlice.share(arrayOf(tankerAlice.getResourceID(encryptor)), shareOptions).get() + val decryptionStream = TankerInputStream(tankerBob.decrypt(encryptor).get()) + String(decryptionStream.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 channel = InputStreamWrapper(plaintext.toByteArray().inputStream()) + val encryptor = tankerAlice.encrypt(channel, encryptOptions).get() + val decryptionStream = TankerInputStream(tankerBob.decrypt(encryptor).get()) + String(decryptionStream.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..03c7b36 --- /dev/null +++ b/tanker-bindings/src/test/kotlin/io/tanker/api/TankerInputStream.kt @@ -0,0 +1,101 @@ +package io.tanker.api + +import io.kotlintest.Description +import io.kotlintest.shouldBe +import io.kotlintest.shouldThrow +import java.io.IOException +import java.nio.ByteBuffer + +class InputStreamTests : TankerSpec() { + lateinit var tanker: Tanker + lateinit var array: ByteArray + lateinit var buffer: ByteBuffer + + 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() + array = ByteArray(10) + buffer = ByteBuffer.allocate(10) + } + + init { + "Attempting to decrypt a non encrypted stream throws" { + val clear = "clear" + val clearChannel = TankerChannels.fromInputStream(clear.byteInputStream()) + shouldThrow { tanker.decrypt(clearChannel).get() } + } + + "Attempting to encrypt a closed stream throws" { + val file = createTempFile() + val channel = TankerChannels.fromInputStream(file.inputStream()) + val encryptionStream = TankerChannels.toInputStream(tanker.encrypt(channel).get()) + channel.close() + shouldThrow { encryptionStream.read() } + } + + "Attempting to decrypt a closed throws" { + val channel = InputStreamWrapper(array.inputStream()) + val encryptionStream = TankerChannels.toInputStream(tanker.encrypt(channel).get()) + encryptionStream.close() + shouldThrow { tanker.decrypt(TankerChannels.fromInputStream(encryptionStream)).get() } + } + + "Reading 0 bytes from a closed stream throws" { + val channel = TankerChannels.fromInputStream(array.inputStream()) + val encryptionStream = TankerChannels.toInputStream(tanker.encrypt(channel).get()) + encryptionStream.close() + shouldThrow { encryptionStream.read(array, 0, 0) shouldBe 0 } + } + + "Reading a byte" { + val channel = TankerChannels.fromInputStream(array.inputStream()) + val decryptionStream = TankerChannels.toInputStream(tanker.decrypt(tanker.encrypt(channel).get()).get()) + decryptionStream.read() shouldBe 0 + } + + "Reading into a whole ByteArray" { + val channel = TankerChannels.fromInputStream(array.inputStream()) + val decryptionStream = TankerChannels.toInputStream(tanker.decrypt(tanker.encrypt(channel).get()).get()) + val b = ByteArray(10) { 1 } + decryptionStream.read(b) shouldBe 10 + b shouldBe array + decryptionStream.read() shouldBe -1 + } + + "Reading 0 bytes should do nothing" { + val channel = TankerChannels.fromInputStream(array.inputStream()) + val encryptionStream = TankerChannels.toInputStream(tanker.encrypt(channel).get()) + val b = ByteArray(10) { 1 } + encryptionStream.read(b, 0, 0) shouldBe 0 + b.all { it == 1.toByte() } shouldBe true + + val empty = ByteArray(0) + encryptionStream.read(empty) shouldBe 0 + empty.size shouldBe 0 + } + + "Giving negative values to read throws" { + val channel = TankerChannels.fromInputStream(array.inputStream()) + val encryptionStream = TankerChannels.toInputStream(tanker.encrypt(channel).get()) + shouldThrow { encryptionStream.read(array, -1, 1) } + shouldThrow { encryptionStream.read(array, 0, -1) } + } + + "Giving a length larger than buffer size - offset throws" { + val channel = TankerChannels.fromInputStream(array.inputStream()) + val encryptionStream = TankerChannels.toInputStream(tanker.encrypt(channel).get()) + shouldThrow { encryptionStream.read(array, 9, 10) } + } + + "Reading into a ByteArray twice" { + val channel = TankerChannels.fromInputStream(array.inputStream()) + val decryptionStream = TankerChannels.toInputStream(tanker.decrypt(tanker.encrypt(channel).get()).get()) + val b = ByteArray(10) { 1 } + decryptionStream.read(b, 0, 5) shouldBe 5 + decryptionStream.read(b, 5, 5) shouldBe 5 + b shouldBe array + } + } +} diff --git a/tanker-bindings/src/test/kotlin/io/tanker/api/TankerResourceChannel.kt b/tanker-bindings/src/test/kotlin/io/tanker/api/TankerResourceChannel.kt new file mode 100644 index 0000000..7d4ec9a --- /dev/null +++ b/tanker-bindings/src/test/kotlin/io/tanker/api/TankerResourceChannel.kt @@ -0,0 +1,189 @@ +package io.tanker.api + +import androidx.annotation.RequiresApi +import io.kotlintest.Description +import io.kotlintest.shouldBe +import io.kotlintest.shouldNotBe +import io.kotlintest.shouldThrow +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 API26StreamChannelTestHelper(tanker: Tanker) { + val dummyChannel = DummyChannel() + val clearChannel = TankerChannels.toAsynchronousByteChannel(dummyChannel) + 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 { + val encryptionChannel = tanker.encrypt(TankerChannels.fromAsynchronousByteChannel(clearChannel)).get() + decryptor = TankerChannels.toAsynchronousByteChannel(tanker.decrypt(encryptionChannel).get()) + } +} + +class StreamChannelTestHelper(tanker: Tanker) { + val clearChannel = DummyChannel() + var err: Throwable? = null + var nbRead = 0 + var decryptor: TankerAsynchronousByteChannel + 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()) + shouldThrow { helper.decryptor.read(secondBuffer, Unit, helper.callback()) } + } + } +} + +@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.dummyChannel.clearBuffer.position(0) + helper.decryptedBuffer shouldBe helper.dummyChannel.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()) + shouldThrow { helper.decryptor.read(secondBuffer, Unit, helper.callback()) } + } + } +}