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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .codacy.yml
Original file line number Diff line number Diff line change
@@ -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/**"
6 changes: 6 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 4 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ android {
externalNativeBuild {
cmake {
version = "${ndkEnv["CMAKE_VERSION"]}"
path = file("src/main/cpp/CMakeLists.txt")
}
}

Expand All @@ -107,7 +108,7 @@ android {
compileSdk = 36

ndk {
abiFilters += listOf("arm64-v8a", "x86_64", "x86")
abiFilters += listOf("arm64-v8a", "x86_64")
}

buildConfigField("boolean", "CI", ciBuild.toString())
Expand Down Expand Up @@ -192,6 +193,7 @@ android {
viewBinding = true
aidl = true
compose = true
prefab = true
}

compileOptions {
Expand Down Expand Up @@ -444,6 +446,7 @@ dependencies {

// region Crypto
implementation(libs.conscrypt.android)
implementation(libs.openssl)
// endregion

// region Library
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.nextcloud</groupId>
<artifactId>openssl</artifactId>
<version>3.5.6</version>
<packaging>aar</packaging>
</project>
Original file line number Diff line number Diff line change
@@ -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)
}
}
24 changes: 24 additions & 0 deletions app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
)
100 changes: 100 additions & 0 deletions app/src/main/cpp/cms_verifier.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

#include <jni.h>
#include <android/log.h>
#include <openssl/bio.h>
#include <openssl/cms.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <cstring>

#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);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

jbyte* cmsBytes = env->GetByteArrayElements(cmsDataArray, nullptr);
BIO* cmsBio = BIO_new_mem_buf(cmsBytes, static_cast<int>(cmsLen));

jsize msgLen = env->GetArrayLength(messageDataArray);
jbyte* msgBytes = env->GetByteArrayElements(messageDataArray, nullptr);
BIO* dataBio = BIO_new_mem_buf(msgBytes, static_cast<int>(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<jstring>(env->GetObjectArrayElement(certPemArray, i));
const char* pemChars = env->GetStringUTFChars(certPem, nullptr);

BIO* certBio = BIO_new(BIO_s_mem());
BIO_write(certBio, pemChars, static_cast<int>(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;
}
18 changes: 18 additions & 0 deletions app/src/main/java/com/nextcloud/utils/CmsSignatureVerifier.kt
Original file line number Diff line number Diff line change
@@ -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<String>): Boolean

companion object {
init {
System.loadLibrary("cms_verifier")
}
}
}
Loading
Loading