-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Implemented tests for encrypted folders #17697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
daniele-verducci
wants to merge
12
commits into
master
Choose a base branch
from
improvement/internal-105265-tests
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
9d22f9f
Refactoring to extract key generation for better testing
daniele-verducci 64a5ade
Working create and read tests for folder and subfolder
daniele-verducci afe6da4
Fix lint + a small refactoring to make generatePrivateKey() more read…
daniele-verducci b8631ce
Cleanup keys after tests run
daniele-verducci 2fe4ec0
Added metadata folder check in folder opening check
daniele-verducci d168de3
Fixed empty capability in test
daniele-verducci 8922d3e
Working encrypted folder test
daniele-verducci ad689a7
Refactored & small fixes
daniele-verducci 8da63d1
Fixed copypasted file copyright text for a newly created file
daniele-verducci ae74111
Implemented unencryption test
daniele-verducci f3a83a0
Restricted tests to Nextcloud >= 30
daniele-verducci ea4b44a
Fixed lint
daniele-verducci File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
242 changes: 242 additions & 0 deletions
242
app/src/androidTest/java/com/owncloud/android/EncryptedFoldersIT.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.