Skip to content

Commit 16fcdbd

Browse files
Merge pull request #16916 from nextcloud/fix/e2ee-verify-metadata
fix(end-to-encryption): verify metadata
2 parents 6d07966 + bcef379 commit 16fcdbd

13 files changed

Lines changed: 330 additions & 91 deletions

File tree

.codacy.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
exclude_paths:
5+
- "app/src/main/cpp/**"

REUSE.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ precedence = "aggregate"
2929
SPDX-FileCopyrightText = "2015-2016 ownCloud Inc."
3030
SPDX-License-Identifier = "GPL-2.0-only"
3131

32+
[[annotations]]
33+
path = ["app/libs/local-maven/**/*.aar", "app/libs/local-maven/**/*.pom"]
34+
precedence = "aggregate"
35+
SPDX-FileCopyrightText = "2026 Nextcloud GmbH and Nextcloud contributors"
36+
SPDX-License-Identifier = "AGPL-3.0-or-later"
37+
3238
[[annotations]]
3339
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"]
3440
precedence = "aggregate"

app/build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ android {
8686
externalNativeBuild {
8787
cmake {
8888
version = "${ndkEnv["CMAKE_VERSION"]}"
89+
path = file("src/main/cpp/CMakeLists.txt")
8990
}
9091
}
9192

@@ -107,7 +108,7 @@ android {
107108
compileSdk = 36
108109

109110
ndk {
110-
abiFilters += listOf("arm64-v8a", "x86_64", "x86")
111+
abiFilters += listOf("arm64-v8a", "x86_64")
111112
}
112113

113114
buildConfigField("boolean", "CI", ciBuild.toString())
@@ -192,6 +193,7 @@ android {
192193
viewBinding = true
193194
aidl = true
194195
compose = true
196+
prefab = true
195197
}
196198

197199
compileOptions {
@@ -444,6 +446,7 @@ dependencies {
444446

445447
// region Crypto
446448
implementation(libs.conscrypt.android)
449+
implementation(libs.openssl)
447450
// endregion
448451

449452
// region Library
Binary file not shown.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project>
3+
<modelVersion>4.0.0</modelVersion>
4+
<groupId>com.nextcloud</groupId>
5+
<artifactId>openssl</artifactId>
6+
<version>3.5.6</version>
7+
<packaging>aar</packaging>
8+
</project>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.owncloud.android.utils
9+
10+
import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFolderMetadataFile
11+
import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedMetadata
12+
import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedUser
13+
import org.junit.Assert.assertTrue
14+
import org.junit.Test
15+
16+
class EncryptionUtilsMetadataVerificationTests {
17+
18+
private val sut = EncryptionUtilsV2()
19+
20+
@Test
21+
fun testVerifyMetadataWhenGivenValidInputsShouldReturnTrue() {
22+
val metadata = EncryptedFolderMetadataFile(
23+
metadata =
24+
EncryptedMetadata(
25+
authenticationTag = "xkVxj0NbQEXIEMlulYZJgg==",
26+
nonce = "HzRiseUfoFJ5lqUi",
27+
ciphertext = "EOnzuyVn9R8qDUBY4yeuJbhQdkOHBMy3nyRGwY0y/+oWctV17XvE0RIbOhH7+smKV3orJKatu5fG6iIZN+" +
28+
"HZUQASTCdQ0mdFVPJmdk20UH5nFZ/ilQIyyXAFhLHdYwWA/M7wKYoh5W9fDXNX9cZvHgjWPdT9Pq99PUv37atYxj7Je" +
29+
"25GenbtxkVxj0NbQEXIEMlulYZJgg=="
30+
),
31+
users = listOf(
32+
EncryptedUser(
33+
userId = "admin",
34+
certificate = "-----BEGIN CERTIFICATE-----\nMIIC7jCCAdagAwIBAgIBADANBgkqhkiG9w0BAQUFADAQMQ4wDAYD" +
35+
"VQQDEwVhZG1p\nbjAeFw0yNjA0MjcwNzI5NDdaFw00NjA0MjIwNzI5NDdaMBAxDjAMBgNVBAMTBWFk\nbWluMIIBIjAN" +
36+
"BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwE513/kdkIp+Z5pI\n7rq1UKV5LBiB6dl4Wh46nI3mhVacOA1dJJWIU" +
37+
"xRkkrUNWJewe8eJ7QWmhSpeBauA\n06PrAOTd1ZA4gSUWKpsYJqKm5Nxjp+BUMK1nHGQCkNQWjRllhyKTJeG/9PPc0Z" +
38+
"rJ\nh4V27bCXC9iX5l/ve35fp99VR4tQ947HWObe07EdPIEFNYfT/IurPwMySZ1WH1gy\nDTw7IxMXcPVg+GUlfBoSV" +
39+
"gQ1UdCkvgHc9pE1LuFmyBguAzGLbXDfspUuTs85RLGX\nGYdv2vZU/R2kJEs3ePMtaGXw6DVSx82RkPFVaLDCdShX24" +
40+
"yk6gLNEv9oTUXY40i3\nn8njRQIDAQABo1MwUTAdBgNVHQ4EFgQUDE381mprCEvSLaFeOwZRliBSJnwwHwYD\nVR0j" +
41+
"BBgwFoAUDE381mprCEvSLaFeOwZRliBSJnwwDwYDVR0TAQH/BAUwAwEB/zAN\nBgkqhkiG9w0BAQUFAAOCAQEAgU0o8" +
42+
"Qp5wn3vkcQLYao2heWKsbYYl8wqkztRVVKb\n+qMe2m/FOOMK1Rxv/anEVHHN+SnTc481fHd8z3w6II28LxJ5M+Ixzo" +
43+
"AFTj6gCv8+\nrL1R9kE91401d1+ulAiJR92ykOcB1h8bk5yoCZSRLIXwViCGUrbC3iu2NLWQDYk4\nvjxwqCSJOWUQh" +
44+
"+qaYGCjB6mgkBMAnXGJCN2fV7sAR7N8Hy7Yh5jvuQOgY574FSoS\nuKCMGJZ6ecJlw+rB5pqanlLS9+HNnQ655/gTYg" +
45+
"VBJClFClh4nwdPHtpyTySwgx1V\nr3VDvglfnZM+gD/D2d9nTLIlT3MZqhGOIkxKvpVVkdJKzg" +
46+
"==\n-----END CERTIFICATE-----",
47+
encryptedMetadataKey = "coawvmhMoAl3iL5okD7K4a4au0Jt0SqUXp6pHP8WD1YTOemFVPsz+ts7TD5kB7ha6Ja3tLdG" +
48+
"Mq76LP/d2/pbHUiKBd6rytUo6ioHsNmmlTGHAlk9VTDY9fcvtVgkNzy7qyXvsdsUn0gBQ18l526J/bt1uRlClYNKva" +
49+
"EnIh2l3B8X58pzNZqhAKNI7z7WRDbXOVskr4rnqWr2ExBeaZgFwo5nNi9yiqpckICb1S2qwuZJbItqZ8VR2bOG+WpC" +
50+
"MwrgcE5UJ6ZvaKLREfmR+qoYYB1oyUuy78eA+sDa3rO5bSgs/9I/cli1b3lZ8JFfgHXRiUYUmBcxZOmUE2IfRSHFTA=="
51+
)
52+
),
53+
version = "2.0",
54+
filedrop = mutableMapOf()
55+
)
56+
val message = EncryptionUtils.serializeJSON(metadata, true)
57+
58+
val cert = """
59+
-----BEGIN CERTIFICATE-----
60+
MIIC7jCCAdagAwIBAgIBADANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDEwVhZG1p
61+
bjAeFw0yNjA0MjcwNzI5NDdaFw00NjA0MjIwNzI5NDdaMBAxDjAMBgNVBAMTBWFk
62+
bWluMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwE513/kdkIp+Z5pI
63+
7rq1UKV5LBiB6dl4Wh46nI3mhVacOA1dJJWIUxRkkrUNWJewe8eJ7QWmhSpeBauA
64+
06PrAOTd1ZA4gSUWKpsYJqKm5Nxjp+BUMK1nHGQCkNQWjRllhyKTJeG/9PPc0ZrJ
65+
h4V27bCXC9iX5l/ve35fp99VR4tQ947HWObe07EdPIEFNYfT/IurPwMySZ1WH1gy
66+
DTw7IxMXcPVg+GUlfBoSVgQ1UdCkvgHc9pE1LuFmyBguAzGLbXDfspUuTs85RLGX
67+
GYdv2vZU/R2kJEs3ePMtaGXw6DVSx82RkPFVaLDCdShX24yk6gLNEv9oTUXY40i3
68+
n8njRQIDAQABo1MwUTAdBgNVHQ4EFgQUDE381mprCEvSLaFeOwZRliBSJnwwHwYD
69+
VR0jBBgwFoAUDE381mprCEvSLaFeOwZRliBSJnwwDwYDVR0TAQH/BAUwAwEB/zAN
70+
BgkqhkiG9w0BAQUFAAOCAQEAgU0o8Qp5wn3vkcQLYao2heWKsbYYl8wqkztRVVKb
71+
+qMe2m/FOOMK1Rxv/anEVHHN+SnTc481fHd8z3w6II28LxJ5M+IxzoAFTj6gCv8+
72+
rL1R9kE91401d1+ulAiJR92ykOcB1h8bk5yoCZSRLIXwViCGUrbC3iu2NLWQDYk4
73+
vjxwqCSJOWUQh+qaYGCjB6mgkBMAnXGJCN2fV7sAR7N8Hy7Yh5jvuQOgY574FSoS
74+
uKCMGJZ6ecJlw+rB5pqanlLS9+HNnQ655/gTYgVBJClFClh4nwdPHtpyTySwgx1V
75+
r3VDvglfnZM+gD/D2d9nTLIlT3MZqhGOIkxKvpVVkdJKzg==
76+
-----END CERTIFICATE-----
77+
""".trimIndent()
78+
val certs = listOf(EncryptionUtils.convertCertFromString(cert))
79+
val signature = """
80+
MIIE1wYJKoZIhvcNAQcCoIIEyDCCBMQCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0BBwGgggLyMIIC7jCCAdagAwIBAgIB
81+
ADANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDEwVhZG1pbjAeFw0yNjA0MjcwNzI5NDdaFw00NjA0MjIwNzI5NDdaMBAxDjAMBgNV
82+
BAMTBWFkbWluMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwE513/kdkIp+Z5pI7rq1UKV5LBiB6dl4Wh46nI3mhVa
83+
cOA1dJJWIUxRkkrUNWJewe8eJ7QWmhSpeBauA06PrAOTd1ZA4gSUWKpsYJqKm5Nxjp+BUMK1nHGQCkNQWjRllhyKTJeG/9PPc0Zr
84+
Jh4V27bCXC9iX5l/ve35fp99VR4tQ947HWObe07EdPIEFNYfT/IurPwMySZ1WH1gyDTw7IxMXcPVg+GUlfBoSVgQ1UdCkvgHc9p
85+
E1LuFmyBguAzGLbXDfspUuTs85RLGXGYdv2vZU/R2kJEs3ePMtaGXw6DVSx82RkPFVaLDCdShX24yk6gLNEv9oTUXY40i3n8nj
86+
RQIDAQABo1MwUTAdBgNVHQ4EFgQUDE381mprCEvSLaFeOwZRliBSJnwwHwYDVR0jBBgwFoAUDE381mprCEvSLaFeOwZRliBSJnww
87+
DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAgU0o8Qp5wn3vkcQLYao2heWKsbYYl8wqkztRVVKb+qMe2m/FOOMK
88+
1Rxv/anEVHHN+SnTc481fHd8z3w6II28LxJ5M+IxzoAFTj6gCv8+rL1R9kE91401d1+ulAiJR92ykOcB1h8bk5yoCZSRLIXwViCG
89+
UrbC3iu2NLWQDYk4vjxwqCSJOWUQh+qaYGCjB6mgkBMAnXGJCN2fV7sAR7N8Hy7Yh5jvuQOgY574FSoSuKCMGJZ6ecJlw+rB5pqa
90+
nlLS9+HNnQ655/gTYgVBJClFClh4nwdPHtpyTySwgx1Vr3VDvglfnZM+gD/D2d9nTLIlT3MZqhGOIkxKvpVVkdJKzjGCAakwggGl
91+
AgEAMBUwEDEOMAwGA1UEAxMFYWRtaW4CAQAwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMC8GCSqG
92+
SIb3DQEJBDEiBCDJDYSA3+VA0KfGQbP7BQsnL/s24W/WIb99zb+4uQ8KLjAcBgkqhkiG9w0BCQUxDxcNMjYwNDI3MDczMDAyWjAL
93+
BgkqhkiG9w0BAQsEggEAYgDB02/z+KaLvieL1hMMA9IZN8KKc4igvilBoS5W7isiArP8D/GIxghMZkrC0Tzqs+/VRlfFREUgf4aBd
94+
9GVzd86Qfrhcrzrdd8hoDQvOw/X3UGftqbgJQmOjZUDpI3TiupyQvOU/zqlIjOq5BiZN6RNti2BTcbNyjaTeVh6u1tcqVVSp/Z0ke
95+
Ub+CnJFtIk6WhFepJMWI0vN84OyegNsjzIMSU2WjiN3i0jmYc62MpxUN0ZzmNgdZ7y6exe1Sb8EYUYL83BehQUPKO5EwEjEwX+ScYz
96+
iWK0atXZioZYI2XLejVbQm1/czPTlA3frywKyM1dnkiufzmRpB49QN4o3g==
97+
""".trimIndent()
98+
val signedData = sut.getSignedData(signature, message)
99+
val result = sut.verifySignedData(signedData, certs)
100+
assertTrue(result)
101+
}
102+
}

app/src/main/cpp/CMakeLists.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Nextcloud - Android Client
2+
#
3+
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
4+
# SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
cmake_minimum_required(VERSION 3.10.2)
7+
8+
project(cms_verifier VERSION 1.0)
9+
10+
find_package(openssl REQUIRED CONFIG)
11+
12+
add_library(
13+
cms_verifier
14+
SHARED
15+
cms_verifier.cpp
16+
)
17+
18+
target_link_libraries(
19+
cms_verifier
20+
openssl::ssl
21+
openssl::crypto
22+
android
23+
log
24+
)

app/src/main/cpp/cms_verifier.cpp

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
#include <jni.h>
9+
#include <android/log.h>
10+
#include <openssl/bio.h>
11+
#include <openssl/cms.h>
12+
#include <openssl/pem.h>
13+
#include <openssl/x509.h>
14+
#include <cstring>
15+
16+
#define LOG_TAG "CmsVerifier"
17+
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
18+
19+
extern "C" JNIEXPORT jboolean JNICALL
20+
Java_com_nextcloud_utils_CmsSignatureVerifier_verifySignedData(
21+
JNIEnv* env,
22+
jobject /* thiz */,
23+
jbyteArray cmsDataArray,
24+
jbyteArray messageDataArray,
25+
jobjectArray certPemArray
26+
) {
27+
jsize cmsLen = env->GetArrayLength(cmsDataArray);
28+
jbyte* cmsBytes = env->GetByteArrayElements(cmsDataArray, nullptr);
29+
BIO* cmsBio = BIO_new_mem_buf(cmsBytes, static_cast<int>(cmsLen));
30+
31+
jsize msgLen = env->GetArrayLength(messageDataArray);
32+
jbyte* msgBytes = env->GetByteArrayElements(messageDataArray, nullptr);
33+
BIO* dataBio = BIO_new_mem_buf(msgBytes, static_cast<int>(msgLen));
34+
35+
CMS_ContentInfo* contentInfo = d2i_CMS_bio(cmsBio, nullptr);
36+
37+
BIO_free(cmsBio);
38+
env->ReleaseByteArrayElements(cmsDataArray, cmsBytes, JNI_ABORT);
39+
40+
if (contentInfo == nullptr) {
41+
LOGE("Failed to parse CMS content info");
42+
BIO_free(dataBio);
43+
env->ReleaseByteArrayElements(messageDataArray, msgBytes, JNI_ABORT);
44+
return JNI_FALSE;
45+
}
46+
47+
int verifyResult = CMS_verify(
48+
contentInfo,
49+
nullptr,
50+
nullptr,
51+
dataBio,
52+
nullptr,
53+
CMS_DETACHED | CMS_NO_SIGNER_CERT_VERIFY
54+
);
55+
56+
BIO_free(dataBio);
57+
env->ReleaseByteArrayElements(messageDataArray, msgBytes, JNI_ABORT);
58+
59+
if (verifyResult != 1) {
60+
LOGE("CMS_verify failed");
61+
CMS_ContentInfo_free(contentInfo);
62+
return JNI_FALSE;
63+
}
64+
65+
STACK_OF(CMS_SignerInfo)* signerInfos = CMS_get0_SignerInfos(contentInfo);
66+
int numSigners = sk_CMS_SignerInfo_num(signerInfos);
67+
jsize numCerts = env->GetArrayLength(certPemArray);
68+
jboolean matched = JNI_FALSE;
69+
70+
for (jsize i = 0; i < numCerts && !matched; ++i) {
71+
auto certPem = reinterpret_cast<jstring>(env->GetObjectArrayElement(certPemArray, i));
72+
const char* pemChars = env->GetStringUTFChars(certPem, nullptr);
73+
74+
BIO* certBio = BIO_new(BIO_s_mem());
75+
BIO_write(certBio, pemChars, static_cast<int>(strlen(pemChars)));
76+
X509* certX509 = PEM_read_bio_X509(certBio, nullptr, nullptr, nullptr);
77+
78+
BIO_free(certBio);
79+
env->ReleaseStringUTFChars(certPem, pemChars);
80+
env->DeleteLocalRef(certPem);
81+
82+
if (certX509 == nullptr) {
83+
LOGE("Failed to parse PEM certificate at index %d", i);
84+
continue;
85+
}
86+
87+
for (int j = 0; j < numSigners; ++j) {
88+
CMS_SignerInfo* signerInfo = sk_CMS_SignerInfo_value(signerInfos, j);
89+
if (CMS_SignerInfo_cert_cmp(signerInfo, certX509) == 0) {
90+
matched = JNI_TRUE;
91+
break;
92+
}
93+
}
94+
95+
X509_free(certX509);
96+
}
97+
98+
CMS_ContentInfo_free(contentInfo);
99+
return matched;
100+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.utils
9+
10+
class CmsSignatureVerifier {
11+
external fun verifySignedData(cmsData: ByteArray, messageData: ByteArray, certificates: Array<String>): Boolean
12+
13+
companion object {
14+
init {
15+
System.loadLibrary("cms_verifier")
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)