diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png new file mode 100644 index 000000000000..3977140e8e90 Binary files /dev/null and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png new file mode 100644 index 000000000000..1ffb0f987e34 Binary files /dev/null and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png new file mode 100644 index 000000000000..365aed3a29b5 Binary files /dev/null and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png new file mode 100644 index 000000000000..e9fd77fa3813 Binary files /dev/null and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png index ab19cd1bc591..72d4ba359fe0 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png index 509802a9051e..237dcd156a4b 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png index 6b0f9ad6ce81..f8d775768078 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png index 6b0f9ad6ce81..f8d775768078 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png index 172bbde365b1..188f61ca8655 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png index 857de92bec66..2ced2a166583 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png index 7b5befd0c571..75c1bdc09f9b 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png differ diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java index 98fe9c75e576..f3c6e4c76494 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java @@ -415,10 +415,11 @@ protected void enableRTL() { } protected void resetLocale() { + Locale locale = new Locale("en"); Resources resources = InstrumentationRegistry.getInstrumentation().getTargetContext().getResources(); - Configuration defaultConfig = resources.getConfiguration(); - defaultConfig.setLocale(Locale.getDefault()); - resources.updateConfiguration(defaultConfig, null); + Configuration config = resources.getConfiguration(); + config.setLocale(locale); + resources.updateConfiguration(config, null); } protected void screenshot(View view) { 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 3853215b7754..beefce3cda72 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 @@ -108,10 +108,7 @@ private FileDisplayActivity getFileDisplayActivity() { Intent intent = new Intent(targetContext, FileDisplayActivity.class); return activityRule.launchActivity(intent); } - - @Rule - public GrantPermissionRule permissionRule = GrantPermissionRule.grant( - android.Manifest.permission.POST_NOTIFICATIONS); + @After public void quitLooperIfNeeded() { diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt b/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt index 5383c95dac3f..a1854907e9bb 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt @@ -54,7 +54,7 @@ class LoadingDialog : DialogFragment(), Injectable { viewThemeUtils?.platform?.tintDrawable(requireContext(), loadingDrawable) } - viewThemeUtils?.platform?.colorViewBackground(binding.loadingLayout, ColorRole.SURFACE_VARIANT) + viewThemeUtils?.platform?.colorViewBackground(binding.loadingLayout, ColorRole.SURFACE) return binding.root } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.java deleted file mode 100644 index 634cebb63f9a..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.java +++ /dev/null @@ -1,551 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Tobias Kaminsky - * @author TSI-mc - * Copyright (C) 2017 Tobias Kaminsky - * Copyright (C) 2017 Nextcloud GmbH. - * Copyright (C) 2023 TSI-mc - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.owncloud.android.ui.dialog; - -import android.accounts.AccountManager; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Button; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.nextcloud.client.account.User; -import com.nextcloud.client.di.Injectable; -import com.owncloud.android.R; -import com.owncloud.android.databinding.SetupEncryptionDialogBinding; -import com.owncloud.android.datamodel.ArbitraryDataProvider; -import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; -import com.owncloud.android.lib.common.accounts.AccountUtils; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.users.DeletePublicKeyOperation; -import com.owncloud.android.lib.resources.users.GetPrivateKeyOperation; -import com.owncloud.android.lib.resources.users.GetPublicKeyOperation; -import com.owncloud.android.lib.resources.users.SendCSROperation; -import com.owncloud.android.lib.resources.users.StorePrivateKeyOperation; -import com.owncloud.android.utils.CsrHelper; -import com.owncloud.android.utils.EncryptionUtils; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; - -import static com.owncloud.android.utils.EncryptionUtils.MNEMONIC; -import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes; -import static com.owncloud.android.utils.EncryptionUtils.decryptStringAsymmetric; -import static com.owncloud.android.utils.EncryptionUtils.encodeBytesToBase64String; -import static com.owncloud.android.utils.EncryptionUtils.generateKey; - -/* - * Dialog to setup encryption - */ -public class SetupEncryptionDialogFragment extends DialogFragment implements Injectable { - - public static final String SUCCESS = "SUCCESS"; - public static final int SETUP_ENCRYPTION_RESULT_CODE = 101; - public static final int SETUP_ENCRYPTION_REQUEST_CODE = 100; - public static final String SETUP_ENCRYPTION_DIALOG_TAG = "SETUP_ENCRYPTION_DIALOG_TAG"; - public static final String ARG_POSITION = "ARG_POSITION"; - - public static final String RESULT_REQUEST_KEY = "RESULT_REQUEST"; - public static final String RESULT_KEY_CANCELLED = "IS_CANCELLED"; - - private static final String ARG_USER = "ARG_USER"; - private static final String TAG = SetupEncryptionDialogFragment.class.getSimpleName(); - - private static final String KEY_CREATED = "KEY_CREATED"; - private static final String KEY_EXISTING_USED = "KEY_EXISTING_USED"; - private static final String KEY_FAILED = "KEY_FAILED"; - private static final String KEY_GENERATE = "KEY_GENERATE"; - - @Inject ViewThemeUtils viewThemeUtils; - - private User user; - private ArbitraryDataProvider arbitraryDataProvider; - private Button positiveButton; - private Button neutralButton; - private DownloadKeysAsyncTask task; - private String keyResult; - private ArrayList keyWords; - private SetupEncryptionDialogBinding binding; - - /** - * Public factory method to create new SetupEncryptionDialogFragment instance - * - * @return Dialog ready to show. - */ - public static SetupEncryptionDialogFragment newInstance(User user, int position) { - SetupEncryptionDialogFragment fragment = new SetupEncryptionDialogFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_USER, user); - args.putInt(ARG_POSITION, position); - fragment.setArguments(args); - return fragment; - } - - @Override - public void onStart() { - super.onStart(); - - AlertDialog alertDialog = (AlertDialog) getDialog(); - - if (alertDialog != null) { - positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); - neutralButton = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL); - viewThemeUtils.platform.colorTextButtons(positiveButton, neutralButton); - } - - task = new DownloadKeysAsyncTask(requireContext()); - task.execute(); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - if (getArguments() == null) { - throw new IllegalStateException("Arguments may not be null"); - } - user = getArguments().getParcelable(ARG_USER); - - if (savedInstanceState != null) { - keyWords = savedInstanceState.getStringArrayList(MNEMONIC); - } - - arbitraryDataProvider = new ArbitraryDataProviderImpl(getContext()); - - // Inflate the layout for the dialog - LayoutInflater inflater = requireActivity().getLayoutInflater(); - binding = SetupEncryptionDialogBinding.inflate(inflater, null, false); - - // Setup layout - viewThemeUtils.material.colorTextInputLayout(binding.encryptionPasswordInputContainer); - - return createDialog(binding.getRoot()); - } - - @NonNull - private Dialog createDialog(View v) { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(v.getContext()); - builder.setView(v).setPositiveButton(R.string.common_ok, null) - .setNeutralButton(R.string.common_cancel, (dialog, which) -> { - dialog.cancel(); - }) - .setTitle(R.string.end_to_end_encryption_title); - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(v.getContext(), builder); - - Dialog dialog = builder.create(); - dialog.setCanceledOnTouchOutside(false); - - dialog.setOnShowListener(dialog1 -> { - - Button button = ((AlertDialog) dialog1).getButton(AlertDialog.BUTTON_POSITIVE); - button.setOnClickListener(view -> { - switch (keyResult) { - case KEY_CREATED: - Log_OC.d(TAG, "New keys generated and stored."); - - dialog1.dismiss(); - - notifyResult(); - break; - - case KEY_EXISTING_USED: - Log_OC.d(TAG, "Decrypt private key"); - - binding.encryptionStatus.setText(R.string.end_to_end_encryption_decrypting); - - try { - String privateKey = task.get(); - String mnemonicUnchanged = binding.encryptionPasswordInput.getText().toString(); - String mnemonic = binding.encryptionPasswordInput.getText().toString().replaceAll("\\s", "") - .toLowerCase(Locale.ROOT); - String decryptedPrivateKey = EncryptionUtils.decryptPrivateKey(privateKey, - mnemonic); - - arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), - EncryptionUtils.PRIVATE_KEY, decryptedPrivateKey); - - dialog1.dismiss(); - Log_OC.d(TAG, "Private key successfully decrypted and stored"); - - arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), - EncryptionUtils.MNEMONIC, - mnemonicUnchanged); - - // check if private key and public key match - String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), - EncryptionUtils.PUBLIC_KEY); - - byte[] key1 = generateKey(); - String base64encodedKey = encodeBytesToBase64String(key1); - - String encryptedString = EncryptionUtils.encryptStringAsymmetric(base64encodedKey, - publicKey); - String decryptedString = decryptStringAsymmetric(encryptedString, - decryptedPrivateKey); - - byte[] key2 = decodeStringToBase64Bytes(decryptedString); - - if (!Arrays.equals(key1, key2)) { - throw new Exception("Keys do not match"); - } - - notifyResult(); - - } catch (Exception e) { - binding.encryptionStatus.setText(R.string.end_to_end_encryption_wrong_password); - Log_OC.d(TAG, "Error while decrypting private key: " + e.getMessage()); - } - break; - - case KEY_GENERATE: - binding.encryptionPassphrase.setVisibility(View.GONE); - positiveButton.setVisibility(View.GONE); - neutralButton.setVisibility(View.GONE); - getDialog().setTitle(R.string.end_to_end_encryption_storing_keys); - - GenerateNewKeysAsyncTask newKeysTask = new GenerateNewKeysAsyncTask(requireContext()); - newKeysTask.execute(); - break; - - default: - dialog1.dismiss(); - break; - } - }); - }); - return dialog; - } - - private void notifyResult() { - final Fragment targetFragment = getTargetFragment(); - if (targetFragment != null) { - targetFragment.onActivityResult(getTargetRequestCode(), - SETUP_ENCRYPTION_RESULT_CODE, getResultIntent()); - } - getParentFragmentManager().setFragmentResult(RESULT_REQUEST_KEY, getResultBundle()); - } - - @NonNull - private Intent getResultIntent() { - Intent intentCreated = new Intent(); - intentCreated.putExtra(SUCCESS, true); - intentCreated.putExtra(ARG_POSITION, getArguments().getInt(ARG_POSITION)); - return intentCreated; - } - - @NonNull - private Bundle getResultBundle() { - final Bundle bundle = new Bundle(); - bundle.putBoolean(SUCCESS, true); - bundle.putInt(ARG_POSITION, getArguments().getInt(ARG_POSITION)); - return bundle; - } - - - @Override - public void onCancel(@NonNull DialogInterface dialog) { - super.onCancel(dialog); - final Bundle bundle = new Bundle(); - bundle.putBoolean(RESULT_KEY_CANCELLED, true); - getParentFragmentManager().setFragmentResult(RESULT_REQUEST_KEY, bundle); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putStringArrayList(MNEMONIC, keyWords); - super.onSaveInstanceState(outState); - } - - public class DownloadKeysAsyncTask extends AsyncTask { - private final WeakReference mWeakContext; - - public DownloadKeysAsyncTask(Context context) { - mWeakContext = new WeakReference<>(context); - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - - binding.encryptionStatus.setText(R.string.end_to_end_encryption_retrieving_keys); - positiveButton.setVisibility(View.INVISIBLE); - } - - @Override - protected String doInBackground(Void... voids) { - // fetch private/public key - // if available - // - store public key - // - decrypt private key, store unencrypted private key in database - - Context context = mWeakContext.get(); - GetPublicKeyOperation publicKeyOperation = new GetPublicKeyOperation(); - if (user != null) { - RemoteOperationResult publicKeyResult = publicKeyOperation.execute(user, context); - - if (publicKeyResult.isSuccess()) { - Log_OC.d(TAG, "public key successful downloaded for " + user.getAccountName()); - - String publicKeyFromServer = publicKeyResult.getResultData(); - if (arbitraryDataProvider != null) { - arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), - EncryptionUtils.PUBLIC_KEY, - publicKeyFromServer); - } else { - return null; - } - } else { - return null; - } - - RemoteOperationResult privateKeyResult = - new GetPrivateKeyOperation().execute(user, context); - - if (privateKeyResult.isSuccess()) { - Log_OC.d(TAG, "private key successful downloaded for " + user.getAccountName()); - - keyResult = KEY_EXISTING_USED; - return privateKeyResult.getResultData().getKey(); - } - } - return null; - } - - @Override - protected void onPostExecute(String privateKey) { - super.onPostExecute(privateKey); - - Context context = mWeakContext.get(); - if (context == null) { - Log_OC.e(TAG, "Context lost after fetching private keys."); - return; - } - - if (privateKey == null) { - // first show info - try { - if (keyWords == null || keyWords.isEmpty()) { - keyWords = EncryptionUtils.getRandomWords(12, context); - } - showMnemonicInfo(); - } catch (IOException e) { - binding.encryptionStatus.setText(R.string.common_error); - } - } else if (!privateKey.isEmpty()) { - binding.encryptionStatus.setText(R.string.end_to_end_encryption_enter_password); - binding.encryptionPasswordInputContainer.setVisibility(View.VISIBLE); - positiveButton.setVisibility(View.VISIBLE); - } else { - Log_OC.e(TAG, "Got empty private key string"); - } - } - } - - public class GenerateNewKeysAsyncTask extends AsyncTask { - - private final WeakReference mWeakContext; - - public GenerateNewKeysAsyncTask(Context context) { - mWeakContext = new WeakReference<>(context); - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - - binding.encryptionStatus.setText(R.string.end_to_end_encryption_generating_keys); - } - - @Override - protected String doInBackground(Void... voids) { - // - create CSR, push to server, store returned public key in database - // - encrypt private key, push key to server, store unencrypted private key in database - - try { - Context context = mWeakContext.get(); - - String publicKeyString; - - // Create public/private key pair - KeyPair keyPair = EncryptionUtils.generateKeyPair(); - - // create CSR - AccountManager accountManager = AccountManager.get(context); - String userId = accountManager.getUserData(user.toPlatformAccount(), AccountUtils.Constants.KEY_USER_ID); - String urlEncoded = CsrHelper.generateCsrPemEncodedString(keyPair, userId); - - SendCSROperation operation = new SendCSROperation(urlEncoded); - RemoteOperationResult result = operation.execute(user, context); - - if (result.isSuccess()) { - publicKeyString = (String) result.getData().get(0); - - if (!EncryptionUtils.isMatchingKeys(keyPair, publicKeyString)) { - throw new RuntimeException("Wrong CSR returned"); - } - - Log_OC.d(TAG, "public key success"); - } else { - keyResult = KEY_FAILED; - return ""; - } - - PrivateKey privateKey = keyPair.getPrivate(); - String privateKeyString = EncryptionUtils.encodeBytesToBase64String(privateKey.getEncoded()); - String privatePemKeyString = EncryptionUtils.privateKeyToPEM(privateKey); - String encryptedPrivateKey = EncryptionUtils.encryptPrivateKey(privatePemKeyString, - generateMnemonicString(false)); - - // upload encryptedPrivateKey - StorePrivateKeyOperation storePrivateKeyOperation = new StorePrivateKeyOperation(encryptedPrivateKey); - RemoteOperationResult storePrivateKeyResult = storePrivateKeyOperation.execute(user, context); - - if (storePrivateKeyResult.isSuccess()) { - Log_OC.d(TAG, "private key success"); - - arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), - EncryptionUtils.PRIVATE_KEY, - privateKeyString); - arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), - EncryptionUtils.PUBLIC_KEY, - publicKeyString); - arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), - EncryptionUtils.MNEMONIC, - generateMnemonicString(true)); - - keyResult = KEY_CREATED; - return (String) storePrivateKeyResult.getData().get(0); - } else { - DeletePublicKeyOperation deletePublicKeyOperation = new DeletePublicKeyOperation(); - deletePublicKeyOperation.execute(user, context); - } - } catch (Exception e) { - Log_OC.e(TAG, e.getMessage()); - } - - keyResult = KEY_FAILED; - return ""; - } - - @Override - protected void onPostExecute(String s) { - super.onPostExecute(s); - - Context context = mWeakContext.get(); - if (context == null) { - Log_OC.e(TAG, "Context lost after generating new private keys."); - return; - } - - if (s.isEmpty()) { - errorSavingKeys(); - } else { - if (getDialog() == null) { - Log_OC.e(TAG, "Dialog is null cannot proceed further."); - return; - } - - requireDialog().dismiss(); - notifyResult(); - } - } - } - - private String generateMnemonicString(boolean withWhitespace) { - StringBuilder stringBuilder = new StringBuilder(); - - for (String string : keyWords) { - stringBuilder.append(string); - if (withWhitespace) { - stringBuilder.append(' '); - } - } - - return stringBuilder.toString(); - } - - @VisibleForTesting - public void showMnemonicInfo() { - if (getDialog() == null) { - Log_OC.e(TAG, "Dialog is null cannot proceed further."); - return; - } - requireDialog().setTitle(R.string.end_to_end_encryption_passphrase_title); - - binding.encryptionStatus.setText(R.string.end_to_end_encryption_keywords_description); - viewThemeUtils.material.colorTextInputLayout(binding.encryptionPasswordInputContainer); - - binding.encryptionPassphrase.setText(generateMnemonicString(true)); - - binding.encryptionPassphrase.setVisibility(View.VISIBLE); - positiveButton.setText(R.string.end_to_end_encryption_confirm_button); - positiveButton.setVisibility(View.VISIBLE); - - neutralButton.setVisibility(View.VISIBLE); - viewThemeUtils.platform.colorTextButtons(positiveButton, neutralButton); - - keyResult = KEY_GENERATE; - } - - @VisibleForTesting - public void errorSavingKeys() { - if (getDialog() == null) { - Log_OC.e(TAG, "Dialog is null cannot proceed further."); - return; - } - - keyResult = KEY_FAILED; - - requireDialog().setTitle(R.string.common_error); - binding.encryptionStatus.setText(R.string.end_to_end_encryption_unsuccessful); - binding.encryptionPassphrase.setVisibility(View.GONE); - positiveButton.setText(R.string.end_to_end_encryption_dialog_close); - positiveButton.setVisibility(View.VISIBLE); - viewThemeUtils.platform.colorTextButtons(positiveButton); - } - - @VisibleForTesting - public void setMnemonic(ArrayList keyWords) { - this.keyWords = keyWords; - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt new file mode 100644 index 000000000000..c78f4ed7716e --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt @@ -0,0 +1,563 @@ +/* + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * @author TSI-mc + * Copyright (C) 2017 Tobias Kaminsky + * Copyright (C) 2017 Nextcloud GmbH. + * Copyright (C) 2023 TSI-mc + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.ui.dialog + +import android.accounts.AccountManager +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.os.AsyncTask +import android.os.Build +import android.os.Bundle +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.client.account.User +import com.nextcloud.client.di.Injectable +import com.owncloud.android.R +import com.owncloud.android.databinding.SetupEncryptionDialogBinding +import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.lib.common.accounts.AccountUtils +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.users.DeletePublicKeyOperation +import com.owncloud.android.lib.resources.users.GetPrivateKeyOperation +import com.owncloud.android.lib.resources.users.GetPublicKeyOperation +import com.owncloud.android.lib.resources.users.SendCSROperation +import com.owncloud.android.lib.resources.users.StorePrivateKeyOperation +import com.owncloud.android.utils.CsrHelper +import com.owncloud.android.utils.EncryptionUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import java.io.IOException +import java.lang.ref.WeakReference +import java.util.Arrays +import javax.inject.Inject + +/* + * Dialog to setup encryption + */ +class SetupEncryptionDialogFragment : DialogFragment(), Injectable { + + @JvmField + @Inject + var viewThemeUtils: ViewThemeUtils? = null + + private var user: User? = null + private var arbitraryDataProvider: ArbitraryDataProvider? = null + private var positiveButton: MaterialButton? = null + private var negativeButton: MaterialButton? = null + private var task: DownloadKeysAsyncTask? = null + private var keyResult: String? = null + private var keyWords: ArrayList? = null + + private lateinit var binding: SetupEncryptionDialogBinding + + override fun onStart() { + super.onStart() + + setupAlertDialog() + executeTask() + } + + private fun setupAlertDialog() { + val alertDialog = dialog as AlertDialog? + + if (alertDialog != null) { + positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton? + negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton? + + if (positiveButton != null) { + viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton!!) + } + + if (negativeButton != null) { + viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(negativeButton!!) + } + } + } + + private fun executeTask() { + task = DownloadKeysAsyncTask(requireContext()) + task?.execute() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + checkNotNull(arguments) { "Arguments may not be null" } + + user = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requireArguments().getParcelable(ARG_USER, User::class.java) + } else { + @Suppress("DEPRECATION") + requireArguments().getParcelable(ARG_USER) + } + + if (savedInstanceState != null) { + keyWords = savedInstanceState.getStringArrayList(EncryptionUtils.MNEMONIC) + } + + arbitraryDataProvider = ArbitraryDataProviderImpl(context) + + // Inflate the layout for the dialog + val inflater = requireActivity().layoutInflater + binding = SetupEncryptionDialogBinding.inflate(inflater, null, false) + + // Setup layout + viewThemeUtils?.material?.colorTextInputLayout(binding.encryptionPasswordInputContainer) + + return createDialog(binding.root) + } + + private fun createDialog(v: View): Dialog { + val builder = MaterialAlertDialogBuilder(v.context) + + builder + .setView(v) + .setPositiveButton(R.string.common_ok, null) + .setNegativeButton(R.string.common_cancel) { dialog: DialogInterface, _: Int -> dialog.cancel() } + .setTitle(R.string.end_to_end_encryption_title) + + viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(v.context, builder) + + val dialog: Dialog = builder.create() + dialog.setCanceledOnTouchOutside(false) + dialog.setOnShowListener { dialog1: DialogInterface -> + val button = (dialog1 as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE) + button.setOnClickListener { positiveButtonOnClick(dialog) } + } + + return dialog + } + + private fun positiveButtonOnClick(dialog: DialogInterface) { + when (keyResult) { + KEY_CREATED -> { + Log_OC.d(TAG, "New keys generated and stored.") + dialog.dismiss() + notifyResult() + } + KEY_EXISTING_USED -> { + decryptPrivateKey(dialog) + } + + KEY_GENERATE -> { + generateKey() + } + else -> dialog.dismiss() + } + } + + @Suppress("TooGenericExceptionCaught", "TooGenericExceptionThrown") + private fun decryptPrivateKey(dialog: DialogInterface) { + Log_OC.d(TAG, "Decrypt private key") + binding.encryptionStatus.setText(R.string.end_to_end_encryption_decrypting) + + try { + val privateKey = task?.get() + val mnemonicUnchanged = binding.encryptionPasswordInput.text.toString() + val mnemonic = + binding.encryptionPasswordInput.text.toString().replace("\\s".toRegex(), "") + .lowercase() + val decryptedPrivateKey = EncryptionUtils.decryptPrivateKey( + privateKey, + mnemonic + ) + + val accountName = user?.accountName ?: return + + arbitraryDataProvider?.storeOrUpdateKeyValue( + accountName, + EncryptionUtils.PRIVATE_KEY, + decryptedPrivateKey + ) + dialog.dismiss() + + Log_OC.d(TAG, "Private key successfully decrypted and stored") + + arbitraryDataProvider?.storeOrUpdateKeyValue( + accountName, + EncryptionUtils.MNEMONIC, + mnemonicUnchanged + ) + + // check if private key and public key match + val publicKey = arbitraryDataProvider?.getValue( + accountName, + EncryptionUtils.PUBLIC_KEY + ) + + val firstKey = EncryptionUtils.generateKey() + val base64encodedKey = EncryptionUtils.encodeBytesToBase64String(firstKey) + val encryptedString = EncryptionUtils.encryptStringAsymmetric( + base64encodedKey, + publicKey + ) + val decryptedString = EncryptionUtils.decryptStringAsymmetric( + encryptedString, + decryptedPrivateKey + ) + val secondKey = EncryptionUtils.decodeStringToBase64Bytes(decryptedString) + + if (!Arrays.equals(firstKey, secondKey)) { + throw Exception("Keys do not match") + } + + notifyResult() + } catch (e: Exception) { + binding.encryptionStatus.setText(R.string.end_to_end_encryption_wrong_password) + Log_OC.d(TAG, "Error while decrypting private key: " + e.message) + } + } + + private fun generateKey() { + binding.encryptionPassphrase.visibility = View.GONE + positiveButton?.visibility = View.GONE + negativeButton?.visibility = View.GONE + + dialog?.setTitle(R.string.end_to_end_encryption_storing_keys) + + val newKeysTask = GenerateNewKeysAsyncTask(requireContext()) + newKeysTask.execute() + } + + private fun notifyResult() { + val targetFragment = targetFragment + targetFragment?.onActivityResult( + targetRequestCode, + SETUP_ENCRYPTION_RESULT_CODE, + resultIntent + ) + parentFragmentManager.setFragmentResult(RESULT_REQUEST_KEY, resultBundle) + } + + private val resultIntent: Intent + get() { + val intentCreated = Intent() + intentCreated.putExtra(SUCCESS, true) + intentCreated.putExtra(ARG_POSITION, requireArguments().getInt(ARG_POSITION)) + return intentCreated + } + private val resultBundle: Bundle + get() { + val bundle = Bundle() + bundle.putBoolean(SUCCESS, true) + bundle.putInt(ARG_POSITION, requireArguments().getInt(ARG_POSITION)) + return bundle + } + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + val bundle = Bundle() + bundle.putBoolean(RESULT_KEY_CANCELLED, true) + parentFragmentManager.setFragmentResult(RESULT_REQUEST_KEY, bundle) + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putStringArrayList(EncryptionUtils.MNEMONIC, keyWords) + super.onSaveInstanceState(outState) + } + + @SuppressLint("StaticFieldLeak") + inner class DownloadKeysAsyncTask(context: Context) : AsyncTask() { + private val mWeakContext: WeakReference + + init { + mWeakContext = WeakReference(context) + } + + @Suppress("ReturnCount") + @Deprecated("Deprecated in Java") + override fun doInBackground(vararg params: Void?): String? { + // fetch private/public key + // if available + // - store public key + // - decrypt private key, store unencrypted private key in database + val context = mWeakContext.get() + val publicKeyOperation = GetPublicKeyOperation() + val user = user ?: return null + + val publicKeyResult = publicKeyOperation.execute(user, context) + + if (publicKeyResult.isSuccess) { + Log_OC.d(TAG, "public key successful downloaded for " + user.accountName) + + val publicKeyFromServer = publicKeyResult.resultData + if (arbitraryDataProvider != null) { + arbitraryDataProvider?.storeOrUpdateKeyValue( + user.accountName, + EncryptionUtils.PUBLIC_KEY, + publicKeyFromServer + ) + } else { + return null + } + } else { + return null + } + + val privateKeyResult = GetPrivateKeyOperation().execute(user, context) + if (privateKeyResult.isSuccess) { + Log_OC.d(TAG, "private key successful downloaded for " + user!!.accountName) + keyResult = KEY_EXISTING_USED + return privateKeyResult.resultData.getKey() + } + + return null + } + + @Deprecated("Deprecated in Java") + override fun onPreExecute() { + super.onPreExecute() + + binding.encryptionStatus.setText(R.string.end_to_end_encryption_retrieving_keys) + positiveButton?.visibility = View.INVISIBLE + } + + @Deprecated("Deprecated in Java") + override fun onPostExecute(privateKey: String?) { + super.onPostExecute(privateKey) + + val context = mWeakContext.get() + if (context == null) { + Log_OC.e(TAG, "Context lost after fetching private keys.") + return + } + if (privateKey == null) { + // first show info + try { + if (keyWords == null || keyWords!!.isEmpty()) { + keyWords = EncryptionUtils.getRandomWords(NUMBER_OF_WORDS, context) + } + showMnemonicInfo() + } catch (e: IOException) { + binding.encryptionStatus.setText(R.string.common_error) + } + } else if (privateKey.isNotEmpty()) { + binding.encryptionStatus.setText(R.string.end_to_end_encryption_enter_password) + binding.encryptionPasswordInputContainer.visibility = View.VISIBLE + positiveButton?.visibility = View.VISIBLE + } else { + Log_OC.e(TAG, "Got empty private key string") + } + } + } + + @SuppressLint("StaticFieldLeak") + inner class GenerateNewKeysAsyncTask(context: Context) : AsyncTask() { + private val mWeakContext: WeakReference + + init { + mWeakContext = WeakReference(context) + } + + @Deprecated("Deprecated in Java") + override fun onPreExecute() { + super.onPreExecute() + binding.encryptionStatus.setText(R.string.end_to_end_encryption_generating_keys) + } + + @Suppress("TooGenericExceptionCaught", "TooGenericExceptionThrown", "ReturnCount") + @Deprecated("Deprecated in Java") + override fun doInBackground(vararg voids: Void?): String { + // - create CSR, push to server, store returned public key in database + // - encrypt private key, push key to server, store unencrypted private key in database + try { + val context = mWeakContext.get() + val publicKeyString: String + + // Create public/private key pair + val keyPair = EncryptionUtils.generateKeyPair() + + // create CSR + val accountManager = AccountManager.get(context) + val user = user ?: return "" + + val userId = accountManager.getUserData(user.toPlatformAccount(), AccountUtils.Constants.KEY_USER_ID) + val urlEncoded = CsrHelper.generateCsrPemEncodedString(keyPair, userId) + val operation = SendCSROperation(urlEncoded) + val result = operation.execute(user, context) + + if (result.isSuccess) { + publicKeyString = result.data[0] as String + if (!EncryptionUtils.isMatchingKeys(keyPair, publicKeyString)) { + throw RuntimeException("Wrong CSR returned") + } + Log_OC.d(TAG, "public key success") + } else { + keyResult = KEY_FAILED + return "" + } + + val privateKey = keyPair.private + val privateKeyString = EncryptionUtils.encodeBytesToBase64String(privateKey.encoded) + val privatePemKeyString = EncryptionUtils.privateKeyToPEM(privateKey) + val encryptedPrivateKey = EncryptionUtils.encryptPrivateKey( + privatePemKeyString, + generateMnemonicString(false) + ) + + // upload encryptedPrivateKey + val storePrivateKeyOperation = StorePrivateKeyOperation(encryptedPrivateKey) + val storePrivateKeyResult = storePrivateKeyOperation.execute(user, context) + if (storePrivateKeyResult.isSuccess) { + Log_OC.d(TAG, "private key success") + arbitraryDataProvider?.storeOrUpdateKeyValue( + user.accountName, + EncryptionUtils.PRIVATE_KEY, + privateKeyString + ) + arbitraryDataProvider?.storeOrUpdateKeyValue( + user.accountName, + EncryptionUtils.PUBLIC_KEY, + publicKeyString + ) + arbitraryDataProvider?.storeOrUpdateKeyValue( + user.accountName, + EncryptionUtils.MNEMONIC, + generateMnemonicString(true) + ) + keyResult = KEY_CREATED + + return storePrivateKeyResult.data[0] as String + } else { + val deletePublicKeyOperation = DeletePublicKeyOperation() + deletePublicKeyOperation.execute(user, context) + } + } catch (e: Exception) { + Log_OC.e(TAG, e.message) + } + keyResult = KEY_FAILED + return "" + } + + @Deprecated("Deprecated in Java") + override fun onPostExecute(s: String) { + super.onPostExecute(s) + val context = mWeakContext.get() + if (context == null) { + Log_OC.e(TAG, "Context lost after generating new private keys.") + return + } + if (s.isEmpty()) { + errorSavingKeys() + } else { + if (dialog == null) { + Log_OC.e(TAG, "Dialog is null cannot proceed further.") + return + } + requireDialog().dismiss() + notifyResult() + } + } + } + + private fun generateMnemonicString(withWhitespace: Boolean): String { + val stringBuilder = StringBuilder() + for (string in keyWords!!) { + stringBuilder.append(string) + if (withWhitespace) { + stringBuilder.append(' ') + } + } + return stringBuilder.toString() + } + + @VisibleForTesting + fun showMnemonicInfo() { + if (dialog == null) { + Log_OC.e(TAG, "Dialog is null cannot proceed further.") + return + } + requireDialog().setTitle(R.string.end_to_end_encryption_passphrase_title) + binding.encryptionStatus.setText(R.string.end_to_end_encryption_keywords_description) + viewThemeUtils!!.material.colorTextInputLayout(binding.encryptionPasswordInputContainer) + binding.encryptionPassphrase.text = generateMnemonicString(true) + binding.encryptionPassphrase.visibility = View.VISIBLE + positiveButton!!.setText(R.string.end_to_end_encryption_confirm_button) + positiveButton!!.visibility = View.VISIBLE + negativeButton!!.visibility = View.VISIBLE + viewThemeUtils!!.platform.colorTextButtons(positiveButton!!, negativeButton!!) + keyResult = KEY_GENERATE + } + + @VisibleForTesting + fun errorSavingKeys() { + if (dialog == null) { + Log_OC.e(TAG, "Dialog is null cannot proceed further.") + return + } + + keyResult = KEY_FAILED + requireDialog().setTitle(R.string.common_error) + binding.encryptionStatus.setText(R.string.end_to_end_encryption_unsuccessful) + binding.encryptionPassphrase.visibility = View.GONE + + positiveButton?.setText(R.string.end_to_end_encryption_dialog_close) + positiveButton?.visibility = View.VISIBLE + + if (positiveButton != null) { + viewThemeUtils?.platform?.colorTextButtons(positiveButton!!) + } + } + + @VisibleForTesting + fun setMnemonic(keyWords: ArrayList?) { + this.keyWords = keyWords + } + + companion object { + const val SUCCESS = "SUCCESS" + const val SETUP_ENCRYPTION_RESULT_CODE = 101 + const val SETUP_ENCRYPTION_REQUEST_CODE = 100 + const val SETUP_ENCRYPTION_DIALOG_TAG = "SETUP_ENCRYPTION_DIALOG_TAG" + const val ARG_POSITION = "ARG_POSITION" + const val RESULT_REQUEST_KEY = "RESULT_REQUEST" + const val RESULT_KEY_CANCELLED = "IS_CANCELLED" + private const val NUMBER_OF_WORDS = 12 + private const val ARG_USER = "ARG_USER" + private val TAG = SetupEncryptionDialogFragment::class.java.simpleName + private const val KEY_CREATED = "KEY_CREATED" + private const val KEY_EXISTING_USED = "KEY_EXISTING_USED" + private const val KEY_FAILED = "KEY_FAILED" + private const val KEY_GENERATE = "KEY_GENERATE" + + /** + * Public factory method to create new SetupEncryptionDialogFragment instance + * + * @return Dialog ready to show. + */ + @JvmStatic + fun newInstance(user: User?, position: Int): SetupEncryptionDialogFragment { + val fragment = SetupEncryptionDialogFragment() + val args = Bundle() + args.putParcelable(ARG_USER, user) + args.putInt(ARG_POSITION, position) + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/res/layout/setup_encryption_dialog.xml b/app/src/main/res/layout/setup_encryption_dialog.xml index 988fbc7c16a7..733b7a8b14d4 100644 --- a/app/src/main/res/layout/setup_encryption_dialog.xml +++ b/app/src/main/res/layout/setup_encryption_dialog.xml @@ -26,14 +26,14 @@ android:orientation="vertical" android:padding="@dimen/dialog_padding"> - - Lint Report: 78 warnings + Lint Report: 77 warnings