Merge branch 'master' into bugfix/after-selecting-file-from-search-it-does-not-open-9652165
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"name": "NextcloudAndroid",
|
"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() {
|
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) {
|
||||||
|
|
|
@ -109,9 +109,6 @@ public class DialogFragmentIT extends AbstractIT {
|
||||||
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() {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|