diff --git a/.github/workflows/publish-android.yml b/.github/workflows/publish-android.yml index 1a90eeb514..cbbb3b73f1 100644 --- a/.github/workflows/publish-android.yml +++ b/.github/workflows/publish-android.yml @@ -51,6 +51,7 @@ jobs: - name: Publish package run: | cd crypto-ffi/bindings + ./gradlew uniffi-android:publishAllPublicationsToMavenCentral --no-configuration-cache ./gradlew android:publishAllPublicationsToMavenCentral --no-configuration-cache env: ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} diff --git a/.github/workflows/publish-jvm.yml b/.github/workflows/publish-jvm.yml index 71d5626cea..d9dc4da9db 100644 --- a/.github/workflows/publish-jvm.yml +++ b/.github/workflows/publish-jvm.yml @@ -111,6 +111,7 @@ jobs: - name: Publish package run: | cd crypto-ffi/bindings + ./gradlew :uniffi-jvm:publishAllPublicationsToMavenCentral --no-configuration-cache ./gradlew :jvm:publishAllPublicationsToMavenCentral --no-configuration-cache env: ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} diff --git a/.gitignore b/.gitignore index 5468cca36d..b94bc0cacf 100644 --- a/.gitignore +++ b/.gitignore @@ -33,10 +33,8 @@ DerivedData/ .exrc # Kotlin -crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/CoreCrypto.kt -crypto-ffi/bindings/jvm/src/main/kotlin/uniffi/core_crypto/core_crypto.kt -crypto-ffi/bindings/android/src/main/kotlin/com/wire/crypto/CoreCrypto.kt -crypto-ffi/bindings/android/src/main/kotlin/uniffi/core_crypto/core_crypto.kt +crypto-ffi/bindings/uniffi-jvm/src +crypto-ffi/bindings/uniffi-android/src # Test databases leftovers *.edb diff --git a/crypto-ffi/Makefile.toml b/crypto-ffi/Makefile.toml index cb74791afb..33b3d56a7a 100644 --- a/crypto-ffi/Makefile.toml +++ b/crypto-ffi/Makefile.toml @@ -152,19 +152,17 @@ args = [ "generate", "--config", "uniffi-android.toml", "--language", "kotlin", - "--out-dir", "./bindings/android/src/main/kotlin/", + "--out-dir", "./bindings/uniffi-android/src/main/kotlin/", "--library", "../target/release/libcore_crypto_ffi.${LIBRARY_EXTENSION}" ] [tasks.ffi-kotlin-android] dependencies = ["compile-ffi-kotlin-android"] script = ''' - mv ./bindings/android/src/main/kotlin/com/wire/crypto/core_crypto_ffi.kt ./bindings/android/src/main/kotlin/com/wire/crypto/CoreCrypto.kt - perl -i \ -pe 's/\bCryptoException\b/CryptoError/g;' \ -pe 's/\bE2eIdentityException\b/E2eIdentityError/g;' \ - ./bindings/android/src/main/kotlin/uniffi/core_crypto/core_crypto.kt + ./bindings/uniffi-android/src/main/kotlin/uniffi/core_crypto/core_crypto.kt ''' [tasks.compile-ffi-kotlin-jvm] @@ -177,19 +175,17 @@ args = [ "--bin", "uniffi-bindgen", "generate", "--language", "kotlin", - "--out-dir", "./bindings/jvm/src/main/kotlin/", + "--out-dir", "./bindings/uniffi-jvm/src/main/kotlin/", "--library", "../target/release/libcore_crypto_ffi.${LIBRARY_EXTENSION}" ] [tasks.ffi-kotlin-jvm] dependencies = ["compile-ffi-kotlin-jvm"] script = ''' - mv ./bindings/jvm/src/main/kotlin/com/wire/crypto/core_crypto_ffi.kt ./bindings/jvm/src/main/kotlin/com/wire/crypto/CoreCrypto.kt - perl -i \ -pe 's/\bCryptoException\b/CryptoError/g;' \ -pe 's/\bE2eIdentityException\b/E2eIdentityError/g;' \ - ./bindings/jvm/src/main/kotlin/uniffi/core_crypto/core_crypto.kt + ./bindings/uniffi-jvm/src/main/kotlin/uniffi/core_crypto/core_crypto.kt ''' [tasks.ffi] diff --git a/crypto-ffi/bindings/android/build.gradle.kts b/crypto-ffi/bindings/android/build.gradle.kts index f4fb41202f..de8361a67b 100644 --- a/crypto-ffi/bindings/android/build.gradle.kts +++ b/crypto-ffi/bindings/android/build.gradle.kts @@ -15,15 +15,14 @@ val copyBindings by tasks.register("copyBindings") { group = "uniffi" from(kotlinSources) include("**/*") - exclude("**/CoreCrypto.kt", "**/core_crypto.kt") into(generatedDir) } dependencies { + implementation(project(":uniffi-android")) implementation(platform(kotlin("bom"))) implementation(platform(libs.coroutines.bom)) implementation(kotlin("stdlib-jdk7")) - implementation("${libs.jna.get()}@aar") implementation(libs.appCompat) implementation(libs.ktx.core) implementation(libs.coroutines.core) @@ -59,31 +58,6 @@ android { } } -val processedResourcesDir = buildDir.resolve("processedResources") - -fun registerCopyJvmBinaryTask(target: String, jniTarget: String, include: String = "*.so"): TaskProvider = - tasks.register("copy-${target}") { - group = "uniffi" - from(projectDir.resolve("../../../target/${target}/release")) - include(include) - into(processedResourcesDir.resolve(jniTarget)) - } - -val copyBinariesTasks = listOf( - registerCopyJvmBinaryTask("aarch64-linux-android", "arm64-v8a"), - registerCopyJvmBinaryTask("armv7-linux-androideabi", "armeabi-v7a"), - registerCopyJvmBinaryTask("x86_64-linux-android", "x86_64") -) - -project.afterEvaluate { - tasks.getByName("mergeReleaseJniLibFolders") { dependsOn(copyBinariesTasks) } - tasks.getByName("mergeDebugJniLibFolders") { dependsOn(copyBinariesTasks) } -} - -tasks.withType { - dependsOn(copyBinariesTasks) -} - tasks.withType { dependsOn(copyBindings) } @@ -92,11 +66,6 @@ tasks.withType { dependsOn(copyBindings) } -tasks.withType { - enabled = false // FIXME: find a way to do this at some point - dependsOn(copyBinariesTasks) -} - kotlin.sourceSets.getByName("main").apply { kotlin.srcDir(generatedDir.resolve("main")) } @@ -105,10 +74,6 @@ kotlin.sourceSets.getByName("androidTest").apply { kotlin.srcDir(generatedDir.resolve("test")) } -android.sourceSets.getByName("main").apply { - jniLibs.srcDir(processedResourcesDir) -} - // Allows skipping signing jars published to 'MavenLocal' repository tasks.withType().configureEach { if (System.getenv("CI") == null) { // i.e. not in Github Action runner diff --git a/crypto-ffi/bindings/gradle/libs.versions.toml b/crypto-ffi/bindings/gradle/libs.versions.toml index a60a15a0f9..6589086aca 100644 --- a/crypto-ffi/bindings/gradle/libs.versions.toml +++ b/crypto-ffi/bindings/gradle/libs.versions.toml @@ -2,14 +2,14 @@ kotlin = "1.9.0" app-compat = "1.6.1" coroutines = "1.7.3" -jna = "5.13.0" +jna = "5.14.0" ktx-core = "1.10.1" slf4j = "2.0.7" assertj = "3.24.2" espresso = "3.5.1" android-junit = "1.1.5" android-logback = "2.0.0" -android-tools = "8.0.0" +android-tools = "8.1.1" sdk-compile = "34" sdk-min = "26" gradle = "8.2.1" diff --git a/crypto-ffi/bindings/jvm/build.gradle.kts b/crypto-ffi/bindings/jvm/build.gradle.kts index 72c9da9606..ca6ed5bd2a 100644 --- a/crypto-ffi/bindings/jvm/build.gradle.kts +++ b/crypto-ffi/bindings/jvm/build.gradle.kts @@ -1,6 +1,3 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.gradle.api.tasks.testing.logging.TestLogEvent - plugins { kotlin("jvm") id("java-library") @@ -13,38 +10,13 @@ java { } dependencies { - implementation(platform(kotlin("bom"))) - implementation(platform(libs.coroutines.bom)) - implementation(kotlin("stdlib-jdk7")) - implementation(libs.jna) implementation(libs.coroutines.core) + implementation(project(":uniffi-jvm")) testImplementation(kotlin("test")) testImplementation(libs.coroutines.test) testImplementation(libs.assertj.core) } -val processedResourcesDir = buildDir.resolve("processedResources") - -fun registerCopyJvmBinaryTask(target: String, jniTarget: String, include: String = "*.so"): TaskProvider = - tasks.register("copy-${target}") { - group = "uniffi" - from(projectDir.resolve("../../../target/${target}/release")) - include(include) - into(processedResourcesDir.resolve(jniTarget)) - } - -val copyBinariesTasks = listOf( - registerCopyJvmBinaryTask("x86_64-unknown-linux-gnu", "linux-x86-64"), - registerCopyJvmBinaryTask("aarch64-apple-darwin", "darwin-aarch64", "*.dylib"), - registerCopyJvmBinaryTask("x86_64-apple-darwin", "darwin-x86-64", "*.dylib"), -) - -tasks.withType { dependsOn(copyBinariesTasks) } - -tasks.withType { dependsOn(copyBinariesTasks) } - -sourceSets { main { resources { srcDir(processedResourcesDir) } } } - // Allows skipping signing jars published to 'MavenLocal' repository project.afterEvaluate { tasks.named("signMavenPublication").configure { diff --git a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/CoreCrypto.kt b/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/CoreCrypto.kt new file mode 100644 index 0000000000..69a3d9e8c5 --- /dev/null +++ b/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/CoreCrypto.kt @@ -0,0 +1,125 @@ +package com.wire.crypto.client + +import com.wire.crypto.* +import java.util.* +import kotlin.reflect.typeOf + +typealias EnrollmentHandle = ByteArray + +/** + * Initializes the logging inside Core Crypto. Not required to be called and by default there will be no logging. + * + * @param logger a callback to implement the platform specific logging. It will receive the string with the log text from Core Crypto + **/ +fun setLogger(logger: CoreCryptoLogger) { + com.wire.crypto.setLoggerOnly(logger) +} + +/** + * Set maximum log level of logs which are forwarded to the [CoreCryptoLogger]. + * + * @param level the max level that should be logged, by default it will be WARN + */ +fun setMaxLogLevel(level: CoreCryptoLogLevel) { + com.wire.crypto.setMaxLogLevel(level) +} + +class CoreCrypto(val cc: com.wire.crypto.CoreCrypto) { + + companion object { + internal const val DEFAULT_NB_KEY_PACKAGE: UInt = 100U + + suspend operator fun invoke( + keystore: String, + databaseKey: String + ): CoreCrypto { + val cc = coreCryptoDeferredInit(keystore, databaseKey) + cc.setCallbacks(Callbacks()) + return CoreCrypto(cc) + } + } + + internal fun lower() = cc + + /** + * Starts a transaction in Core Crypto. If the callback succeeds, it will be committed, otherwise, every operation + * performed with the context will be discarded. + * + * @param block the function to be executed within the transaction context. A [CoreCryptoContext] will be given as parameter to this function + * + * @return the return of the function passed as parameter + */ + @Suppress("unchecked_cast") + suspend fun transaction(block: suspend (context: CoreCryptoContext) -> R): R { + var result: R? = null + var error: Throwable? = null + try { + this.cc.transaction(object : CoreCryptoCommand { + override suspend fun execute(context: com.wire.crypto.CoreCryptoContext) { + try { + result = block(CoreCryptoContext(context)) + } catch (e: Throwable) { + // We want to catch the error before it gets wrapped by core crypto. + error = e + // This is to tell core crypto that there was an error inside the transaction. + throw e + } + } + }) + // Catch the wrapped error, which we don't need, because we caught the original error above. + } catch (_: Throwable) { } + if (error != null) { + throw error as Throwable + } + + // Since we know that transaction will either run or throw it's safe to do unchecked cast here + return result as R + } + + suspend fun proteusInit() { + cc.proteusInit() + } + + /** + * Dumps the PKI environment as PEM + * + * @return a struct with different fields representing the PKI environment as PEM strings + */ + suspend fun e2eiDumpPKIEnv(): E2eiDumpedPkiEnv? { + return cc.e2eiDumpPkiEnv() + } + + /** + * Returns whether the E2EI PKI environment is setup (i.e. Root CA, Intermediates, CRLs) + */ + suspend fun e2eiIsPKIEnvSetup(): Boolean { + return cc.e2eiIsPkiEnvSetup() + } + + /** + * Closes this [CoreCryptoCentral] instance and deallocates all loaded resources. + * + * **CAUTION**: This {@link CoreCrypto} instance won't be usable after a call to this method, but there's no way to express this requirement in Kotlin, so you'll get errors instead! + */ + fun close() { + cc.close() + } +} + +private class Callbacks : CoreCryptoCallbacks { + + override suspend fun authorize(conversationId: ByteArray, clientId: ByteArray): Boolean = true + + override suspend fun userAuthorize( + conversationId: ByteArray, + externalClientId: ByteArray, + existingClients: List + ): Boolean = true + + override suspend fun clientIsExistingGroupUser( + conversationId: ByteArray, + clientId: ByteArray, + existingClients: List, + parentConversationClients: List? + ): Boolean = true +} diff --git a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/CoreCryptoCentral.kt b/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/CoreCryptoCentral.kt deleted file mode 100644 index 551076b525..0000000000 --- a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/CoreCryptoCentral.kt +++ /dev/null @@ -1,351 +0,0 @@ -package com.wire.crypto.client - -import com.wire.crypto.* -import com.wire.crypto.CoreCryptoCallbacks -import java.io.File - -typealias EnrollmentHandle = ByteArray - -private class Callbacks : CoreCryptoCallbacks { - - override suspend fun authorize(conversationId: ByteArray, clientId: ByteArray): Boolean = true - - override suspend fun userAuthorize( - conversationId: ByteArray, - externalClientId: ByteArray, - existingClients: List - ): Boolean = true - - override suspend fun clientIsExistingGroupUser( - conversationId: ByteArray, - clientId: ByteArray, - existingClients: List, - parentConversationClients: List? - ): Boolean = true -} - -/** - * Starts a transaction in Core Crypto. If the callback succeeds, it will be committed, otherwise, every operation - * performed with the context will be discarded. - * - * @param block the function to be executed within the transaction context. A [CoreCryptoContext] will be given as parameter to this function - * - * @return the return of the function passed as parameter - */ -suspend fun CoreCryptoCentral.transaction(block: suspend (context: CoreCryptoContext) -> R): R? { - var result: R? = null - var error: Throwable? = null - try { - this.cc.transaction(object : CoreCryptoCommand { - override suspend fun execute(context: com.wire.crypto.CoreCryptoContext) { - try { - result = block(CoreCryptoContext(context)) - } catch (e: Throwable) { - // We want to catch the error before it gets wrapped by core crypto. - error = e - // This is to tell core crypto that there was an error inside the transaction. - throw e - } - } - }) - // Catch the wrapped error, which we don't need, because we caught the original error above. - } catch (_: Throwable) {} - if (error != null) { - throw error as Throwable - } - return result -} - -/** - * Initializes the logging inside Core Crypto. Not required to be called and by default there will be no logging. - * - * @param logger a callback to implement the platform specific logging. It will receive the string with the log text from Core Crypto - * @param level the max level that should be logged - **/ -@Deprecated("Use setLogger and setMaxLogLevel instead") -fun initLogger(logger: CoreCryptoLogger, level: CoreCryptoLogLevel) { - com.wire.crypto.setLoggerOnly(logger) - com.wire.crypto.setMaxLogLevel(level) -} - -/** - * Initializes the logging inside Core Crypto. Not required to be called and by default there will be no logging. - * - * @param logger a callback to implement the platform specific logging. It will receive the string with the log text from Core Crypto - **/ -fun setLogger(logger: CoreCryptoLogger) { - com.wire.crypto.setLoggerOnly(logger) -} - -/** - * Set maximum log level of logs which are forwarded to the [CoreCryptoLogger]. - * - * @param level the max level that should be logged, by default it will be WARN - */ -fun setMaxLogLevel(level: CoreCryptoLogLevel) { - com.wire.crypto.setMaxLogLevel(level) -} - - -@Suppress("TooManyFunctions") -@OptIn(ExperimentalUnsignedTypes::class) -class CoreCryptoCentral private constructor(internal val cc: CoreCrypto, private val rootDir: String) { - suspend fun proteusClient(): ProteusClient = ProteusClientImpl(cc, rootDir) - - internal fun lower() = cc - - /** - * When you have a [ClientId], use this method to initialize your [MLSClient]. - * If you don't have a [ClientId], use [externallyGeneratedMlsClient] - * - * @param id client identifier - * @param ciphersuites for which a Basic Credential has to be initialized - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun mlsClient(id: ClientId, ciphersuites: Ciphersuites = Ciphersuites.DEFAULT): MLSClient { - return MLSClient(cc).apply { mlsInit(id, ciphersuites) } - } - - /** - * When you are relying on the DS to create a unique [ClientId] use this method. - * It will just initialize the crypto backend and return a handle to continue the initialization process later with - * [MLSClient.mlsInitWithClientId]. - * - * @param ciphersuites for which a Basic Credential has to be initialized - * @return a partially initialized [MLSClient] and a [ExternallyGeneratedHandle] to use in [MLSClient.mlsInitWithClientId] - */ - @Deprecated("Inside a transaction call CoreCryptoContext.mlsGenerateKeypairs() to get the handle. The CoreCryptoContext itself is a replacement for the MLSClient") - suspend fun externallyGeneratedMlsClient(ciphersuites: Ciphersuites = Ciphersuites.DEFAULT): Pair { - val client = MLSClient(cc) - val handle = client.mlsGenerateKeypairs(ciphersuites) - return client to handle - } - - /** - * Creates an enrollment instance with private key material you can use in order to fetch a new x509 certificate from the acme server. - * - * @param clientId client identifier e.g. `b7ac11a4-8f01-4527-af88-1c30885a7931:6add501bacd1d90e@example.com` - * @param displayName human-readable name displayed in the application e.g. `Smith, Alice M (QA)` - * @param handle user handle e.g. `alice.smith.qa@example.com` - * @param expirySec generated x509 certificate expiry - * @param ciphersuite for generating signing key material - * @param team name of the Wire team a user belongs to - * @return The new [E2EIEnrollment] enrollment to use with [e2eiMlsInitOnly] - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun e2eiNewEnrollment( - clientId: String, - displayName: String, - handle: String, - expirySec: UInt, - ciphersuite: Ciphersuite, - team: String? = null, - ): E2EIEnrollment { - return E2EIEnrollment(cc.e2eiNewEnrollment(clientId, displayName, handle, team, expirySec, ciphersuite.lower())) - } - - /** - * Generates an E2EI enrollment instance for a "regular" client (with a Basic credential) willing to migrate to E2EI. - * Once the enrollment is finished, use the instance in [e2eiRotateAll] to do the rotation. - * - * @param displayName human-readable name displayed in the application e.g. `Smith, Alice M (QA)` - * @param handle user handle e.g. `alice.smith.qa@example.com` - * @param expirySec generated x509 certificate expiry - * @param ciphersuite for generating signing key material - * @param team name of the Wire team a user belongs to - * @return The new [E2EIEnrollment] enrollment to use with [e2eiRotateAll] - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun e2eiNewActivationEnrollment( - displayName: String, - handle: String, - expirySec: UInt, - ciphersuite: Ciphersuite, - team: String? = null, - ): E2EIEnrollment { - return E2EIEnrollment( - cc.e2eiNewActivationEnrollment( - displayName, - handle, - team, - expirySec, - ciphersuite.lower() - ) - ) - } - - /** - * Generates an E2EI enrollment instance for a E2EI client (with a X509 certificate credential) having to change/rotate - * their credential, either because the former one is expired or it has been revoked. It lets you change the DisplayName - * or the handle if you need to. Once the enrollment is finished, use the instance in [e2eiRotateAll] to do the rotation. - * - * @param expirySec generated x509 certificate expiry - * @param ciphersuite for generating signing key material - * @param displayName human-readable name displayed in the application e.g. `Smith, Alice M (QA)` - * @param handle user handle e.g. `alice.smith.qa@example.com` - * @param team name of the Wire team a user belongs to - * @return The new [E2EIEnrollment] enrollment to use with [e2eiRotateAll] - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun e2eiNewRotateEnrollment( - expirySec: UInt, - ciphersuite: Ciphersuite, - displayName: String? = null, - handle: String? = null, - team: String? = null, - ): E2EIEnrollment { - return E2EIEnrollment( - cc.e2eiNewRotateEnrollment( - displayName, - handle, - team, - expirySec, - ciphersuite.lower() - ) - ) - } - - /** - * Use this method to initialize end-to-end identity when a client signs up and the grace period is already expired ; - * that means he cannot initialize with a Basic credential - * - * @param enrollment the enrollment instance used to fetch the certificates - * @param certificateChain the raw response from ACME server - * @param nbKeyPackage number of initial KeyPackage to create when initializing the client - * @return a [MLSClient] initialized with only a x509 credential - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun e2eiMlsInitOnly( - enrollment: E2EIEnrollment, - certificateChain: String, - nbKeyPackage: UInt? = DEFAULT_NB_KEY_PACKAGE - ): Pair { - val crlsDps = cc.e2eiMlsInitOnly(enrollment.lower(), certificateChain, nbKeyPackage) - return MLSClient(cc) to crlsDps?.toCrlDistributionPoint() - } - - /** - * Dumps the PKI environment as PEM - * - * @return a struct with different fields representing the PKI environment as PEM strings - */ - suspend fun e2eiDumpPKIEnv(): E2eiDumpedPkiEnv? { - return cc.e2eiDumpPkiEnv() - } - - /** - * Returns whether the E2EI PKI environment is setup (i.e. Root CA, Intermediates, CRLs) - */ - suspend fun e2eiIsPKIEnvSetup(): Boolean { - return cc.e2eiIsPkiEnvSetup() - } - - /** - * Registers a Root Trust Anchor CA for the use in E2EI processing. - * - * Please note that without a Root Trust Anchor, all validations *will* fail; - * So this is the first step to perform after initializing your E2EI client - * - * @param trustAnchorPEM - PEM certificate to anchor as a Trust Root - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun e2eiRegisterAcmeCA(trustAnchorPEM: String) { - return cc.e2eiRegisterAcmeCa(trustAnchorPEM) - } - - /** - * Registers an Intermediate CA for the use in E2EI processing. - * - * Please note that a Root Trust Anchor CA is needed to validate Intermediate CAs; - * You **need** to have a Root CA registered before calling this - * - * @param certPEM PEM certificate to register as an Intermediate CA - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun e2eiRegisterIntermediateCA(certPEM: String): CrlDistributionPoints? { - return cc.e2eiRegisterIntermediateCa(certPEM)?.toCrlDistributionPoint() - } - - /** - * Registers a CRL for the use in E2EI processing. - * - * Please note that a Root Trust Anchor CA is needed to validate CRLs; - * You **need** to have a Root CA registered before calling this - * - * @param crlDP CRL Distribution Point; Basically the URL you fetched it from - * @param crlDER DER representation of the CRL - * @return A [CrlRegistration] with the dirty state of the new CRL (see struct) and its expiration timestamp - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun e2eiRegisterCRL(crlDP: String, crlDER: ByteArray): CRLRegistration { - return cc.e2eiRegisterCrl(crlDP, crlDER).lift() - } - - /** - * Creates a commit in all local conversations for changing the credential. Requires first having enrolled a new X509 - * certificate with either [e2eiNewActivationEnrollment] or []e2eiNewRotateEnrollment] - * - * @param enrollment the enrollment instance used to fetch the certificates - * @param certificateChain the raw response from ACME server - * @param newKeyPackageCount number of KeyPackages with the new identity to create - * @return a [RotateBundle] with commits to fan-out to other group members, KeyPackages to upload and old ones to delete - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun e2eiRotateAll( - enrollment: E2EIEnrollment, - certificateChain: String, - newKeyPackageCount: UInt - ): RotateBundle { - return cc.e2eiRotateAll(enrollment.lower(), certificateChain, newKeyPackageCount).toRotateBundle() - } - - /** - * Allows persisting an active enrollment (for example while redirecting the user during OAuth) in order to resume - * it later with [e2eiEnrollmentStashPop] - * - * @param enrollment the enrollment instance to persist - * @return a handle to fetch the enrollment later with [e2eiEnrollmentStashPop] - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun e2eiEnrollmentStash(enrollment: E2EIEnrollment): EnrollmentHandle { - return cc.e2eiEnrollmentStash(enrollment.lower()).toUByteArray().asByteArray() - } - - /** - * Fetches the persisted enrollment and deletes it from the keystore - * - * @param handle returned by [e2eiEnrollmentStash] - * @returns the persisted enrollment instance - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun e2eiEnrollmentStashPop(handle: EnrollmentHandle): E2EIEnrollment { - return E2EIEnrollment(cc.e2eiEnrollmentStashPop(handle)) - } - - /** - * Closes this [CoreCryptoCentral] instance and deallocates all loaded resources. - * - * **CAUTION**: This {@link CoreCrypto} instance won't be usable after a call to this method, but there's no way to express this requirement in Kotlin, so you'll get errors instead! - */ - suspend fun close() { - cc.close() - } - - companion object { - private const val KEYSTORE_NAME = "keystore" - internal const val DEFAULT_NB_KEY_PACKAGE: UInt = 100U - - suspend operator fun invoke( - rootDir: String, - databaseKey: String, - ciphersuites: Ciphersuites = Ciphersuites.DEFAULT - ): CoreCryptoCentral { - val path = "$rootDir/$KEYSTORE_NAME" - File(rootDir).mkdirs() - val cc = coreCryptoDeferredInit(path, databaseKey) - cc.setCallbacks(Callbacks()) - return CoreCryptoCentral(cc, rootDir) - } - } -} - diff --git a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/CoreCryptoContext.kt b/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/CoreCryptoContext.kt index a2ed86dbcc..9c2b86f60a 100644 --- a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/CoreCryptoContext.kt +++ b/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/CoreCryptoContext.kt @@ -22,7 +22,7 @@ import com.wire.crypto.CoreCryptoContext import com.wire.crypto.CoreCryptoException import com.wire.crypto.CrlRegistration import com.wire.crypto.E2eiDumpedPkiEnv -import com.wire.crypto.client.CoreCryptoCentral.Companion.DEFAULT_NB_KEY_PACKAGE +import com.wire.crypto.client.CoreCrypto.Companion.DEFAULT_NB_KEY_PACKAGE import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration diff --git a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/E2eiClient.kt b/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/E2eiClient.kt index d1677454b7..3b5c92e203 100644 --- a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/E2eiClient.kt +++ b/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/E2eiClient.kt @@ -1,5 +1,8 @@ package com.wire.crypto.client +import com.wire.crypto.client.CoreCrypto +import com.wire.crypto.client.CoreCryptoContext + typealias JsonRawData = ByteArray data class AcmeDirectory(private val delegate: com.wire.crypto.AcmeDirectory) { @@ -162,8 +165,8 @@ class E2EIEnrollment(private val delegate: com.wire.crypto.E2eiEnrollment) { * @param challenge HTTP response body * @see https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1 */ - @Deprecated("Use contextOidcChallengeResponse() with the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun oidcChallengeResponse(cc: CoreCryptoCentral, challenge: JsonRawData) = delegate.newOidcChallengeResponse(cc.lower(), challenge) + @Deprecated("Use contextOidcChallengeResponse() with the CoreCryptoContext object created from a CoreCrypto.transaction call") + suspend fun oidcChallengeResponse(cc: CoreCrypto, challenge: JsonRawData) = delegate.newOidcChallengeResponse(cc.lower(), challenge) /** * Parses the response from `POST /acme/{provisioner-name}/challenge/{challenge-id}` for OIDC challenge within a CoreCryptoContext. diff --git a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/MLSClient.kt b/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/MLSClient.kt deleted file mode 100644 index 8ace63f96c..0000000000 --- a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/MLSClient.kt +++ /dev/null @@ -1,518 +0,0 @@ -/* - * Wire - * Copyright (C) 2023 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ - -package com.wire.crypto.client - -import com.wire.crypto.client.CoreCryptoCentral.Companion.DEFAULT_NB_KEY_PACKAGE -import kotlin.time.Duration -import kotlin.time.DurationUnit -import kotlin.time.toDuration - -@Suppress("TooManyFunctions") -class MLSClient(private val cc: com.wire.crypto.CoreCrypto) { - - companion object { - private val keyRotationDuration: Duration = 30.toDuration(DurationUnit.DAYS) - private val defaultGroupConfiguration = com.wire.crypto.CustomConfiguration( - java.time.Duration.ofDays(keyRotationDuration.inWholeDays), - com.wire.crypto.MlsWirePolicy.PLAINTEXT - ) - } - - /** - * This is your entrypoint to initialize [com.wire.crypto.client.MLSClient] with a Basic Credential - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun mlsInit( - id: ClientId, - ciphersuites: Ciphersuites = Ciphersuites.DEFAULT, - nbKeyPackage: UInt? = DEFAULT_NB_KEY_PACKAGE - ) { - cc.mlsInit(id.lower(), ciphersuites.lower(), nbKeyPackage) - } - - /** - * Generates a MLS KeyPair/CredentialBundle with a temporary, random client ID. - * This method is designed to be used in conjunction with [mlsInitWithClientId] and represents the first step in this process - * - * @param ciphersuites - All the ciphersuites supported by this MLS client - * @return a list of random ClientId to use in [mlsInitWithClientId] - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun mlsGenerateKeypairs(ciphersuites: Ciphersuites = Ciphersuites.DEFAULT): ExternallyGeneratedHandle { - return cc.mlsGenerateKeypairs(ciphersuites.lower()).toExternallyGeneratedHandle() - } - - /** - * Updates the current temporary Client ID with the newly provided one. This is the second step in the externally-generated clients process. - * - * **Important:** This is designed to be called after [mlsGenerateKeypairs] - * - * @param clientId - The newly allocated Client ID from the MLS Authentication Service - * @param tmpClientIds - The random clientId you obtained in [mlsGenerateKeypairs], for authentication purposes - * @param ciphersuites - All the ciphersuites supported by this MLS client - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun mlsInitWithClientId( - clientId: ClientId, - tmpClientIds: ExternallyGeneratedHandle, - ciphersuites: Ciphersuites = Ciphersuites.DEFAULT - ) { - cc.mlsInitWithClientId(clientId.lower(), tmpClientIds.lower(), ciphersuites.lower()) - } - - /** - * Get the client's public signature key. To upload to the DS for further backend side validation - * - * @param ciphersuite of the signature key to get - * @return the client's public signature key - */ - suspend fun getPublicKey(ciphersuite: Ciphersuite = Ciphersuite.DEFAULT, credentialType: CredentialType = CredentialType.DEFAULT,): SignaturePublicKey { - return cc.clientPublicKey(ciphersuite.lower(), credentialType.lower()).toSignaturePublicKey() - } - - /** - * Generates the requested number of KeyPackages ON TOP of the existing ones e.g. if you already have created 100 - * KeyPackages (default value), requesting 10 will return the 10 oldest. Otherwise, if you request 200, 100 new will - * be generated. - * Unless explicitly deleted, KeyPackages are deleted upon [processWelcomeMessage] - * - * @param amount required amount - * @param ciphersuite of the KeyPackage to create - * @param credentialType of the KeyPackage to create - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun generateKeyPackages( - amount: UInt, - ciphersuite: Ciphersuite = Ciphersuite.DEFAULT, - credentialType: CredentialType = CredentialType.DEFAULT, - ): List { - return cc.clientKeypackages(ciphersuite.lower(), credentialType.lower(), amount).map { it.toMLSKeyPackage() } - } - - /** - * Number of unexpired KeyPackages currently in store - * - * @param ciphersuite of the KeyPackage to count - * @param credentialType of the KeyPackage to count - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun validKeyPackageCount( - ciphersuite: Ciphersuite = Ciphersuite.DEFAULT, - credentialType: CredentialType = CredentialType.DEFAULT - ): ULong { - return cc.clientValidKeypackagesCount(ciphersuite.lower(), credentialType.lower()) - } - - /** - * Prunes local KeyPackages after making sure they also have been deleted on the backend side. - * You should only use this after [CoreCryptoCentral.e2eiRotateAll] - * - * @param refs KeyPackage references from the [RotateBundle] - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun deleteKeyPackages(refs: List) { - // cannot be tested with the current API & helpers - return cc.deleteKeypackages(refs.map { it.lower() }) - } - - /** - * Checks if the Client is member of a given conversation and if the MLS Group is loaded up. - * - * @param id conversation identifier - */ - suspend fun conversationExists(id: MLSGroupId): Boolean = cc.conversationExists(id.lower()) - - /** - * Returns the current epoch of a conversation - * - * @param id conversation identifier - */ - suspend fun conversationEpoch(id: MLSGroupId): ULong = cc.conversationEpoch(id.lower()) - - /** - * Creates a new external Add proposal for self client to join a conversation. - * - * @param id conversation identifier - * @param epoch conversation epoch - * @param ciphersuite of the conversation to join - * @param ciphersuite to join the conversation with - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun joinConversation( - id: MLSGroupId, - epoch: ULong, - ciphersuite: Ciphersuite = Ciphersuite.DEFAULT, - credentialType: CredentialType = CredentialType.DEFAULT, - ): MlsMessage { - return cc.newExternalAddProposal(id.lower(), epoch, ciphersuite.lower(), credentialType.lower()).toMlsMessage() - } - - /** - * Allows to create an external commit to "apply" to join a group through its GroupInfo. - * - * If the DS accepts the external commit, you have to [mergePendingGroupFromExternalCommit] in order to get back - * a functional MLS group. On the opposite, if it rejects it, you can either retry by just calling again - * [joinByExternalCommit], no need to [clearPendingGroupFromExternalCommit]. If you want to abort the operation - * (too many retries or the user decided to abort), you can use [clearPendingGroupFromExternalCommit] in order not - * to bloat the user's storage but nothing bad can happen if you forget to except some storage space wasted. - * - * @param groupInfo a TLS encoded GroupInfo fetched from the Delivery Service - * @param credentialType to join the group with - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun joinByExternalCommit( - groupInfo: GroupInfo, - credentialType: CredentialType = CredentialType.DEFAULT, - configuration: com.wire.crypto.CustomConfiguration = defaultGroupConfiguration, - ): CommitBundle { - // cannot be tested since the groupInfo required is not wrapped in a MlsMessage whereas the one returned - // in Commit Bundles is... because that's the API the backend imposed - return cc.joinByExternalCommit(groupInfo.lower(), configuration, credentialType.lower()).lift() - } - - /** - * This merges the commit generated by [joinByExternalCommit], persists the group permanently - * and deletes the temporary one. This step makes the group operational and ready to encrypt/decrypt message. - * - * @param id conversation identifier - * @return eventually decrypted buffered messages if any - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun mergePendingGroupFromExternalCommit(id: MLSGroupId): List? { - return cc.mergePendingGroupFromExternalCommit(id.lower())?.map { it.lift() } - } - - /** - * In case the external commit generated by [joinByExternalCommit] is rejected by the Delivery Service, and we - * want to abort this external commit once for all, we can wipe out the pending group from the keystore in order - * not to waste space. - * - * @param id conversation identifier - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun clearPendingGroupFromExternalCommit(id: MLSGroupId) = cc.clearPendingGroupFromExternalCommit(id.lower()) - - /** - * Creates a new conversation with the current client being the sole member. - * You will want to use [addMember] afterward to add clients to this conversation. - * - * @param id conversation identifier - * @param ciphersuite of the conversation. A credential for the given ciphersuite must already have been created - * @param creatorCredentialType kind of credential the creator wants to create the group with - * @param externalSenders keys fetched from backend for validating external remove proposals - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun createConversation( - id: MLSGroupId, - ciphersuite: Ciphersuite = Ciphersuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519, - creatorCredentialType: CredentialType = CredentialType.Basic, - externalSenders: List = emptyList(), - ) { - val cfg = com.wire.crypto.ConversationConfiguration( - ciphersuite.lower(), - externalSenders.map { it.lower() }, - defaultGroupConfiguration, - ) - - cc.createConversation(id.lower(), creatorCredentialType.lower(), cfg) - } - - /** - * Wipes and destroys the local storage of a given conversation / MLS group. - * - * @param id conversation identifier - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun wipeConversation(id: MLSGroupId) = cc.wipeConversation(id.lower()) - - /** - * Ingest a TLS-serialized MLS welcome message to join an existing MLS group. - * - * Important: you have to catch the error `OrphanWelcome`, ignore it and then try to join this group with an external commit. - * - * @param welcome - TLS-serialized MLS Welcome message - * @param configuration - configuration of the MLS group - * @return The conversation ID of the newly joined group. You can use the same ID to decrypt/encrypt messages - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun processWelcomeMessage( - welcome: Welcome, - configuration: com.wire.crypto.CustomConfiguration = defaultGroupConfiguration - ): WelcomeBundle { - return cc.processWelcomeMessage(welcome.lower(), configuration).lift() - } - - /** - * Encrypts a message for a given conversation. - * - * @param id conversation identifier - * @param message - The plaintext message to encrypt - * @return the encrypted payload for the given group. This needs to be fanned out to the other members of the group. - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun encryptMessage(id: MLSGroupId, message: PlaintextMessage): MlsMessage { - return cc.encryptMessage(id.lower(), message.lower()).toMlsMessage() - } - - /** - * Decrypts a message for a given conversation - * - * @param id conversation identifier - * @param message [MlsMessage] (either Application or Handshake message) from the DS - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun decryptMessage(id: MLSGroupId, message: MlsMessage): DecryptedMessage { - return cc.decryptMessage(id.lower(), message.lower()).lift() - } - - /** - * Adds new clients to a conversation, assuming the current client has the right to add new clients to the conversation. - * - * **CAUTION**: [commitAccepted] **HAS TO** be called afterward **ONLY IF** the Delivery Service responds'200 OK' to the [CommitBundle] upload. - * It will "merge" the commit locally i.e. increment the local group epoch, use new encryption secrets etc... - * - * @param id conversation identifier - * @param KeyPackages of the new clients to add - * @return a [CommitBundle] to upload to the backend and if it succeeds call [commitAccepted] - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun addMember(id: MLSGroupId, keyPackages: List): CommitBundle { - return cc.addClientsToConversation(id.lower(), keyPackages.map { it.lower() }).lift() - } - - /** - * Removes the provided clients from a conversation; Assuming those clients exist and the current client is allowed - * to do so, otherwise this operation does nothing. - * - * **CAUTION**: [commitAccepted] **HAS TO** be called afterward **ONLY IF** the Delivery Service responds'200 OK' to the [CommitBundle] upload. - * It will "merge" the commit locally i.e. increment the local group epoch, use new encryption secrets etc... - * - * @param id conversation identifier - * @param members client identifier to delete - * @return a [CommitBundle] to upload to the backend and if it succeeds call [commitAccepted] - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun removeMember(id: MLSGroupId, members: List): CommitBundle { - val clientIds = members.map { it.lower() } - return cc.removeClientsFromConversation(id.lower(), clientIds).lift() - } - - /** - * Creates an update commit which forces every client to update their LeafNode in the conversation. - * - * **CAUTION**: [commitAccepted] **HAS TO** be called afterward **ONLY IF** the Delivery Service responds'200 OK' to the [CommitBundle] upload. - * It will "merge" the commit locally i.e. increment the local group epoch, use new encryption secrets etc... - * - * @param id conversation identifier - * @return a [CommitBundle] to upload to the backend and if it succeeds call [commitAccepted] - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun updateKeyingMaterial(id: MLSGroupId) = cc.updateKeyingMaterial(id.lower()).lift() - - /** - * Creates an update commit which replaces your leaf containing basic credentials with a leaf node containing x509 credentials in the conversation. - * - * NOTE: you can only call this after you've completed the enrollment for an end-to-end identity, calling this without - * a valid end-to-end identity will result in an error. - * - * **CAUTION**: [commitAccepted] **HAS TO** be called afterward **ONLY IF** the Delivery Service responds'200 OK' to the [CommitBundle] upload. - * It will "merge" the commit locally i.e. increment the local group epoch, use new encryption secrets etc... - * - * @param id conversation identifier - * @return a [CommitBundle] to upload to the backend and if it succeeds call [commitAccepted] - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun e2eiRotate(id: MLSGroupId) = cc.e2eiRotate(id.lower()).lift() - - /** - * Commits the local pending proposals and returns the {@link CommitBundle} object containing what can result from this operation. - * - * *CAUTION**: [commitAccepted] **HAS TO** be called afterward **ONLY IF** the Delivery Service responds'200 OK' to the [CommitBundle] upload. - * It will "merge" the commit locally i.e. increment the local group epoch, use new encryption secrets etc... - * - * @param id conversation identifier - * @return a [CommitBundle] to upload to the backend and if it succeeds call [commitAccepted] - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun commitPendingProposals(id: MLSGroupId): CommitBundle? { - return cc.commitPendingProposals(id.lower())?.lift() - } - - /** - * Creates a new proposal for adding a client to the MLS group - * - * @param id conversation identifier - * @param keyPackage (TLS serialized) fetched from the DS - * @return a [ProposalBundle] which allows to roll back this proposal with [clearPendingProposal] in case the DS rejects it - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun newAddProposal(id: MLSGroupId, keyPackage: MLSKeyPackage): ProposalBundle { - return cc.newAddProposal(id.lower(), keyPackage.lower()).lift() - } - - /** - * Creates a new proposal for removing a client from the MLS group - * - * @param id conversation identifier - * @param clientId of the client to remove - * @return a [ProposalBundle] which allows to roll back this proposal with [clearPendingProposal] in case the DS rejects it - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun newRemoveProposal(id: MLSGroupId, clientId: ClientId): ProposalBundle { - return cc.newRemoveProposal(id.lower(), clientId.lower()).lift() - } - - /** - * Creates a new proposal to update the current client LeafNode key material within the MLS group - * - * @param id conversation identifier - * @return a [ProposalBundle] which allows to roll back this proposal with [clearPendingProposal] in case the DS rejects it - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun newUpdateProposal(id: MLSGroupId): ProposalBundle { - return cc.newUpdateProposal(id.lower()).lift() - } - - /** - * Allows to mark the latest commit produced as "accepted" and be able to safely merge it into the local group state - * - * @param id conversation identifier - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun commitAccepted(id: MLSGroupId): List? { - return cc.commitAccepted(id.lower())?.map { it.lift() } - } - - /** - * Allows to remove a pending proposal (rollback). Use this when backend rejects the proposal you just sent e.g. if permissions have changed meanwhile. - * - * **CAUTION**: only use this when you had an explicit response from the Delivery Service - * e.g. 403 or 409. Do not use otherwise e.g. 5xx responses, timeout etc… - * - * @param id conversation identifier - * @param proposalRef you get from a [ProposalBundle] - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun clearPendingProposal(id: MLSGroupId, proposalRef: ProposalRef) { - cc.clearPendingProposal(id.lower(), proposalRef.lower()) - } - - /** - * Allows to remove a pending commit (rollback). Use this when backend rejects the commit you just sent e.g. if permissions have changed meanwhile. - * - * **CAUTION**: only use this when you had an explicit response from the Delivery Service - * e.g. 403. Do not use otherwise e.g. 5xx responses, timeout etc... - * **DO NOT** use when Delivery Service responds 409, pending state will be renewed in [decryptMessage] - * - * @param id conversation identifier - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun clearPendingCommit(id: MLSGroupId) { - cc.clearPendingCommit(id.lower()) - } - - /** - * Returns all clients from group's members - * - * @param id conversation identifier - * @return All the clients from the members of the group - */ - suspend fun members(id: MLSGroupId): List { - return cc.getClientIds(id.lower()).map { it.toClientId() } - } - - /** - * Derives a new key from the group to use with AVS - * - * @param id conversation identifier - * @param keyLength the length of the key to be derived. If the value is higher than the bounds of `u16` or the context hash * 255, an error will be returned - */ - suspend fun deriveAvsSecret(id: MLSGroupId, keyLength: UInt): AvsSecret { - return cc.exportSecretKey(id.lower(), keyLength).toAvsSecret() - } - - /** - * Returns the raw public key of the single external sender present in this group. - * This should be used to initialize a subconversation - * - * @param id conversation identifier - * @param keyLength the length of the key to be derived. If the value is higher than the bounds of `u16` or the context hash * 255, an error will be returned - */ - suspend fun getExternalSender(id: MLSGroupId): ExternalSenderKey { - return cc.getExternalSender(id.lower()).toExternalSenderKey() - } - - /** - * Indicates when to mark a conversation as not verified i.e. when not all its members have a X509. - * Credential generated by Wire's end-to-end identity enrollment - * - * @param id conversation identifier - * @return the conversation state given current members - */ - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - suspend fun e2eiConversationState(id: MLSGroupId): com.wire.crypto.E2eiConversationState { - return cc.e2eiConversationState(id.lower()) - } - - /** - * Returns true when end-to-end-identity is enabled for the given Ciphersuite - * - * @param ciphersuite of the credential to check - * @returns true if end-to-end identity is enabled for the given ciphersuite - */ - suspend fun e2eiIsEnabled(ciphersuite: Ciphersuite = Ciphersuite.DEFAULT): Boolean { - return cc.e2eiIsEnabled(ciphersuite.lower()) - } - - /** - * From a given conversation, get the identity of the members supplied. Identity is only present for members with a - * Certificate Credential (after turning on end-to-end identity). - * - * @param id conversation identifier - * @param deviceIds identifiers of the devices - * @returns identities or if no member has a x509 certificate, it will return an empty List - */ - suspend fun getDeviceIdentities(id: MLSGroupId, deviceIds: List): List { - return cc.getDeviceIdentities(id.lower(), deviceIds.map { it.lower() }).map { it.lift() } - } - - /** - * From a given conversation, get the identity of the users (device holders) supplied. - * Identity is only present for devices with a Certificate Credential (after turning on end-to-end identity). - * If no member has a x509 certificate, it will return an empty Vec. - * - * @param id conversation identifier - * @param userIds user identifiers hyphenated UUIDv4 e.g. 'bd4c7053-1c5a-4020-9559-cd7bf7961954' - * @returns a Map with all the identities for a given users. Consumers are then recommended to reduce those identities to determine the actual status of a user. - */ - suspend fun getUserIdentities(id: MLSGroupId, userIds: List): Map> { - return cc.getUserIdentities(id.lower(), userIds).mapValues { (_, v) -> v.map { it.lift() } } - } - - /** - * Gets the e2ei conversation state from a `GroupInfo`. Useful to check if the group has e2ei - * turned on or not before joining it. - * - * @param groupInfo a TLS encoded GroupInfo fetched from the Delivery Service - * @param credentialType kind of Credential to check usage of. Defaults to X509 for now as no other value will give any result. - */ - suspend fun getCredentialInUse(groupInfo: GroupInfo, credentialType: CredentialType = CredentialType.X509): com.wire.crypto.E2eiConversationState { - return cc.getCredentialInUse(groupInfo.lower(), credentialType.lower()) - } -} diff --git a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/ProteusClient.kt b/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/ProteusClient.kt deleted file mode 100644 index 81382124a7..0000000000 --- a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/ProteusClient.kt +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Wire - * Copyright (C) 2023 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ - -package com.wire.crypto.client - -import com.wire.crypto.CoreCrypto -import com.wire.crypto.CoreCryptoException -import java.io.File - -typealias SessionId = String - -data class PreKey( - val id: UShort, - val data: ByteArray -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as PreKey - - if (id != other.id) return false - if (!data.contentEquals(other.data)) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + data.contentHashCode() - return result - } -} - -interface ProteusClient { - - suspend fun getIdentity(): ByteArray - - suspend fun getLocalFingerprint(): ByteArray - - suspend fun getRemoteFingerprint(sessionId: SessionId): ByteArray - - suspend fun newPreKeys(from: Int, count: Int): ArrayList - - suspend fun newLastPreKey(): PreKey - - suspend fun doesSessionExist(sessionId: SessionId): Boolean - - suspend fun createSession(preKeyCrypto: PreKey, sessionId: SessionId) - - suspend fun deleteSession(sessionId: SessionId) - - suspend fun decrypt(message: ByteArray, sessionId: SessionId): ByteArray - - suspend fun encrypt(message: ByteArray, sessionId: SessionId): ByteArray - - suspend fun encryptBatched(message: ByteArray, sessionIds: List): Map - - suspend fun encryptWithPreKey( - message: ByteArray, - preKey: PreKey, - sessionId: SessionId - ): ByteArray -} - -@Suppress("TooManyFunctions") -class ProteusClientImpl private constructor(private val coreCrypto: CoreCrypto): ProteusClient { - override suspend fun getIdentity(): ByteArray { - return ByteArray(0) - } - - override suspend fun getLocalFingerprint(): ByteArray { - return wrapException { coreCrypto.proteusFingerprint().toByteArray() } - } - - override suspend fun getRemoteFingerprint(sessionId: SessionId): ByteArray { - return wrapException { coreCrypto.proteusFingerprintRemote(sessionId).toByteArray() } - } - - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - override suspend fun newPreKeys(from: Int, count: Int): ArrayList { - return wrapException { - from.until(from + count).map { - toPreKey(it.toUShort(), coreCrypto.proteusNewPrekey(it.toUShort())) - } as ArrayList - } - } - - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - override suspend fun newLastPreKey(): PreKey { - return wrapException { toPreKey(coreCrypto.proteusLastResortPrekeyId(), coreCrypto.proteusLastResortPrekey()) } - } - - override suspend fun doesSessionExist(sessionId: SessionId): Boolean { - return wrapException { - coreCrypto.proteusSessionExists(sessionId) - } - } - - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - override suspend fun createSession(preKeyCrypto: PreKey, sessionId: SessionId) { - wrapException { coreCrypto.proteusSessionFromPrekey(sessionId, preKeyCrypto.data) } - } - - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - override suspend fun deleteSession(sessionId: SessionId) { - wrapException { - coreCrypto.proteusSessionDelete(sessionId) - } - } - - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - override suspend fun decrypt(message: ByteArray, sessionId: SessionId): ByteArray { - val sessionExists = doesSessionExist(sessionId) - - return wrapException { - if (sessionExists) { - val decryptedMessage = coreCrypto.proteusDecrypt(sessionId, message) - coreCrypto.proteusSessionSave(sessionId) - decryptedMessage - } else { - val decryptedMessage = coreCrypto.proteusSessionFromMessage(sessionId, message) - coreCrypto.proteusSessionSave(sessionId) - decryptedMessage - } - } - } - - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - override suspend fun encrypt(message: ByteArray, sessionId: SessionId): ByteArray { - return wrapException { - val encryptedMessage = coreCrypto.proteusEncrypt(sessionId, message) - coreCrypto.proteusSessionSave(sessionId) - encryptedMessage - } - } - - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - override suspend fun encryptBatched(message: ByteArray, sessionIds: List): Map { - return wrapException { - coreCrypto.proteusEncryptBatched(sessionIds.map { it }, message).mapNotNull { entry -> - entry.key to entry.value - } - }.toMap() - } - - @Deprecated("Use this method from the CoreCryptoContext object created from a CoreCryptoCentral.transaction call") - override suspend fun encryptWithPreKey( - message: ByteArray, - preKey: PreKey, - sessionId: SessionId - ): ByteArray { - return wrapException { - coreCrypto.proteusSessionFromPrekey(sessionId, preKey.data) - val encryptedMessage = coreCrypto.proteusEncrypt(sessionId, message) - coreCrypto.proteusSessionSave(sessionId) - encryptedMessage - } - } - - @Suppress("TooGenericExceptionCaught") - private suspend fun wrapException(b: suspend () -> T): T { - try { - return b() - } catch (e: CoreCryptoException) { - throw ProteusException(e.message, ProteusException.fromProteusCode(coreCrypto.proteusLastErrorCode().toInt()), e.cause) - } catch (e: Exception) { - throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause) - } - } - - @OptIn(ExperimentalUnsignedTypes::class) - companion object { - private fun toUByteList(value: ByteArray): List = value.asUByteArray().asList() - private fun toByteArray(value: List) = value.toUByteArray().asByteArray() - private fun toPreKey(id: UShort, data: ByteArray): PreKey = - PreKey(id, data) - - public fun needsMigration(rootDir: File): Boolean { - return cryptoBoxFilesExists(rootDir) - } - - private fun cryptoBoxFilesExists(rootDir: File): Boolean = - CRYPTO_BOX_FILES.any { - rootDir.resolve(it).exists() - } - - private val CRYPTO_BOX_FILES = listOf("identities", "prekeys", "sessions", "version") - - private fun deleteCryptoBoxFiles(rootDir: String): Boolean = - CRYPTO_BOX_FILES.fold(true) { acc, file -> - acc && File(rootDir).resolve(file).deleteRecursively() - } - - private suspend fun migrateFromCryptoBoxIfNecessary(coreCrypto: CoreCrypto, rootDir: String) { - if (cryptoBoxFilesExists(File(rootDir))) { - coreCrypto.proteusCryptoboxMigrate(rootDir) - deleteCryptoBoxFiles(rootDir) - } - } - - suspend operator fun invoke(coreCrypto: CoreCrypto, rootDir: String): ProteusClientImpl { - try { - migrateFromCryptoBoxIfNecessary(coreCrypto, rootDir) - coreCrypto.proteusInit() - return ProteusClientImpl(coreCrypto) - } catch (e: CoreCryptoException) { - throw ProteusException(e.message, ProteusException.fromProteusCode(coreCrypto.proteusLastErrorCode().toInt()), e.cause) - } catch (e: Exception) { - throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause) - } - } - } -} diff --git a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/ProteusModel.kt b/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/ProteusModel.kt new file mode 100644 index 0000000000..99b5e877a8 --- /dev/null +++ b/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/client/ProteusModel.kt @@ -0,0 +1,44 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.crypto.client + +typealias SessionId = String + +data class PreKey( + val id: UShort, + val data: ByteArray +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PreKey + + if (id != other.id) return false + if (!data.contentEquals(other.data)) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + data.contentHashCode() + return result + } +} diff --git a/crypto-ffi/bindings/jvm/src/test/kotlin/com/wire/crypto/client/E2EITest.kt b/crypto-ffi/bindings/jvm/src/test/kotlin/com/wire/crypto/client/E2EITest.kt index af134e87a3..6b1b2f0aa8 100644 --- a/crypto-ffi/bindings/jvm/src/test/kotlin/com/wire/crypto/client/E2EITest.kt +++ b/crypto-ffi/bindings/jvm/src/test/kotlin/com/wire/crypto/client/E2EITest.kt @@ -33,14 +33,14 @@ internal class E2EITest { fun sample_e2ei_enrollment_should_succeed() = runTest { val root = Files.createTempDirectory("mls").toFile() val keyStore = root.resolve("keystore-$aliceId") - val cc = CoreCryptoCentral(keyStore.absolutePath, "secret") - val enrollment = cc.e2eiNewEnrollment( + val cc = CoreCrypto(keyStore.absolutePath, "secret") + val enrollment = cc.transaction { it.e2eiNewEnrollment( clientId = "b7ac11a4-8f01-4527-af88-1c30885a7931:6c1866f567616f31@wire.com", displayName = "Alice Smith", handle = "alice_wire", expirySec = (90 * 24 * 3600).toUInt(), ciphersuite = Ciphersuite.DEFAULT - ) + )} val directoryResponse = """{ "newNonce": "https://example.com/acme/new-nonce", "newAccount": "https://example.com/acme/new-account", @@ -200,20 +200,20 @@ internal class E2EITest { fun conversation_should_be_not_verified_when_at_least_1_of_the_members_uses_a_Basic_credential() = runTest { val (alice, bob) = newClients(aliceId, bobId) - bob.createConversation(id) + bob.transaction { it.createConversation(id) } - val aliceKp = alice.generateKeyPackages(1U, Ciphersuite.DEFAULT, CredentialType.DEFAULT).first() - val welcome = bob.addMember(id, listOf(aliceKp)).welcome!! - bob.commitAccepted(id) - val groupId = alice.processWelcomeMessage(welcome).id + val aliceKp = alice.transaction { it.generateKeyPackages(1U, Ciphersuite.DEFAULT, CredentialType.DEFAULT).first() } + val welcome = bob.transaction { it.addMember(id, listOf(aliceKp)).welcome!! } + bob.transaction { it.commitAccepted(id) } + val groupId = alice.transaction { it.processWelcomeMessage(welcome).id } - assertThat(alice.e2eiConversationState(groupId)).isEqualTo(E2eiConversationState.NOT_ENABLED) - assertThat(bob.e2eiConversationState(groupId)).isEqualTo(E2eiConversationState.NOT_ENABLED) + assertThat(alice.transaction { it.e2eiConversationState(groupId) }).isEqualTo(E2eiConversationState.NOT_ENABLED) + assertThat(bob.transaction { it.e2eiConversationState(groupId) }).isEqualTo(E2eiConversationState.NOT_ENABLED) } @Test fun e2ei_should_not_be_enabled_for_a_Basic_Credential() = runTest { val (alice) = newClients(aliceId) - assertThat(alice.e2eiIsEnabled()).isFalse() + assertThat(alice.transaction { it.e2eiIsEnabled()}).isFalse() } } diff --git a/crypto-ffi/bindings/jvm/src/test/kotlin/com/wire/crypto/client/MLSTest.kt b/crypto-ffi/bindings/jvm/src/test/kotlin/com/wire/crypto/client/MLSTest.kt index 2a9468916b..42dc36c1d3 100644 --- a/crypto-ffi/bindings/jvm/src/test/kotlin/com/wire/crypto/client/MLSTest.kt +++ b/crypto-ffi/bindings/jvm/src/test/kotlin/com/wire/crypto/client/MLSTest.kt @@ -19,6 +19,7 @@ package com.wire.crypto.client import com.wire.crypto.CoreCryptoException +import com.wire.crypto.coreCryptoDeferredInit import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat @@ -59,8 +60,9 @@ class MLSTest { @Test fun externally_generated_ClientId_should_init_the_MLS_client() = runTest { - val (alice, handle) = initCc().externallyGeneratedMlsClient() - alice.mlsInitWithClientId(aliceId.toClientId(), handle) + val alice = initCc() + val handle = alice.transaction { it.mlsGenerateKeypairs() } + alice.transaction { it.mlsInitWithClientId(aliceId.toClientId(), handle) } } @Test @@ -121,15 +123,15 @@ class MLSTest { @Test fun getPublicKey_should_return_non_empty_result() = runTest { val (alice) = newClients(aliceId) - assertThat(alice.getPublicKey(Ciphersuite.DEFAULT).value).isNotEmpty() + assertThat(alice.transaction { it.getPublicKey(Ciphersuite.DEFAULT).value }).isNotEmpty() } @Test fun conversationExists_should_return_true() = runTest { val (alice) = newClients(aliceId) - assertThat(alice.conversationExists(id)).isFalse() - alice.createConversation(id) - assertThat(alice.conversationExists(id)).isTrue() + assertThat(alice.transaction { it.conversationExists(id) }).isFalse() + alice.transaction { it.createConversation(id) } + assertThat(alice.transaction { it.conversationExists(id) }).isTrue() } @Test @@ -137,34 +139,32 @@ class MLSTest { val (alice) = newClients(aliceId) // by default - assertThat(alice.validKeyPackageCount()).isEqualTo(100.toULong()) - - assertThat(alice.generateKeyPackages(200U)).isNotEmpty().hasSize(200) - - assertThat(alice.validKeyPackageCount()).isEqualTo(200.toULong()) + assertThat(alice.transaction { it.validKeyPackageCount() }).isEqualTo(100.toULong()) + assertThat(alice.transaction { it.generateKeyPackages(200U) }).isNotEmpty().hasSize(200) + assertThat(alice.transaction { it.validKeyPackageCount() }).isEqualTo(200.toULong()) } @Test fun given_new_conversation_when_calling_conversationEpoch_should_return_epoch_0() = runTest { val (alice) = newClients(aliceId) - alice.createConversation(id) - assertThat(alice.conversationEpoch(id)).isEqualTo(0UL) + alice.transaction { it.createConversation(id) } + assertThat(alice.transaction { it.conversationEpoch(id) }).isEqualTo(0UL) } @Test fun updateKeyingMaterial_should_process_the_commit_message() = runTest { val (alice, bob) = newClients(aliceId, bobId) - bob.createConversation(id) + bob.transaction { it.createConversation(id) } - val aliceKp = alice.generateKeyPackages(1U).first() - val welcome = bob.addMember(id, listOf(aliceKp)).welcome!! - bob.commitAccepted(id) - val groupId = alice.processWelcomeMessage(welcome).id + val aliceKp = alice.transaction { it.generateKeyPackages(1U).first() } + val welcome = bob.transaction { it.addMember(id, listOf(aliceKp)).welcome!! } + bob.transaction { it.commitAccepted(id) } - val commit = bob.updateKeyingMaterial(id).commit + val groupId = alice.transaction { it.processWelcomeMessage(welcome).id } + val commit = bob.transaction { it.updateKeyingMaterial(id).commit } - val decrypted = alice.decryptMessage(groupId, commit) + val decrypted = alice.transaction { it.decryptMessage(groupId, commit) } assertThat(decrypted.message).isNull() assertThat(decrypted.commitDelay).isNull() assertThat(decrypted.senderClientId).isNull() @@ -175,11 +175,11 @@ class MLSTest { fun addMember_should_allow_joining_a_conversation_with_a_Welcome() = runTest { val (alice, bob) = newClients(aliceId, bobId) - bob.createConversation(id) + bob.transaction { it.createConversation(id) } - val aliceKp = alice.generateKeyPackages(1U).first() - val welcome = bob.addMember(id, listOf(aliceKp)).welcome!! - val groupId = alice.processWelcomeMessage(welcome) + val aliceKp = alice.transaction { it.generateKeyPackages(1U).first() } + val welcome = bob.transaction { it.addMember(id, listOf(aliceKp)).welcome!! } + val groupId = alice.transaction { it.processWelcomeMessage(welcome) } // FIXME: simplify when https://youtrack.jetbrains.com/issue/KT-24874 fixed assertThat(groupId.id.toString()).isEqualTo(id.value.toHex()) @@ -189,17 +189,20 @@ class MLSTest { fun joinConversation_should_generate_an_Add_proposal() = runTest { val (alice1, alice2, bob) = newClients(aliceId, aliceId2, bobId) - bob.createConversation(id) + bob.transaction { it.createConversation(id) } - val alice1Kp = alice1.generateKeyPackages(1U).first() - bob.addMember(id, listOf(alice1Kp)) - bob.commitAccepted(id) + val alice1Kp = alice1.transaction { it.generateKeyPackages(1U).first() } + bob.transaction { + it.addMember(id, listOf(alice1Kp)) + it.commitAccepted(id) + Unit + } - val proposal = alice2.joinConversation(id, 1UL, Ciphersuite.DEFAULT, CredentialType.DEFAULT) - bob.decryptMessage(id, proposal) - val welcome = bob.commitPendingProposals(id)?.welcome!! - bob.commitAccepted(id) - val groupId = alice2.processWelcomeMessage(welcome) + val proposal = alice2.transaction { it.joinConversation(id, 1UL, Ciphersuite.DEFAULT, CredentialType.DEFAULT) } + bob.transaction { it.decryptMessage(id, proposal) } + val welcome = bob.transaction { it.commitPendingProposals(id)?.welcome!! } + bob.transaction { it.commitAccepted(id) } + val groupId = alice2.transaction { it.processWelcomeMessage(welcome) } // FIXME: simplify when https://youtrack.jetbrains.com/issue/KT-24874 fixed assertThat(groupId.id.toString()).isEqualTo(id.value.toHex()) @@ -209,18 +212,18 @@ class MLSTest { fun encryptMessage_should_encrypt_then_receiver_should_decrypt() = runTest { val (alice, bob) = newClients(aliceId, bobId) - bob.createConversation(id) + bob.transaction { it.createConversation(id) } - val aliceKp = alice.generateKeyPackages(1U).first() - val welcome = bob.addMember(id, listOf(aliceKp)).welcome!! - bob.commitAccepted(id) - val groupId = alice.processWelcomeMessage(welcome).id + val aliceKp = alice.transaction { it.generateKeyPackages(1U).first() } + val welcome = bob.transaction { it.addMember(id, listOf(aliceKp)).welcome!! } + bob.transaction { it.commitAccepted(id) } + val groupId = alice.transaction { it.processWelcomeMessage(welcome).id } val msg = "Hello World !" - val ciphertextMsg = alice.encryptMessage(groupId, msg.toPlaintextMessage()) + val ciphertextMsg = alice.transaction { it.encryptMessage(groupId, msg.toPlaintextMessage()) } assertThat(ciphertextMsg).isNotEqualTo(msg) - val plaintextMsg = bob.decryptMessage(groupId, ciphertextMsg).message!! + val plaintextMsg = bob.transaction { it.decryptMessage(groupId, ciphertextMsg).message!! } assertThat(String(plaintextMsg)).isNotEmpty().isEqualTo(msg) } @@ -228,33 +231,34 @@ class MLSTest { fun addMember_should_add_members_to_the_MLS_group() = runTest { val (alice, bob, carol) = newClients(aliceId, bobId, carolId) - bob.createConversation(id) - val aliceKp = alice.generateKeyPackages(1U).first() - val welcome = bob.addMember(id, listOf(aliceKp)).welcome!! - bob.commitAccepted(id) + bob.transaction { it.createConversation(id) } + val aliceKp = alice.transaction { it.generateKeyPackages(1U).first() } + val welcome = bob.transaction { it.addMember(id, listOf(aliceKp)).welcome!! } + bob.transaction { it.commitAccepted(id) } - alice.processWelcomeMessage(welcome) + alice.transaction { it.processWelcomeMessage(welcome) } - val carolKp = carol.generateKeyPackages(1U).first() - val commit = bob.addMember(id, listOf(carolKp)).commit + val carolKp = carol.transaction { it.generateKeyPackages(1U).first() } + val commit = bob.transaction { it.addMember(id, listOf(carolKp)).commit } - val decrypted = alice.decryptMessage(id, commit) + val decrypted = alice.transaction { it.decryptMessage(id, commit) } assertThat(decrypted.message).isNull() - assertThat(alice.members(id).containsAll(listOf(aliceId, bobId, carolId).map { it.toClientId() })) + val members = alice.transaction { it.members(id) } + assertThat(members.containsAll(listOf(aliceId, bobId, carolId).map { it.toClientId() })) } @Test fun addMember_should_return_a_valid_Welcome_message() = runTest { val (alice, bob) = newClients(aliceId, bobId) - bob.createConversation(id) + bob.transaction { it.createConversation(id) } - val aliceKp = alice.generateKeyPackages(1U).first() - val welcome = bob.addMember(id, listOf(aliceKp)).welcome!! - bob.commitAccepted((id)) + val aliceKp = alice.transaction { it.generateKeyPackages(1U).first() } + val welcome = bob.transaction { it.addMember(id, listOf(aliceKp)).welcome!! } + bob.transaction { it.commitAccepted((id)) } - val groupId = alice.processWelcomeMessage(welcome) + val groupId = alice.transaction { it.processWelcomeMessage(welcome) } // FIXME: simplify when https://youtrack.jetbrains.com/issue/KT-24874 fixed assertThat(groupId.id.toString()).isEqualTo(id.value.toHex()) } @@ -263,18 +267,18 @@ class MLSTest { fun removeMember_should_remove_members_from_the_MLS_group() = runTest { val (alice, bob, carol) = newClients(aliceId, bobId, carolId) - bob.createConversation(id) + bob.transaction { it.createConversation(id) } - val aliceKp = alice.generateKeyPackages(1U).first() - val carolKp = carol.generateKeyPackages(1U).first() - val welcome = bob.addMember(id, listOf(aliceKp, carolKp)).welcome!! - bob.commitAccepted(id) - val conversationId = alice.processWelcomeMessage(welcome).id + val aliceKp = alice.transaction { it.generateKeyPackages(1U).first() } + val carolKp = carol.transaction { it.generateKeyPackages(1U).first() } + val welcome = bob.transaction { it.addMember(id, listOf(aliceKp, carolKp)).welcome!! } + bob.transaction { it.commitAccepted(id) } + val conversationId = alice.transaction { it.processWelcomeMessage(welcome).id } val carolMember = listOf(carolId.toClientId()) - val commit = bob.removeMember(conversationId, carolMember).commit + val commit = bob.transaction { it.removeMember(conversationId, carolMember).commit } - val decrypted = alice.decryptMessage(conversationId, commit) + val decrypted = alice.transaction { it.decryptMessage(conversationId, commit) } assertThat(decrypted.message).isNull() } @@ -282,60 +286,60 @@ class MLSTest { fun creating_proposals_and_removing_them() = runTest { val (alice, bob, carol) = newClients(aliceId, bobId, carolId) - alice.createConversation(id) + alice.transaction { it.createConversation(id) } - val bobKp = bob.generateKeyPackages(1U).first() + val bobKp = bob.transaction { it.generateKeyPackages(1U).first() } // Add proposal - alice.newAddProposal(id, bobKp) - val welcome = alice.commitPendingProposals(id)!!.welcome!! - alice.commitAccepted(id) + alice.transaction { it.newAddProposal(id, bobKp) } + val welcome = alice.transaction { it.commitPendingProposals(id)!!.welcome!! } + alice.transaction { it.commitAccepted(id) } - bob.processWelcomeMessage(welcome) + bob.transaction { it.processWelcomeMessage(welcome) } // Now creating & clearing proposal - val carolKp = carol.generateKeyPackages(1U).first() - val addProposal = alice.newAddProposal(id, carolKp) - val removeProposal = alice.newRemoveProposal(id, bobId.toClientId()) - val updateProposal = alice.newUpdateProposal(id) + val carolKp = carol.transaction { it.generateKeyPackages(1U).first() } + val addProposal = alice.transaction { it.newAddProposal(id, carolKp) } + val removeProposal = alice.transaction { it.newRemoveProposal(id, bobId.toClientId()) } + val updateProposal = alice.transaction { it.newUpdateProposal(id) } val proposals = listOf(addProposal, removeProposal, updateProposal) - proposals.forEach { - alice.clearPendingProposal(id, it.proposalRef) + proposals.forEach { proposal -> + alice.transaction { it.clearPendingProposal(id, proposal.proposalRef) } } // should be null since we cleared all proposals - assertThat(alice.commitPendingProposals(id)).isNull() + assertThat(alice.transaction { it.commitPendingProposals(id) }).isNull() } @Test fun clearPendingCommit_should_clear_the_pending_commit() = runTest { val (alice) = newClients(aliceId) - alice.createConversation(id) - - alice.updateKeyingMaterial(id) - alice.clearPendingCommit(id) + alice.transaction { + it.createConversation(id) + it.updateKeyingMaterial(id) + it.clearPendingCommit(id) + } // encrypting a message would have failed if there was a pending commit - assertThat(alice.encryptMessage(id, "Hello".toPlaintextMessage())) + assertThat(alice.transaction { it.encryptMessage(id, "Hello".toPlaintextMessage()) }) } @Test fun wipeConversation_should_delete_the_conversation_from_the_keystore() = runTest { val (alice) = newClients(aliceId) - alice.createConversation(id) + alice.transaction { it.createConversation(id) } assertThatNoException().isThrownBy { - runBlocking { alice.wipeConversation(id) } + runBlocking { alice.transaction { it.wipeConversation(id) } } } } @Test fun deriveAvsSecret_should_generate_a_secret_with_the_right_length() = runTest { val (alice) = newClients(aliceId) - alice.createConversation(id) - + alice.transaction { it.createConversation(id) } val n = 50 val secrets = (0 until n).map { - val secret = alice.deriveAvsSecret(id, 32U) + val secret = alice.transaction { it.deriveAvsSecret(id, 32U) } assertThat(secret.value).hasSize(32) secret }.toSet() @@ -344,13 +348,17 @@ class MLSTest { } fun newClients(vararg clientIds: String) = runBlocking { - clientIds.map { initCc().mlsClient(it.toClientId()) } + clientIds.map { clientID -> + val cc = initCc() + cc.transaction { it.mlsInit(clientID.toClientId()) } + cc + } } -fun initCc(): CoreCryptoCentral = runBlocking { +fun initCc(): CoreCrypto = runBlocking { val root = Files.createTempDirectory("mls").toFile() val keyStore = root.resolve("keystore-${randomIdentifier()}") - CoreCryptoCentral(keyStore.absolutePath, "secret") + CoreCrypto(keyStore.absolutePath, "secret") } fun randomIdentifier(n: Int = 12): String { diff --git a/crypto-ffi/bindings/jvm/src/test/kotlin/com/wire/crypto/client/ProteusClientTest.kt b/crypto-ffi/bindings/jvm/src/test/kotlin/com/wire/crypto/client/ProteusClientTest.kt index 48ad90e8c2..b17487e846 100644 --- a/crypto-ffi/bindings/jvm/src/test/kotlin/com/wire/crypto/client/ProteusClientTest.kt +++ b/crypto-ffi/bindings/jvm/src/test/kotlin/com/wire/crypto/client/ProteusClientTest.kt @@ -17,13 +17,12 @@ */ import com.wire.crypto.client.* -import kotlinx.coroutines.ExperimentalCoroutinesApi + import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import java.nio.file.Files import kotlin.test.* -@OptIn(ExperimentalCoroutinesApi::class) internal class ProteusClientTest { companion object { @@ -33,23 +32,25 @@ internal class ProteusClientTest { private const val bobSessionId = "bob1_session1" } - private suspend fun newProteusClient(clientId: ClientId): ProteusClient = runBlocking { + private fun newProteusClient(clientId: ClientId): CoreCrypto = runBlocking { val root = Files.createTempDirectory("mls").toFile() val keyStore = root.resolve("keystore-$clientId") - CoreCryptoCentral(keyStore.absolutePath, "secret").proteusClient() + val cc = CoreCrypto(keyStore.absolutePath, "secret") + cc.proteusInit() + cc } @Test fun givenProteusClient_whenCallingNewLastKey_thenItReturnsALastPreKey() = runTest { val aliceClient = newProteusClient(alice) - val lastPreKey = aliceClient.newLastPreKey() + val lastPreKey = aliceClient.transaction { it.proteusNewLastPreKey() } assertEquals(65535u, lastPreKey.id) } @Test fun givenProteusClient_whenCallingNewPreKeys_thenItReturnsAListOfPreKeys() = runTest { val aliceClient = newProteusClient(alice) - val preKeyList = aliceClient.newPreKeys(0, 10) + val preKeyList = aliceClient.transaction { it.proteusNewPreKeys(0, 10) } assertEquals(preKeyList.size, 10) } @@ -59,9 +60,9 @@ internal class ProteusClientTest { val bobClient = newProteusClient(bob) val message = "Hi Alice!" - val aliceKey = aliceClient.newPreKeys(0, 10).first() - val encryptedMessage = bobClient.encryptWithPreKey(message.encodeToByteArray(), aliceKey, aliceSessionId) - val decryptedMessage = aliceClient.decrypt(encryptedMessage, bobSessionId) + val aliceKey = aliceClient.transaction { it.proteusNewPreKeys(0, 10).first() } + val encryptedMessage = bobClient.transaction { it.proteusEncryptWithPreKey(message.encodeToByteArray(), aliceKey, aliceSessionId) } + val decryptedMessage = aliceClient.transaction { it.proteusDecrypt(encryptedMessage, bobSessionId) } assertEquals(message, decryptedMessage.decodeToString()) } @@ -69,14 +70,14 @@ internal class ProteusClientTest { fun givenSessionAlreadyExists_whenCallingDecrypt_thenMessageIsDecrypted() = runTest { val aliceClient = newProteusClient(alice) val bobClient = newProteusClient(bob) - val aliceKey = aliceClient.newPreKeys(0, 10).first() + val aliceKey = aliceClient.transaction { it.proteusNewPreKeys(0, 10).first() } val message1 = "Hi Alice!" - val encryptedMessage1 = bobClient.encryptWithPreKey(message1.encodeToByteArray(), aliceKey, aliceSessionId) - aliceClient.decrypt(encryptedMessage1, bobSessionId) + val encryptedMessage1 = bobClient.transaction { it.proteusEncryptWithPreKey(message1.encodeToByteArray(), aliceKey, aliceSessionId) } + aliceClient.transaction { it.proteusDecrypt(encryptedMessage1, bobSessionId) } val message2 = "Hi again Alice!" - val encryptedMessage2 = bobClient.encrypt(message2.encodeToByteArray(), aliceSessionId) - val decryptedMessage2 = aliceClient.decrypt(encryptedMessage2, bobSessionId) + val encryptedMessage2 = bobClient.transaction { it.proteusEncrypt(message2.encodeToByteArray(), aliceSessionId) } + val decryptedMessage2 = aliceClient.transaction { it.proteusDecrypt(encryptedMessage2, bobSessionId) } assertEquals(message2, decryptedMessage2.decodeToString()) } @@ -85,13 +86,13 @@ internal class ProteusClientTest { fun givenReceivingSameMessageTwice_whenCallingDecrypt_thenDuplicateMessageError() = runTest { val aliceClient = newProteusClient(alice) val bobClient = newProteusClient(bob) - val aliceKey = aliceClient.newPreKeys(0, 10).first() + val aliceKey = aliceClient.transaction { it.proteusNewPreKeys(0, 10).first() } val message1 = "Hi Alice!" - val encryptedMessage1 = bobClient.encryptWithPreKey(message1.encodeToByteArray(), aliceKey, aliceSessionId) - aliceClient.decrypt(encryptedMessage1, bobSessionId) + val encryptedMessage1 = bobClient.transaction { it.proteusEncryptWithPreKey(message1.encodeToByteArray(), aliceKey, aliceSessionId) } + aliceClient.transaction { it.proteusDecrypt(encryptedMessage1, bobSessionId) } val exception: ProteusException = assertFailsWith { - aliceClient.decrypt(encryptedMessage1, bobSessionId) + aliceClient.transaction { it.proteusDecrypt(encryptedMessage1, bobSessionId) } } assertEquals(ProteusException.Code.DUPLICATE_MESSAGE, exception.code) } @@ -100,13 +101,13 @@ internal class ProteusClientTest { fun givenMissingSession_whenCallingEncryptBatched_thenMissingSessionAreIgnored() = runTest { val aliceClient = newProteusClient(alice) val bobClient = newProteusClient(bob) - val aliceKey = aliceClient.newPreKeys(0, 10).first() + val aliceKey = aliceClient.transaction { it.proteusNewPreKeys(0, 10).first() } val message1 = "Hi Alice!" - bobClient.createSession(aliceKey, aliceSessionId) + bobClient.transaction { it.proteusCreateSession(aliceKey, aliceSessionId) } val missingAliceSessionId = "missing_session" val encryptedMessages = - bobClient.encryptBatched(message1.encodeToByteArray(), listOf(aliceSessionId, missingAliceSessionId)) + bobClient.transaction { it.proteusEncryptBatched(listOf(aliceSessionId, missingAliceSessionId), message1.encodeToByteArray()) } assertEquals(1, encryptedMessages.size) assertTrue(encryptedMessages.containsKey(aliceSessionId)) @@ -117,8 +118,8 @@ internal class ProteusClientTest { val aliceClient = newProteusClient(alice) val bobClient = newProteusClient(bob) - val aliceKey = aliceClient.newPreKeys(0, 10).first() - bobClient.createSession(aliceKey, aliceSessionId) - assertNotNull(bobClient.encrypt("Hello World".encodeToByteArray(), aliceSessionId)) + val aliceKey = aliceClient.transaction { it.proteusNewPreKeys(0, 10).first() } + bobClient.transaction { it.proteusCreateSession(aliceKey, aliceSessionId) } + assertNotNull(bobClient.transaction { it.proteusEncrypt("Hello World".encodeToByteArray(), aliceSessionId) }) } } diff --git a/crypto-ffi/bindings/settings.gradle.kts b/crypto-ffi/bindings/settings.gradle.kts index 0d5cfd834c..f125a7e034 100644 --- a/crypto-ffi/bindings/settings.gradle.kts +++ b/crypto-ffi/bindings/settings.gradle.kts @@ -7,4 +7,4 @@ pluginManagement { } } -include("jvm", "android") +include("jvm", "android", "uniffi-jvm", "uniffi-android") diff --git a/crypto-ffi/bindings/uniffi-android/build.gradle.kts b/crypto-ffi/bindings/uniffi-android/build.gradle.kts new file mode 100644 index 0000000000..159c582a7d --- /dev/null +++ b/crypto-ffi/bindings/uniffi-android/build.gradle.kts @@ -0,0 +1,80 @@ +plugins { + id("com.android.library") + kotlin("android") + id("com.vanniktech.maven.publish") +} + +dependencies { + implementation(platform(kotlin("bom"))) + implementation(platform(libs.coroutines.bom)) + implementation(kotlin("stdlib-jdk7")) + implementation("${libs.jna.get()}@aar") + implementation(libs.appCompat) + implementation(libs.ktx.core) + implementation(libs.coroutines.core) + implementation(libs.slf4j) + testImplementation(kotlin("test")) + testImplementation(libs.android.logback) + testImplementation(libs.android.junit) + testImplementation(libs.espresso) + testImplementation(libs.coroutines.test) + testImplementation(libs.assertj.core) +} + +android { + namespace = "com.wire.crypto" + compileSdk = libs.versions.sdk.compile.get().toInt() + defaultConfig { + minSdk = libs.versions.sdk.min.get().toInt() + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(file("proguard-android-optimize.txt"), file("proguard-rules.pro")) + } + } +} + +val processedResourcesDir = buildDir.resolve("processedResources") + +fun registerCopyJvmBinaryTask(target: String, jniTarget: String, include: String = "*.so"): TaskProvider = + tasks.register("copy-${target}") { + group = "uniffi" + from(projectDir.resolve("../../../target/${target}/release")) + include(include) + into(processedResourcesDir.resolve(jniTarget)) + } + +val copyBinariesTasks = listOf( + registerCopyJvmBinaryTask("aarch64-linux-android", "arm64-v8a"), + registerCopyJvmBinaryTask("armv7-linux-androideabi", "armeabi-v7a"), + registerCopyJvmBinaryTask("x86_64-linux-android", "x86_64") +) + +project.afterEvaluate { + tasks.getByName("mergeReleaseJniLibFolders") { dependsOn(copyBinariesTasks) } + tasks.getByName("mergeDebugJniLibFolders") { dependsOn(copyBinariesTasks) } +} + +tasks.withType { + dependsOn(copyBinariesTasks) +} + +android.sourceSets.getByName("main").apply { + jniLibs.srcDir(processedResourcesDir) +} + +// Allows skipping signing jars published to 'MavenLocal' repository +tasks.withType().configureEach { + if (System.getenv("CI") == null) { // i.e. not in Github Action runner + enabled = false + } +} diff --git a/crypto-ffi/bindings/uniffi-android/gradle.properties b/crypto-ffi/bindings/uniffi-android/gradle.properties new file mode 100644 index 0000000000..64d9a855e2 --- /dev/null +++ b/crypto-ffi/bindings/uniffi-android/gradle.properties @@ -0,0 +1 @@ +POM_ARTIFACT_ID=core-crypto-uniffi-android diff --git a/crypto-ffi/bindings/uniffi-jvm/build.gradle.kts b/crypto-ffi/bindings/uniffi-jvm/build.gradle.kts new file mode 100644 index 0000000000..80bd2ff611 --- /dev/null +++ b/crypto-ffi/bindings/uniffi-jvm/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + kotlin("jvm") + id("java-library") + id("com.vanniktech.maven.publish") +} + +dependencies { + implementation(platform(kotlin("bom"))) + implementation(platform(libs.coroutines.bom)) + implementation(kotlin("stdlib-jdk7")) + implementation(libs.jna) + implementation(libs.coroutines.core) + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +val processedResourcesDir = buildDir.resolve("processedResources") + +fun registerCopyJvmBinaryTask(target: String, jniTarget: String, include: String = "*.so"): TaskProvider = + tasks.register("copy-${target}") { + group = "uniffi" + from(projectDir.resolve("../../../target/${target}/release")) + include(include) + into(processedResourcesDir.resolve(jniTarget)) + } + +val copyBinariesTasks = listOf( + registerCopyJvmBinaryTask("x86_64-unknown-linux-gnu", "linux-x86-64"), + registerCopyJvmBinaryTask("aarch64-apple-darwin", "darwin-aarch64", "*.dylib"), + registerCopyJvmBinaryTask("x86_64-apple-darwin", "darwin-x86-64", "*.dylib"), +) + +tasks.withType { dependsOn(copyBinariesTasks) } + +tasks.withType { dependsOn(copyBinariesTasks) } + +sourceSets { main { resources { srcDir(processedResourcesDir) } } } + +// Allows skipping signing jars published to 'MavenLocal' repository +project.afterEvaluate { + tasks.named("signMavenPublication").configure { + if (System.getenv("CI") == null) { // i.e. not in Github Action runner + enabled = false + } + } +} diff --git a/crypto-ffi/bindings/uniffi-jvm/gradle.properties b/crypto-ffi/bindings/uniffi-jvm/gradle.properties new file mode 100644 index 0000000000..7aec02137d --- /dev/null +++ b/crypto-ffi/bindings/uniffi-jvm/gradle.properties @@ -0,0 +1 @@ +POM_ARTIFACT_ID=core-crypto-uniffi-jvm