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

View file

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

View file

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

View file

@ -24,9 +24,11 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.security.KeyChain;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -64,9 +66,12 @@ import net.orange_box.storebox.listeners.OnPreferenceValueChangedListener;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import java.net.CookieManager; import java.net.CookieManager;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects;
import javax.inject.Inject; import javax.inject.Inject;
@ -126,6 +131,9 @@ public class SettingsController extends BaseController {
@BindView(R.id.message_view) @BindView(R.id.message_view)
MaterialPreferenceCategory messageView; MaterialPreferenceCategory messageView;
@BindView(R.id.settings_client_cert)
MaterialStandardPreference certificateSetup;
@BindView(R.id.message_text) @BindView(R.id.message_text)
TextView messageText; TextView messageText;
@ -221,6 +229,45 @@ public class SettingsController extends BaseController {
SwitchAccountController()).pushChangeHandler(new VerticalChangeHandler()) SwitchAccountController()).pushChangeHandler(new VerticalChangeHandler())
.popChangeHandler(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 @Override
@ -290,7 +337,7 @@ public class SettingsController extends BaseController {
dbQueryDisposable = userUtils.createOrUpdateUser(null, dbQueryDisposable = userUtils.createOrUpdateUser(null,
null, null,
null, displayName, null, true, null, displayName, null, true,
userProfileOverall.getOcs().getData().getUserId(), userEntity.getId(), null) userProfileOverall.getOcs().getData().getUserId(), userEntity.getId(), null, null)
.subscribeOn(Schedulers.newThread()) .subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(userEntityResult -> { .subscribe(userEntityResult -> {

View file

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

View file

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

View file

@ -25,10 +25,28 @@ import android.util.Log;
import android.view.View; import android.view.View;
import com.bluelinelabs.conductor.Controller; 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.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 { public abstract class BaseController extends RefWatchingController {
@Inject
AppPreferences appPreferences;
private List<String> temporaryClassNames = new ArrayList<>();
private static final String TAG = "BaseController"; private static final String TAG = "BaseController";
protected BaseController() { protected BaseController() {
@ -38,6 +56,12 @@ public abstract class BaseController extends RefWatchingController {
super(args); 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* // 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. // be accessed. In a production app, this would use Dagger instead.
protected ActionBar getActionBar() { protected ActionBar getActionBar() {
@ -54,6 +78,14 @@ public abstract class BaseController extends RefWatchingController {
protected void onAttach(@NonNull View view) { protected void onAttach(@NonNull View view) {
setTitle(); setTitle();
getActionBar().setDisplayHomeAsUpEnabled(false); 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); super.onAttach(view);
} }

View file

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

View file

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

View file

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

View file

@ -30,7 +30,9 @@ import com.nextcloud.talk.BuildConfig;
import com.nextcloud.talk.api.NcApi; import com.nextcloud.talk.api.NcApi;
import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.utils.ApiUtils; 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.preferences.AppPreferences;
import com.nextcloud.talk.utils.ssl.MagicKeyManager;
import com.nextcloud.talk.utils.ssl.MagicTrustManager; import com.nextcloud.talk.utils.ssl.MagicTrustManager;
import com.nextcloud.talk.utils.ssl.SSLSocketFactoryCompat; import com.nextcloud.talk.utils.ssl.SSLSocketFactoryCompat;
@ -38,9 +40,16 @@ import java.io.IOException;
import java.net.CookieManager; import java.net.CookieManager;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Proxy; 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 java.util.concurrent.TimeUnit;
import javax.inject.Singleton; import javax.inject.Singleton;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.X509KeyManager;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
@ -110,8 +119,35 @@ public class RestModule {
@Provides @Provides
@Singleton @Singleton
SSLSocketFactoryCompat provideSslSocketFactoryCompat(MagicTrustManager magicTrustManager) { MagicKeyManager provideKeyManager(AppPreferences appPreferences, UserUtils userUtils) {
return new SSLSocketFactoryCompat(magicTrustManager); 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 @Provides
@ -145,10 +181,10 @@ public class RestModule {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
httpClient.addInterceptor(loggingInterceptor); httpClient.addInterceptor(loggingInterceptor);
} }
// Trust own CA and all self-signed certs
httpClient.sslSocketFactory(sslSocketFactoryCompat, magicTrustManager); httpClient.sslSocketFactory(sslSocketFactoryCompat, magicTrustManager);
httpClient.retryOnConnectionFailure(true); httpClient.retryOnConnectionFailure(true);
httpClient.hostnameVerifier(magicTrustManager.getHostnameVerifier(OkHostnameVerifier.INSTANCE)); httpClient.hostnameVerifier(magicTrustManager.getHostnameVerifier(OkHostnameVerifier.INSTANCE));

View file

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

View file

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

View file

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

View file

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

View file

@ -115,8 +115,19 @@ public interface AppPreferences {
void setPushToken(String pushToken); void setPushToken(String pushToken);
@KeyByString("push_token") @KeyByString("push_token")
@RemoveMethod
void removePushToken(); void removePushToken();
@KeyByString("tempClientCertAlias")
String getTemporaryClientCertAlias();
@KeyByString("tempClientCertAlias")
void setTemporaryClientCertAlias(String alias);
@KeyByString("tempClientCertAlias")
@RemoveMethod
void removeTemporaryClientCertAlias();
@KeyByString("pushToTalk_intro_shown") @KeyByString("pushToTalk_intro_shown")
boolean getPushToTalkIntroShown(); boolean getPushToTalkIntroShown();
@ -124,9 +135,9 @@ public interface AppPreferences {
void setPushToTalkIntroShown(boolean shown); void setPushToTalkIntroShown(boolean shown);
@KeyByString("pushToTalk_intro_shown") @KeyByString("pushToTalk_intro_shown")
@RemoveMethod
void removePushToTalkIntroShown(); void removePushToTalkIntroShown();
@ClearMethod @ClearMethod
void clear(); 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.net.Socket
import java.security.GeneralSecurityException import java.security.GeneralSecurityException
import java.util.* import java.util.*
import javax.net.ssl.SSLContext import javax.net.ssl.*
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
class SSLSocketFactoryCompat(trustManager: X509TrustManager) : SSLSocketFactory() { class SSLSocketFactoryCompat(keyManager: KeyManager?,
trustManager: X509TrustManager) : SSLSocketFactory() {
private var delegate: SSLSocketFactory private var delegate: SSLSocketFactory
@ -101,7 +99,10 @@ class SSLSocketFactoryCompat(trustManager: X509TrustManager) : SSLSocketFactory(
init { init {
try { try {
val sslContext = SSLContext.getInstance("TLS") 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 delegate = sslContext.socketFactory
} catch (e: GeneralSecurityException) { } catch (e: GeneralSecurityException) {
throw IllegalStateException() // system has no TLS throw IllegalStateException() // system has no TLS

View file

@ -91,4 +91,19 @@
android:textAllCaps="true" android:textAllCaps="true"
android:textColor="@color/nc_light_blue_color"/> 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> </RelativeLayout>

View file

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

View file

@ -50,6 +50,8 @@
<string name="nc_settings_use_credentials_key">proxy_credentials</string> <string name="nc_settings_use_credentials_key">proxy_credentials</string>
<string name="nc_settings_switch_account">Switch between accounts</string> <string name="nc_settings_switch_account">Switch between accounts</string>
<string name="nc_settings_reauthorize">Reauthorize</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_remove_account">Remove account</string>
<string name="nc_settings_add_account">Add a new 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> <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">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_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_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. <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! Nextcloud Talk is easy to use and will always be completely free!