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
242 changes: 242 additions & 0 deletions app/src/androidTest/java/com/owncloud/android/EncryptedFoldersIT.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/

package com.owncloud.android

import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.account.UserAccountManagerImpl
import com.nextcloud.client.database.entity.FileEntity
import com.nextcloud.client.network.NetworkModule
import com.nextcloud.test.SinceServer
import com.nextcloud.utils.e2ee.E2EEActionResolver
import com.nextcloud.utils.e2ee.E2EEKeyInspector
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.lib.resources.e2ee.ToggleEncryptionRemoteOperation
import com.owncloud.android.lib.resources.status.GetCapabilitiesRemoteOperation
import com.owncloud.android.lib.resources.status.NextcloudVersion
import com.owncloud.android.operations.CreateFolderOperation
import com.owncloud.android.operations.RefreshFolderOperation
import com.owncloud.android.operations.common.SyncOperation
import com.owncloud.android.operations.e2e.E2EDeletionService
import com.owncloud.android.ui.dialog.setupEncryption.CertificateValidator
import com.owncloud.android.ui.dialog.setupEncryption.EncryptionKeyGenerator
import com.owncloud.android.utils.EncryptionUtils
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

@SinceServer(majorVersion = 30)
open class EncryptedFoldersIT : AbstractOnServerIT() {
companion object {
val FOLDER = "/encryptedFolder/"
val SUBFOLDER = "encryptedSubfolder/"
val KEYWORDS = arrayListOf(
"ability",
"able",
"about",
"above",
"absent",
"absorb",
"abstract",
"absurd",
"abuse",
"access",
"accident",
"account",
"accuse"
)
}

private var e2eeActionResolver: E2EEActionResolver
private var encryptionKeyGenerator: EncryptionKeyGenerator

init {
val accountManager: UserAccountManager = UserAccountManagerImpl.fromContext(targetContext)
val inspector = E2EEKeyInspector(
targetContext,
storageManager,
CertificateValidator(),
arbitraryDataProvider,
accountManager
)
e2eeActionResolver = E2EEActionResolver(
storageManager,
arbitraryDataProvider,
accountManager,
connectivityServiceMock,
inspector
)
encryptionKeyGenerator = EncryptionKeyGenerator(targetContext, user)
}

/**
* This test covers both encrypted folder creation and encryption of an existing (empty) folder,
* as they are basically the same action (folder creation + encryption), the only difference being
* the latter is executed manually by the user later.
*/

@Test
fun testCreateEncryptedFolder() {
createEncryptedFolder(FOLDER)
}

@Test
fun testCreateEncryptedSubfolder() {
val parent = createEncryptedFolder(FOLDER)
createEncryptedSubfolder(SUBFOLDER, parent)
}

@Test
fun testReadEncryptedFolder() {
val remotePath = FOLDER
val ocFile = createEncryptedFolder(remotePath)
val files = listEncryptedFolder(ocFile)
assertEquals(files.size, 0)
}

@Test
fun testReadEncryptedSubfolder() {
createEncryptedFolder(FOLDER)
val subOCFile = createEncryptedFolder(SUBFOLDER)
val files = listEncryptedFolder(subOCFile)
assertEquals(files.size, 0)
}

@Test
fun testUnencryptFolder() {
// Create encrypted folder
val ocFile = createEncryptedFolder(FOLDER)
assertTrue(ocFile.isFolder && ocFile.isEncrypted)

// Unencrypt it
encryptFolder(ocFile, false)
assertFalse(ocFile.isEncrypted)
}

@Before
fun encryptionSetup() {
testOnlyOnServer(NextcloudVersion.nextcloud_30)

// Fetch capability
val capability = GetCapabilitiesRemoteOperation(null).execute(client).getResultData()
storageManager.saveCapabilities(capability)

// Check if server supports end2end capability
assertTrue(capability.endToEndEncryption.isTrue)

// Delete existing encryption key, if any
assertTrue(
E2EDeletionService(NetworkModule().clientFactory(targetContext)).deleteKeysAndFiles(user)
)

// Create new encryption key
val privateKey: String = runBlocking {
encryptionKeyGenerator.generatePrivateKey(KEYWORDS)
}

// Check the key was generated
assertNotEquals(privateKey, "")
}

@After
fun encryptionCleanup() {
// Delete existing encryption key, if any
assertTrue(
E2EDeletionService(NetworkModule().clientFactory(targetContext)).deleteKeysAndFiles(user)
)
}

private fun createEncryptedFolder(remotePath: String): OCFile {
val created = CreateFolderOperation(remotePath, user, targetContext, storageManager).apply {
setEncrypt(true)
}.execute(client)
assertTrue(created.toString(), created.isSuccess)

val ocFile = storageManager.getFileByRemotePath(remotePath)
assertNotNull(ocFile)

encryptFolder(ocFile!!, true)

return ocFile
}

fun encryptFolder(ocFile: OCFile, encrypt: Boolean) {
val encrypted = ToggleEncryptionRemoteOperation(ocFile.localId, ocFile.remotePath, encrypt)
.execute(client)
assertTrue(encrypted.toString(), encrypted.isSuccess)

val publicKey = arbitraryDataProvider.getValue(user, EncryptionUtils.PUBLIC_KEY)
val privateKey = arbitraryDataProvider.getValue(user, EncryptionUtils.PRIVATE_KEY)
val uploadedMetadata = encryptionKeyGenerator.uploadEncryptedFolderMetadata(
ocFile,
client,
publicKey,
privateKey,
storageManager,
arbitraryDataProvider
)
assertTrue(uploadedMetadata)

// Set file as encrypted locally
ocFile.isEncrypted = encrypt
assertTrue(storageManager.saveFile(ocFile))
}

fun createEncryptedSubfolder(folderName: String, parent: OCFile) {
// An encrypted subfolder is a normal folder inside an encrypted one
assertTrue(parent.isFolder && parent.isEncrypted)

// Create folder
val path = "${parent.remotePath}${OCFile.PATH_SEPARATOR}${folderName}${OCFile.PATH_SEPARATOR}"
val syncOp: SyncOperation = CreateFolderOperation(
path,
user,
targetContext,
storageManager
)
val result = syncOp.execute(client)
assertTrue(result.toString(), result.isSuccess)

// Check folder exists
val ocFile = storageManager.getFileByRemotePath(path)
assertTrue(ocFile?.isFolder ?: false)
}

private fun listEncryptedFolder(ocFile: OCFile): List<FileEntity> {
assertNotNull(ocFile)
val parent = storageManager.getFileById(ocFile.parentId)
assertNotNull(parent)

// Refresh folder
val refreshResult = RefreshFolderOperation(
parent,
System.currentTimeMillis(),
false,
false,
storageManager,
user,
targetContext
).execute(client)
assertTrue(refreshResult.toString(), refreshResult.isSuccess)

// Check folder metadata
runBlocking {
assertTrue(e2eeActionResolver.checkFolderMetadataKey(ocFile))
}

// Open folder
return runBlocking {
storageManager.fileDao.getFolderContentSuspended(ocFile.fileId)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class E2EEActionResolver @Inject constructor(
}

suspend fun checkFolderMetadataKey(file: OCFile): Boolean = withContext(Dispatchers.IO) {
val capability = storageManager.getCapability(accountManager.user)
val capability = storageManager.getCapability(accountManager.user.accountName)
val canDecrypt = inspector.canDecryptFolderMetadata(file, capability)
storageManager.setReadOnly(file, !canDecrypt)
return@withContext canDecrypt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.lib.resources.e2ee.DeleteEncryptedFilesRemoteOperation
import com.owncloud.android.lib.resources.users.DeletePrivateKeyRemoteOperation
import com.owncloud.android.lib.resources.users.DeletePublicKeyRemoteOperation
import org.jetbrains.annotations.VisibleForTesting

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please use androidx.annotation.* instead.


@Suppress("MagicNumber")
class E2EDeletionService(private val clientFactory: ClientFactory) {
Expand All @@ -41,36 +42,38 @@ class E2EDeletionService(private val clientFactory: ClientFactory) {

private fun deleteKeysAndFiles(user: User, onResult: (Boolean) -> Unit) {
Thread {
val result = runCatching {
val client = clientFactory.createNextcloudClient(user)
var successfulOperationResultCount = 3
val result = deleteKeysAndFiles(user)
mainHandler.post { onResult(result) }
}.start()
}

if (!DeletePrivateKeyRemoteOperation().execute(client).isSuccess) {
successfulOperationResultCount -= 1
}
@VisibleForTesting
fun deleteKeysAndFiles(user: User): Boolean = runCatching {
val client = clientFactory.createNextcloudClient(user)
var successfulOperationResultCount = 3

Log_OC.i(TAG, "🔑" + "private key is deleted")
if (!DeletePrivateKeyRemoteOperation().execute(client).isSuccess) {
successfulOperationResultCount -= 1
}

if (!DeletePublicKeyRemoteOperation().execute(client).isSuccess) {
successfulOperationResultCount -= 1
}
Log_OC.i(TAG, "🔑" + "private key is deleted")

Log_OC.i(TAG, "🗝" + "public key is deleted")
if (!DeletePublicKeyRemoteOperation().execute(client).isSuccess) {
successfulOperationResultCount -= 1
}

if (!DeleteEncryptedFilesRemoteOperation().execute(client).isSuccess) {
successfulOperationResultCount -= 1
}
Log_OC.i(TAG, "🗝" + "public key is deleted")

Log_OC.i(TAG, "🗂️" + "encrypted files are deleted")
if (!DeleteEncryptedFilesRemoteOperation().execute(client).isSuccess) {
successfulOperationResultCount -= 1
}

successfulOperationResultCount == 3
}.getOrElse { e ->
Log.e(TAG, "Cannot delete E2E keys and files", e)
false
}
Log_OC.i(TAG, "🗂️" + "encrypted files are deleted")

mainHandler.post { onResult(result) }
}.start()
successfulOperationResultCount == 3
}.getOrElse { e ->
Log.e(TAG, "Cannot delete E2E keys and files", e)
false
}

companion object {
Expand Down
Loading
Loading