Merge branch 'master' into bugfix/after-selecting-file-from-search-it-does-not-open-9652165

This commit is contained in:
Andy Scherzinger 2023-11-02 10:52:37 +01:00 committed by GitHub
commit dbb49b5f8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 578 additions and 564 deletions

View file

@ -1,4 +1,4 @@
{ {
"name": "NextcloudAndroid", "name": "NextcloudAndroid",
"dockerFile": "Dockerfile", "dockerFile": "Dockerfile"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -415,10 +415,11 @@ public abstract class AbstractIT {
} }
protected void resetLocale() { protected void resetLocale() {
Locale locale = new Locale("en");
Resources resources = InstrumentationRegistry.getInstrumentation().getTargetContext().getResources(); Resources resources = InstrumentationRegistry.getInstrumentation().getTargetContext().getResources();
Configuration defaultConfig = resources.getConfiguration(); Configuration config = resources.getConfiguration();
defaultConfig.setLocale(Locale.getDefault()); config.setLocale(locale);
resources.updateConfiguration(defaultConfig, null); resources.updateConfiguration(config, null);
} }
protected void screenshot(View view) { protected void screenshot(View view) {

View file

@ -108,10 +108,7 @@ public class DialogFragmentIT extends AbstractIT {
Intent intent = new Intent(targetContext, FileDisplayActivity.class); Intent intent = new Intent(targetContext, FileDisplayActivity.class);
return activityRule.launchActivity(intent); return activityRule.launchActivity(intent);
} }
@Rule
public GrantPermissionRule permissionRule = GrantPermissionRule.grant(
android.Manifest.permission.POST_NOTIFICATIONS);
@After @After
public void quitLooperIfNeeded() { public void quitLooperIfNeeded() {

View file

@ -54,7 +54,7 @@ class LoadingDialog : DialogFragment(), Injectable {
viewThemeUtils?.platform?.tintDrawable(requireContext(), loadingDrawable) viewThemeUtils?.platform?.tintDrawable(requireContext(), loadingDrawable)
} }
viewThemeUtils?.platform?.colorViewBackground(binding.loadingLayout, ColorRole.SURFACE_VARIANT) viewThemeUtils?.platform?.colorViewBackground(binding.loadingLayout, ColorRole.SURFACE)
return binding.root return binding.root
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> 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<Void, Void, String> {
private final WeakReference<Context> 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<String> 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<com.owncloud.android.lib.ocs.responses.PrivateKey> 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<Void, Void, String> {
private final WeakReference<Context> 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<String> keyWords) {
this.keyWords = keyWords;
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String>? = 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<Void?, Void?, String?>() {
private val mWeakContext: WeakReference<Context>
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<Void?, Void?, String>() {
private val mWeakContext: WeakReference<Context>
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<String>?) {
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
}
}
}

View file

@ -26,14 +26,14 @@
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/dialog_padding"> android:padding="@dimen/dialog_padding">
<TextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/encryption_status" android:id="@+id/encryption_status"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/standard_margin" android:layout_marginBottom="@dimen/standard_margin"
tools:text="@string/end_to_end_encryption_keywords_description" /> tools:text="@string/end_to_end_encryption_keywords_description" />
<TextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/encryption_passphrase" android:id="@+id/encryption_passphrase"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -443,7 +443,7 @@ Attention, la suppression est irréversible.</string>
<string name="invalid_url">URL invalide</string> <string name="invalid_url">URL invalide</string>
<string name="invisible">Invisible</string> <string name="invisible">Invisible</string>
<string name="label_empty">Le libellé ne peut pas être vide</string> <string name="label_empty">Le libellé ne peut pas être vide</string>
<string name="last_backup">Dernière sauvegarde: %1$s</string> <string name="last_backup">Dernière sauvegarde : %1$s</string>
<string name="link">Lien</string> <string name="link">Lien</string>
<string name="link_name">Nom du lien</string> <string name="link_name">Nom du lien</string>
<string name="link_share_allow_upload_and_editing">Autoriser le téléversement et la modification</string> <string name="link_share_allow_upload_and_editing">Autoriser le téléversement et la modification</string>

View file

@ -18,6 +18,7 @@
<string name="actionbar_copy">复制</string> <string name="actionbar_copy">复制</string>
<string name="actionbar_mkdir">新建文件夹</string> <string name="actionbar_mkdir">新建文件夹</string>
<string name="actionbar_move">移动</string> <string name="actionbar_move">移动</string>
<string name="actionbar_move_or_copy">移动或复制</string>
<string name="actionbar_open_with">打开方式</string> <string name="actionbar_open_with">打开方式</string>
<string name="actionbar_search">搜索</string> <string name="actionbar_search">搜索</string>
<string name="actionbar_see_details">详细信息</string> <string name="actionbar_see_details">详细信息</string>
@ -324,6 +325,7 @@
<string name="file_delete">删除</string> <string name="file_delete">删除</string>
<string name="file_detail_activity_error">获取文件动态时出错</string> <string name="file_detail_activity_error">获取文件动态时出错</string>
<string name="file_details_no_content">加载详情失败</string> <string name="file_details_no_content">加载详情失败</string>
<string name="file_downloader_notification_title_prefix">下载中 \u0020</string>
<string name="file_icon">文件</string> <string name="file_icon">文件</string>
<string name="file_keep">保留</string> <string name="file_keep">保留</string>
<string name="file_list_empty">上传一些内容或与您的设备同步。</string> <string name="file_list_empty">上传一些内容或与您的设备同步。</string>
@ -947,7 +949,9 @@
<string name="wait_a_moment">请稍等…</string> <string name="wait_a_moment">请稍等…</string>
<string name="wait_checking_credentials">正在检查保存的证书</string> <string name="wait_checking_credentials">正在检查保存的证书</string>
<string name="wait_for_tmp_copy_from_private_storage">正在从私有存储中复制文件</string> <string name="wait_for_tmp_copy_from_private_storage">正在从私有存储中复制文件</string>
<string name="webview_version_check_alert_dialog_message">请更新 Android System WebView 应用程序以进行登录</string>
<string name="webview_version_check_alert_dialog_positive_button_title">更新</string> <string name="webview_version_check_alert_dialog_positive_button_title">更新</string>
<string name="webview_version_check_alert_dialog_title">更新 Android System WebView</string>
<string name="what_s_new_image">有什么新图片</string> <string name="what_s_new_image">有什么新图片</string>
<string name="whats_new_skip">跳过</string> <string name="whats_new_skip">跳过</string>
<string name="whats_new_title">新建%1$s</string> <string name="whats_new_title">新建%1$s</string>

View file

@ -1,2 +1,2 @@
DO NOT TOUCH; GENERATED BY DRONE DO NOT TOUCH; GENERATED BY DRONE
<span class="mdl-layout-title">Lint Report: 78 warnings</span> <span class="mdl-layout-title">Lint Report: 77 warnings</span>