Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/actions/make/android/package/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ inputs:
runs:
using: composite
steps:
- uses: actions/checkout@v5
- name: download armv7-linux-androideabi binaries
uses: ./.github/actions/make/android/lib
with:
Expand Down
38 changes: 34 additions & 4 deletions .github/workflows/interop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,28 @@ jobs:
if: github.event_name == 'pull_request'
runs-on: self-hosted

env:
ANDROID_NDK_VERSION: 28.1.13356709

steps:
- uses: actions/checkout@v6
- name: Setup rust macOS
uses: ./.github/actions/setup-and-cache-rust
with:
rustflags: ''
cache-key-prefix: e2e-interop-test
- uses: actions/checkout@v6
- name: set up jdk 17
uses: actions/setup-java@v5
with:
java-version: "17"
distribution: "adopt"
- name: gradle setup
uses: gradle/actions/setup-gradle@v5
- name: validate gradle wrapper
uses: gradle/actions/wrapper-validation@v5
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
Expand Down Expand Up @@ -57,25 +72,40 @@ jobs:
with:
gh-token: ${{ secrets.GITHUB_TOKEN }}

- name: download android package
uses: ./.github/actions/make/android/package
with:
gh-token: ${{ secrets.GITHUB_TOKEN }}

- name: download interop test binary
uses: ./.github/actions/make/interop-build
with:
gh-token: ${{ secrets.GITHUB_TOKEN }}

- name: create simulator
- name: create iOS simulator
run: |
echo "SIMULATOR=$(./scripts/create-ios-sim-device.sh "iPhone 16 e2e-interop-test")" >> $GITHUB_ENV

- name: build & install iOS Interop client
run: |
cd interop/src/clients/InteropClient
xcodebuild -scheme InteropClient -sdk iphonesimulator -configuration Release \
-destination 'platform=iOS Simulator,name=iPhone 16 e2e-interop-test' clean build install DSTROOT=./Products
./install-interop-client.sh ${{ env.SIMULATOR }}
- name: run e2e interop test

- name: build android interop client & run e2e interop test
env:
RUST_LOG: interop=info
# We're not building the interop binary in release mode, so it's in `target/debug`.
run: ./target/debug/interop
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 32
arch: arm64-v8a
working-directory: .
# We're not building the interop binary in release mode, so it's in `target/debug`.
script: |
(cd interop/src/clients && ./gradlew android-interop:installRelease)
./target/debug/interop

# we separate shutdown from deletion to make sure the device is always removed, even when shutdown failed
- name: delete simulator
if: always()
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ jobs:
- build-ios
- bindings-swift
- bindings-ts
- package-android
- build-interop
uses: ./.github/workflows/interop.yml

Expand Down
13 changes: 7 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ android-env:
@ndk_version=$$(perl -ne 's/Pkg\.Revision = // and print' $(ANDROID_NDK_HOME)/source.properties) && \
echo "Using Android NDK $${ndk_version} at $(ANDROID_NDK_HOME)"; \

ANDROID_ARMv7 := target/armv7-linux-androideabi/$(RELEASE_MODE)/libcore_crypto_ffi.$(LIBRARY_EXTENSION)
ANDROID_ARMv7 := target/armv7-linux-androideabi/$(RELEASE_MODE)/libcore_crypto_ffi.so
android-armv7-deps := $(RUST_SOURCES)
$(ANDROID_ARMv7): $(android-armv7-deps) | android-env
cargo rustc --locked \
Expand All @@ -323,7 +323,7 @@ $(ANDROID_ARMv7): $(android-armv7-deps) | android-env
.PHONY: android-armv7
android-armv7: $(ANDROID_ARMv7) ## Build core-crypto-ffi for armv7-linux-androideabi

ANDROID_ARMv8 := target/aarch64-linux-android/$(RELEASE_MODE)/libcore_crypto_ffi.$(LIBRARY_EXTENSION)
ANDROID_ARMv8 := target/aarch64-linux-android/$(RELEASE_MODE)/libcore_crypto_ffi.so
android-armv8-deps := $(RUST_SOURCES)
$(ANDROID_ARMv8): $(android-armv8-deps) | android-env
cargo rustc --locked \
Expand All @@ -335,7 +335,7 @@ $(ANDROID_ARMv8): $(android-armv8-deps) | android-env
.PHONY: android-armv8
android-armv8: $(ANDROID_ARMv8) ## Build core-crypto-ffi for aarch64-linux-android

ANDROID_X86 := target/x86_64-linux-android/$(RELEASE_MODE)/libcore_crypto_ffi.$(LIBRARY_EXTENSION)
ANDROID_X86 := target/x86_64-linux-android/$(RELEASE_MODE)/libcore_crypto_ffi.so
android-x86-deps := $(RUST_SOURCES)
$(ANDROID_X86): $(android-x86-deps) | android-env
# Link clang_rt.builtins statically for x86_64 Android
Expand Down Expand Up @@ -653,17 +653,18 @@ swift-check: $(STAMPS)/swift-check ## Lint Swift files via swift-format and swif

KT_WRAPPER = ./crypto-ffi/bindings/jvm/src/main/kotlin
KT_TESTS = ./crypto-ffi/bindings/jvm/src/test
KT_FILES := $(shell find $(KT_WRAPPER) $(KT_TESTS) -type f -name '*.kt')
KT_INTEROP = ./interop/src/clients/android-interop/src/main/java
KT_FILES := $(shell find $(KT_WRAPPER) $(KT_TESTS) $(KT_INTEROP) -type f -name '*.kt')

$(STAMPS)/kotlin-fmt: $(KT_FILES)
ktlint --format $(KT_WRAPPER) $(KT_TESTS)
ktlint --format $(KT_WRAPPER) $(KT_TESTS) $(KT_INTEROP)
$(TOUCH_STAMP)

.PHONY: kotlin-fmt
kotlin-fmt: $(STAMPS)/kotlin-fmt ## Format Kotlin files via ktlint

$(STAMPS)/kotlin-check: $(KT_FILES)
ktlint $(KT_WRAPPER) $(KT_TESTS)
ktlint $(KT_WRAPPER) $(KT_TESTS) $(KT_INTEROP)
$(TOUCH_STAMP)

.PHONY: kotlin-check
Expand Down
2 changes: 1 addition & 1 deletion crypto-ffi/bindings/android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import org.gradle.api.tasks.bundling.Jar

plugins {
id("com.android.library")
alias(libs.plugins.android.library)
kotlin("android")
id("com.vanniktech.maven.publish.base")
}
Expand Down
2 changes: 2 additions & 0 deletions crypto-ffi/bindings/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ buildscript {
}
}

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.library) apply false
id(libs.plugins.vanniktech.publish.get().pluginId) version libs.versions.vanniktech.publish
id(libs.plugins.dokka.get().pluginId) version libs.versions.dokka
}
Expand Down
4 changes: 3 additions & 1 deletion crypto-ffi/bindings/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
kotlin = "1.9.25"
kotlin = "2.0.21"
coroutines = "1.7.3"
jna = "5.17.0"
assertj = "3.24.2"
Expand All @@ -13,8 +13,10 @@ vanniktech-publish = "0.34.0"
kotlin-gradle = "1.9.21"
dokka = "2.0.0"
detekt = "1.23.8"
agp = "8.12.1"

[plugins]
android-library = { id = "com.android.library", version.ref = "agp" }
vanniktech-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech-publish" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
Expand Down
2 changes: 1 addition & 1 deletion crypto-ffi/bindings/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ pluginManagement {
}
}

include("jvm", "android")
include(":jvm", ":android")
2 changes: 2 additions & 0 deletions interop/src/clients/android-interop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/build

42 changes: 42 additions & 0 deletions interop/src/clients/android-interop/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialisation)
}

android {
namespace = "com.wire.androidinterop"
compileSdk = 36

defaultConfig {
applicationId = "com.wire.androidinterop"
minSdk = 30
targetSdk = 36
versionCode = 1
versionName = "1.0"
}

buildTypes {
release {
isMinifyEnabled = false
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be set to VERSION_17 to be consistent with crypto-ffi.

}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this kotlin block?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was part of the default project. Are you saying it's unnecessary specify which jvm we are targeting?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, we can just omit the whole kotlin block. 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting errors about conflicting JVM versions if I remove it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange, it works for me. Did you make sure to sync the compileOptions above with those of crypto-ffi?
I have this:

diff --git a/interop/src/clients/android-interop/build.gradle.kts b/interop/src/clients/android-interop/build.gradle.kts
index 45518ebfc..0aca03fdd 100644
--- a/interop/src/clients/android-interop/build.gradle.kts
+++ b/interop/src/clients/android-interop/build.gradle.kts
@@ -25,13 +25,8 @@ android {
         }
     }
     compileOptions {
-        sourceCompatibility = JavaVersion.VERSION_11
-        targetCompatibility = JavaVersion.VERSION_11
-    }
-    kotlin {
-        compilerOptions {
-            jvmTarget.set(JvmTarget.JVM_11)
-        }
+        sourceCompatibility = JavaVersion.VERSION_17
+        targetCompatibility = JavaVersion.VERSION_17
     }
 }

}

dependencies {
implementation("core-crypto-kotlin:android")
implementation(libs.ktxSerialization)
implementation(libs.androidx.activity)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:allowBackup="false"
android:supportsRtl="true">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we don't need to set these two attributes.

<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:exported="true"
android:label="AndroidInterop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we don't actually need action.MAIN and category.LAUNCHER -- this is essentially a background service, not a regular app that has a launcher etc.

Copy link
Member Author

@typfel typfel Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need it because you can't launch the application without this intent.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is it possible then that the tests pass without it?

Copy link
Member Author

@typfel typfel Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK maybe you right the interop doesn't need it, but for debugging purposes it's very convenient to be able to launch the interop client in the debugger.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you're using a graphical debugger? I guess it makes sense to keep it then, if it makes debugging easier. 👍

<action android:name="android.intent.action.RUN"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.wire.androidinterop

import android.content.Intent
import kotlinx.serialization.Serializable
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

@Serializable
sealed class InteropAction {
sealed class MLS : InteropAction() {
class InitMLS(val clientId: ByteArray, val ciphersuite: Int) : MLS()

class GetKeyPackage(val ciphersuite: Int) : MLS()

class AddClient(val conversationId: ByteArray, val keyPackage: ByteArray) : MLS()

class RemoveClient(val conversationId: ByteArray, val clientId: ByteArray) : MLS()

class ProcessWelcome(val welcome: ByteArray) : MLS()

class EncryptMessage(val conversationId: ByteArray, val message: ByteArray) : MLS()

class DecryptMessage(val conversationId: ByteArray, val message: ByteArray) : MLS()
}

sealed class Proteus : InteropAction() {
class InitProteus : Proteus()

class GetPrekey(val id: UShort) : Proteus()

class SessionFromPrekey(val sessionId: String, val prekey: ByteArray) : Proteus()

class SessionFromMessage(val sessionId: String, val message: ByteArray) : Proteus()

class EncryptProteusMessage(val sessionId: String, val message: ByteArray) : Proteus()

class DecryptProteusMessage(val sessionId: String, val message: ByteArray) : Proteus()

class GetProteusFingerprint() : Proteus()
}

companion object {
@OptIn(ExperimentalEncodingApi::class)
fun fromIntent(intent: Intent): InteropAction {
return when (intent.getStringExtra("action")) {
"init-mls" -> {
val clientId = intent.getStringExtra("client_id") ?: throw IllegalArgumentException("client_id is missing")
val ciphersuite = intent.getIntExtra("ciphersuite", 0)

MLS.InitMLS(clientId = Base64.Default.decode(clientId), ciphersuite = ciphersuite)
}

"get-key-package" -> {
val ciphersuite = intent.getIntExtra("ciphersuite", 0)
MLS.GetKeyPackage(ciphersuite = ciphersuite)
}

"add-client" -> {
val conversationId = intent.getStringExtra("cid") ?: throw IllegalArgumentException("conversation_id is missing")
val keyPackage = intent.getStringExtra("kp") ?: throw IllegalArgumentException("key_package is missing")

MLS.AddClient(conversationId = Base64.Default.decode(conversationId), keyPackage = Base64.Default.decode(keyPackage))
}

"remove-client" -> {
val conversationId = intent.getStringExtra("cid") ?: throw IllegalArgumentException("conversation_id is missing")
val clientId = intent.getStringExtra("client_id") ?: throw IllegalArgumentException("client_id is missing")

MLS.RemoveClient(conversationId = Base64.Default.decode(conversationId), clientId = Base64.Default.decode(clientId))
}

"process-welcome" -> {
val welcome = intent.getStringExtra("welcome") ?: throw IllegalArgumentException("welcome is missing")

MLS.ProcessWelcome(Base64.Default.decode(welcome))
}

"encrypt-message" -> {
val conversationId = intent.getStringExtra("cid") ?: throw IllegalArgumentException("conversation_id is missing")
val message = intent.getStringExtra("message") ?: throw IllegalArgumentException("message is missing")

MLS.EncryptMessage(Base64.Default.decode(conversationId), Base64.Default.decode(message))
}

"decrypt-message" -> {
val conversationId = intent.getStringExtra("cid") ?: throw IllegalArgumentException("conversation_id is missing")
val message = intent.getStringExtra("message") ?: throw IllegalArgumentException("message is missing")

MLS.DecryptMessage(Base64.Default.decode(conversationId), Base64.Default.decode(message))
}

"init-proteus" -> {
Proteus.InitProteus()
}

"get-prekey" -> {
val id = intent.getIntExtra("id", 0).toUShort()
Proteus.GetPrekey(id)
}

"session-from-prekey" -> {
val sessionId = intent.getStringExtra("session_id") ?: throw IllegalArgumentException("session_id is missing")
val prekey = intent.getStringExtra("prekey") ?: throw IllegalArgumentException("prekey is missing")

Proteus.SessionFromPrekey(sessionId = sessionId, prekey = Base64.Default.decode(prekey))
}

"session-from-message" -> {
val sessionId = intent.getStringExtra("session_id") ?: throw IllegalArgumentException("session_id is missing")
val message = intent.getStringExtra("message") ?: throw IllegalArgumentException("message is missing")

Proteus.SessionFromMessage(sessionId = sessionId, message = Base64.Default.decode(message))
}

"encrypt-proteus" -> {
val sessionId = intent.getStringExtra("session_id") ?: throw IllegalArgumentException("session_id is missing")
val message = intent.getStringExtra("message") ?: throw IllegalArgumentException("message is missing")

Proteus.EncryptProteusMessage(sessionId = sessionId, message = Base64.Default.decode(message))
}

"decrypt-proteus" -> {
val sessionId = intent.getStringExtra("session_id") ?: throw IllegalArgumentException("session_id is missing")
val message = intent.getStringExtra("message") ?: throw IllegalArgumentException("message is missing")

Proteus.DecryptProteusMessage(sessionId = sessionId, message = Base64.Default.decode(message))
}

"get-fingerprint" -> {
Proteus.GetProteusFingerprint()
}

else -> {
throw IllegalArgumentException("Unknown action: ${intent.getStringExtra("action")}")
}
}
}
}
}
Loading
Loading