diff --git a/.codacy.yml b/.codacy.yml
new file mode 100644
index 000000000000..4c277340d882
--- /dev/null
+++ b/.codacy.yml
@@ -0,0 +1,5 @@
+# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+exclude_paths:
+ - "app/src/main/cpp/**"
diff --git a/REUSE.toml b/REUSE.toml
index 428de14d9e48..58c20a299d7c 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -29,6 +29,12 @@ precedence = "aggregate"
SPDX-FileCopyrightText = "2015-2016 ownCloud Inc."
SPDX-License-Identifier = "GPL-2.0-only"
+[[annotations]]
+path = ["app/libs/local-maven/**/*.aar", "app/libs/local-maven/**/*.pom"]
+precedence = "aggregate"
+SPDX-FileCopyrightText = "2026 Nextcloud GmbH and Nextcloud contributors"
+SPDX-License-Identifier = "AGPL-3.0-or-later"
+
[[annotations]]
path = ["app/src/**/res/mipmap-**dpi/ic_launcher.png", "app/src/**/ic_launcher-web.png", "src/**/fastlane/metadata/en-US/images/*.png", "src/generic/fastlane/metadata/android/en-US/images/icon.png", "src/versionDev/fastlane/metadata/android/en-US/images/icon.png", "app/src/main/ic_launcher-web-round.png"]
precedence = "aggregate"
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index cf92e76b7c45..3cb1048e3413 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -86,7 +86,12 @@ val ncTestServerBaseUrl = configProps["NC_TEST_SERVER_BASEURL"]
android {
// install this NDK version and Cmake to produce smaller APKs. Build will still work if not installed
ndkVersion = "${ndkEnv["NDK_VERSION"]}"
-
+ externalNativeBuild {
+ cmake {
+ version = "${ndkEnv["CMAKE_VERSION"]}"
+ path = file("src/main/cpp/CMakeLists.txt")
+ }
+ }
namespace = "com.owncloud.android"
testNamespace = "${namespace}.test"
@@ -104,6 +109,10 @@ android {
targetSdk = 36
compileSdk = 36
+ ndk {
+ abiFilters += listOf("arm64-v8a", "x86_64")
+ }
+
buildConfigField("boolean", "CI", ciBuild.toString())
buildConfigField("boolean", "RUNTIME_PERF_ANALYSIS", perfAnalysis.toString())
@@ -192,6 +201,7 @@ android {
viewBinding = true
aidl = true
compose = true
+ prefab = true
}
compileOptions {
@@ -445,6 +455,7 @@ dependencies {
// region Crypto
implementation(libs.conscrypt.android)
+ implementation(libs.openssl)
// endregion
// region Library
diff --git a/app/libs/local-maven/com/nextcloud/openssl/3.5.6/openssl-3.5.6.aar b/app/libs/local-maven/com/nextcloud/openssl/3.5.6/openssl-3.5.6.aar
new file mode 100644
index 000000000000..4ad6d0743358
Binary files /dev/null and b/app/libs/local-maven/com/nextcloud/openssl/3.5.6/openssl-3.5.6.aar differ
diff --git a/app/libs/local-maven/com/nextcloud/openssl/3.5.6/openssl-3.5.6.pom b/app/libs/local-maven/com/nextcloud/openssl/3.5.6/openssl-3.5.6.pom
new file mode 100644
index 000000000000..cd21b9cd1056
--- /dev/null
+++ b/app/libs/local-maven/com/nextcloud/openssl/3.5.6/openssl-3.5.6.pom
@@ -0,0 +1,8 @@
+
+
+ 4.0.0
+ com.nextcloud
+ openssl
+ 3.5.6
+ aar
+
diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsMetadataVerificationTests.kt b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsMetadataVerificationTests.kt
new file mode 100644
index 000000000000..bf4c936f3674
--- /dev/null
+++ b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsMetadataVerificationTests.kt
@@ -0,0 +1,102 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.utils
+
+import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFolderMetadataFile
+import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedMetadata
+import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedUser
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class EncryptionUtilsMetadataVerificationTests {
+
+ private val sut = EncryptionUtilsV2()
+
+ @Test
+ fun testVerifyMetadataWhenGivenValidInputsShouldReturnTrue() {
+ val metadata = EncryptedFolderMetadataFile(
+ metadata =
+ EncryptedMetadata(
+ authenticationTag = "xkVxj0NbQEXIEMlulYZJgg==",
+ nonce = "HzRiseUfoFJ5lqUi",
+ ciphertext = "EOnzuyVn9R8qDUBY4yeuJbhQdkOHBMy3nyRGwY0y/+oWctV17XvE0RIbOhH7+smKV3orJKatu5fG6iIZN+" +
+ "HZUQASTCdQ0mdFVPJmdk20UH5nFZ/ilQIyyXAFhLHdYwWA/M7wKYoh5W9fDXNX9cZvHgjWPdT9Pq99PUv37atYxj7Je" +
+ "25GenbtxkVxj0NbQEXIEMlulYZJgg=="
+ ),
+ users = listOf(
+ EncryptedUser(
+ userId = "admin",
+ certificate = "-----BEGIN CERTIFICATE-----\nMIIC7jCCAdagAwIBAgIBADANBgkqhkiG9w0BAQUFADAQMQ4wDAYD" +
+ "VQQDEwVhZG1p\nbjAeFw0yNjA0MjcwNzI5NDdaFw00NjA0MjIwNzI5NDdaMBAxDjAMBgNVBAMTBWFk\nbWluMIIBIjAN" +
+ "BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwE513/kdkIp+Z5pI\n7rq1UKV5LBiB6dl4Wh46nI3mhVacOA1dJJWIU" +
+ "xRkkrUNWJewe8eJ7QWmhSpeBauA\n06PrAOTd1ZA4gSUWKpsYJqKm5Nxjp+BUMK1nHGQCkNQWjRllhyKTJeG/9PPc0Z" +
+ "rJ\nh4V27bCXC9iX5l/ve35fp99VR4tQ947HWObe07EdPIEFNYfT/IurPwMySZ1WH1gy\nDTw7IxMXcPVg+GUlfBoSV" +
+ "gQ1UdCkvgHc9pE1LuFmyBguAzGLbXDfspUuTs85RLGX\nGYdv2vZU/R2kJEs3ePMtaGXw6DVSx82RkPFVaLDCdShX24" +
+ "yk6gLNEv9oTUXY40i3\nn8njRQIDAQABo1MwUTAdBgNVHQ4EFgQUDE381mprCEvSLaFeOwZRliBSJnwwHwYD\nVR0j" +
+ "BBgwFoAUDE381mprCEvSLaFeOwZRliBSJnwwDwYDVR0TAQH/BAUwAwEB/zAN\nBgkqhkiG9w0BAQUFAAOCAQEAgU0o8" +
+ "Qp5wn3vkcQLYao2heWKsbYYl8wqkztRVVKb\n+qMe2m/FOOMK1Rxv/anEVHHN+SnTc481fHd8z3w6II28LxJ5M+Ixzo" +
+ "AFTj6gCv8+\nrL1R9kE91401d1+ulAiJR92ykOcB1h8bk5yoCZSRLIXwViCGUrbC3iu2NLWQDYk4\nvjxwqCSJOWUQh" +
+ "+qaYGCjB6mgkBMAnXGJCN2fV7sAR7N8Hy7Yh5jvuQOgY574FSoS\nuKCMGJZ6ecJlw+rB5pqanlLS9+HNnQ655/gTYg" +
+ "VBJClFClh4nwdPHtpyTySwgx1V\nr3VDvglfnZM+gD/D2d9nTLIlT3MZqhGOIkxKvpVVkdJKzg" +
+ "==\n-----END CERTIFICATE-----",
+ encryptedMetadataKey = "coawvmhMoAl3iL5okD7K4a4au0Jt0SqUXp6pHP8WD1YTOemFVPsz+ts7TD5kB7ha6Ja3tLdG" +
+ "Mq76LP/d2/pbHUiKBd6rytUo6ioHsNmmlTGHAlk9VTDY9fcvtVgkNzy7qyXvsdsUn0gBQ18l526J/bt1uRlClYNKva" +
+ "EnIh2l3B8X58pzNZqhAKNI7z7WRDbXOVskr4rnqWr2ExBeaZgFwo5nNi9yiqpckICb1S2qwuZJbItqZ8VR2bOG+WpC" +
+ "MwrgcE5UJ6ZvaKLREfmR+qoYYB1oyUuy78eA+sDa3rO5bSgs/9I/cli1b3lZ8JFfgHXRiUYUmBcxZOmUE2IfRSHFTA=="
+ )
+ ),
+ version = "2.0",
+ filedrop = mutableMapOf()
+ )
+ val message = EncryptionUtils.serializeJSON(metadata, true)
+
+ val cert = """
+ -----BEGIN CERTIFICATE-----
+ MIIC7jCCAdagAwIBAgIBADANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDEwVhZG1p
+ bjAeFw0yNjA0MjcwNzI5NDdaFw00NjA0MjIwNzI5NDdaMBAxDjAMBgNVBAMTBWFk
+ bWluMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwE513/kdkIp+Z5pI
+ 7rq1UKV5LBiB6dl4Wh46nI3mhVacOA1dJJWIUxRkkrUNWJewe8eJ7QWmhSpeBauA
+ 06PrAOTd1ZA4gSUWKpsYJqKm5Nxjp+BUMK1nHGQCkNQWjRllhyKTJeG/9PPc0ZrJ
+ h4V27bCXC9iX5l/ve35fp99VR4tQ947HWObe07EdPIEFNYfT/IurPwMySZ1WH1gy
+ DTw7IxMXcPVg+GUlfBoSVgQ1UdCkvgHc9pE1LuFmyBguAzGLbXDfspUuTs85RLGX
+ GYdv2vZU/R2kJEs3ePMtaGXw6DVSx82RkPFVaLDCdShX24yk6gLNEv9oTUXY40i3
+ n8njRQIDAQABo1MwUTAdBgNVHQ4EFgQUDE381mprCEvSLaFeOwZRliBSJnwwHwYD
+ VR0jBBgwFoAUDE381mprCEvSLaFeOwZRliBSJnwwDwYDVR0TAQH/BAUwAwEB/zAN
+ BgkqhkiG9w0BAQUFAAOCAQEAgU0o8Qp5wn3vkcQLYao2heWKsbYYl8wqkztRVVKb
+ +qMe2m/FOOMK1Rxv/anEVHHN+SnTc481fHd8z3w6II28LxJ5M+IxzoAFTj6gCv8+
+ rL1R9kE91401d1+ulAiJR92ykOcB1h8bk5yoCZSRLIXwViCGUrbC3iu2NLWQDYk4
+ vjxwqCSJOWUQh+qaYGCjB6mgkBMAnXGJCN2fV7sAR7N8Hy7Yh5jvuQOgY574FSoS
+ uKCMGJZ6ecJlw+rB5pqanlLS9+HNnQ655/gTYgVBJClFClh4nwdPHtpyTySwgx1V
+ r3VDvglfnZM+gD/D2d9nTLIlT3MZqhGOIkxKvpVVkdJKzg==
+ -----END CERTIFICATE-----
+ """.trimIndent()
+ val certs = listOf(EncryptionUtils.convertCertFromString(cert))
+ val signature = """
+ MIIE1wYJKoZIhvcNAQcCoIIEyDCCBMQCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0BBwGgggLyMIIC7jCCAdagAwIBAgIB
+ ADANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDEwVhZG1pbjAeFw0yNjA0MjcwNzI5NDdaFw00NjA0MjIwNzI5NDdaMBAxDjAMBgNV
+ BAMTBWFkbWluMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwE513/kdkIp+Z5pI7rq1UKV5LBiB6dl4Wh46nI3mhVa
+ cOA1dJJWIUxRkkrUNWJewe8eJ7QWmhSpeBauA06PrAOTd1ZA4gSUWKpsYJqKm5Nxjp+BUMK1nHGQCkNQWjRllhyKTJeG/9PPc0Zr
+ Jh4V27bCXC9iX5l/ve35fp99VR4tQ947HWObe07EdPIEFNYfT/IurPwMySZ1WH1gyDTw7IxMXcPVg+GUlfBoSVgQ1UdCkvgHc9p
+ E1LuFmyBguAzGLbXDfspUuTs85RLGXGYdv2vZU/R2kJEs3ePMtaGXw6DVSx82RkPFVaLDCdShX24yk6gLNEv9oTUXY40i3n8nj
+ RQIDAQABo1MwUTAdBgNVHQ4EFgQUDE381mprCEvSLaFeOwZRliBSJnwwHwYDVR0jBBgwFoAUDE381mprCEvSLaFeOwZRliBSJnww
+ DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAgU0o8Qp5wn3vkcQLYao2heWKsbYYl8wqkztRVVKb+qMe2m/FOOMK
+ 1Rxv/anEVHHN+SnTc481fHd8z3w6II28LxJ5M+IxzoAFTj6gCv8+rL1R9kE91401d1+ulAiJR92ykOcB1h8bk5yoCZSRLIXwViCG
+ UrbC3iu2NLWQDYk4vjxwqCSJOWUQh+qaYGCjB6mgkBMAnXGJCN2fV7sAR7N8Hy7Yh5jvuQOgY574FSoSuKCMGJZ6ecJlw+rB5pqa
+ nlLS9+HNnQ655/gTYgVBJClFClh4nwdPHtpyTySwgx1Vr3VDvglfnZM+gD/D2d9nTLIlT3MZqhGOIkxKvpVVkdJKzjGCAakwggGl
+ AgEAMBUwEDEOMAwGA1UEAxMFYWRtaW4CAQAwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMC8GCSqG
+ SIb3DQEJBDEiBCDJDYSA3+VA0KfGQbP7BQsnL/s24W/WIb99zb+4uQ8KLjAcBgkqhkiG9w0BCQUxDxcNMjYwNDI3MDczMDAyWjAL
+ BgkqhkiG9w0BAQsEggEAYgDB02/z+KaLvieL1hMMA9IZN8KKc4igvilBoS5W7isiArP8D/GIxghMZkrC0Tzqs+/VRlfFREUgf4aBd
+ 9GVzd86Qfrhcrzrdd8hoDQvOw/X3UGftqbgJQmOjZUDpI3TiupyQvOU/zqlIjOq5BiZN6RNti2BTcbNyjaTeVh6u1tcqVVSp/Z0ke
+ Ub+CnJFtIk6WhFepJMWI0vN84OyegNsjzIMSU2WjiN3i0jmYc62MpxUN0ZzmNgdZ7y6exe1Sb8EYUYL83BehQUPKO5EwEjEwX+ScYz
+ iWK0atXZioZYI2XLejVbQm1/czPTlA3frywKyM1dnkiufzmRpB49QN4o3g==
+ """.trimIndent()
+ val signedData = sut.getSignedData(signature, message)
+ val result = sut.verifySignedData(signedData, certs)
+ assertTrue(result)
+ }
+}
diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt
new file mode 100644
index 000000000000..7a7a10b2eabc
--- /dev/null
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,24 @@
+# Nextcloud - Android Client
+#
+# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+cmake_minimum_required(VERSION 3.10.2)
+
+project(cms_verifier VERSION 1.0)
+
+find_package(openssl REQUIRED CONFIG)
+
+add_library(
+ cms_verifier
+ SHARED
+ cms_verifier.cpp
+)
+
+target_link_libraries(
+ cms_verifier
+ openssl::ssl
+ openssl::crypto
+ android
+ log
+)
\ No newline at end of file
diff --git a/app/src/main/cpp/cms_verifier.cpp b/app/src/main/cpp/cms_verifier.cpp
new file mode 100644
index 000000000000..f369212a02c3
--- /dev/null
+++ b/app/src/main/cpp/cms_verifier.cpp
@@ -0,0 +1,100 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define LOG_TAG "CmsVerifier"
+#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+
+extern "C" JNIEXPORT jboolean JNICALL
+Java_com_nextcloud_utils_CmsSignatureVerifier_verifySignedData(
+ JNIEnv* env,
+ jobject /* thiz */,
+ jbyteArray cmsDataArray,
+ jbyteArray messageDataArray,
+ jobjectArray certPemArray
+) {
+ jsize cmsLen = env->GetArrayLength(cmsDataArray);
+ jbyte* cmsBytes = env->GetByteArrayElements(cmsDataArray, nullptr);
+ BIO* cmsBio = BIO_new_mem_buf(cmsBytes, static_cast(cmsLen));
+
+ jsize msgLen = env->GetArrayLength(messageDataArray);
+ jbyte* msgBytes = env->GetByteArrayElements(messageDataArray, nullptr);
+ BIO* dataBio = BIO_new_mem_buf(msgBytes, static_cast(msgLen));
+
+ CMS_ContentInfo* contentInfo = d2i_CMS_bio(cmsBio, nullptr);
+
+ BIO_free(cmsBio);
+ env->ReleaseByteArrayElements(cmsDataArray, cmsBytes, JNI_ABORT);
+
+ if (contentInfo == nullptr) {
+ LOGE("Failed to parse CMS content info");
+ BIO_free(dataBio);
+ env->ReleaseByteArrayElements(messageDataArray, msgBytes, JNI_ABORT);
+ return JNI_FALSE;
+ }
+
+ int verifyResult = CMS_verify(
+ contentInfo,
+ nullptr,
+ nullptr,
+ dataBio,
+ nullptr,
+ CMS_DETACHED | CMS_NO_SIGNER_CERT_VERIFY
+ );
+
+ BIO_free(dataBio);
+ env->ReleaseByteArrayElements(messageDataArray, msgBytes, JNI_ABORT);
+
+ if (verifyResult != 1) {
+ LOGE("CMS_verify failed");
+ CMS_ContentInfo_free(contentInfo);
+ return JNI_FALSE;
+ }
+
+ STACK_OF(CMS_SignerInfo)* signerInfos = CMS_get0_SignerInfos(contentInfo);
+ int numSigners = sk_CMS_SignerInfo_num(signerInfos);
+ jsize numCerts = env->GetArrayLength(certPemArray);
+ jboolean matched = JNI_FALSE;
+
+ for (jsize i = 0; i < numCerts && !matched; ++i) {
+ auto certPem = reinterpret_cast(env->GetObjectArrayElement(certPemArray, i));
+ const char* pemChars = env->GetStringUTFChars(certPem, nullptr);
+
+ BIO* certBio = BIO_new(BIO_s_mem());
+ BIO_write(certBio, pemChars, static_cast(strlen(pemChars)));
+ X509* certX509 = PEM_read_bio_X509(certBio, nullptr, nullptr, nullptr);
+
+ BIO_free(certBio);
+ env->ReleaseStringUTFChars(certPem, pemChars);
+ env->DeleteLocalRef(certPem);
+
+ if (certX509 == nullptr) {
+ LOGE("Failed to parse PEM certificate at index %d", i);
+ continue;
+ }
+
+ for (int j = 0; j < numSigners; ++j) {
+ CMS_SignerInfo* signerInfo = sk_CMS_SignerInfo_value(signerInfos, j);
+ if (CMS_SignerInfo_cert_cmp(signerInfo, certX509) == 0) {
+ matched = JNI_TRUE;
+ break;
+ }
+ }
+
+ X509_free(certX509);
+ }
+
+ CMS_ContentInfo_free(contentInfo);
+ return matched;
+}
diff --git a/app/src/main/java/com/nextcloud/utils/CmsSignatureVerifier.kt b/app/src/main/java/com/nextcloud/utils/CmsSignatureVerifier.kt
new file mode 100644
index 000000000000..77ee70659aef
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/utils/CmsSignatureVerifier.kt
@@ -0,0 +1,18 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.utils
+
+class CmsSignatureVerifier {
+ external fun verifySignedData(cmsData: ByteArray, messageData: ByteArray, certificates: Array): Boolean
+
+ companion object {
+ init {
+ System.loadLibrary("cms_verifier")
+ }
+ }
+}
diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt
index e87ef399d635..63d7616d920a 100644
--- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt
+++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt
@@ -12,6 +12,7 @@ import android.content.Context
import androidx.annotation.VisibleForTesting
import com.google.gson.reflect.TypeToken
import com.nextcloud.client.account.User
+import com.nextcloud.utils.CmsSignatureVerifier
import com.nextcloud.utils.autoRename.AutoRename
import com.nextcloud.utils.e2ee.E2EVersionHelper
import com.nextcloud.utils.extensions.showToast
@@ -668,97 +669,12 @@ class EncryptionUtilsV2 {
}
}
- // TODO verify metadata
- // if (!verifyMetadata(decryptedFolderMetadata)) {
- // throw IllegalStateException("Metadata is corrupt!")
- // }
-
// Auto rename if oc capability enabled for windows compatibility
decryptedFolderMetadata.metadata.files.values.forEach { file ->
file.filename = AutoRename.rename(file.filename, storageManager.getCapability(user))
}
return decryptedFolderMetadata
-
- // handle filesDrops
- // TODO re-add
-// try {
-// int filesDropCountBefore = encryptedFolderMetadata.getFiledrop().size();
-// DecryptedFolderMetadataFile decryptedFolderMetadata = new EncryptionUtilsV2().decryptFolderMetadataFile(
-// encryptedFolderMetadata,
-// privateKey);
-//
-// boolean transferredFiledrop = filesDropCountBefore > 0 && decryptedFolderMetadata.getFiles().size() ==
-// encryptedFolderMetadata.getFiles().size() + filesDropCountBefore;
-//
-// if (transferredFiledrop) {
-// // lock folder, only if not already locked
-// String token;
-// if (existingLockToken == null) {
-// token = EncryptionUtils.lockFolder(folder, client);
-// } else {
-// token = existingLockToken;
-// }
-//
-// // upload metadata
-// EncryptedFolderMetadataFile encryptedFolderMetadataNew =
-// encryptFolderMetadata(decryptedFolderMetadata, privateKey);
-//
-// String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadataNew);
-//
-// EncryptionUtils.uploadMetadata(folder,
-// serializedFolderMetadata,
-// token,
-// client,
-// true);
-//
-// // unlock folder, only if not previously locked
-// if (existingLockToken == null) {
-// RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(folder, client, token);
-//
-// if (!unlockFolderResult.isSuccess()) {
-// Log_OC.e(TAG, unlockFolderResult.getMessage());
-//
-// return null;
-// }
-// }
-// }
-//
-// return decryptedFolderMetadata;
-// } catch (Exception e) {
-// Log_OC.e(TAG, e.getMessage());
-// return null;
-// }
-
- // TODO to check
-// try {
-// int filesDropCountBefore = 0;
-// if (encryptedFolderMetadata.getFiledrop() != null) {
-// filesDropCountBefore = encryptedFolderMetadata.getFiledrop().size();
-// }
-// DecryptedFolderMetadataFile decryptedFolderMetadata = EncryptionUtils.decryptFolderMetaData(
-// encryptedFolderMetadata,
-// privateKey,
-// arbitraryDataProvider,
-// user,
-// folder.getLocalId());
-//
-// boolean transferredFiledrop = filesDropCountBefore > 0 &&
-// decryptedFolderMetadata.getFiles().size() ==
-// encryptedFolderMetadata.getFiles().size() + filesDropCountBefore;
-//
-// if (transferredFiledrop) {
-// // lock folder
-// String token = EncryptionUtils.lockFolder(folder, client);
-//
-// // upload metadata
-// EncryptedFolderMetadata encryptedFolderMetadataNew =
-// encryptFolderMetadata(decryptedFolderMetadata,
-// publicKey,
-// arbitraryDataProvider,
-// user,
-// folder.getLocalId());
-//
}
@Throws(UploadException::class)
@@ -978,7 +894,7 @@ class EncryptionUtilsV2 {
return true
}
- private fun getSignedData(base64encodedSignature: String, message: String): CMSSignedData {
+ fun getSignedData(base64encodedSignature: String, message: String): CMSSignedData {
val signature = EncryptionUtils.decodeStringToBase64Bytes(base64encodedSignature)
val asn1Signature = ASN1Sequence.fromByteArray(signature)
val contentInfo = ContentInfo.getInstance(asn1Signature)
@@ -990,21 +906,53 @@ class EncryptionUtilsV2 {
return CMSSignedData(cmsProcessableByteArray, contentInfo)
}
- @Suppress("TooGenericExceptionCaught")
fun verifySignedData(data: CMSSignedData, certs: List): Boolean {
- val signer = data.signerInfos.signers.first() as SignerInformation
+ val cmsBytes = data.toASN1Structure().encoded
+
+ val messageBytes = ByteArrayOutputStream().also { data.signedContent.write(it) }.toByteArray()
+
+ val certificatesAsPEMs = certs.map { cert -> toPemString(cert) }.toTypedArray()
+
+ return runCatching {
+ CmsSignatureVerifier().verifySignedData(cmsBytes, messageBytes, certificatesAsPEMs)
+ }.getOrElse {
+ Log_OC.w(TAG, "Exception verifySignedData: $it, trying bouncy castle")
+ verifySignedDataViaBouncyCastle(data, certs)
+ }
+ }
+
+ @Suppress("TooGenericExceptionCaught", "ReturnCount")
+ private fun verifySignedDataViaBouncyCastle(data: CMSSignedData, certs: List): Boolean {
+ val signers = data.signerInfos.signers
+ if (signers.isEmpty()) {
+ Log_OC.e(TAG, "signers are empty")
+ return false
+ }
+
+ val signer: SignerInformation? = signers.first()
+ if (signer == null) {
+ Log_OC.e(TAG, "signer is null")
+ return false
+ }
+
val verifierBuilder = JcaSimpleSignerInfoVerifierBuilder()
return certs.any { cert ->
runCatching {
- signer.verify(verifierBuilder.build(cert.publicKey))
+ val verifier = verifierBuilder.build(cert.publicKey)
+ signer.verify(verifier)
}.getOrElse {
- Log_OC.e(TAG, "Exception verifySignedData: $it")
+ Log_OC.e(TAG, "Exception verifySignedDataViaBouncyCastle: $it")
false
}
}
}
+ private fun toPemString(cert: X509Certificate): String {
+ val encoded = java.util.Base64.getMimeEncoder(PEM_LINE_LENGTH, "\n".toByteArray()).encodeToString(cert.encoded)
+ return "-----BEGIN CERTIFICATE-----\n$encoded\n-----END CERTIFICATE-----\n"
+ }
+
private fun signMessage(cert: X509Certificate, key: PrivateKey, data: ByteArray): CMSSignedData {
val content = CMSProcessableByteArray(data)
val certs = JcaCertStore(listOf(cert))
@@ -1096,5 +1044,6 @@ class EncryptionUtilsV2 {
companion object {
private val TAG = EncryptionUtils::class.java.simpleName
+ private const val PEM_LINE_LENGTH = 64
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 64e567311f0c..8122d7e33a91 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -65,6 +65,7 @@ mockitoVersion = "5.22.0"
mockkVersion = "1.14.9"
nnioVersion = "0.3.1"
objenesis = "3.5"
+opensslVersion = "3.5.6"
orchestratorVersion = "1.6.1"
orgJbundleUtilOsgiWrappedOrgApacheHttpClientVersion = "4.1.2"
osmdroidAndroidVersion = "6.1.20"
@@ -100,6 +101,7 @@ document-scanning-android-sdk = { module = "com.github.Hazzatur:Document-Scannin
fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtxVersion" }
exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "exifinterfaceVersion" }
material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "materialIconsCoreVersion" }
+openssl = { module = "com.nextcloud:openssl", version.ref = "opensslVersion" }
webkit = { module = "androidx.webkit:webkit", version.ref = "webkitVersion" }
splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splash-screen" }
sectioned-recyclerview = { module = "com.github.nextcloud-deps:sectioned-recyclerview", version.ref = "sectionedRecyclerviewVersion" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index a4c4e12af79b..8865a73a3faa 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -26197,6 +26197,14 @@
+
+
+
+
+
+
+
+
@@ -26798,6 +26806,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ndk.env b/ndk.env
index 540d18b717aa..5b4ec8961eb6 100644
--- a/ndk.env
+++ b/ndk.env
@@ -1,2 +1,2 @@
-NDK_VERSION=21.4.7075529
-CMAKE_VERSION=3.18.1
+NDK_VERSION=29.0.14206865
+CMAKE_VERSION=4.1.2
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 3583331170f3..1e9624e78ec5 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -28,6 +28,9 @@ pluginManagement {
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
+ maven {
+ url = uri("${rootProject.projectDir}/app/libs/local-maven")
+ }
google {
content {
includeGroupByRegex("com\\.android.*")