Skip to content

Commit 7c2f8a6

Browse files
committed
E2EE related changes with test cases.
1 parent 985d4c2 commit 7c2f8a6

File tree

16 files changed

+2646
-1989
lines changed

16 files changed

+2646
-1989
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Nextcloud Android client application
3+
*
4+
* @author Álvaro Brey Vilas
5+
* Copyright (C) 2022 Álvaro Brey Vilas
6+
* Copyright (C) 2022 Nextcloud GmbH
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU General Public License as published by
10+
* the Free Software Foundation, either version 3 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU General Public License
19+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
*/
21+
package com.nmc.android
22+
23+
import androidx.test.core.app.launchActivity
24+
import androidx.test.ext.junit.runners.AndroidJUnit4
25+
import com.nextcloud.client.account.User
26+
import com.nextcloud.client.jobs.download.FileDownloadWorker
27+
import com.nextcloud.client.jobs.upload.FileUploadHelper
28+
import com.nextcloud.test.TestActivity
29+
import com.nextcloud.utils.EditorUtils
30+
import com.owncloud.android.AbstractIT
31+
import com.owncloud.android.R
32+
import com.owncloud.android.datamodel.ArbitraryDataProvider
33+
import com.owncloud.android.datamodel.FileDataStorageManager
34+
import com.owncloud.android.datamodel.OCFile
35+
import com.owncloud.android.files.FileMenuFilter
36+
import com.owncloud.android.lib.resources.status.CapabilityBooleanType
37+
import com.owncloud.android.lib.resources.status.OCCapability
38+
import com.owncloud.android.services.OperationsService
39+
import com.owncloud.android.ui.activity.ComponentsGetter
40+
import com.owncloud.android.utils.MimeType
41+
import io.mockk.MockKAnnotations
42+
import io.mockk.every
43+
import io.mockk.impl.annotations.MockK
44+
import org.junit.Assert.assertFalse
45+
import org.junit.Assert.assertTrue
46+
import org.junit.Before
47+
import org.junit.Test
48+
import org.junit.runner.RunWith
49+
import java.security.SecureRandom
50+
51+
@RunWith(AndroidJUnit4::class)
52+
class FileMenuFilterIT : AbstractIT() {
53+
54+
@MockK
55+
private lateinit var mockComponentsGetter: ComponentsGetter
56+
57+
@MockK
58+
private lateinit var mockStorageManager: FileDataStorageManager
59+
60+
@MockK
61+
private lateinit var mockFileUploaderBinder: FileUploadHelper
62+
63+
@MockK
64+
private lateinit var mockOperationsServiceBinder: OperationsService.OperationsServiceBinder
65+
66+
@MockK
67+
private lateinit var mockFileDownloadProgressListener: FileDownloadWorker.FileDownloadProgressListener
68+
69+
@MockK
70+
private lateinit var mockArbitraryDataProvider: ArbitraryDataProvider
71+
72+
private lateinit var editorUtils: EditorUtils
73+
74+
@Before
75+
fun setup() {
76+
MockKAnnotations.init(this)
77+
every { mockFileUploaderBinder.isUploading(any(), any()) } returns false
78+
every { mockComponentsGetter.fileUploaderHelper } returns mockFileUploaderBinder
79+
every { mockFileDownloadProgressListener.isDownloading(any(), any()) } returns false
80+
every { mockComponentsGetter.fileDownloadProgressListener } returns mockFileDownloadProgressListener
81+
every { mockOperationsServiceBinder.isSynchronizing(any(), any()) } returns false
82+
every { mockComponentsGetter.operationsServiceBinder } returns mockOperationsServiceBinder
83+
every { mockStorageManager.getFileById(any()) } returns OCFile("/")
84+
every { mockStorageManager.getFolderContent(any(), any()) } returns ArrayList<OCFile>()
85+
every { mockArbitraryDataProvider.getValue(any<User>(), any()) } returns ""
86+
editorUtils = EditorUtils(mockArbitraryDataProvider)
87+
}
88+
89+
@Test
90+
fun hide_shareAndFavouriteMenu_encryptedFolder() {
91+
val capability = OCCapability().apply {
92+
endToEndEncryption = CapabilityBooleanType.TRUE
93+
}
94+
95+
val encryptedFolder = OCFile("/encryptedFolder/").apply {
96+
isEncrypted = true
97+
mimeType = MimeType.DIRECTORY
98+
fileLength = SecureRandom().nextLong()
99+
}
100+
101+
configureCapability(capability)
102+
103+
launchActivity<TestActivity>().use {
104+
it.onActivity { activity ->
105+
val filterFactory =
106+
FileMenuFilter.Factory(mockStorageManager, activity, editorUtils)
107+
108+
val sut = filterFactory.newInstance(encryptedFolder, mockComponentsGetter, true, user)
109+
val toHide = sut.getToHide(false)
110+
111+
// encrypted folder
112+
assertTrue(toHide.contains(R.id.action_see_details))
113+
assertTrue(toHide.contains(R.id.action_favorite))
114+
assertTrue(toHide.contains(R.id.action_unset_favorite))
115+
}
116+
}
117+
}
118+
119+
@Test
120+
fun show_shareAndFavouriteMenu_normalFolder() {
121+
val capability = OCCapability().apply {
122+
endToEndEncryption = CapabilityBooleanType.TRUE
123+
}
124+
125+
val normalFolder = OCFile("/folder/").apply {
126+
mimeType = MimeType.DIRECTORY
127+
fileLength = SecureRandom().nextLong()
128+
}
129+
130+
configureCapability(capability)
131+
132+
launchActivity<TestActivity>().use {
133+
it.onActivity { activity ->
134+
val filterFactory =
135+
FileMenuFilter.Factory(mockStorageManager, activity, editorUtils)
136+
137+
val sut = filterFactory.newInstance(normalFolder, mockComponentsGetter, true, user)
138+
val toHide = sut.getToHide(false)
139+
140+
// normal folder
141+
assertFalse(toHide.contains(R.id.action_see_details))
142+
assertFalse(toHide.contains(R.id.action_favorite))
143+
assertTrue(toHide.contains(R.id.action_unset_favorite))
144+
}
145+
}
146+
}
147+
148+
private fun configureCapability(capability: OCCapability) {
149+
every { mockStorageManager.getCapability(any<User>()) } returns capability
150+
every { mockStorageManager.getCapability(any<String>()) } returns capability
151+
}
152+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.nmc.android
2+
3+
import androidx.test.espresso.Espresso.onView
4+
import androidx.test.espresso.assertion.ViewAssertions.matches
5+
import androidx.test.espresso.intent.rule.IntentsTestRule
6+
import androidx.test.espresso.matcher.ViewMatchers.withHint
7+
import androidx.test.espresso.matcher.ViewMatchers.withId
8+
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
9+
import com.nextcloud.test.TestActivity
10+
import com.owncloud.android.AbstractIT
11+
import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment
12+
import org.junit.Rule
13+
import org.junit.Test
14+
import com.owncloud.android.R
15+
16+
class SetupEncryptionDialogFragmentIT : AbstractIT() {
17+
18+
@get:Rule
19+
val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
20+
21+
@Test
22+
fun validatePassphraseInputHint() {
23+
val activity = testActivityRule.launchActivity(null)
24+
25+
val sut = SetupEncryptionDialogFragment.newInstance(user, 0)
26+
27+
sut.show(activity.supportFragmentManager, "1")
28+
29+
val keyWords = arrayListOf(
30+
"ability",
31+
"able",
32+
"about",
33+
"above",
34+
"absent",
35+
"absorb",
36+
"abstract",
37+
"absurd",
38+
"abuse",
39+
"access",
40+
"accident",
41+
"account",
42+
"accuse"
43+
)
44+
45+
shortSleep()
46+
47+
UiThreadStatement.runOnUiThread {
48+
sut.setMnemonic(keyWords)
49+
sut.showMnemonicInfo()
50+
}
51+
52+
waitForIdleSync()
53+
54+
onView(withId(R.id.encryption_passwordInput)).check(matches(withHint("Passphrase…")))
55+
}
56+
}

app/src/main/java/com/owncloud/android/datamodel/OCFile.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,14 +623,19 @@ public boolean isHidden() {
623623
}
624624

625625
/**
626+
* The unique fileId for the file within the instance This only works if we have 12 digits for instanceId RemoteId
627+
* is a combination of localId + instanceId If a file has remoteId: 4174305739oc97a8ddfc96, in this 4174305739 is
628+
* localId & oc97a8ddfc96 is instanceId which is of 12 digits
629+
*
626630
* unique fileId for the file within the instance
627631
*/
628632
@SuppressFBWarnings("STT")
629633
public long getLocalId() {
630634
if (localId > 0) {
631635
return localId;
632636
} else if (remoteId != null && remoteId.length() > 8) {
633-
return Long.parseLong(remoteId.substring(0, 8).replaceAll("^0*", ""));
637+
//NMC Customization --> for long remote id's
638+
return Long.parseLong(remoteId.substring(0, remoteId.length() - 12).replaceAll("^0*", ""));
634639
} else {
635640
return -1;
636641
}

app/src/main/java/com/owncloud/android/files/FileMenuFilter.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ private List<Integer> filter(boolean inSingleFileFragment) {
175175

176176

177177
private void filterShareFile(List<Integer> toHide, OCCapability capability) {
178-
if (!isSingleSelection() || containsEncryptedFile() || hasEncryptedParent() ||
178+
if (!isSingleSelection() || containsEncryptedFile() || hasEncryptedParent() || containsEncryptedFolder() ||
179179
(!isShareViaLinkAllowed() && !isShareWithUsersAllowed()) ||
180180
!isShareApiEnabled(capability) || !files.iterator().next().canReshare()) {
181181
toHide.add(R.id.action_send_share_file);
@@ -191,19 +191,21 @@ private void filterSendFiles(List<Integer> toHide, boolean inSingleFileFragment)
191191
}
192192

193193
private void filterDetails(Collection<Integer> toHide) {
194-
if (!isSingleSelection()) {
194+
if (!isSingleSelection() || containsEncryptedFolder() || containsEncryptedFile()) {
195195
toHide.add(R.id.action_see_details);
196196
}
197197
}
198198

199199
private void filterFavorite(List<Integer> toHide, boolean synchronizing) {
200-
if (files.isEmpty() || synchronizing || allFavorites()) {
200+
if (files.isEmpty() || synchronizing || allFavorites() || containsEncryptedFile()
201+
|| containsEncryptedFolder()) {
201202
toHide.add(R.id.action_favorite);
202203
}
203204
}
204205

205206
private void filterUnfavorite(List<Integer> toHide, boolean synchronizing) {
206-
if (files.isEmpty() || synchronizing || allNotFavorites()) {
207+
if (files.isEmpty() || synchronizing || allNotFavorites() || containsEncryptedFile()
208+
|| containsEncryptedFolder()) {
207209
toHide.add(R.id.action_unset_favorite);
208210
}
209211
}
@@ -332,8 +334,10 @@ private void filterSelectAll(List<Integer> toHide, boolean inSingleFileFragment)
332334
}
333335

334336
private void filterRemove(List<Integer> toHide, boolean synchronizing) {
335-
if (files.isEmpty() || synchronizing || containsLockedFile()
336-
|| containsEncryptedFolder() || isFolderAndContainsEncryptedFile()) {
337+
if ((files.isEmpty() || synchronizing || containsLockedFile()
338+
|| containsEncryptedFolder() || isFolderAndContainsEncryptedFile())
339+
//show delete option for encrypted sub-folder
340+
&& !hasEncryptedParent()) {
337341
toHide.add(R.id.action_remove_file);
338342
}
339343
}

app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ open class FolderPickerActivity :
6161
private var mSyncBroadcastReceiver: SyncBroadcastReceiver? = null
6262
private var mSyncInProgress = false
6363
private var mSearchOnlyFolders = false
64+
private var mShowOnlyFolder = false
65+
private var mHideEncryptedFolder = false
6466
var isDoNotEnterEncryptedFolder = false
6567
private set
6668

@@ -114,6 +116,9 @@ open class FolderPickerActivity :
114116
}
115117

116118
private fun setupAction() {
119+
mShowOnlyFolder = intent.getBooleanExtra(EXTRA_SHOW_ONLY_FOLDER, false)
120+
mHideEncryptedFolder = intent.getBooleanExtra(EXTRA_HIDE_ENCRYPTED_FOLDER, false)
121+
117122
action = intent.getStringExtra(EXTRA_ACTION)
118123

119124
if (action != null && action == CHOOSE_LOCATION) {
@@ -359,7 +364,7 @@ open class FolderPickerActivity :
359364
}
360365

361366
private fun refreshListOfFilesFragment(fromSearch: Boolean) {
362-
listOfFilesFragment?.listDirectory(false, fromSearch)
367+
listOfFilesFragment?.listDirectoryFolder(false, fromSearch, mShowOnlyFolder, mHideEncryptedFolder)
363368
}
364369

365370
fun browseToRoot() {
@@ -685,6 +690,12 @@ open class FolderPickerActivity :
685690
@JvmField
686691
val EXTRA_ACTION = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_ACTION")
687692

693+
@JvmField
694+
val EXTRA_SHOW_ONLY_FOLDER = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_SHOW_ONLY_FOLDER")
695+
696+
@JvmField
697+
val EXTRA_HIDE_ENCRYPTED_FOLDER = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_HIDE_ENCRYPTED_FOLDER")
698+
688699
const val MOVE_OR_COPY = "MOVE_OR_COPY"
689700
const val CHOOSE_LOCATION = "CHOOSE_LOCATION"
690701
private val TAG = FolderPickerActivity::class.java.simpleName

app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -426,10 +426,14 @@ private void setupE2EPreference(PreferenceCategory preferenceCategoryMore) {
426426
Preference preference = findPreference("setup_e2e");
427427

428428
if (preference != null) {
429+
430+
if (!CapabilityUtils.getCapability(this).getEndToEndEncryption().isTrue()) {
431+
preferenceCategoryMore.removePreference(preference);
432+
return;
433+
}
434+
429435
if (FileOperationsHelper.isEndToEndEncryptionSetup(this, user) ||
430-
CapabilityUtils.getCapability(this).getEndToEndEncryptionKeysExist().isTrue() ||
431-
CapabilityUtils.getCapability(this).getEndToEndEncryptionKeysExist().isUnknown()
432-
) {
436+
CapabilityUtils.getCapability(this).getEndToEndEncryptionKeysExist().isTrue()) {
433437
preferenceCategoryMore.removePreference(preference);
434438
} else {
435439
preference.setOnPreferenceClickListener(p -> {
@@ -510,7 +514,7 @@ private void removeE2E(PreferenceCategory preferenceCategoryMore) {
510514
}
511515

512516
private void showRemoveE2EAlertDialog(PreferenceCategory preferenceCategoryMore, Preference preference) {
513-
new MaterialAlertDialogBuilder(this, R.style.FallbackTheming_Dialog)
517+
new MaterialAlertDialogBuilder(this)
514518
.setTitle(R.string.prefs_e2e_mnemonic)
515519
.setMessage(getString(R.string.remove_e2e_message))
516520
.setCancelable(true)
@@ -524,6 +528,9 @@ private void showRemoveE2EAlertDialog(PreferenceCategory preferenceCategoryMore,
524528
preferenceCategoryMore.removePreference(pMnemonic);
525529
}
526530

531+
// NMC: restart to show the preferences correctly
532+
restartSettingsActivity();
533+
527534
dialog.dismiss();
528535
})
529536
.create()
@@ -982,13 +989,18 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
982989
} else if (requestCode == ACTION_SHOW_MNEMONIC && resultCode == RESULT_OK) {
983990
handleMnemonicRequest(data);
984991
} else if (requestCode == ACTION_E2E && data != null && data.getBooleanExtra(SetupEncryptionDialogFragment.SUCCESS, false)) {
985-
Intent i = new Intent(this, SettingsActivity.class);
986-
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
987-
i.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
988-
startActivity(i);
992+
//restart to show the preferences correctly
993+
restartSettingsActivity();
989994
}
990995
}
991996

997+
private void restartSettingsActivity() {
998+
Intent i = new Intent(this, SettingsActivity.class);
999+
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
1000+
i.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
1001+
startActivity(i);
1002+
}
1003+
9921004
@VisibleForTesting
9931005
public void handleMnemonicRequest(Intent data) {
9941006
if (data == null) {
@@ -1006,8 +1018,8 @@ public void handleMnemonicRequest(Intent data) {
10061018
}
10071019

10081020
private void showMnemonicAlertDialogDialog(String mnemonic) {
1009-
new MaterialAlertDialogBuilder(this, R.style.FallbackTheming_Dialog)
1010-
.setTitle(R.string.prefs_e2e_mnemonic)
1021+
new MaterialAlertDialogBuilder(this)
1022+
.setTitle(R.string.dialog_e2e_mnemonic_title)
10111023
.setMessage(mnemonic)
10121024
.setPositiveButton(R.string.common_ok, (dialog, which) -> dialog.dismiss())
10131025
.setNegativeButton(R.string.common_cancel, (dialog, i) -> dialog.dismiss())

0 commit comments

Comments
 (0)