Initial support for client certificate auth

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2018-04-10 16:34:03 +02:00
parent 473becd623
commit ea80b3c9d9
22 changed files with 546 additions and 100 deletions

View file

@ -260,7 +260,7 @@ public class CallActivity extends AppCompatActivity {
if (!userEntity.getCurrent()) {
userUtils.createOrUpdateUser(null,
null, null, null,
null, true, null, userEntity.getId(), null)
null, true, null, userEntity.getId(), null, null)
.subscribe(new Observer<UserEntity>() {
@Override
public void onSubscribe(Disposable d) {

View file

@ -45,6 +45,7 @@ import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.ApplicationWideMessageHolder;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import com.nextcloud.talk.utils.database.user.UserUtils;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import java.net.CookieManager;
@ -72,6 +73,9 @@ public class AccountVerificationController extends BaseController {
@Inject
CookieManager cookieManager;
@Inject
AppPreferences appPreferences;
@BindView(R.id.progress_text)
TextView progressText;
@ -198,7 +202,8 @@ public class AccountVerificationController extends BaseController {
if (!TextUtils.isEmpty(displayName)) {
dbQueryDisposable = userUtils.createOrUpdateUser(username, token,
baseUrl, displayName, null, true,
userProfileOverall.getOcs().getData().getUserId(), null, null)
userProfileOverall.getOcs().getData().getUserId(), null, null,
appPreferences.getTemporaryClientCertAlias())
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(userEntity -> {

View file

@ -51,7 +51,7 @@ public class MagicBottomNavigationController extends BottomNavigationController
BottomNavigationMenuItem.getEnum(itemId).getControllerClass().getConstructors();
Controller controller = null;
try {
/* Determine default or Bundle constructor */
/* Determine default or Bundle constructor */
for (Constructor constructor : constructors) {
if (constructor.getParameterTypes().length == 0) {
controller = (Controller) constructor.newInstance();

View file

@ -20,10 +20,12 @@
package com.nextcloud.talk.controllers;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.net.Uri;
import android.os.Bundle;
import android.security.KeyChain;
import android.support.annotation.NonNull;
import android.text.Editable;
import android.text.TextUtils;
@ -46,6 +48,7 @@ import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.ApplicationWideMessageHolder;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import com.nextcloud.talk.utils.database.user.UserUtils;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import java.security.cert.CertificateException;
@ -53,6 +56,7 @@ import javax.inject.Inject;
import autodagger.AutoInjector;
import butterknife.BindView;
import butterknife.OnClick;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
@ -72,6 +76,8 @@ public class ServerSelectionController extends BaseController {
ProgressBar progressBar;
@BindView(R.id.helper_text_view)
TextView providersTextView;
@BindView(R.id.cert_text_view)
TextView certTextView;
@Inject
NcApi ncApi;
@ -79,6 +85,9 @@ public class ServerSelectionController extends BaseController {
@Inject
UserUtils userUtils;
@Inject
AppPreferences appPreferences;
private Disposable statusQueryDisposable;
@Override
@ -86,6 +95,22 @@ public class ServerSelectionController extends BaseController {
return inflater.inflate(R.layout.controller_server_selection, container, false);
}
@SuppressLint("LongLogTag")
@OnClick(R.id.cert_text_view)
public void onCertClick() {
if (getActivity() != null) {
KeyChain.choosePrivateKeyAlias(getActivity(), alias -> {
if (alias != null) {
appPreferences.setTemporaryClientCertAlias(alias);
} else {
appPreferences.removeTemporaryClientCertAlias();
}
setCertTextView();
}, new String[]{"RSA", "EC"}, null, null, -1, null);
}
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
@ -108,7 +133,7 @@ public class ServerSelectionController extends BaseController {
if (TextUtils.isEmpty(getResources().getString(R.string.nc_providers_url)) && (TextUtils.isEmpty(getResources
().getString(R.string.nc_import_account_type)))) {
providersTextView.setVisibility(View.GONE);
providersTextView.setVisibility(View.INVISIBLE);
} else {
if ((TextUtils.isEmpty(getResources
().getString(R.string.nc_import_account_type)) ||
@ -151,7 +176,7 @@ public class ServerSelectionController extends BaseController {
.popChangeHandler(new HorizontalChangeHandler()));
});
} else {
providersTextView.setVisibility(View.GONE);
providersTextView.setVisibility(View.INVISIBLE);
}
}
@ -204,8 +229,9 @@ public class ServerSelectionController extends BaseController {
serverEntry.setEnabled(false);
progressBar.setVisibility(View.VISIBLE);
if (providersTextView.getVisibility() != View.GONE) {
if (providersTextView.getVisibility() != View.INVISIBLE) {
providersTextView.setVisibility(View.INVISIBLE);
certTextView.setVisibility(View.INVISIBLE);
}
if (url.endsWith("/")) {
@ -279,8 +305,9 @@ public class ServerSelectionController extends BaseController {
}
progressBar.setVisibility(View.INVISIBLE);
if (providersTextView.getVisibility() != View.GONE) {
if (providersTextView.getVisibility() != View.INVISIBLE) {
providersTextView.setVisibility(View.VISIBLE);
certTextView.setVisibility(View.VISIBLE);
}
toggleProceedButton(false);
@ -288,8 +315,9 @@ public class ServerSelectionController extends BaseController {
}
}, () -> {
progressBar.setVisibility(View.INVISIBLE);
if (providersTextView.getVisibility() != View.GONE) {
if (providersTextView.getVisibility() != View.INVISIBLE) {
providersTextView.setVisibility(View.VISIBLE);
certTextView.setVisibility(View.VISIBLE);
}
dispose();
});
@ -315,6 +343,23 @@ public class ServerSelectionController extends BaseController {
}
ApplicationWideMessageHolder.getInstance().setMessageType(null);
}
setCertTextView();
}
private void setCertTextView() {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
if (!TextUtils.isEmpty(appPreferences.getTemporaryClientCertAlias())) {
certTextView.setText(R.string.nc_change_cert_auth);
} else {
certTextView.setText(R.string.nc_configure_cert_auth);
}
textFieldBoxes.setError("", true);
toggleProceedButton(true);
});
}
}
@Override

View file

@ -24,9 +24,11 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Intent;
import android.net.Uri;
import android.security.KeyChain;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -64,9 +66,12 @@ import net.orange_box.storebox.listeners.OnPreferenceValueChangedListener;
import org.greenrobot.eventbus.EventBus;
import java.net.CookieManager;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import javax.inject.Inject;
@ -126,6 +131,9 @@ public class SettingsController extends BaseController {
@BindView(R.id.message_view)
MaterialPreferenceCategory messageView;
@BindView(R.id.settings_client_cert)
MaterialStandardPreference certificateSetup;
@BindView(R.id.message_text)
TextView messageText;
@ -221,6 +229,45 @@ public class SettingsController extends BaseController {
SwitchAccountController()).pushChangeHandler(new VerticalChangeHandler())
.popChangeHandler(new VerticalChangeHandler()));
});
if (userEntity.getClientCertificate() != null) {
certificateSetup.setTitle(R.string.nc_client_cert_change);
}
String host = null;
int port = -1;
URI uri;
try {
uri = new URI(userEntity.getBaseUrl());
host = uri.getHost();
port = uri.getPort();
} catch (URISyntaxException e) {
Log.e(TAG, "Failed to create uri");
}
String finalHost = host;
int finalPort = port;
certificateSetup.addPreferenceClickListener(v -> KeyChain.choosePrivateKeyAlias(Objects.requireNonNull(getActivity()), alias -> {
String finalAlias = alias;
getActivity().runOnUiThread(() -> {
if (finalAlias != null) {
certificateSetup.setTitle(R.string.nc_client_cert_change);
} else {
certificateSetup.setTitle(R.string.nc_client_cert_setup);
}
});
if (alias == null) {
alias = "";
}
userUtils.createOrUpdateUser(null, null, null, null, null, null, null, userEntity.getId(),
null, alias);
}, new String[]{"RSA", "EC"}, null, finalHost, finalPort, userEntity.getClientCertificate
()));
}
@Override
@ -290,7 +337,7 @@ public class SettingsController extends BaseController {
dbQueryDisposable = userUtils.createOrUpdateUser(null,
null,
null, displayName, null, true,
userProfileOverall.getOcs().getData().getUserId(), userEntity.getId(), null)
userProfileOverall.getOcs().getData().getUserId(), userEntity.getId(), null, null)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(userEntityResult -> {

View file

@ -100,7 +100,7 @@ public class SwitchAccountController extends BaseController {
UserEntity userEntity = ((AdvancedUserItem) userItems.get(position)).getEntity();
userUtils.createOrUpdateUser(null,
null, null, null,
null, true, null, userEntity.getId(), null)
null, true, null, userEntity.getId(), null, null)
.subscribe(new Observer<UserEntity>() {
@Override
public void onSubscribe(Disposable d) {

View file

@ -20,6 +20,7 @@
*/
package com.nextcloud.talk.controllers;
import android.annotation.SuppressLint;
import android.content.pm.ActivityInfo;
import android.net.http.SslCertificate;
import android.net.http.SslError;
@ -29,7 +30,6 @@ import android.security.KeyChain;
import android.security.KeyChainException;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -52,13 +52,13 @@ import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.utils.ApplicationWideMessageHolder;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import com.nextcloud.talk.utils.database.user.UserUtils;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import com.nextcloud.talk.utils.ssl.MagicTrustManager;
import org.greenrobot.eventbus.EventBus;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.CookieManager;
import java.net.URLDecoder;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
@ -86,13 +86,16 @@ public class WebViewLoginController extends BaseController {
@Inject
UserUtils userUtils;
@Inject
AppPreferences appPreferences;
@Inject
ReactiveEntityStore<Persistable> dataStore;
@Inject
MagicTrustManager magicTrustManager;
@Inject
EventBus eventBus;
@Inject
java.net.CookieManager cookieManager;
CookieManager cookieManager;
@BindView(R.id.webview)
WebView webView;
@ -140,6 +143,7 @@ public class WebViewLoginController extends BaseController {
return inflater.inflate(R.layout.controller_web_view_login, container, false);
}
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
@ -167,6 +171,7 @@ public class WebViewLoginController extends BaseController {
webView.clearCache(true);
webView.clearFormData();
webView.clearHistory();
WebView.clearClientCertPreferences(null);
CookieSyncManager.createInstance(getActivity());
android.webkit.CookieManager.getInstance().removeAllCookies(null);
@ -204,7 +209,7 @@ public class WebViewLoginController extends BaseController {
if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
if (loginStep == 1) {
webView.loadUrl("javascript: {document.getElementsByClassName('login')[0].click(); };");
} else if (!automatedLoginAttempted){
} else if (!automatedLoginAttempted) {
automatedLoginAttempted = true;
webView.loadUrl("javascript: {" +
"document.getElementById('user').value = '" + username + "';" +
@ -218,32 +223,55 @@ public class WebViewLoginController extends BaseController {
@Override
public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
String host = null;
UserEntity userEntity;
try {
URL url = new URL(webView.getUrl());
host = url.getHost();
} catch (MalformedURLException e) {
Log.d(TAG, "Failed to create url");
String alias = null;
if (!isPasswordUpdate) {
alias = appPreferences.getTemporaryClientCertAlias();
}
KeyChain.choosePrivateKeyAlias(getActivity(), alias -> {
try {
if (alias != null) {
PrivateKey privateKey = KeyChain.getPrivateKey(getActivity(), alias);
X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), alias);
request.proceed(privateKey, certificates);
if (TextUtils.isEmpty(alias) && (userEntity = userUtils.getCurrentUser()) != null) {
alias = userEntity.getClientCertificate();
}
if (!TextUtils.isEmpty(alias)) {
String finalAlias = alias;
new Thread(() -> {
try {
PrivateKey privateKey = KeyChain.getPrivateKey(getActivity(), finalAlias);
X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), finalAlias);
if (privateKey != null && certificates != null) {
request.proceed(privateKey, certificates);
} else {
request.cancel();
}
} catch (KeyChainException | InterruptedException e) {
request.cancel();
}
}).start();
} else {
KeyChain.choosePrivateKeyAlias(getActivity(), chosenAlias -> {
if (chosenAlias != null) {
appPreferences.setTemporaryClientCertAlias(chosenAlias);
new Thread(() -> {
PrivateKey privateKey = null;
try {
privateKey = KeyChain.getPrivateKey(getActivity(), chosenAlias);
X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), chosenAlias);
if (privateKey != null && certificates != null) {
request.proceed(privateKey, certificates);
} else {
request.cancel();
}
} catch (KeyChainException | InterruptedException e) {
request.cancel();
}
}).start();
} else {
request.cancel();
}
} catch (KeyChainException e) {
Log.e(TAG, "Failed to get keys via keychain exception");
request.cancel();
} catch (InterruptedException e) {
Log.e(TAG, "Failed to get keys due to interruption");
request.cancel();
}
}, new String[]{"RSA"}, null, host, -1, null);
}, new String[]{"RSA", "EC"}, null, request.getHost(), request.getPort(), null);
}
}
@Override
@ -339,7 +367,7 @@ public class WebViewLoginController extends BaseController {
if (currentUser != null) {
userQueryDisposable = userUtils.createOrUpdateUser(null, null,
null, null, null, true,
null, currentUser.getId(), null).
null, currentUser.getId(), null, appPreferences.getTemporaryClientCertAlias()).
subscribe(userEntity -> {
if (finalMessageType != null) {
ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType);

View file

@ -25,10 +25,28 @@ import android.util.Log;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.controllers.AccountVerificationController;
import com.nextcloud.talk.controllers.ServerSelectionController;
import com.nextcloud.talk.controllers.WebViewLoginController;
import com.nextcloud.talk.controllers.base.providers.ActionBarProvider;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import autodagger.AutoInjector;
@AutoInjector(NextcloudTalkApplication.class)
public abstract class BaseController extends RefWatchingController {
@Inject
AppPreferences appPreferences;
private List<String> temporaryClassNames = new ArrayList<>();
private static final String TAG = "BaseController";
protected BaseController() {
@ -38,6 +56,12 @@ public abstract class BaseController extends RefWatchingController {
super(args);
}
@Override
protected void onViewBound(@NonNull View view) {
NextcloudTalkApplication.getSharedApplication().getComponentApplication().inject(this);
super.onViewBound(view);
}
// Note: This is just a quick demo of how an ActionBar *can* be accessed, not necessarily how it *should*
// be accessed. In a production app, this would use Dagger instead.
protected ActionBar getActionBar() {
@ -54,6 +78,14 @@ public abstract class BaseController extends RefWatchingController {
protected void onAttach(@NonNull View view) {
setTitle();
getActionBar().setDisplayHomeAsUpEnabled(false);
temporaryClassNames.add(ServerSelectionController.class.getName());
temporaryClassNames.add(AccountVerificationController.class.getName());
temporaryClassNames.add(WebViewLoginController.class.getName());
if (!temporaryClassNames.contains(getClass().getName())) {
appPreferences.removeTemporaryClientCertAlias();
}
super.onAttach(view);
}

View file

@ -121,7 +121,7 @@ public abstract class BottomNavigationController extends BaseController {
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
/* Setup the BottomNavigationView with the constructor supplied Menu resource */
/* Setup the BottomNavigationView with the constructor supplied Menu resource */
bottomNavigationView.inflateMenu(getMenuResource());
bottomNavigationView.setOnNavigationItemSelectedListener(
@ -138,20 +138,20 @@ public abstract class BottomNavigationController extends BaseController {
protected void onAttach(@NonNull View view) {
super.onAttach(view);
/* Fresh start, setup everything */
/* Fresh start, setup everything */
if (routerSavedStateBundles == null) {
Menu menu = bottomNavigationView.getMenu();
int menuSize = menu.size();
routerSavedStateBundles = new SparseArray<>(menuSize);
for (int i = 0; i < menuSize; i++) {
MenuItem menuItem = menu.getItem(i);
/* Ensure the first checked item is shown */
/* Ensure the first checked item is shown */
if (menuItem.isChecked()) {
/*
* Seems like the BottomNavigationView always initializes index 0 as isChecked / Selected,
* regardless of what was set in the menu xml originally.
* So basically all we're doing here is always setting up menuItem index 0.
*/
/*
* Seems like the BottomNavigationView always initializes index 0 as isChecked / Selected,
* regardless of what was set in the menu xml originally.
* So basically all we're doing here is always setting up menuItem index 0.
*/
int itemId = menuItem.getItemId();
configureRouter(getChildRouter(itemId), itemId);
bottomNavigationView.setSelectedItemId(itemId);
@ -160,11 +160,11 @@ public abstract class BottomNavigationController extends BaseController {
}
}
} else {
/*
* Since we are restoring our state,
* and onRestoreInstanceState is called before onViewBound,
* all we need to do is rebind.
*/
/*
* Since we are restoring our state,
* and onRestoreInstanceState is called before onViewBound,
* all we need to do is rebind.
*/
Router childRouter = getChildRouter(currentlySelectedItemId);
childRouter.rebindIfNeeded();
lastActiveChildRouter = childRouter;
@ -235,7 +235,7 @@ public abstract class BottomNavigationController extends BaseController {
if (currentlySelectedItemId != itemId) {
destroyChildRouter(lastActiveChildRouter, currentlySelectedItemId);
/* Ensure correct Checked state based on new selection */
/* Ensure correct Checked state based on new selection */
Menu menu = bottomNavigationView.getMenu();
for (int i = 0; i < menu.size(); i++) {
MenuItem menuItem = menu.getItem(i);
@ -249,21 +249,21 @@ public abstract class BottomNavigationController extends BaseController {
currentlySelectedItemId = itemId;
Router childRouter = getChildRouter(currentlySelectedItemId);
if (configureRouter(childRouter, currentlySelectedItemId)) {
/* Determine if a Controller of same class already exists in the backstack */
/* Determine if a Controller of same class already exists in the backstack */
Controller backstackController;
int size = childRouter.getBackstackSize();
for (int i = 0; i < size; i++) {
backstackController = childRouter.getBackstack().get(i).controller();
if (BottomNavigationUtils.equals(backstackController.getClass(), controller.getClass())) {
/* Match found at root - so just set new root */
/* Match found at root - so just set new root */
if (i == size - 1) {
childRouter.setRoot(RouterTransaction.with(controller));
} else {
/* Match found at i - pop until we're at the matching Controller */
/* Match found at i - pop until we're at the matching Controller */
for (int j = size; j < i; j--) {
childRouter.popCurrentController();
}
/* Replace the existing matching Controller with the new */
/* Replace the existing matching Controller with the new */
childRouter.replaceTopController(RouterTransaction.with(controller));
}
}
@ -301,21 +301,21 @@ public abstract class BottomNavigationController extends BaseController {
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
if (lastActiveChildRouter == null && cachedSavedInstanceState != null) {
/*
* Here we assume that we're in a state
* where the BottomNavigationController itself is in the backstack,
* it has been restored, and is now being saved again.
* In this case, the BottomNavigationController won't ever have had onViewBound() called,
* and thus won't have any views to setup the Child Routers with.
* In this case, we assume that we've previously had onSaveInstanceState() called
* on us successfully, and thus have a cachedSavedInstanceState to use.
*
* To replicate issue this solves:
* Navigate from BottomNavigationController to another controller not hosted in
* the childRouter, background the app
* (with developer setting "don't keep activities in memory" enabled on the device),
* open the app again, and background it once more, and open it again to see it crash.
*/
/*
* Here we assume that we're in a state
* where the BottomNavigationController itself is in the backstack,
* it has been restored, and is now being saved again.
* In this case, the BottomNavigationController won't ever have had onViewBound() called,
* and thus won't have any views to setup the Child Routers with.
* In this case, we assume that we've previously had onSaveInstanceState() called
* on us successfully, and thus have a cachedSavedInstanceState to use.
*
* To replicate issue this solves:
* Navigate from BottomNavigationController to another controller not hosted in
* the childRouter, background the app
* (with developer setting "don't keep activities in memory" enabled on the device),
* open the app again, and background it once more, and open it again to see it crash.
*/
outState.putSparseParcelableArray(
KEY_STATE_ROUTER_BUNDLES,
cachedSavedInstanceState.getSparseParcelableArray(KEY_STATE_ROUTER_BUNDLES));
@ -323,20 +323,20 @@ public abstract class BottomNavigationController extends BaseController {
KEY_STATE_CURRENTLY_SELECTED_ID,
cachedSavedInstanceState.getInt(KEY_STATE_CURRENTLY_SELECTED_ID));
} else if (currentlySelectedItemId != 0) {
/*
* Only save state if we have a valid item selected.
*
* Otherwise we may be in a state where we are in a backstack, but have never been shown.
* I.e. if we are put in a synthesized backstack, we've never been shown any UI,
* and therefore have nothing to save.
*/
/*
* Only save state if we have a valid item selected.
*
* Otherwise we may be in a state where we are in a backstack, but have never been shown.
* I.e. if we are put in a synthesized backstack, we've never been shown any UI,
* and therefore have nothing to save.
*/
save(lastActiveChildRouter, currentlySelectedItemId);
outState.putSparseParcelableArray(KEY_STATE_ROUTER_BUNDLES, routerSavedStateBundles);
/*
* For some reason the BottomNavigationView does not seem to correctly restore its
* selectedId, even though the view appears with the correct state.
* So we keep track of it manually
*/
/*
* For some reason the BottomNavigationView does not seem to correctly restore its
* selectedId, even though the view appears with the correct state.
* So we keep track of it manually
*/
outState.putInt(KEY_STATE_CURRENTLY_SELECTED_ID, currentlySelectedItemId);
lastActiveChildRouter = null;
}
@ -344,10 +344,10 @@ public abstract class BottomNavigationController extends BaseController {
@Override
public boolean handleBack() {
/*
* The childRouter should handleBack,
* as this BottomNavigationController doesn't have a back step sensible to the user.
*/
/*
* The childRouter should handleBack,
* as this BottomNavigationController doesn't have a back step sensible to the user.
*/
return lastActiveChildRouter != null && lastActiveChildRouter.handleBack();
}

View file

@ -352,6 +352,7 @@ public class OperationsMenuController extends BaseController {
}
private void showResultImage(boolean everythingOK, boolean isGuestSupportError) {
progressBar.setVisibility(View.GONE);

View file

@ -49,7 +49,7 @@ public class DatabaseModule {
return new SqlCipherDatabaseSource(context, Models.DEFAULT,
context.getResources().getString(R.string.nc_app_name).toLowerCase()
.replace(" ", "_").trim() + ".sqlite",
context.getString(R.string.nc_talk_database_encryption_key), 3);
context.getString(R.string.nc_talk_database_encryption_key), 4);
}
@Provides

View file

@ -30,7 +30,9 @@ import com.nextcloud.talk.BuildConfig;
import com.nextcloud.talk.api.NcApi;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.database.user.UserUtils;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import com.nextcloud.talk.utils.ssl.MagicKeyManager;
import com.nextcloud.talk.utils.ssl.MagicTrustManager;
import com.nextcloud.talk.utils.ssl.SSLSocketFactoryCompat;
@ -38,9 +40,16 @@ import java.io.IOException;
import java.net.CookieManager;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.concurrent.TimeUnit;
import javax.inject.Singleton;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.X509KeyManager;
import dagger.Module;
import dagger.Provides;
@ -110,8 +119,35 @@ public class RestModule {
@Provides
@Singleton
SSLSocketFactoryCompat provideSslSocketFactoryCompat(MagicTrustManager magicTrustManager) {
return new SSLSocketFactoryCompat(magicTrustManager);
MagicKeyManager provideKeyManager(AppPreferences appPreferences, UserUtils userUtils) {
KeyStore keyStore = null;
try {
keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, null);
X509KeyManager origKm = (X509KeyManager) kmf.getKeyManagers()[0];
return new MagicKeyManager(origKm, userUtils, appPreferences);
} catch (KeyStoreException e) {
Log.e(TAG, "KeyStoreException " + e.getLocalizedMessage());
} catch (CertificateException e) {
Log.e(TAG, "CertificateException " + e.getLocalizedMessage());
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException " + e.getLocalizedMessage());
} catch (IOException e) {
Log.e(TAG, "IOException " + e.getLocalizedMessage());
} catch (UnrecoverableKeyException e) {
Log.e(TAG, "UnrecoverableKeyException " + e.getLocalizedMessage());
}
return null;
}
@Provides
@Singleton
SSLSocketFactoryCompat provideSslSocketFactoryCompat(MagicKeyManager keyManager, MagicTrustManager
magicTrustManager) {
return new SSLSocketFactoryCompat(keyManager, magicTrustManager);
}
@Provides
@ -145,10 +181,10 @@ public class RestModule {
if (BuildConfig.DEBUG) {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
httpClient.addInterceptor(loggingInterceptor);
}
// Trust own CA and all self-signed certs
httpClient.sslSocketFactory(sslSocketFactoryCompat, magicTrustManager);
httpClient.retryOnConnectionFailure(true);
httpClient.hostnameVerifier(magicTrustManager.getHostnameVerifier(OkHostnameVerifier.INSTANCE));

View file

@ -118,7 +118,7 @@ public class CapabilitiesJob extends Job {
userUtils.createOrUpdateUser(null, null,
null, null,
null, null, null, internalUserEntity.getId(),
LoganSquare.serialize(capabilitiesOverall.getOcs().getData().getCapabilities()))
LoganSquare.serialize(capabilitiesOverall.getOcs().getData().getCapabilities()), null)
.subscribeOn(Schedulers.newThread())
.subscribe(new Observer<UserEntity>() {
@Override

View file

@ -55,6 +55,8 @@ public interface User extends Parcelable, Persistable, Serializable {
String getCapabilities();
String getClientCertificate();
boolean getCurrent();
boolean getScheduledForDeletion();

View file

@ -318,7 +318,7 @@ public class PushUtils {
null, null,
userEntity.getDisplayName(),
LoganSquare.serialize(pushConfigurationState), null,
null, userEntity.getId(), null)
null, userEntity.getId(), null, null)
.subscribe(new Observer<UserEntity>() {
@Override
public void onSubscribe(Disposable d) {

View file

@ -161,7 +161,8 @@ public class UserUtils {
@Nullable Boolean currentUser,
@Nullable String userId,
@Nullable Long internalId,
@Nullable String capabilities) {
@Nullable String capabilities,
@Nullable String certificateAlias) {
Result findUserQueryResult;
if (internalId == null) {
findUserQueryResult = dataStore.select(User.class).where(UserEntity.USERNAME.eq(username).
@ -194,6 +195,10 @@ public class UserUtils {
user.setCapabilities(capabilities);
}
if (!TextUtils.isEmpty(certificateAlias)) {
user.setClientCertificate(certificateAlias);
}
user.setCurrent(true);
} else {
@ -218,6 +223,11 @@ public class UserUtils {
user.setCapabilities(capabilities);
}
if (certificateAlias != null && !certificateAlias.equals(user.getClientCertificate())) {
user.setClientCertificate(certificateAlias);
}
if (currentUser != null) {
user.setCurrent(currentUser);
}

View file

@ -115,8 +115,19 @@ public interface AppPreferences {
void setPushToken(String pushToken);
@KeyByString("push_token")
@RemoveMethod
void removePushToken();
@KeyByString("tempClientCertAlias")
String getTemporaryClientCertAlias();
@KeyByString("tempClientCertAlias")
void setTemporaryClientCertAlias(String alias);
@KeyByString("tempClientCertAlias")
@RemoveMethod
void removeTemporaryClientCertAlias();
@KeyByString("pushToTalk_intro_shown")
boolean getPushToTalkIntroShown();
@ -124,9 +135,9 @@ public interface AppPreferences {
void setPushToTalkIntroShown(boolean shown);
@KeyByString("pushToTalk_intro_shown")
@RemoveMethod
void removePushToTalkIntroShown();
@ClearMethod
void clear();
}

View file

@ -0,0 +1,202 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.utils.ssl;
import android.content.Context;
import android.security.KeyChain;
import android.security.KeyChainException;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.utils.database.user.UserUtils;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import java.net.Socket;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.net.ssl.X509KeyManager;
public class MagicKeyManager implements X509KeyManager {
private static final String TAG = "MagicKeyManager";
private final X509KeyManager keyManager;
private UserUtils userUtils;
private AppPreferences appPreferences;
private Context context;
public MagicKeyManager(X509KeyManager keyManager, UserUtils userUtils, AppPreferences appPreferences) {
this.keyManager = keyManager;
this.userUtils = userUtils;
this.appPreferences = appPreferences;
context = NextcloudTalkApplication.getSharedApplication().getApplicationContext();
}
@Override
public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
String alias;
if (!TextUtils.isEmpty(alias = userUtils.getCurrentUser().getClientCertificate()) ||
!TextUtils.isEmpty(alias = appPreferences.getTemporaryClientCertAlias())
&& new ArrayList<>(Arrays.asList(getClientAliases())).contains(alias)) {
return alias;
}
return null;
}
@Override
public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
return null;
}
private X509Certificate[] getCertificatesForAlias(@Nullable String alias) {
if (alias != null) {
GetCertificatesForAliasRunnable getCertificatesForAliasRunnable = new GetCertificatesForAliasRunnable(alias);
Thread getCertificatesThread = new Thread(getCertificatesForAliasRunnable);
getCertificatesThread.start();
try {
getCertificatesThread.join();
return getCertificatesForAliasRunnable.getCertificates();
} catch (InterruptedException e) {
Log.e(TAG, "Failed to join the thread while getting certificates: " + e.getLocalizedMessage());
}
}
return null;
}
private PrivateKey getPrivateKeyForAlias(@Nullable String alias) {
if (alias != null) {
GetPrivateKeyForAliasRunnable getPrivateKeyForAliasRunnable = new GetPrivateKeyForAliasRunnable(alias);
Thread getPrivateKeyThread = new Thread(getPrivateKeyForAliasRunnable);
getPrivateKeyThread.start();
try {
getPrivateKeyThread.join();
return getPrivateKeyForAliasRunnable.getPrivateKey();
} catch (InterruptedException e) {
Log.e(TAG, "Failed to join the thread while getting private key: " + e.getLocalizedMessage());
}
}
return null;
}
@Override
public X509Certificate[] getCertificateChain(String s) {
if (new ArrayList<>(Arrays.asList(getClientAliases())).contains(s)) {
return getCertificatesForAlias(s);
}
return null;
}
private String[] getClientAliases() {
Set<String> aliases = new HashSet<>();
String alias;
if (!TextUtils.isEmpty(alias = appPreferences.getTemporaryClientCertAlias())) {
aliases.add(alias);
}
List<UserEntity> userEntities = userUtils.getUsers();
for (int i = 0; i < userEntities.size(); i++) {
if (!TextUtils.isEmpty(alias = userEntities.get(i).getClientCertificate())) {
aliases.add(alias);
}
}
return aliases.toArray(new String[aliases.size()]);
}
@Override
public String[] getClientAliases(String s, Principal[] principals) {
return getClientAliases();
}
@Override
public String[] getServerAliases(String s, Principal[] principals) {
return null;
}
@Override
public PrivateKey getPrivateKey(String s) {
if (new ArrayList<>(Arrays.asList(getClientAliases())).contains(s)) {
return getPrivateKeyForAlias(s);
}
return null;
}
private class GetCertificatesForAliasRunnable implements Runnable {
private volatile X509Certificate[] certificates;
private String alias;
public GetCertificatesForAliasRunnable(String alias) {
this.alias = alias;
}
@Override
public void run() {
try {
certificates = KeyChain.getCertificateChain(context, alias);
} catch (KeyChainException | InterruptedException e) {
Log.e(TAG, e.getLocalizedMessage());
}
}
public X509Certificate[] getCertificates() {
return certificates;
}
}
private class GetPrivateKeyForAliasRunnable implements Runnable {
private volatile PrivateKey privateKey;
private String alias;
public GetPrivateKeyForAliasRunnable(String alias) {
this.alias = alias;
}
@Override
public void run() {
try {
privateKey = KeyChain.getPrivateKey(context, alias);
} catch (KeyChainException | InterruptedException e) {
Log.e(TAG, e.getLocalizedMessage());
}
}
public PrivateKey getPrivateKey() {
return privateKey;
}
}
}

View file

@ -14,12 +14,10 @@ import java.net.InetAddress
import java.net.Socket
import java.security.GeneralSecurityException
import java.util.*
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
import javax.net.ssl.*
class SSLSocketFactoryCompat(trustManager: X509TrustManager) : SSLSocketFactory() {
class SSLSocketFactoryCompat(keyManager: KeyManager?,
trustManager: X509TrustManager) : SSLSocketFactory() {
private var delegate: SSLSocketFactory
@ -101,7 +99,10 @@ class SSLSocketFactoryCompat(trustManager: X509TrustManager) : SSLSocketFactory(
init {
try {
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(trustManager), null)
sslContext.init(
if (keyManager != null) arrayOf(keyManager) else null,
arrayOf(trustManager),
null)
delegate = sslContext.socketFactory
} catch (e: GeneralSecurityException) {
throw IllegalStateException() // system has no TLS

View file

@ -91,4 +91,19 @@
android:textAllCaps="true"
android:textColor="@color/nc_light_blue_color"/>
<TextView
android:id="@+id/cert_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/helper_text_view"
android:layout_centerHorizontal="true"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginTop="16dp"
android:lines="2"
android:text="@string/nc_configure_cert_auth"
android:textAlignment="center"
android:textAllCaps="true"
android:textColor="@color/nc_light_blue_color"/>
</RelativeLayout>

View file

@ -86,10 +86,17 @@
apc:mp_title="@string/nc_settings_reauthorize"/>
<com.yarolegovich.mp.MaterialStandardPreference
android:id="@+id/settings_remove_account"
android:id="@+id/settings_client_cert"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/settings_reauthorize"
apc:mp_title="@string/nc_client_cert_setup"/>
<com.yarolegovich.mp.MaterialStandardPreference
android:id="@+id/settings_remove_account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/settings_client_cert"
apc:mp_title="@string/nc_settings_remove_account"/>
<com.yarolegovich.mp.MaterialStandardPreference

View file

@ -50,6 +50,8 @@
<string name="nc_settings_use_credentials_key">proxy_credentials</string>
<string name="nc_settings_switch_account">Switch between accounts</string>
<string name="nc_settings_reauthorize">Reauthorize</string>
<string name="nc_client_cert_setup">Set up client certificate</string>
<string name="nc_client_cert_change">Change client certificate</string>
<string name="nc_settings_remove_account">Remove account</string>
<string name="nc_settings_add_account">Add a new account</string>
<string name="nc_settings_wrong_account">Only current account can be reauthorized</string>
@ -135,6 +137,8 @@
<string name="nc_push_to_talk">Push-to-talk</string>
<string name="nc_push_to_talk_desc">With microphone disabled, click&amp;hold to use Push-to-talk</string>
<string name="nc_store_short_desc">Have private video calls and chat using your own server.</string>
<string name="nc_configure_cert_auth">Select authentication certificate</string>
<string name="nc_change_cert_auth">Change authentication certificate</string>
<string name="nc_store_full_desc">Use Nextcloud Talk to have one-on-one or group audio or video calls, create or join web conferences and send chat messages. All communication is fully encrypted and mediated by your own server, providing the highest degree of privacy possible.
Nextcloud Talk is easy to use and will always be completely free!