Merge branch 'master' into bugfix/after-selecting-file-from-search-it-does-not-open-9652165
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"name": "NextcloudAndroid",
|
||||
"dockerFile": "Dockerfile",
|
||||
"dockerFile": "Dockerfile"
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 9 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
|
@ -415,10 +415,11 @@ public abstract class AbstractIT {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -108,10 +108,7 @@ public class DialogFragmentIT extends AbstractIT {
|
|||
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() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,14 +26,14 @@
|
|||
android:orientation="vertical"
|
||||
android:padding="@dimen/dialog_padding">
|
||||
|
||||
<TextView
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/encryption_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/standard_margin"
|
||||
tools:text="@string/end_to_end_encryption_keywords_description" />
|
||||
|
||||
<TextView
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/encryption_passphrase"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -443,7 +443,7 @@ Attention, la suppression est irréversible.</string>
|
|||
<string name="invalid_url">URL invalide</string>
|
||||
<string name="invisible">Invisible</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_name">Nom du lien</string>
|
||||
<string name="link_share_allow_upload_and_editing">Autoriser le téléversement et la modification</string>
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
<string name="actionbar_copy">复制</string>
|
||||
<string name="actionbar_mkdir">新建文件夹</string>
|
||||
<string name="actionbar_move">移动</string>
|
||||
<string name="actionbar_move_or_copy">移动或复制</string>
|
||||
<string name="actionbar_open_with">打开方式</string>
|
||||
<string name="actionbar_search">搜索</string>
|
||||
<string name="actionbar_see_details">详细信息</string>
|
||||
|
@ -324,6 +325,7 @@
|
|||
<string name="file_delete">删除</string>
|
||||
<string name="file_detail_activity_error">获取文件动态时出错</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_keep">保留</string>
|
||||
<string name="file_list_empty">上传一些内容或与您的设备同步。</string>
|
||||
|
@ -947,7 +949,9 @@
|
|||
<string name="wait_a_moment">请稍等…</string>
|
||||
<string name="wait_checking_credentials">正在检查保存的证书</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_title">更新 Android System WebView</string>
|
||||
<string name="what_s_new_image">有什么新图片</string>
|
||||
<string name="whats_new_skip">跳过</string>
|
||||
<string name="whats_new_title">新建%1$s</string>
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
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>
|
||||
|
|