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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@

package com.nextcloud.utils

import android.util.Base64
import androidx.test.platform.app.InstrumentationRegistry
import com.google.gson.Gson
import com.owncloud.android.datamodel.Credentials
import com.owncloud.android.ui.dialog.setupEncryption.CertificateValidator
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.io.InputStreamReader
import java.security.KeyPairGenerator

class CertificateValidatorTests {

Expand All @@ -30,16 +34,48 @@ class CertificateValidatorTests {
sut = null
}

@Test
fun testValidateWhenGivenValidServerKeyAndCertificateShouldReturnTrue() {
private fun readCredentials(): Credentials {
val inputStream =
InstrumentationRegistry.getInstrumentation().context.assets.open("credentials.json")

val credentials = InputStreamReader(inputStream).use { reader ->
return InputStreamReader(inputStream).use { reader ->
Gson().fromJson(reader, Credentials::class.java)
}
}

private fun generateUnrelatedPublicKeyPem(): String {
val publicKey = KeyPairGenerator.getInstance("RSA")
.apply { initialize(RSA_KEY_SIZE) }
.generateKeyPair()
.public
val base64 = Base64.encodeToString(publicKey.encoded, Base64.DEFAULT)
return "-----BEGIN PUBLIC KEY-----\n$base64-----END PUBLIC KEY-----\n"
}

@Test
fun testValidateWhenGivenValidServerKeyAndCertificateShouldReturnTrue() {
val credentials = readCredentials()

assertTrue(sut?.validate(credentials.publicKey, credentials.certificate) ?: false)
}

@Test
fun testValidateWhenCertificateIsNotSignedByServerKeyShouldReturnFalse() {
val credentials = readCredentials()

assertFalse(sut?.validate(generateUnrelatedPublicKeyPem(), credentials.certificate) ?: true)
}

@Test
fun testValidateWhenGivenMalformedInputShouldReturnFalse() {
val credentials = readCredentials()

assertFalse(sut?.validate("", credentials.certificate) ?: true)
assertFalse(sut?.validate(credentials.publicKey, "") ?: true)
assertFalse(sut?.validate(credentials.publicKey, credentials.publicKey) ?: true)
}

val isCertificateValid = sut?.validate(credentials.publicKey, credentials.certificate) ?: false
assert(isCertificateValid)
companion object {
private const val RSA_KEY_SIZE = 2048
}
}
12 changes: 12 additions & 0 deletions app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,16 @@ target_link_libraries(
openssl::crypto
android
log
)

add_library(
certificate_validator
SHARED
certificate_validator.cpp
)

target_link_libraries(
certificate_validator
openssl::crypto
log
)
91 changes: 91 additions & 0 deletions app/src/main/cpp/certificate_validator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

#include <jni.h>
#include <android/log.h>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/x509.h>

#define LOG_TAG "CertificateValidator"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

static constexpr size_t OPENSSL_ERROR_BUFFER_SIZE = 256;

static void logOpenSslError(const char* message) {
char reason[OPENSSL_ERROR_BUFFER_SIZE];
ERR_error_string_n(ERR_get_error(), reason, sizeof(reason));
LOGE("%s: %s", message, reason);
ERR_clear_error();
}

static EVP_PKEY* readPublicKey(JNIEnv* env, jstring publicKeyPem) {
const char* chars = env->GetStringUTFChars(publicKeyPem, nullptr);
BIO* bio = BIO_new_mem_buf(chars, -1);
EVP_PKEY* publicKey = bio == nullptr ? nullptr : PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr);
BIO_free(bio);
env->ReleaseStringUTFChars(publicKeyPem, chars);
return publicKey;
}

static X509* readCertificate(JNIEnv* env, jstring certificatePem) {
const char* chars = env->GetStringUTFChars(certificatePem, nullptr);
BIO* bio = BIO_new_mem_buf(chars, -1);
X509* certificate = bio == nullptr ? nullptr : PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
BIO_free(bio);
env->ReleaseStringUTFChars(certificatePem, chars);
return certificate;
}

// X509_cmp_current_time returns -1 when the time is in the past, 1 when it is in the future and 0 on error.
static bool isWithinValidityPeriod(const X509* certificate) {
return X509_cmp_current_time(X509_get0_notBefore(certificate)) < 0 &&
X509_cmp_current_time(X509_get0_notAfter(certificate)) > 0;
}

extern "C" JNIEXPORT jboolean JNICALL
Java_com_owncloud_android_ui_dialog_setupEncryption_CertificateValidator_verifyCertificate(
JNIEnv* env,
jobject /* thiz */,
jstring serverPublicKeyPem,
jstring certificatePem
) {
if (serverPublicKeyPem == nullptr || certificatePem == nullptr) {
LOGE("Server public key or certificate is missing");
return JNI_FALSE;
}

EVP_PKEY* serverPublicKey = readPublicKey(env, serverPublicKeyPem);
if (serverPublicKey == nullptr) {
logOpenSslError("Failed to parse server public key");
return JNI_FALSE;
}

X509* certificate = readCertificate(env, certificatePem);
if (certificate == nullptr) {
logOpenSslError("Failed to parse certificate");
EVP_PKEY_free(serverPublicKey);
return JNI_FALSE;
}

jboolean isValid = JNI_FALSE;
if (!isWithinValidityPeriod(certificate)) {
LOGE("Certificate is expired or not yet valid");
} else if (X509_verify(certificate, serverPublicKey) != 1) {
logOpenSslError("Certificate is not signed by the server public key");
} else {
LOGD("Client certificate is valid against server public key");
isValid = JNI_TRUE;
}

X509_free(certificate);
EVP_PKEY_free(serverPublicKey);
return isValid;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,24 @@

package com.owncloud.android.ui.dialog.setupEncryption

import android.util.Base64
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.utils.crypto.CryptoStringUtils
import java.io.ByteArrayInputStream
import java.security.KeyFactory
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.X509EncodedKeySpec
import javax.inject.Inject

@Suppress("EmptyClassBlock")
class CertificateValidator @Inject constructor() {
private val tag = "CertificateValidator"

/**
* Validates certificate with given public key
*
* @param serverPublicKeyString Public key with header
* @param serverPublicKeyString Public key in PEM format
* @param certificate Certificate in PEM format
*/
@Suppress("TooGenericExceptionCaught")
fun validate(serverPublicKeyString: String, certificate: String): Boolean {
val contentOfServerKey = CryptoStringUtils.rawPublicKey(serverPublicKeyString)
fun validate(serverPublicKeyString: String, certificate: String): Boolean =
verifyCertificate(serverPublicKeyString, certificate)

return try {
val decodedPublicKey = Base64.decode(contentOfServerKey, Base64.NO_WRAP)
private external fun verifyCertificate(serverPublicKeyPem: String, certificatePem: String): Boolean

val keySpec = X509EncodedKeySpec(decodedPublicKey)
val keyFactory = KeyFactory.getInstance("RSA")
val serverPublicKey = keyFactory.generatePublic(keySpec)

val certificateFactory = CertificateFactory.getInstance("X.509")
val certificateInputStream = ByteArrayInputStream(certificate.toByteArray())
val x509Certificate = certificateFactory.generateCertificate(certificateInputStream) as X509Certificate

// Check date of the certificate
x509Certificate.checkValidity()

// Verify certificate with serverPublicKey
x509Certificate.verify(serverPublicKey)
Log_OC.d(tag, "Client certificate is valid against server public key")
true
} catch (e: Exception) {
Log_OC.d(tag, "Client certificate is not valid against the server public key: $e")
false
companion object {
init {
System.loadLibrary("certificate_validator")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@
package com.owncloud.android.utils.crypto

object CryptoStringUtils {
fun rawPublicKey(publicKey: String): String = publicKey
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replace("\\s+".toRegex(), "")

fun rawPrivateKey(privateKey: String): String = privateKey
.replace("\n".toRegex(), "")
.replace("-----BEGIN PRIVATE KEY-----", "")
Expand Down
Loading