diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java index eb14c5a7d3c9..e9050a69b11f 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java @@ -344,6 +344,11 @@ public void createFolder() { } + @Override + public void createEncryptedFolder() { + + } + @Override public void uploadFromApp() { diff --git a/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java b/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java index e6c2d454b707..c51ae323b309 100644 --- a/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java +++ b/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java @@ -237,7 +237,7 @@ private void filterUnlock(List toHide, boolean fileLockingEnabled) { private void filterEncrypt(List toHide, boolean endToEndEncryptionEnabled) { if (files.isEmpty() || !isSingleSelection() || isSingleFile() || isEncryptedFolder() || isGroupFolder() - || !endToEndEncryptionEnabled || !isEmptyFolder() || isShared()) { + || !endToEndEncryptionEnabled || !isEmptyFolder() || isShared() || isInSubFolder()) { toHide.add(R.id.action_encrypted); } } @@ -581,4 +581,15 @@ private boolean isShared() { } return false; } + + private boolean isInSubFolder() { + OCFile folder = files.iterator().next(); + OCFile parent = storageManager.getFileById(folder.getParentId()); + + if (parent == null) { + return false; + } + + return !OCFile.ROOT_PATH.equals(parent.getRemotePath()); + } } diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java index f006888799f0..e27a5044c013 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java @@ -60,16 +60,31 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper private RemoteFile createdRemoteFolder; private User user; private Context context; + private boolean encrypted; /** * Constructor */ - public CreateFolderOperation(String remotePath, User user, Context context, FileDataStorageManager storageManager) { + public CreateFolderOperation(String remotePath, + User user, + Context context, + FileDataStorageManager storageManager + ) { + this(remotePath, false, user, context, storageManager); + } + + public CreateFolderOperation(String remotePath, + boolean encrypted, + User user, + Context context, + FileDataStorageManager storageManager + ) { super(storageManager); this.remotePath = remotePath; this.user = user; this.context = context; + this.encrypted = encrypted; } @Override @@ -105,7 +120,7 @@ protected RemoteOperationResult run(OwnCloudClient client) { } return new RemoteOperationResult(new IllegalStateException("E2E not supported")); } else { - return normalCreate(client); + return normalCreate(client, encrypted); } } @@ -473,7 +488,7 @@ private String createRandomFileName(DecryptedFolderMetadataFileV1 metadata) { return encryptedFileName; } - private RemoteOperationResult normalCreate(OwnCloudClient client) { + private RemoteOperationResult normalCreate(OwnCloudClient client, boolean encrypted) { RemoteOperationResult result = new CreateFolderRemoteOperation(remotePath, true).execute(client); if (result.isSuccess()) { @@ -482,8 +497,118 @@ private RemoteOperationResult normalCreate(OwnCloudClient client) { createdRemoteFolder = (RemoteFile) remoteFolderOperationResult.getData().get(0); saveFolderInDB(); - } else { - Log_OC.e(TAG, remotePath + " hasn't been created"); + + if (encrypted) { + final OCFile folder = getStorageManager().getFileByDecryptedRemotePath(remotePath); + + final RemoteOperationResult remoteOperationResult = + new ToggleEncryptionRemoteOperation(folder.getLocalId(), + remotePath, + true) + .execute(client); + + if (remoteOperationResult.isSuccess()) { + + ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context); + String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY); + String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY); + String token = null; + E2EVersion e2EVersion = getStorageManager().getCapability(user.getAccountName()).getEndToEndEncryptionApiVersion(); + + try { + // lock folder + token = EncryptionUtils.lockFolder(folder, client); + + if (e2EVersion == E2EVersion.V2_0) { + // Update metadata + Pair metadataPair = EncryptionUtils.retrieveMetadata(folder, + client, + privateKey, + publicKey, + getStorageManager(), + user, + context, + arbitraryDataProvider); + + boolean metadataExists = metadataPair.first; + DecryptedFolderMetadataFile metadata = metadataPair.second; + + new EncryptionUtilsV2().serializeAndUploadMetadata(folder, + metadata, + token, + client, + metadataExists, + context, + user, + getStorageManager()); + + // unlock folder + RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(folder, client, token); + + if (unlockFolderResult.isSuccess()) { + token = null; + } else { + // TODO E2E: do better + throw new RuntimeException("Could not unlock folder!"); + } + + } else if (e2EVersion == E2EVersion.V1_0 || + e2EVersion == E2EVersion.V1_1 || + e2EVersion == E2EVersion.V1_2 + ) { + // unlock folder + RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolderV1(folder, client, token); + + if (unlockFolderResult.isSuccess()) { + token = null; + } else { + // TODO E2E: do better + throw new RuntimeException("Could not unlock folder!"); + } + } else if (e2EVersion == E2EVersion.UNKNOWN) { + return new RemoteOperationResult(new IllegalStateException("E2E not supported")); + } + + folder.setEncrypted(true); + getStorageManager().saveFile(folder); + } catch (Throwable e) { + if (token != null) { + if (e2EVersion == E2EVersion.V2_0) { + if (!EncryptionUtils.unlockFolder(folder, client, token).isSuccess()) { + throw new RuntimeException("Could not clean up after failing folder creation!", e); + } + } else if (e2EVersion == E2EVersion.V1_0 || + e2EVersion == E2EVersion.V1_1 || + e2EVersion == E2EVersion.V1_2) { + if (!EncryptionUtils.unlockFolderV1(folder, client, token).isSuccess()) { + throw new RuntimeException("Could not clean up after failing folder creation!", e); + } + } + } + // TODO E2E: do better + return new RemoteOperationResult(new Exception(e)); + } finally { + // unlock folder + if (token != null) { + RemoteOperationResult unlockFolderResult = null; + if (e2EVersion == E2EVersion.V2_0) { + unlockFolderResult = EncryptionUtils.unlockFolder(folder, client, token); + } else if (e2EVersion == E2EVersion.V1_0 || + e2EVersion == E2EVersion.V1_1 || + e2EVersion == E2EVersion.V1_2) { + unlockFolderResult = EncryptionUtils.unlockFolderV1(folder, client, token); + } + + if (unlockFolderResult != null && !unlockFolderResult.isSuccess()) { + // TODO E2E: do better + throw new RuntimeException("Could not unlock folder!"); + } + } + } + } + } else { + Log_OC.e(TAG, remotePath + " hasn't been created"); + } } return result; diff --git a/app/src/main/java/com/owncloud/android/services/OperationsService.java b/app/src/main/java/com/owncloud/android/services/OperationsService.java index 648c7ce1b444..570abafac7f0 100644 --- a/app/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/app/src/main/java/com/owncloud/android/services/OperationsService.java @@ -84,6 +84,7 @@ public class OperationsService extends Service { public static final String EXTRA_ACCOUNT = "ACCOUNT"; public static final String EXTRA_SERVER_URL = "SERVER_URL"; public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH"; + public static final String EXTRA_ENCRYPTED = "ENCRYPTED"; public static final String EXTRA_NEWNAME = "NEWNAME"; public static final String EXTRA_REMOVE_ONLY_LOCAL = "REMOVE_LOCAL_COPY"; public static final String EXTRA_SYNC_FILE_CONTENTS = "SYNC_FILE_CONTENTS"; @@ -685,7 +686,9 @@ private Pair newOperation(Intent operationIntent) { case ACTION_CREATE_FOLDER: remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + boolean encrypted = operationIntent.getBooleanExtra(EXTRA_ENCRYPTED, false); operation = new CreateFolderOperation(remotePath, + encrypted, user, getApplicationContext(), fileDataStorageManager); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index eba470e3697d..777b562e2cfc 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -337,6 +337,10 @@ public int getItemCount() { @Nullable public OCFile getItem(int position) { + if (position == -1) { + return null; + } + int newPosition = position; if (shouldShowHeader() && position > 0) { diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt index 6642b025902a..325630033c8a 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt @@ -66,6 +66,7 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList private var parentFolder: OCFile? = null private var positiveButton: MaterialButton? = null + private var encrypted = false private lateinit var binding: EditBoxDialogBinding @@ -100,6 +101,7 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList @Suppress("EmptyFunctionBlock") override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { parentFolder = arguments?.getParcelableArgument(ARG_PARENT_FOLDER, OCFile::class.java) + encrypted = arguments?.getBoolean(ARG_ENCRYPTED) ?: false val inflater = requireActivity().layoutInflater binding = EditBoxDialogBinding.inflate(inflater, null, false) @@ -183,7 +185,7 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList val path = parentFolder?.decryptedRemotePath + newFolderName + OCFile.PATH_SEPARATOR connectivityService.isNetworkAndServerAvailable { result -> if (result) { - typedActivity()?.fileOperationsHelper?.createFolder(path) + typedActivity()?.fileOperationsHelper?.createFolder(path, encrypted) } else { Log_OC.d(TAG, "Network not available, creating offline operation") fileDataStorageManager.addCreateFolderOfflineOperation( @@ -201,8 +203,13 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList companion object { private const val TAG = "CreateFolderDialogFragment" private const val ARG_PARENT_FOLDER = "PARENT_FOLDER" + private const val ARG_ENCRYPTED = "ENCRYPTED" const val CREATE_FOLDER_FRAGMENT = "CREATE_FOLDER_FRAGMENT" + @JvmStatic + fun newInstance(parentFolder: OCFile?): CreateFolderDialogFragment { + return newInstance(parentFolder, false) + } /** * Public factory method to create new CreateFolderDialogFragment instances. * @@ -210,9 +217,10 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList * @return Dialog ready to show. */ @JvmStatic - fun newInstance(parentFolder: OCFile?): CreateFolderDialogFragment { + fun newInstance(parentFolder: OCFile?, encrypted: Boolean): CreateFolderDialogFragment { val bundle = Bundle().apply { putParcelable(ARG_PARENT_FOLDER, parentFolder) + putBoolean(ARG_ENCRYPTED, encrypted) } return CreateFolderDialogFragment().apply { diff --git a/app/src/main/java/com/owncloud/android/ui/events/EncryptionEvent.kt b/app/src/main/java/com/owncloud/android/ui/events/EncryptionEvent.kt index 495d230518e9..f49b8e169fbb 100644 --- a/app/src/main/java/com/owncloud/android/ui/events/EncryptionEvent.kt +++ b/app/src/main/java/com/owncloud/android/ui/events/EncryptionEvent.kt @@ -10,8 +10,12 @@ package com.owncloud.android.ui.events * Event for set folder as encrypted/decrypted */ class EncryptionEvent( + @JvmField val localId: Long, + @JvmField val remoteId: String, + @JvmField val remotePath: String, + @JvmField val shouldBeEncrypted: Boolean ) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java index c28f1e9837f9..4b4bc13a9940 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java @@ -18,6 +18,11 @@ public interface OCFileListBottomSheetActions { */ void createFolder(); + /** + * creates an encrypted folder within the actual folder + */ + void createEncryptedFolder(); + /** * offers a file upload with the Android OS file picker to the current folder. */ diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java index f5458da9e546..f4b9c8a5ad82 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java @@ -86,9 +86,8 @@ protected void onCreate(Bundle savedInstanceState) { binding.addToCloud.setText(getContext().getResources().getString(R.string.add_to_cloud, themeUtils.getDefaultDisplayNameForRootFolder(getContext()))); - OCCapability capability = fileActivity.getCapabilities(); - if (capability != null && - capability.getRichDocuments().isTrue() && + OCCapability capability = fileActivity.getStorageManager().getCapability(user.getAccountName()); + if (capability.getRichDocuments().isTrue() && capability.getRichDocumentsDirectEditing().isTrue() && capability.getRichDocumentsTemplatesAvailable().isTrue() && !file.isEncrypted()) { @@ -136,6 +135,12 @@ protected void onCreate(Bundle savedInstanceState) { binding.menuDirectCameraUpload.setVisibility(View.GONE); } + if (capability.getEndToEndEncryption().isTrue() && OCFile.ROOT_PATH.equals(file.getRemotePath())) { + binding.menuEncryptedMkdir.setVisibility(View.VISIBLE); + } else { + binding.menuEncryptedMkdir.setVisibility(View.GONE); + } + // create rich workspace if (editorUtils.isEditorAvailable(user, MimeTypeUtil.MIMETYPE_TEXT_MARKDOWN) && @@ -171,6 +176,11 @@ private void setupClickListener() { dismiss(); }); + binding.menuEncryptedMkdir.setOnClickListener(v -> { + actions.createEncryptedFolder(); + dismiss(); + }); + binding.menuUploadFromApp.setOnClickListener(v -> { actions.uploadFromApp(); dismiss(); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index 812974d98083..fbc9074a4065 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -515,6 +515,14 @@ public void createFolder() { .show(getActivity().getSupportFragmentManager(), DIALOG_CREATE_FOLDER); } + @Override + public void createEncryptedFolder() { + if (checkEncryptionIsSetup(null)) { + CreateFolderDialogFragment.newInstance(mFile, true) + .show(getActivity().getSupportFragmentManager(), DIALOG_CREATE_FOLDER); + } + } + @Override public void uploadFromApp() { Intent action = new Intent(Intent.ACTION_GET_CONTENT); @@ -1271,10 +1279,11 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { int position = data.getIntExtra(SetupEncryptionDialogFragment.ARG_POSITION, -1); OCFile file = mAdapter.getItem(position); - if (file != null) { - mContainerActivity.getFileOperationsHelper().toggleEncryption(file, true); - mAdapter.setEncryptionAttributeForItemID(file.getRemoteId(), true); + if (file == null) { + return; } + mContainerActivity.getFileOperationsHelper().toggleEncryption(file, true); + mAdapter.setEncryptionAttributeForItemID(file.getRemoteId(), true); // update state and view of this fragment searchFragment = false; @@ -1891,49 +1900,52 @@ protected RemoteOperation getSearchRemoteOperation(final User currentUser, final @Subscribe(threadMode = ThreadMode.BACKGROUND) public void onMessageEvent(EncryptionEvent event) { + if (checkEncryptionIsSetup(event.remoteId)) { + encryptFolder(event.localId, event.remoteId, event.remotePath, event.shouldBeEncrypted); + } + } + + private boolean checkEncryptionIsSetup(@Nullable String remoteId) { final User user = accountManager.getUser(); // check if keys are stored String publicKey = arbitraryDataProvider.getValue(user, EncryptionUtils.PUBLIC_KEY); String privateKey = arbitraryDataProvider.getValue(user, EncryptionUtils.PRIVATE_KEY); - FileDataStorageManager storageManager = mContainerActivity.getStorageManager(); - OCFile file = storageManager.getFileByRemoteId(event.getRemoteId()); - if (publicKey.isEmpty() || privateKey.isEmpty()) { Log_OC.d(TAG, "no public key for " + user.getAccountName()); + FileDataStorageManager storageManager = mContainerActivity.getStorageManager(); int position = -1; - if (file != null) { - position = mAdapter.getItemPosition(file); + if (remoteId != null) { + OCFile file = storageManager.getFileByRemoteId(remoteId); + if (file != null) { + position = mAdapter.getItemPosition(file); + } } SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(user, position); dialog.setTargetFragment(this, SETUP_ENCRYPTION_REQUEST_CODE); dialog.show(getParentFragmentManager(), SETUP_ENCRYPTION_DIALOG_TAG); + + return false; } else { - // TODO E2E: if encryption fails, to not set it as encrypted! - encryptFolder(file, - event.getLocalId(), - event.getRemoteId(), - event.getRemotePath(), - event.getShouldBeEncrypted(), - publicKey, - privateKey, - storageManager); + return true; } } - private void encryptFolder(OCFile folder, - long localId, + private void encryptFolder(long localId, String remoteId, String remotePath, - boolean shouldBeEncrypted, - String publicKeyString, - String privateKeyString, - FileDataStorageManager storageManager) { + boolean shouldBeEncrypted) { try { - Log_OC.d(TAG, "encrypt folder " + folder.getRemoteId()); + Log_OC.d(TAG, "encrypt folder " + remoteId); User user = accountManager.getUser(); + String publicKey = arbitraryDataProvider.getValue(user, EncryptionUtils.PUBLIC_KEY); + String privateKey = arbitraryDataProvider.getValue(user, EncryptionUtils.PRIVATE_KEY); + + FileDataStorageManager storageManager = mContainerActivity.getStorageManager(); + OCFile folder = storageManager.getFileByRemoteId(remoteId); + OwnCloudClient client = clientFactory.create(user); RemoteOperationResult remoteOperationResult = new ToggleEncryptionRemoteOperation(localId, remotePath, @@ -1950,8 +1962,8 @@ private void encryptFolder(OCFile folder, // Update metadata Pair metadataPair = EncryptionUtils.retrieveMetadata(folder, client, - privateKeyString, - publicKeyString, + privateKey, + publicKey, storageManager, user, requireContext(), diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index 25a5e4a739f4..329d24ab016b 100755 --- a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java @@ -948,11 +948,16 @@ public void removeFiles(Collection files, boolean onlyLocalCopy, boolean public void createFolder(String remotePath) { + createFolder(remotePath, false); + } + + public void createFolder(String remotePath, boolean encrypted) { // Create Folder Intent service = new Intent(fileActivity, OperationsService.class); service.setAction(OperationsService.ACTION_CREATE_FOLDER); service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); service.putExtra(OperationsService.EXTRA_REMOTE_PATH, remotePath); + service.putExtra(OperationsService.EXTRA_ENCRYPTED, encrypted); mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment));