From 2d14ef1b6f5c47fb5da0f038edcbfc411dcb9482 Mon Sep 17 00:00:00 2001 From: Galen Abell Date: Wed, 7 Nov 2018 17:28:25 -0500 Subject: [PATCH] Added SSL Validation Override and CA Selection - Added fields to login page to a) disable ssl validation or b) select a custom Certificate Authority certificate to use with the server. - Changed visibility of widgets on login page from INVISIBLE to GONE so they don't take up space while hidden (since this was causing weird spacing issues with the new fields). - Added state to settings to store ssl validation choice or certificate data. - Added fields to various HTTP methods to disable ssl validation or set valid certificate authority if either setting is enabled. --- .../main/java/com/github/gotify/Settings.java | 11 ++ .../main/java/com/github/gotify/Utils.java | 123 +++++++++++++++++ .../com/github/gotify/api/ClientFactory.java | 19 +-- .../gotify/init/InitializationActivity.java | 2 +- .../github/gotify/login/LoginActivity.java | 130 +++++++++++++++--- .../gotify/messages/MessagesActivity.java | 4 +- .../gotify/service/WebSocketConnection.java | 18 +-- .../gotify/service/WebSocketService.java | 4 +- app/src/main/res/layout/activity_login.xml | 88 +++++++++++- app/src/main/res/values/strings.xml | 8 ++ 10 files changed, 362 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/github/gotify/Settings.java b/app/src/main/java/com/github/gotify/Settings.java index 86726c8..dc04b52 100644 --- a/app/src/main/java/com/github/gotify/Settings.java +++ b/app/src/main/java/com/github/gotify/Settings.java @@ -34,6 +34,8 @@ public class Settings { public void clear() { url(null); token(null); + validateSSL(true); + cert(null); } public void user(String name, boolean admin) { @@ -57,4 +59,13 @@ public class Settings { public void serverVersion(String version) { sharedPreferences.edit().putString("version", version).apply(); } + + // Default to always validating SSL. + public boolean validateSSL() { return sharedPreferences.getBoolean("validateSSL", true); } + + public void validateSSL(boolean validateSSL) { sharedPreferences.edit().putBoolean("validateSSL", validateSSL).apply(); } + + public String cert() { return sharedPreferences.getString("cert", null); } + + public void cert(String cert) { sharedPreferences.edit().putString("cert", cert).apply(); } } diff --git a/app/src/main/java/com/github/gotify/Utils.java b/app/src/main/java/com/github/gotify/Utils.java index 7cc8e1f..08eb439 100644 --- a/app/src/main/java/com/github/gotify/Utils.java +++ b/app/src/main/java/com/github/gotify/Utils.java @@ -1,5 +1,6 @@ package com.github.gotify; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.res.Resources; import android.graphics.Bitmap; @@ -7,14 +8,30 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.text.format.DateUtils; import android.view.View; +import androidx.annotation.NonNull; import com.github.gotify.client.ApiClient; import com.github.gotify.client.JSON; import com.github.gotify.log.Log; import com.google.android.material.snackbar.Snackbar; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; +import okhttp3.OkHttpClient; +import okio.Buffer; import org.threeten.bp.OffsetDateTime; +import javax.net.ssl.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.io.InputStream; +import java.util.Collection; + public class Utils { public static void showSnackBar(Activity activity, String message) { View rootView = activity.getWindow().getDecorView().findViewById(android.R.id.content); @@ -52,4 +69,110 @@ public class Utils { public interface DrawableReceiver { void loaded(Drawable drawable); } + + public static InputStream stringToInputStream(String str) { + if (str == null) return null; + return new Buffer() + .writeUtf8(str) + .inputStream(); + } + + public static String readFileFromStream(@NonNull InputStream inputStream) { + StringBuilder sb = new StringBuilder(); + String currentLine; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + while ((currentLine = reader.readLine()) != null) { + sb.append(currentLine).append("\n"); + } + } catch (IOException e) { + throw new IllegalArgumentException("failed to read input"); + } + + return sb.toString(); + } + + ///////////////////////// + ///// SSL Utilities ///// + ///////////////////////// + public static Certificate parseCertificate(String cert) { + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); + + return certificateFactory.generateCertificate(Utils.stringToInputStream(cert)); + } catch (Exception e) { + throw new IllegalArgumentException("certificate is invalid"); + } + } + + public static class SSLSettings { + boolean validateSSL; + String cert; + + public SSLSettings(boolean validateSSL, String cert) { + this.validateSSL = validateSSL; + this.cert = cert; + } + } + + // TrustManager that trusts all SSL Certs + private static final TrustManager trustAll = new X509TrustManager() { + @SuppressLint("TrustAllX509TrustManager") + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + @SuppressLint("TrustAllX509TrustManager") + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + @Override + public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[]{}; } + }; + + public static void applySslSettings(OkHttpClient.Builder builder, SSLSettings settings) { + // Modified from ApiClient.applySslSettings in the client package. + + try { + TrustManager[] trustManagers = null; + HostnameVerifier hostnameVerifier = null; + if (!settings.validateSSL) { + trustManagers = new TrustManager[]{ trustAll }; + hostnameVerifier = (hostname, session) -> true; + } else if (settings.cert != null) { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + Collection certificates = certificateFactory.generateCertificates(stringToInputStream(settings.cert)); + if (certificates.isEmpty()) { + throw new IllegalArgumentException("expected non-empty set of trusted certificates"); + } + KeyStore caKeyStore = newEmptyKeyStore(); + int index = 0; + for (Certificate certificate : certificates) { + String certificateAlias = "ca" + Integer.toString(index++); + caKeyStore.setCertificateEntry(certificateAlias, certificate); + } + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(caKeyStore); + trustManagers = trustManagerFactory.getTrustManagers(); + } + + if (trustManagers != null && trustManagers.length > 0) { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(new KeyManager[]{}, trustManagers, new SecureRandom()); + builder.sslSocketFactory(context.getSocketFactory(), (X509TrustManager) trustManagers[0]); + } + + if (hostnameVerifier != null) builder.hostnameVerifier(hostnameVerifier); + } catch (Exception e) { + // We shouldn't have issues since the cert is verified on login. + Log.e("Failed to apply SSL settings", e); + } + } + + private static KeyStore newEmptyKeyStore() throws GeneralSecurityException { + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + return keyStore; + } catch (IOException e) { + throw new AssertionError(e); + } + } } diff --git a/app/src/main/java/com/github/gotify/api/ClientFactory.java b/app/src/main/java/com/github/gotify/api/ClientFactory.java index e0e65eb..cb994e4 100644 --- a/app/src/main/java/com/github/gotify/api/ClientFactory.java +++ b/app/src/main/java/com/github/gotify/api/ClientFactory.java @@ -1,6 +1,7 @@ package com.github.gotify.api; import com.github.gotify.Settings; +import com.github.gotify.Utils; import com.github.gotify.client.ApiClient; import com.github.gotify.client.api.UserApi; import com.github.gotify.client.api.VersionApi; @@ -8,14 +9,16 @@ import com.github.gotify.client.auth.ApiKeyAuth; import com.github.gotify.client.auth.HttpBasicAuth; public class ClientFactory { - public static ApiClient unauthorized(String baseUrl) { + public static ApiClient unauthorized(String baseUrl, boolean validateSSL, String cert) { ApiClient client = new ApiClient(); + client.setVerifyingSsl(validateSSL); + client.setSslCaCert(Utils.stringToInputStream(cert)); client.setBasePath(baseUrl); return client; } - public static ApiClient basicAuth(String baseUrl, String username, String password) { - ApiClient client = unauthorized(baseUrl); + public static ApiClient basicAuth(String baseUrl, boolean validateSSL, String cert, String username, String password) { + ApiClient client = unauthorized(baseUrl, validateSSL, cert); HttpBasicAuth auth = (HttpBasicAuth) client.getAuthentication("basicAuth"); auth.setUsername(username); auth.setPassword(password); @@ -23,18 +26,18 @@ public class ClientFactory { return client; } - public static ApiClient clientToken(String baseUrl, String token) { - ApiClient client = unauthorized(baseUrl); + public static ApiClient clientToken(String baseUrl, boolean validateSSL, String cert, String token) { + ApiClient client = unauthorized(baseUrl, validateSSL, cert); ApiKeyAuth tokenAuth = (ApiKeyAuth) client.getAuthentication("clientTokenHeader"); tokenAuth.setApiKey(token); return client; } - public static VersionApi versionApi(String baseUrl) { - return new VersionApi(unauthorized(baseUrl)); + public static VersionApi versionApi(String baseUrl, boolean validateSSL, String cert) { + return new VersionApi(unauthorized(baseUrl, validateSSL, cert)); } public static UserApi userApiWithToken(Settings settings) { - return new UserApi(clientToken(settings.url(), settings.token())); + return new UserApi(clientToken(settings.url(), settings.validateSSL(), settings.cert(), settings.token())); } } diff --git a/app/src/main/java/com/github/gotify/init/InitializationActivity.java b/app/src/main/java/com/github/gotify/init/InitializationActivity.java index f69f5ad..5049358 100644 --- a/app/src/main/java/com/github/gotify/init/InitializationActivity.java +++ b/app/src/main/java/com/github/gotify/init/InitializationActivity.java @@ -116,7 +116,7 @@ public class InitializationActivity extends AppCompatActivity { private void requestVersion( final Callback.SuccessCallback callback, final Callback.ErrorCallback errorCallback) { - VersionApi versionApi = ClientFactory.versionApi(settings.url()); + VersionApi versionApi = ClientFactory.versionApi(settings.url(), settings.validateSSL(), settings.cert()); Api.withLogging(versionApi::getVersionAsync) .handleInUIThread(this, callback, errorCallback); } diff --git a/app/src/main/java/com/github/gotify/login/LoginActivity.java b/app/src/main/java/com/github/gotify/login/LoginActivity.java index ad866e3..7885cf2 100644 --- a/app/src/main/java/com/github/gotify/login/LoginActivity.java +++ b/app/src/main/java/com/github/gotify/login/LoginActivity.java @@ -1,19 +1,17 @@ package com.github.gotify.login; +import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ProgressBar; +import android.widget.*; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import butterknife.OnTextChanged; +import butterknife.*; import com.github.gotify.R; import com.github.gotify.Settings; import com.github.gotify.Utils; @@ -31,8 +29,15 @@ import com.github.gotify.log.Log; import com.github.gotify.log.UncaughtExceptionHandler; import com.squareup.okhttp.HttpUrl; +import java.io.*; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + public class LoginActivity extends AppCompatActivity { + // return value from startActivityForResult when choosing a certificate + private final int FILE_SELECT_CODE = 1; + @BindView(R.id.username) EditText usernameField; @@ -42,6 +47,21 @@ public class LoginActivity extends AppCompatActivity { @BindView(R.id.password) EditText passwordField; + @BindView(R.id.showAdvanced) + Button toggleAdvanced; + + @BindView(R.id.disableValidateSSL) + CheckBox disableSSLValidationCheckBox; + + @BindView(R.id.or) + TextView orTextView; + + @BindView(R.id.selectCACertificate) + Button selectCACertificate; + + @BindView(R.id.caFile) + TextView caFileName; + @BindView(R.id.checkurl) Button checkUrlButton; @@ -54,8 +74,13 @@ public class LoginActivity extends AppCompatActivity { @BindView(R.id.login_progress) ProgressBar loginProgress; + private boolean showAdvanced = false; + private Settings settings; + private boolean disableSSLValidation; + private String caCertContents; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -68,9 +93,9 @@ public class LoginActivity extends AppCompatActivity { @OnTextChanged(R.id.gotify_url) public void onUrlChange() { - usernameField.setVisibility(View.INVISIBLE); - passwordField.setVisibility(View.INVISIBLE); - loginButton.setVisibility(View.INVISIBLE); + usernameField.setVisibility(View.GONE); + passwordField.setVisibility(View.GONE); + loginButton.setVisibility(View.GONE); } @OnClick(R.id.checkurl) @@ -82,18 +107,81 @@ public class LoginActivity extends AppCompatActivity { } checkUrlProgress.setVisibility(View.VISIBLE); - checkUrlButton.setVisibility(View.INVISIBLE); + checkUrlButton.setVisibility(View.GONE); final String fixedUrl = url.endsWith("/") ? url.substring(0, url.length() - 1) : url; - Api.withLogging(ClientFactory.versionApi(fixedUrl)::getVersionAsync) + Api.withLogging(ClientFactory.versionApi(fixedUrl, !disableSSLValidation, caCertContents)::getVersionAsync) .handleInUIThread(this, onValidUrl(fixedUrl), onInvalidUrl(fixedUrl)); } + @OnClick(R.id.showAdvanced) + void doShowAdvanced() { + showAdvanced = !showAdvanced; + disableSSLValidationCheckBox.setVisibility(showAdvanced ? View.VISIBLE : View.GONE); + selectCACertificate.setVisibility(showAdvanced ? View.VISIBLE : View.GONE); + orTextView.setVisibility(showAdvanced ? View.VISIBLE : View.GONE); + caFileName.setVisibility(showAdvanced ? View.VISIBLE : View.GONE); + } + + @OnCheckedChanged(R.id.disableValidateSSL) + void doChangeDisableValidateSSL(boolean disable) { + // temporarily set the ssl validation (don't store to settings until they decide to login) + disableSSLValidation = disable; + } + + @OnClick(R.id.selectCACertificate) + void doSelectCACertificate() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + // we don't really care what kind of file it is as long as we can parse it + intent.setType("*/*"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + + try { + startActivityForResult(Intent.createChooser(intent, getString(R.string.select_ca_file)), FILE_SELECT_CODE); + } catch (ActivityNotFoundException e) { + // case for user not having a file browser installed + Utils.showSnackBar(LoginActivity.this, getString(R.string.please_install_file_browser)); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + try { + if (requestCode == FILE_SELECT_CODE) { + if (resultCode != RESULT_OK) { + throw new IllegalArgumentException(String.format("result was %d", resultCode)); + } else if (data == null) { + throw new IllegalArgumentException("file path was null"); + } + + Uri uri = data.getData(); + if (uri == null) { + throw new IllegalArgumentException("file path was null"); + } + + InputStream fileStream = getContentResolver().openInputStream(uri); + if (fileStream == null) { + throw new IllegalArgumentException("file path was invalid"); + } + + String contents = Utils.readFileFromStream(fileStream); + Certificate ca = Utils.parseCertificate(contents); + + caFileName.setText(((X509Certificate) ca).getSubjectDN().getName()); + // temporarily set the contents (don't store to settings until they decide to login) + caCertContents = contents; + } + } catch (Exception e) { + Utils.showSnackBar(LoginActivity.this, getString(R.string.select_ca_failed, e.getMessage())); + } + } + private Callback.SuccessCallback onValidUrl(String url) { return (version) -> { settings.url(url); - checkUrlProgress.setVisibility(View.INVISIBLE); + checkUrlProgress.setVisibility(View.GONE); checkUrlButton.setVisibility(View.VISIBLE); checkUrlButton.setText(getString(R.string.found_gotify_version, version.getVersion())); usernameField.setVisibility(View.VISIBLE); @@ -105,7 +193,7 @@ public class LoginActivity extends AppCompatActivity { private Callback.ErrorCallback onInvalidUrl(String url) { return (exception) -> { - checkUrlProgress.setVisibility(View.INVISIBLE); + checkUrlProgress.setVisibility(View.GONE); checkUrlButton.setVisibility(View.VISIBLE); Utils.showSnackBar(LoginActivity.this, versionError(url, exception)); }; @@ -116,17 +204,17 @@ public class LoginActivity extends AppCompatActivity { String username = usernameField.getText().toString(); String password = passwordField.getText().toString(); - loginButton.setVisibility(View.INVISIBLE); + loginButton.setVisibility(View.GONE); loginProgress.setVisibility(View.VISIBLE); - ApiClient client = ClientFactory.basicAuth(settings.url(), username, password); + ApiClient client = ClientFactory.basicAuth(settings.url(), !disableSSLValidation, caCertContents, username, password); Api.withLogging(new UserApi(client)::currentUserAsync) .handleInUIThread(this, (user) -> newClientDialog(client), this::onInvalidLogin); } private void onInvalidLogin(ApiException e) { loginButton.setVisibility(View.VISIBLE); - loginProgress.setVisibility(View.INVISIBLE); + loginProgress.setVisibility(View.GONE); Utils.showSnackBar(this, getString(R.string.wronguserpw)); } @@ -153,6 +241,10 @@ public class LoginActivity extends AppCompatActivity { private void onCreatedClient(Client client) { settings.token(client.getToken()); + // Set the final ssl validation status and / or cert + settings.validateSSL(!disableSSLValidation); + settings.cert(caCertContents); + Utils.showSnackBar(this, getString(R.string.created_client)); startActivity(new Intent(this, InitializationActivity.class)); finish(); @@ -160,12 +252,12 @@ public class LoginActivity extends AppCompatActivity { private void onFailedToCreateClient(ApiException e) { Utils.showSnackBar(this, getString(R.string.create_client_failed)); - loginProgress.setVisibility(View.INVISIBLE); + loginProgress.setVisibility(View.GONE); loginButton.setVisibility(View.VISIBLE); } private void onCancelClientDialog(DialogInterface dialog, int which) { - loginProgress.setVisibility(View.INVISIBLE); + loginProgress.setVisibility(View.GONE); loginButton.setVisibility(View.VISIBLE); } diff --git a/app/src/main/java/com/github/gotify/messages/MessagesActivity.java b/app/src/main/java/com/github/gotify/messages/MessagesActivity.java index dcbe10a..1a427e1 100644 --- a/app/src/main/java/com/github/gotify/messages/MessagesActivity.java +++ b/app/src/main/java/com/github/gotify/messages/MessagesActivity.java @@ -107,7 +107,7 @@ public class MessagesActivity extends AppCompatActivity Log.i("Entering " + getClass().getSimpleName()); settings = new Settings(this); - client = ClientFactory.clientToken(settings.url(), settings.token()); + client = ClientFactory.clientToken(settings.url(), settings.validateSSL(), settings.cert(), settings.token()); appsHolder = new ApplicationHolder(this, client); appsHolder.onUpdate(() -> onUpdateApps(appsHolder.get())); appsHolder.request(); @@ -401,7 +401,7 @@ public class MessagesActivity extends AppCompatActivity @Override protected Void doInBackground(Void... ignore) { TokenApi api = - new TokenApi(ClientFactory.clientToken(settings.url(), settings.token())); + new TokenApi(ClientFactory.clientToken(settings.url(), settings.validateSSL(), settings.cert(), settings.token())); stopService(new Intent(MessagesActivity.this, WebSocketService.class)); try { List clients = api.getClients(); diff --git a/app/src/main/java/com/github/gotify/service/WebSocketConnection.java b/app/src/main/java/com/github/gotify/service/WebSocketConnection.java index 650156d..f14b9b8 100644 --- a/app/src/main/java/com/github/gotify/service/WebSocketConnection.java +++ b/app/src/main/java/com/github/gotify/service/WebSocketConnection.java @@ -15,15 +15,9 @@ import okhttp3.WebSocket; import okhttp3.WebSocketListener; public class WebSocketConnection { + private OkHttpClient client; private static final JSON gson = Utils.json(); - private final OkHttpClient client = - new OkHttpClient.Builder() - .readTimeout(0, TimeUnit.MILLISECONDS) - .pingInterval(1, TimeUnit.MINUTES) - .connectTimeout(10, TimeUnit.SECONDS) - .build(); - private final Handler handler = new Handler(); private int errorCount = 0; @@ -38,7 +32,15 @@ public class WebSocketConnection { private Runnable onReconnected; private boolean isClosed; - WebSocketConnection(String baseUrl, String token) { + WebSocketConnection(String baseUrl, boolean validateSSL, String cert, String token) { + OkHttpClient.Builder builder = new OkHttpClient.Builder() + .readTimeout(0, TimeUnit.MILLISECONDS) + .pingInterval(1, TimeUnit.MINUTES) + .connectTimeout(10, TimeUnit.SECONDS); + Utils.applySslSettings(builder, new Utils.SSLSettings(validateSSL, cert)); + + client = builder.build(); + this.baseUrl = baseUrl; this.token = token; } diff --git a/app/src/main/java/com/github/gotify/service/WebSocketService.java b/app/src/main/java/com/github/gotify/service/WebSocketService.java index 8feb227..a66b2c6 100644 --- a/app/src/main/java/com/github/gotify/service/WebSocketService.java +++ b/app/src/main/java/com/github/gotify/service/WebSocketService.java @@ -42,7 +42,7 @@ public class WebSocketService extends Service { super.onCreate(); settings = new Settings(this); missingMessageUtil = - new MissedMessageUtil(ClientFactory.clientToken(settings.url(), settings.token())); + new MissedMessageUtil(ClientFactory.clientToken(settings.url(), settings.validateSSL(), settings.cert(), settings.token())); Log.i("Create " + getClass().getSimpleName()); } @@ -79,7 +79,7 @@ public class WebSocketService extends Service { } connection = - new WebSocketConnection(settings.url(), settings.token()) + new WebSocketConnection(settings.url(), settings.validateSSL(), settings.cert(), settings.token()) .onOpen(this::onOpen) .onClose(() -> foreground(getString(R.string.websocket_closed))) .onBadRequest(this::onBadRequest) diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 3625112..a75a266 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -25,7 +25,7 @@ android:layout_weight="1" android:minWidth="40dp" android:minHeight="40dp" - android:visibility="invisible" + android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.501" app:layout_constraintStart_toStartOf="parent" @@ -38,7 +38,7 @@ android:layout_weight="1" android:minWidth="40dp" android:minHeight="40dp" - android:visibility="invisible" + android:visibility="gone" app:layout_constraintEnd_toEndOf="@+id/checkurl" app:layout_constraintStart_toStartOf="@+id/login" app:layout_constraintTop_toTopOf="@+id/login" /> @@ -85,7 +85,7 @@ android:ems="10" android:hint="@string/username" android:inputType="textPersonName" - android:visibility="invisible" + android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/checkurl" @@ -102,13 +102,91 @@ android:ems="10" android:hint="@string/password" android:inputType="textPassword" - android:visibility="invisible" + android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/username" app:layout_constraintWidth_max="280dp" tools:text="Password" /> +