/*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2017 Tobias Kaminsky
* Copyright (C) 2017 Nextcloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package com.owncloud.android.ui.dialog;
import android.accounts.AccountManager;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.nextcloud.client.account.User;
import com.owncloud.android.R;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
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.ThemeButtonUtils;
import com.owncloud.android.utils.theme.ThemeColorUtils;
import java.io.IOException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.fragment.app.DialogFragment;
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 {
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";
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";
private User user;
private TextView textView;
private TextView passphraseTextView;
private ArbitraryDataProvider arbitraryDataProvider;
private Button positiveButton;
private Button neutralButton;
private DownloadKeysAsyncTask task;
private TextView passwordField;
private String keyResult;
private List keyWords;
/**
* 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();
positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
neutralButton = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL);
ThemeButtonUtils.themeBorderlessButton(positiveButton,
neutralButton);
task = new DownloadKeysAsyncTask();
task.execute();
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
int primaryColor = ThemeColorUtils.primaryColor(getContext());
user = getArguments().getParcelable(ARG_USER);
arbitraryDataProvider = new ArbitraryDataProvider(getContext().getContentResolver());
// Inflate the layout for the dialog
LayoutInflater inflater = getActivity().getLayoutInflater();
// Setup layout
View v = inflater.inflate(R.layout.setup_encryption_dialog, null);
textView = v.findViewById(R.id.encryption_status);
passphraseTextView = v.findViewById(R.id.encryption_passphrase);
passwordField = v.findViewById(R.id.encryption_passwordInput);
passwordField.getBackground().setColorFilter(primaryColor, PorterDuff.Mode.SRC_ATOP);
Drawable wrappedDrawable = DrawableCompat.wrap(passwordField.getBackground());
DrawableCompat.setTint(wrappedDrawable, primaryColor);
passwordField.setBackgroundDrawable(wrappedDrawable);
return createDialog(v);
}
@NonNull
private Dialog createDialog(View v) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(v).setPositiveButton(R.string.common_ok, null)
.setNeutralButton(R.string.common_cancel, null)
.setTitle(R.string.end_to_end_encryption_title);
Dialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(final DialogInterface dialog) {
Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
switch (keyResult) {
case KEY_CREATED:
Log_OC.d(TAG, "New keys generated and stored.");
dialog.dismiss();
Intent intentCreated = new Intent();
intentCreated.putExtra(SUCCESS, true);
intentCreated.putExtra(ARG_POSITION, getArguments().getInt(ARG_POSITION));
getTargetFragment().onActivityResult(getTargetRequestCode(),
SETUP_ENCRYPTION_RESULT_CODE, intentCreated);
break;
case KEY_EXISTING_USED:
Log_OC.d(TAG, "Decrypt private key");
textView.setText(R.string.end_to_end_encryption_decrypting);
try {
String privateKey = task.get();
String mnemonicUnchanged = passwordField.getText().toString();
String mnemonic = passwordField.getText().toString().replaceAll("\\s", "")
.toLowerCase(Locale.ROOT);
String decryptedPrivateKey = EncryptionUtils.decryptPrivateKey(privateKey,
mnemonic);
arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(),
EncryptionUtils.PRIVATE_KEY, decryptedPrivateKey);
dialog.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");
}
Intent intentExisting = new Intent();
intentExisting.putExtra(SUCCESS, true);
intentExisting.putExtra(ARG_POSITION, getArguments().getInt(ARG_POSITION));
getTargetFragment().onActivityResult(getTargetRequestCode(),
SETUP_ENCRYPTION_RESULT_CODE, intentExisting);
} catch (Exception e) {
textView.setText(R.string.end_to_end_encryption_wrong_password);
Log_OC.d(TAG, "Error while decrypting private key: " + e.getMessage());
}
break;
case KEY_GENERATE:
passphraseTextView.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();
newKeysTask.execute();
break;
default:
dialog.dismiss();
break;
}
}
});
}
});
return dialog;
}
public class DownloadKeysAsyncTask extends AsyncTask {
@Override
protected void onPreExecute() {
super.onPreExecute();
textView.setText(R.string.end_to_end_encryption_retrieving_keys);
positiveButton.setVisibility(View.INVISIBLE);
neutralButton.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
GetPublicKeyOperation publicKeyOperation = new GetPublicKeyOperation();
RemoteOperationResult publicKeyResult = publicKeyOperation.execute(user.toPlatformAccount(), getContext());
if (publicKeyResult.isSuccess()) {
Log_OC.d(TAG, "public key successful downloaded for " + user.getAccountName());
String publicKeyFromServer = (String) publicKeyResult.getData().get(0);
arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(),
EncryptionUtils.PUBLIC_KEY,
publicKeyFromServer);
} else {
return null;
}
RemoteOperationResult privateKeyResult =
new GetPrivateKeyOperation().execute(user.toPlatformAccount(), getContext());
if (privateKeyResult.isSuccess()) {
Log_OC.d(TAG, "private key successful downloaded for " + user.getAccountName());
keyResult = KEY_EXISTING_USED;
return privateKeyResult.getResultData().getKey();
} else {
return null;
}
}
@Override
protected void onPostExecute(String privateKey) {
super.onPostExecute(privateKey);
if (privateKey == null) {
// first show info
try {
keyWords = EncryptionUtils.getRandomWords(12, requireContext());
showMnemonicInfo();
} catch (IOException e) {
textView.setText(R.string.common_error);
}
} else if (!privateKey.isEmpty()) {
textView.setText(R.string.end_to_end_encryption_enter_password);
passwordField.setVisibility(View.VISIBLE);
positiveButton.setVisibility(View.VISIBLE);
} else {
Log_OC.e(TAG, "Got empty private key string");
}
}
}
public class GenerateNewKeysAsyncTask extends AsyncTask {
@Override
protected void onPreExecute() {
super.onPreExecute();
textView.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 {
String publicKey;
// Create public/private key pair
KeyPair keyPair = EncryptionUtils.generateKeyPair();
// create CSR
AccountManager accountManager = AccountManager.get(getContext());
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.toPlatformAccount(), getContext());
if (result.isSuccess()) {
Log_OC.d(TAG, "public key success");
publicKey = (String) result.getData().get(0);
} 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.toPlatformAccount(),
getContext());
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, publicKey);
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.toPlatformAccount(), getContext());
}
} catch (Exception e) {
Log_OC.e(TAG, e.getMessage());
}
keyResult = KEY_FAILED;
return "";
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
if (s.isEmpty()) {
errorSavingKeys();
} else {
requireDialog().dismiss();
Intent intentExisting = new Intent();
intentExisting.putExtra(SUCCESS, true);
intentExisting.putExtra(ARG_POSITION, requireArguments().getInt(ARG_POSITION));
getTargetFragment().onActivityResult(getTargetRequestCode(),
SETUP_ENCRYPTION_RESULT_CODE, intentExisting);
}
}
}
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() {
requireDialog().setTitle(R.string.end_to_end_encryption_passphrase_title);
textView.setText(R.string.end_to_end_encryption_keywords_description);
passphraseTextView.setText(generateMnemonicString(true));
passphraseTextView.setVisibility(View.VISIBLE);
positiveButton.setText(R.string.end_to_end_encryption_confirm_button);
positiveButton.setVisibility(View.VISIBLE);
neutralButton.setVisibility(View.VISIBLE);
ThemeButtonUtils.themeBorderlessButton(positiveButton, neutralButton);
keyResult = KEY_GENERATE;
}
@VisibleForTesting
public void errorSavingKeys() {
keyResult = KEY_FAILED;
requireDialog().setTitle(R.string.common_error);
textView.setText(R.string.end_to_end_encryption_unsuccessful);
positiveButton.setText(R.string.end_to_end_encryption_dialog_close);
positiveButton.setVisibility(View.VISIBLE);
positiveButton.setTextColor(ThemeColorUtils.primaryAccentColor(getContext()));
}
@VisibleForTesting
public void setMnemonic(List keyWords) {
this.keyWords = keyWords;
}
}