Merge pull request #1570 from vector-im/feature/self_signed

Allow self-signed certificate (#1564)
This commit is contained in:
Benoit Marty 2020-06-30 18:34:23 +02:00 committed by GitHub
commit 19f16c9e56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 583 additions and 760 deletions

View File

@ -11,6 +11,7 @@ Improvements 🙌:
- Prioritising Recovery key over Recovery passphrase (#1463)
- Room Settings: Name, Topic, Photo, Aliases, History Visibility (#1455)
- Update user avatar (#1054)
- Allow self-signed certificate (#1564)
Bugfix 🐛:
- Fix dark theme issue on login screen (#1097)

View File

@ -89,6 +89,7 @@ interface AuthenticationService {
* Perform a wellknown request, using the domain from the matrixId
*/
fun getWellKnownData(matrixId: String,
homeServerConnectionConfig: HomeServerConnectionConfig?,
callback: MatrixCallback<WellknownResult>): Cancelable
/**

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.failure
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.network.ssl.Fingerprint
import java.io.IOException
/**
@ -32,9 +33,11 @@ import java.io.IOException
sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
data class Unknown(val throwable: Throwable? = null) : Failure(throwable)
data class Cancelled(val throwable: Throwable? = null) : Failure(throwable)
data class UnrecognizedCertificateFailure(val url: String, val fingerprint: Fingerprint) : Failure()
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false")))
// When server send an error, but it cannot be interpreted as a MatrixError
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody"))

View File

@ -16,8 +16,11 @@
package im.vector.matrix.android.api.failure
import im.vector.matrix.android.internal.network.ssl.Fingerprint
// This class will be sent to the bus
sealed class GlobalError {
data class InvalidToken(val softLogout: Boolean) : GlobalError()
data class ConsentNotGivenError(val consentUri: String) : GlobalError()
data class CertificateError(val fingerprint: Fingerprint) : GlobalError()
}

View File

@ -43,6 +43,8 @@ import im.vector.matrix.android.internal.auth.version.isSupportedBySdk
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.network.httpclient.addSocketFactory
import im.vector.matrix.android.internal.network.ssl.UnrecognizedCertificateException
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.launchToCallback
@ -121,7 +123,11 @@ internal class DefaultAuthenticationService @Inject constructor(
callback.onSuccess(it)
},
{
callback.onFailure(it)
if (it is UnrecognizedCertificateException) {
callback.onFailure(Failure.UnrecognizedCertificateFailure(homeServerConnectionConfig.homeServerUri.toString(), it.fingerprint))
} else {
callback.onFailure(it)
}
}
)
}
@ -209,7 +215,7 @@ internal class DefaultAuthenticationService @Inject constructor(
// Create a fake userId, for the getWellknown task
val fakeUserId = "@alice:$domain"
val wellknownResult = getWellknownTask.execute(GetWellknownTask.Params(fakeUserId))
val wellknownResult = getWellknownTask.execute(GetWellknownTask.Params(fakeUserId, homeServerConnectionConfig))
return when (wellknownResult) {
is WellknownResult.Prompt -> {
@ -248,7 +254,7 @@ internal class DefaultAuthenticationService @Inject constructor(
?: let {
pendingSessionData?.homeServerConnectionConfig?.let {
DefaultRegistrationWizard(
okHttpClient,
buildClient(it),
retrofitFactory,
coroutineDispatchers,
sessionCreator,
@ -269,7 +275,7 @@ internal class DefaultAuthenticationService @Inject constructor(
?: let {
pendingSessionData?.homeServerConnectionConfig?.let {
DefaultLoginWizard(
okHttpClient,
buildClient(it),
retrofitFactory,
coroutineDispatchers,
sessionCreator,
@ -321,9 +327,11 @@ internal class DefaultAuthenticationService @Inject constructor(
}
}
override fun getWellKnownData(matrixId: String, callback: MatrixCallback<WellknownResult>): Cancelable {
override fun getWellKnownData(matrixId: String,
homeServerConnectionConfig: HomeServerConnectionConfig?,
callback: MatrixCallback<WellknownResult>): Cancelable {
return getWellknownTask
.configureWith(GetWellknownTask.Params(matrixId)) {
.configureWith(GetWellknownTask.Params(matrixId, homeServerConnectionConfig)) {
this.callback = callback
}
.executeBy(taskExecutor)
@ -347,7 +355,14 @@ internal class DefaultAuthenticationService @Inject constructor(
}
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
val retrofit = retrofitFactory.create(buildClient(homeServerConnectionConfig), homeServerConnectionConfig.homeServerUri.toString())
return retrofit.create(AuthAPI::class.java)
}
private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient {
return okHttpClient.get()
.newBuilder()
.addSocketFactory(homeServerConnectionConfig)
.build()
}
}

View File

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.auth.login
import android.util.Patterns
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.login.LoginWizard
@ -44,7 +43,7 @@ import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
internal class DefaultLoginWizard(
okHttpClient: Lazy<OkHttpClient>,
okHttpClient: OkHttpClient,
retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionCreator: SessionCreator,

View File

@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.network.httpclient.addSocketFactory
import im.vector.matrix.android.internal.task.Task
import okhttp3.OkHttpClient
import javax.inject.Inject
@ -47,7 +48,8 @@ internal class DefaultDirectLoginTask @Inject constructor(
) : DirectLoginTask {
override suspend fun execute(params: DirectLoginTask.Params): Session {
val authAPI = retrofitFactory.create(okHttpClient, params.homeServerConnectionConfig.homeServerUri.toString())
val client = buildClient(params.homeServerConnectionConfig)
val authAPI = retrofitFactory.create(client, params.homeServerConnectionConfig.homeServerUri.toString())
.create(AuthAPI::class.java)
val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName)
@ -58,4 +60,11 @@ internal class DefaultDirectLoginTask @Inject constructor(
return sessionCreator.createSession(credentials, params.homeServerConnectionConfig)
}
private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient {
return okHttpClient.get()
.newBuilder()
.addSocketFactory(homeServerConnectionConfig)
.build()
}
}

View File

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.auth.registration
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
@ -41,7 +40,7 @@ import okhttp3.OkHttpClient
* This class execute the registration request and is responsible to keep the session of interactive authentication
*/
internal class DefaultRegistrationWizard(
private val okHttpClient: Lazy<OkHttpClient>,
private val okHttpClient: OkHttpClient,
private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionCreator: SessionCreator,

View File

@ -29,3 +29,7 @@ internal annotation class AuthenticatedIdentity
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class Unauthenticated
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class UnauthenticatedWithCertificate

View File

@ -1,284 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.riot;
import android.util.Pair;
import androidx.annotation.NonNull;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
import okhttp3.TlsVersion;
import timber.log.Timber;
/*
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
*/
/**
* Various utility classes for dealing with X509Certificates
*/
public class CertUtil {
/**
* Generates the SHA-256 fingerprint of the given certificate
*
* @param cert the certificate.
* @return the finger print
* @throws CertificateException the certificate exception
*/
public static byte[] generateSha256Fingerprint(X509Certificate cert) throws CertificateException {
return generateFingerprint(cert, "SHA-256");
}
/**
* Generates the SHA-1 fingerprint of the given certificate
*
* @param cert the certificated
* @return the SHA1 fingerprint
* @throws CertificateException the certificate exception
*/
public static byte[] generateSha1Fingerprint(X509Certificate cert) throws CertificateException {
return generateFingerprint(cert, "SHA-1");
}
/**
* Generate the fingerprint for a dedicated type.
*
* @param cert the certificate
* @param type the type
* @return the fingerprint
* @throws CertificateException certificate exception
*/
private static byte[] generateFingerprint(X509Certificate cert, String type) throws CertificateException {
final byte[] fingerprint;
final MessageDigest md;
try {
md = MessageDigest.getInstance(type);
} catch (Exception e) {
// This really *really* shouldn't throw, as java should always have a SHA-256 and SHA-1 impl.
throw new CertificateException(e);
}
fingerprint = md.digest(cert.getEncoded());
return fingerprint;
}
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
/**
* Convert the fingerprint to an hexa string.
*
* @param fingerprint the fingerprint
* @return the hexa string.
*/
public static String fingerprintToHexString(byte[] fingerprint) {
return fingerprintToHexString(fingerprint, ' ');
}
public static String fingerprintToHexString(byte[] fingerprint, char sep) {
char[] hexChars = new char[fingerprint.length * 3];
for (int j = 0; j < fingerprint.length; j++) {
int v = fingerprint[j] & 0xFF;
hexChars[j * 3] = hexArray[v >>> 4];
hexChars[j * 3 + 1] = hexArray[v & 0x0F];
hexChars[j * 3 + 2] = sep;
}
return new String(hexChars, 0, hexChars.length - 1);
}
/**
* Recursively checks the exception to see if it was caused by an
* UnrecognizedCertificateException
*
* @param e the throwable.
* @return The UnrecognizedCertificateException if exists, else null.
*/
public static UnrecognizedCertificateException getCertificateException(Throwable e) {
int i = 0; // Just in case there is a getCause loop
while (e != null && i < 10) {
if (e instanceof UnrecognizedCertificateException) {
return (UnrecognizedCertificateException) e;
}
e = e.getCause();
i++;
}
return null;
}
/**
* Create a SSLSocket factory for a HS config.
*
* @param hsConfig the HS config.
* @return SSLSocket factory
*/
public static Pair<SSLSocketFactory, X509TrustManager> newPinnedSSLSocketFactory(HomeServerConnectionConfig hsConfig) {
X509TrustManager defaultTrustManager = null;
// If we haven't specified that we wanted to pin the certs, fallback to standard
// X509 checks if fingerprints don't match.
if (!hsConfig.shouldPin()) {
TrustManagerFactory trustManagerFactory = null;
// get the PKIX instance
try {
trustManagerFactory = TrustManagerFactory.getInstance("PKIX");
} catch (NoSuchAlgorithmException e) {
Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance failed");
}
// it doesn't exist, use the default one.
if (trustManagerFactory == null) {
try {
trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
} catch (NoSuchAlgorithmException e) {
Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance with default algorithm failed");
}
}
if (trustManagerFactory != null) {
try {
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
for (int i = 0; i < trustManagers.length; i++) {
if (trustManagers[i] instanceof X509TrustManager) {
defaultTrustManager = (X509TrustManager) trustManagers[i];
break;
}
}
} catch (KeyStoreException e) {
Timber.e(e, "## newPinnedSSLSocketFactory()");
}
}
}
X509TrustManager trustManager = new PinnedTrustManager(hsConfig.getAllowedFingerprints(), defaultTrustManager);
TrustManager[] trustManagers = new TrustManager[]{
trustManager
};
SSLSocketFactory sslSocketFactory;
try {
if (hsConfig.forceUsageOfTlsVersions() && hsConfig.getAcceptedTlsVersions() != null) {
// Force usage of accepted Tls Versions for Android < 20
sslSocketFactory = new TLSSocketFactory(trustManagers, hsConfig.getAcceptedTlsVersions());
} else {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagers, new java.security.SecureRandom());
sslSocketFactory = sslContext.getSocketFactory();
}
} catch (Exception e) {
// This is too fatal
throw new RuntimeException(e);
}
return new Pair<>(sslSocketFactory, trustManager);
}
/**
* Create a Host name verifier for a hs config.
*
* @param hsConfig the hs config.
* @return a new HostnameVerifier.
*/
public static HostnameVerifier newHostnameVerifier(HomeServerConnectionConfig hsConfig) {
final HostnameVerifier defaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
final List<Fingerprint> trusted_fingerprints = hsConfig.getAllowedFingerprints();
return new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
if (defaultVerifier.verify(hostname, session)) return true;
if (trusted_fingerprints == null || trusted_fingerprints.size() == 0) return false;
// If remote cert matches an allowed fingerprint, just accept it.
try {
for (Certificate cert : session.getPeerCertificates()) {
for (Fingerprint allowedFingerprint : trusted_fingerprints) {
if (allowedFingerprint != null && cert instanceof X509Certificate && allowedFingerprint.matchesCert((X509Certificate) cert)) {
return true;
}
}
}
} catch (SSLPeerUnverifiedException e) {
return false;
} catch (CertificateException e) {
return false;
}
return false;
}
};
}
/**
* Create a list of accepted TLS specifications for a hs config.
*
* @param hsConfig the hs config.
* @param url the url of the end point, used to check if we have to enable CLEARTEXT communication.
* @return a list of accepted TLS specifications.
*/
public static List<ConnectionSpec> newConnectionSpecs(@NonNull HomeServerConnectionConfig hsConfig, @NonNull String url) {
final ConnectionSpec.Builder builder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS);
final List<TlsVersion> tlsVersions = hsConfig.getAcceptedTlsVersions();
if (null != tlsVersions) {
builder.tlsVersions(tlsVersions.toArray(new TlsVersion[0]));
}
final List<CipherSuite> tlsCipherSuites = hsConfig.getAcceptedTlsCipherSuites();
if (null != tlsCipherSuites) {
builder.cipherSuites(tlsCipherSuites.toArray(new CipherSuite[0]));
}
builder.supportsTlsExtensions(hsConfig.shouldAcceptTlsExtensions());
List<ConnectionSpec> list = new ArrayList<>();
list.add(builder.build());
if (url.startsWith("http://")) {
list.add(ConnectionSpec.CLEARTEXT);
}
return list;
}
}

View File

@ -21,8 +21,6 @@ import android.util.Base64;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
/*
@ -40,20 +38,10 @@ public class Fingerprint {
private final HashType mHashType;
private final byte[] mBytes;
private String mDisplayableHexRepr;
public Fingerprint(HashType hashType, byte[] bytes) {
mHashType = hashType;
mBytes = bytes;
mDisplayableHexRepr = null;
}
public static Fingerprint newSha256Fingerprint(X509Certificate cert) throws CertificateException {
return new Fingerprint(HashType.SHA256, CertUtil.generateSha256Fingerprint(cert));
}
public static Fingerprint newSha1Fingerprint(X509Certificate cert) throws CertificateException {
return new Fingerprint(HashType.SHA1, CertUtil.generateSha1Fingerprint(cert));
}
public HashType getType() {
@ -64,14 +52,6 @@ public class Fingerprint {
return mBytes;
}
public String getBytesAsHexString() {
if (mDisplayableHexRepr == null) {
mDisplayableHexRepr = CertUtil.fingerprintToHexString(mBytes);
}
return mDisplayableHexRepr;
}
public JSONObject toJson() throws JSONException {
JSONObject obj = new JSONObject();
obj.put("bytes", Base64.encodeToString(getBytes(), Base64.DEFAULT));
@ -95,24 +75,6 @@ public class Fingerprint {
return new Fingerprint(hashType, fingerprintBytes);
}
public boolean matchesCert(X509Certificate cert) throws CertificateException {
Fingerprint o = null;
switch (mHashType) {
case SHA256:
o = Fingerprint.newSha256Fingerprint(cert);
break;
case SHA1:
o = Fingerprint.newSha1Fingerprint(cert);
break;
}
return equals(o);
}
public String toString() {
return String.format("Fingerprint{type: '%s', fingeprint: '%s'}", mHashType.toString(), getBytesAsHexString());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -1,107 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.riot;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.annotation.Nullable;
import javax.net.ssl.X509TrustManager;
/*
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
*/
/**
* Implements a TrustManager that checks Certificates against an explicit list of known
* fingerprints.
*/
public class PinnedTrustManager implements X509TrustManager {
private final List<Fingerprint> mFingerprints;
@Nullable
private final X509TrustManager mDefaultTrustManager;
/**
* @param fingerprints An array of SHA256 cert fingerprints
* @param defaultTrustManager Optional trust manager to fall back on if cert does not match
* any of the fingerprints. Can be null.
*/
public PinnedTrustManager(List<Fingerprint> fingerprints, @Nullable X509TrustManager defaultTrustManager) {
mFingerprints = fingerprints;
mDefaultTrustManager = defaultTrustManager;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String s) throws CertificateException {
try {
if (mDefaultTrustManager != null) {
mDefaultTrustManager.checkClientTrusted(
chain, s
);
return;
}
} catch (CertificateException e) {
// If there is an exception we fall back to checking fingerprints
if (mFingerprints == null || mFingerprints.size() == 0) {
throw new UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.getCause());
}
}
checkTrusted("client", chain);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String s) throws CertificateException {
try {
if (mDefaultTrustManager != null) {
mDefaultTrustManager.checkServerTrusted(
chain, s
);
return;
}
} catch (CertificateException e) {
// If there is an exception we fall back to checking fingerprints
if (mFingerprints == null || mFingerprints.isEmpty()) {
throw new UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.getCause());
}
}
checkTrusted("server", chain);
}
private void checkTrusted(String type, X509Certificate[] chain) throws CertificateException {
X509Certificate cert = chain[0];
boolean found = false;
if (mFingerprints != null) {
for (Fingerprint allowedFingerprint : mFingerprints) {
if (allowedFingerprint != null && allowedFingerprint.matchesCert(cert)) {
found = true;
break;
}
}
}
if (!found) {
throw new UnrecognizedCertificateException(cert, Fingerprint.newSha256Fingerprint(cert), null);
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}

View File

@ -1,135 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.riot;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import okhttp3.TlsVersion;
import timber.log.Timber;
/*
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
*/
/**
* Force the usage of Tls versions on every created socket
* Inspired from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/
*/
/*package*/ class TLSSocketFactory extends SSLSocketFactory {
private SSLSocketFactory internalSSLSocketFactory;
private String[] enabledProtocols;
/**
* Constructor
*
* @param trustPinned
* @param acceptedTlsVersions
* @throws KeyManagementException
* @throws NoSuchAlgorithmException
*/
/*package*/ TLSSocketFactory(TrustManager[] trustPinned, List<TlsVersion> acceptedTlsVersions) throws KeyManagementException, NoSuchAlgorithmException {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, trustPinned, new SecureRandom());
internalSSLSocketFactory = context.getSocketFactory();
enabledProtocols = new String[acceptedTlsVersions.size()];
int i = 0;
for (TlsVersion tlsVersion : acceptedTlsVersions) {
enabledProtocols[i] = tlsVersion.javaName();
i++;
}
}
@Override
public String[] getDefaultCipherSuites() {
return internalSSLSocketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return internalSSLSocketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket() throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket());
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
}
private Socket enableTLSOnSocket(Socket socket) {
if (socket != null && (socket instanceof SSLSocket)) {
SSLSocket sslSocket = (SSLSocket) socket;
List<String> supportedProtocols = Arrays.asList(sslSocket.getSupportedProtocols());
List<String> filteredEnabledProtocols = new ArrayList<>();
for (String protocol : enabledProtocols) {
if (supportedProtocols.contains(protocol)) {
filteredEnabledProtocols.add(protocol);
}
}
if (!filteredEnabledProtocols.isEmpty()) {
try {
sslSocket.setEnabledProtocols(filteredEnabledProtocols.toArray(new String[filteredEnabledProtocols.size()]));
} catch (Exception e) {
Timber.e(e, "Exception");
}
}
}
return socket;
}
}

View File

@ -1,47 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.riot;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/*
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
*/
/**
* Thrown when we are given a certificate that does match the certificate we were told to
* expect.
*/
public class UnrecognizedCertificateException extends CertificateException {
private final X509Certificate mCert;
private final Fingerprint mFingerprint;
public UnrecognizedCertificateException(X509Certificate cert, Fingerprint fingerprint, Throwable cause) {
super("Unrecognized certificate with unknown fingerprint: " + cert.getSubjectDN(), cause);
mCert = cert;
mFingerprint = fingerprint;
}
public X509Certificate getCertificate() {
return mCert;
}
public Fingerprint getFingerprint() {
return mFingerprint;
}
}

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.network
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.shouldBeRetried
import im.vector.matrix.android.internal.network.ssl.CertUtil
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import org.greenrobot.eventbus.EventBus
@ -26,7 +27,7 @@ import retrofit2.awaitResponse
import java.io.IOException
internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?,
block: Request<DATA>.() -> Unit) = Request<DATA>(eventBus).apply(block).execute()
block: Request<DATA>.() -> Unit) = Request<DATA>(eventBus).apply(block).execute()
internal class Request<DATA : Any>(private val eventBus: EventBus?) {
@ -48,6 +49,15 @@ internal class Request<DATA : Any>(private val eventBus: EventBus?) {
throw response.toFailure(eventBus)
}
} catch (exception: Throwable) {
// Check if this is a certificateException
CertUtil.getCertificateException(exception)
// TODO Support certificate error once logged
// ?.also { unrecognizedCertificateException ->
// // Send the error to the bus, for a global management
// eventBus?.post(GlobalError.CertificateError(unrecognizedCertificateException))
// }
?.also { unrecognizedCertificateException -> throw unrecognizedCertificateException }
if (isRetryable && currentRetryCount++ < maxRetryCount && exception.shouldBeRetried()) {
delay(currentDelay)
currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay)

View File

@ -26,7 +26,19 @@ import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Inject
class RetrofitFactory @Inject constructor(private val moshi: Moshi) {
internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) {
/**
* Use only for authentication service
*/
fun create(okHttpClient: OkHttpClient, baseUrl: String): Retrofit {
return Retrofit.Builder()
.baseUrl(baseUrl.ensureTrailingSlash())
.client(okHttpClient)
.addConverterFactory(UnitConverterFactory)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
}
fun create(okHttpClient: Lazy<OkHttpClient>, baseUrl: String): Retrofit {
return Retrofit.Builder()

View File

@ -16,24 +16,38 @@
package im.vector.matrix.android.internal.network.httpclient
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
import im.vector.matrix.android.internal.network.ssl.CertUtil
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
import okhttp3.OkHttpClient
import timber.log.Timber
internal fun OkHttpClient.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient {
return newBuilder()
.apply {
// Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
interceptors().removeAll(existingCurlInterceptors)
internal fun OkHttpClient.Builder.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient.Builder {
// Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
interceptors().removeAll(existingCurlInterceptors)
addInterceptor(AccessTokenInterceptor(accessTokenProvider))
addInterceptor(AccessTokenInterceptor(accessTokenProvider))
// Re add eventually the curl logging interceptors
existingCurlInterceptors.forEach {
addInterceptor(it)
}
}
.build()
// Re add eventually the curl logging interceptors
existingCurlInterceptors.forEach {
addInterceptor(it)
}
return this
}
internal fun OkHttpClient.Builder.addSocketFactory(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient.Builder {
try {
val pair = CertUtil.newPinnedSSLSocketFactory(homeServerConnectionConfig)
sslSocketFactory(pair.sslSocketFactory, pair.x509TrustManager)
hostnameVerifier(CertUtil.newHostnameVerifier(homeServerConnectionConfig))
connectionSpecs(CertUtil.newConnectionSpecs(homeServerConnectionConfig))
} catch (e: Exception) {
Timber.e(e, "addSocketFactory failed")
}
return this
}

View File

@ -16,29 +16,30 @@
package im.vector.matrix.android.internal.network.ssl
import android.util.Pair
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import okhttp3.ConnectionSpec
import okhttp3.internal.tls.OkHostnameVerifier
import timber.log.Timber
import java.security.KeyStore
import java.security.MessageDigest
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLPeerUnverifiedException
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import kotlin.experimental.and
/**
* Various utility classes for dealing with X509Certificates
*/
internal object CertUtil {
// Set to false to do some test
private const val USE_DEFAULT_HOSTNAME_VERIFIER = true
private val hexArray = "0123456789ABCDEF".toCharArray()
/**
@ -95,11 +96,10 @@ internal object CertUtil {
* @param fingerprint the fingerprint
* @return the hexa string.
*/
@JvmOverloads
fun fingerprintToHexString(fingerprint: ByteArray, sep: Char = ' '): String {
val hexChars = CharArray(fingerprint.size * 3)
for (j in fingerprint.indices) {
val v = (fingerprint[j] and 0xFF.toByte()).toInt()
val v = (fingerprint[j].toInt() and 0xFF)
hexChars[j * 3] = hexArray[v.ushr(4)]
hexChars[j * 3 + 1] = hexArray[v and 0x0F]
hexChars[j * 3 + 2] = sep
@ -128,13 +128,18 @@ internal object CertUtil {
return null
}
internal data class PinnedSSLSocketFactory(
val sslSocketFactory: SSLSocketFactory,
val x509TrustManager: X509TrustManager
)
/**
* Create a SSLSocket factory for a HS config.
*
* @param hsConfig the HS config.
* @return SSLSocket factory
*/
fun newPinnedSSLSocketFactory(hsConfig: HomeServerConnectionConfig): Pair<SSLSocketFactory, X509TrustManager> {
fun newPinnedSSLSocketFactory(hsConfig: HomeServerConnectionConfig): PinnedSSLSocketFactory {
try {
var defaultTrustManager: X509TrustManager? = null
@ -155,7 +160,7 @@ internal object CertUtil {
try {
tf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
} catch (e: Exception) {
Timber.e(e, "## addRule : onBingRuleUpdateFailure failed")
Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance of default failed")
}
}
@ -183,7 +188,7 @@ internal object CertUtil {
sslSocketFactory = sslContext.socketFactory
}
return Pair<SSLSocketFactory, X509TrustManager>(sslSocketFactory, defaultTrustManager)
return PinnedSSLSocketFactory(sslSocketFactory, defaultTrustManager!!)
} catch (e: Exception) {
throw RuntimeException(e)
}
@ -196,11 +201,14 @@ internal object CertUtil {
* @return a new HostnameVerifier.
*/
fun newHostnameVerifier(hsConfig: HomeServerConnectionConfig): HostnameVerifier {
val defaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier()
val defaultVerifier: HostnameVerifier = OkHostnameVerifier // HttpsURLConnection.getDefaultHostnameVerifier()
val trustedFingerprints = hsConfig.allowedFingerprints
return HostnameVerifier { hostname, session ->
if (defaultVerifier.verify(hostname, session)) return@HostnameVerifier true
if (USE_DEFAULT_HOSTNAME_VERIFIER) {
if (defaultVerifier.verify(hostname, session)) return@HostnameVerifier true
}
// TODO How to recover from this error?
if (trustedFingerprints.isEmpty()) return@HostnameVerifier false
// If remote cert matches an allowed fingerprint, just accept it.

View File

@ -32,7 +32,7 @@ data class Fingerprint(
}
@Throws(CertificateException::class)
fun matchesCert(cert: X509Certificate): Boolean {
internal fun matchesCert(cert: X509Certificate): Boolean {
val o: Fingerprint? = when (hashType) {
HashType.SHA256 -> newSha256Fingerprint(cert)
HashType.SHA1 -> newSha1Fingerprint(cert)
@ -57,7 +57,7 @@ data class Fingerprint(
return result
}
companion object {
internal companion object {
@Throws(CertificateException::class)
fun newSha256Fingerprint(cert: X509Certificate): Fingerprint {
@ -79,6 +79,6 @@ data class Fingerprint(
@JsonClass(generateAdapter = false)
enum class HashType {
@Json(name = "sha-1") SHA1,
@Json(name = "sha-256")SHA256
@Json(name = "sha-256") SHA256
}
}

View File

@ -34,16 +34,19 @@ import javax.net.ssl.X509TrustManager
internal class PinnedTrustManager(private val fingerprints: List<Fingerprint>?,
private val defaultTrustManager: X509TrustManager?) : X509TrustManager {
// Set to false to perform some test
private val USE_DEFAULT_TRUST_MANAGER = true
@Throws(CertificateException::class)
override fun checkClientTrusted(chain: Array<X509Certificate>, s: String) {
try {
if (defaultTrustManager != null) {
if (defaultTrustManager != null && USE_DEFAULT_TRUST_MANAGER) {
defaultTrustManager.checkClientTrusted(chain, s)
return
}
} catch (e: CertificateException) {
// If there is an exception we fall back to checking fingerprints
if (fingerprints == null || fingerprints.isEmpty()) {
if (fingerprints.isNullOrEmpty()) {
throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause)
}
}
@ -54,14 +57,14 @@ internal class PinnedTrustManager(private val fingerprints: List<Fingerprint>?,
@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate>, s: String) {
try {
if (defaultTrustManager != null) {
if (defaultTrustManager != null && USE_DEFAULT_TRUST_MANAGER) {
defaultTrustManager.checkServerTrusted(chain, s)
return
}
} catch (e: CertificateException) {
// If there is an exception we fall back to checking fingerprints
if (fingerprints == null || fingerprints.isEmpty()) {
throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause)
throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */)
}
}

View File

@ -26,7 +26,7 @@ import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachmen
import im.vector.matrix.android.internal.di.CacheDirectory
import im.vector.matrix.android.internal.di.ExternalFilesDirectory
import im.vector.matrix.android.internal.di.SessionCacheDirectory
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
@ -49,7 +49,7 @@ internal class DefaultFileService @Inject constructor(
@SessionCacheDirectory
private val sessionCacheDirectory: File,
private val contentUrlResolver: ContentUrlResolver,
@Unauthenticated
@UnauthenticatedWithCertificate
private val okHttpClient: OkHttpClient,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor

View File

@ -48,17 +48,18 @@ import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.eventbus.EventBusTimberLogger
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.DefaultNetworkConnectivityChecker
import im.vector.matrix.android.internal.network.FallbackNetworkCallbackStrategy
import im.vector.matrix.android.internal.network.NetworkCallbackStrategy
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor
import im.vector.matrix.android.internal.network.httpclient.addSocketFactory
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
import im.vector.matrix.android.internal.session.call.CallEventObserver
@ -183,29 +184,34 @@ internal abstract class SessionModule {
.build()
}
@JvmStatic
@Provides
@SessionScope
@UnauthenticatedWithCertificate
fun providesOkHttpClientWithCertificate(@Unauthenticated okHttpClient: OkHttpClient,
homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient {
return okHttpClient
.newBuilder()
.addSocketFactory(homeServerConnectionConfig)
.build()
}
@JvmStatic
@Provides
@SessionScope
@Authenticated
fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient,
fun providesOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient,
@Authenticated accessTokenProvider: AccessTokenProvider,
@SessionId sessionId: String,
@MockHttpInterceptor testInterceptor: TestInterceptor?): OkHttpClient {
return okHttpClient.newBuilder()
return okHttpClient
.newBuilder()
.addAccessTokenInterceptor(accessTokenProvider)
.apply {
// Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
interceptors().removeAll(existingCurlInterceptors)
addInterceptor(AccessTokenInterceptor(accessTokenProvider))
if (testInterceptor != null) {
testInterceptor.sessionId = sessionId
addInterceptor(testInterceptor)
}
// Re add eventually the curl logging interceptors
existingCurlInterceptors.forEach {
addInterceptor(it)
}
}
.build()
}

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.homeserver
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.internal.auth.version.Versions
@ -43,6 +44,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
private val eventBus: EventBus,
private val getWellknownTask: GetWellknownTask,
private val configExtractor: IntegrationManagerConfigExtractor,
private val homeServerConnectionConfig: HomeServerConnectionConfig,
@UserId
private val userId: String
) : GetHomeServerCapabilitiesTask {
@ -78,7 +80,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
}.getOrNull()
val wellknownResult = runCatching {
getWellknownTask.execute(GetWellknownTask.Params(userId))
getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig))
}.getOrNull()
insertInDb(capabilities, uploadCapabilities, versions, wellknownResult)

View File

@ -36,7 +36,7 @@ import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
import im.vector.matrix.android.internal.extensions.observeNotNull
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.session.SessionLifecycleObserver
@ -68,7 +68,7 @@ internal class DefaultIdentityService @Inject constructor(
private val identityPingTask: IdentityPingTask,
private val identityDisconnectTask: IdentityDisconnectTask,
private val identityRequestTokenForBindingTask: IdentityRequestTokenForBindingTask,
@Unauthenticated
@UnauthenticatedWithCertificate
private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
@AuthenticatedIdentity
private val okHttpClient: Lazy<OkHttpClient>,

View File

@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
import im.vector.matrix.android.internal.di.IdentityDatabase
import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
@ -45,9 +45,12 @@ internal abstract class IdentityModule {
@Provides
@SessionScope
@AuthenticatedIdentity
fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient,
fun providesOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient,
@AuthenticatedIdentity accessTokenProvider: AccessTokenProvider): OkHttpClient {
return okHttpClient.addAccessTokenInterceptor(accessTokenProvider)
return okHttpClient
.newBuilder()
.addAccessTokenInterceptor(accessTokenProvider)
.build()
}
@JvmStatic

View File

@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.terms.GetTermsResponse
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
import im.vector.matrix.android.internal.network.NetworkConstants
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
@ -41,7 +41,7 @@ import okhttp3.OkHttpClient
import javax.inject.Inject
internal class DefaultTermsService @Inject constructor(
@Unauthenticated
@UnauthenticatedWithCertificate
private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
private val accountDataDataSource: AccountDataDataSource,
private val termsAPI: TermsAPI,

View File

@ -21,7 +21,7 @@ import dagger.Lazy
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.session.SessionScope
import okhttp3.OkHttpClient
@ -34,7 +34,7 @@ internal abstract class TermsModule {
@Provides
@JvmStatic
@SessionScope
fun providesTermsAPI(@Unauthenticated unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
fun providesTermsAPI(@UnauthenticatedWithCertificate unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
retrofitFactory: RetrofitFactory): TermsAPI {
val retrofit = retrofitFactory.create(unauthenticatedOkHttpClient, "https://foo.bar")
return retrofit.create(TermsAPI::class.java)

View File

@ -19,12 +19,14 @@ package im.vector.matrix.android.internal.wellknown
import android.util.MalformedJsonException
import dagger.Lazy
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.WellKnown
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.network.httpclient.addSocketFactory
import im.vector.matrix.android.internal.session.homeserver.CapabilitiesAPI
import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI
import im.vector.matrix.android.internal.task.Task
@ -36,7 +38,8 @@ import javax.net.ssl.HttpsURLConnection
internal interface GetWellknownTask : Task<GetWellknownTask.Params, WellknownResult> {
data class Params(
val matrixId: String
val matrixId: String,
val homeServerConnectionConfig: HomeServerConnectionConfig?
)
}
@ -56,7 +59,19 @@ internal class DefaultGetWellknownTask @Inject constructor(
val homeServerDomain = params.matrixId.substringAfter(":")
return findClientConfig(homeServerDomain)
val client = buildClient(params.homeServerConnectionConfig)
return findClientConfig(homeServerDomain, client)
}
private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig?): OkHttpClient {
return if (homeServerConnectionConfig != null) {
okHttpClient.get()
.newBuilder()
.addSocketFactory(homeServerConnectionConfig)
.build()
} else {
okHttpClient.get()
}
}
/**
@ -68,8 +83,8 @@ internal class DefaultGetWellknownTask @Inject constructor(
*
* @param domain: homeserver domain, deduced from mx userId (ex: "matrix.org" from userId "@user:matrix.org")
*/
private suspend fun findClientConfig(domain: String): WellknownResult {
val wellKnownAPI = retrofitFactory.create(okHttpClient, "https://dummy.org")
private suspend fun findClientConfig(domain: String, client: OkHttpClient): WellknownResult {
val wellKnownAPI = retrofitFactory.create(client, "https://dummy.org")
.create(WellKnownAPI::class.java)
return try {
@ -84,7 +99,7 @@ internal class DefaultGetWellknownTask @Inject constructor(
} else {
if (homeServerBaseUrl.isValidUrl()) {
// Check that HS is a real one
validateHomeServer(homeServerBaseUrl, wellKnown)
validateHomeServer(homeServerBaseUrl, wellKnown, client)
} else {
WellknownResult.FailError
}
@ -113,8 +128,8 @@ internal class DefaultGetWellknownTask @Inject constructor(
/**
* Return true if home server is valid, and (if applicable) if identity server is pingable
*/
private suspend fun validateHomeServer(homeServerBaseUrl: String, wellKnown: WellKnown): WellknownResult {
val capabilitiesAPI = retrofitFactory.create(okHttpClient, homeServerBaseUrl)
private suspend fun validateHomeServer(homeServerBaseUrl: String, wellKnown: WellKnown, client: OkHttpClient): WellknownResult {
val capabilitiesAPI = retrofitFactory.create(client, homeServerBaseUrl)
.create(CapabilitiesAPI::class.java)
try {
@ -135,7 +150,7 @@ internal class DefaultGetWellknownTask @Inject constructor(
WellknownResult.FailError
} else {
if (identityServerBaseUrl.isValidUrl()) {
if (validateIdentityServer(identityServerBaseUrl)) {
if (validateIdentityServer(identityServerBaseUrl, client)) {
// All is ok
WellknownResult.Prompt(homeServerBaseUrl, identityServerBaseUrl, wellKnown)
} else {
@ -151,8 +166,8 @@ internal class DefaultGetWellknownTask @Inject constructor(
/**
* Return true if identity server is pingable
*/
private suspend fun validateIdentityServer(identityServerBaseUrl: String): Boolean {
val identityPingApi = retrofitFactory.create(okHttpClient, identityServerBaseUrl)
private suspend fun validateIdentityServer(identityServerBaseUrl: String, client: OkHttpClient): Boolean {
val identityPingApi = retrofitFactory.create(client, identityServerBaseUrl)
.create(IdentityAuthAPI::class.java)
return try {

View File

@ -108,14 +108,22 @@ else
chmod u+x ${checkLongFilesScript}
fi
echo
echo "Search for long files..."
maxLines=2500
${checkLongFilesScript} 2500 \
echo
echo "Search for kotlin files with more than ${maxLines} lines..."
${checkLongFilesScript} ${maxLines} \
./matrix-sdk-android/src/main/java \
./matrix-sdk-android-rx/src/main/java \
./vector/src/androidTest/java \
./vector/src/debug/java \
./vector/src/fdroid/java \
./vector/src/gplay/java \
./vector/src/main/java \
./vector/src/main/res/layout \
./vector/src/main/res/values \
./vector/src/main/res/values-v21 \
./vector/src/release/java \
./vector/src/sharedTest/java \
./vector/src/test/java
resultLongFiles=$?

View File

@ -21,6 +21,7 @@ import androidx.fragment.app.FragmentFactory
import androidx.lifecycle.ViewModelProvider
import dagger.BindsInstance
import dagger.Component
import im.vector.riotx.core.dialogs.UnrecognizedCertificateDialog
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.preference.UserAvatarPreference
import im.vector.riotx.features.MainActivity
@ -100,6 +101,7 @@ interface ScreenComponent {
fun navigator(): Navigator
fun errorFormatter(): ErrorFormatter
fun uiStateRepository(): UiStateRepository
fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog
/* ==========================================================================================
* Activities

View File

@ -27,6 +27,7 @@ import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.VectorApplication
import im.vector.riotx.core.dialogs.UnrecognizedCertificateDialog
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.core.utils.AssetReader
@ -89,6 +90,8 @@ interface VectorComponent {
fun activeSessionHolder(): ActiveSessionHolder
fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog
fun emojiCompatFontProvider(): EmojiCompatFontProvider
fun emojiCompatWrapper(): EmojiCompatWrapper

View File

@ -0,0 +1,170 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.dialogs
import android.app.Activity
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import im.vector.matrix.android.internal.network.ssl.Fingerprint
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.StringProvider
import timber.log.Timber
import java.util.HashMap
import java.util.HashSet
import javax.inject.Inject
/**
* This class displays the unknown certificate dialog
*/
class UnrecognizedCertificateDialog @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
private val stringProvider: StringProvider
) {
private val ignoredFingerprints: MutableMap<String, MutableSet<Fingerprint>> = HashMap()
private val openDialogIds: MutableSet<String> = HashSet()
/**
* Display a certificate dialog box, asking the user about an unknown certificate
* To use when user is currently logged in
*
* @param unrecognizedFingerprint the fingerprint for the unknown certificate
* @param callback callback to fire when the user makes a decision
*/
fun show(activity: Activity,
unrecognizedFingerprint: Fingerprint,
callback: Callback) {
val userId = activeSessionHolder.getSafeActiveSession()?.myUserId
val hsConfig = activeSessionHolder.getSafeActiveSession()?.sessionParams?.homeServerConnectionConfig ?: return
internalShow(activity, unrecognizedFingerprint, true, callback, userId, hsConfig.homeServerUri.toString(), hsConfig.allowedFingerprints.isNotEmpty())
}
/**
* To use during login flow
*/
fun show(activity: Activity,
unrecognizedFingerprint: Fingerprint,
homeServerUrl: String,
callback: Callback) {
internalShow(activity, unrecognizedFingerprint, false, callback, null, homeServerUrl, false)
}
/**
* Display a certificate dialog box, asking the user about an unknown certificate
*
* @param unrecognizedFingerprint the fingerprint for the unknown certificate
* @param existing the current session already exist, so it mean that something has changed server side
* @param callback callback to fire when the user makes a decision
*/
private fun internalShow(activity: Activity,
unrecognizedFingerprint: Fingerprint,
existing: Boolean,
callback: Callback,
userId: String?,
homeServerUrl: String,
homeServerConnectionConfigHasFingerprints: Boolean) {
val dialogId = userId ?: homeServerUrl + unrecognizedFingerprint.displayableHexRepr
if (openDialogIds.contains(dialogId)) {
Timber.i("Not opening dialog $dialogId as one is already open.")
return
}
if (userId != null) {
val f: Set<Fingerprint>? = ignoredFingerprints[userId]
if (f != null && f.contains(unrecognizedFingerprint)) {
callback.onIgnore()
return
}
}
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
val layout: View = inflater.inflate(R.layout.dialog_ssl_fingerprint, null)
val sslFingerprintTitle = layout.findViewById<TextView>(R.id.ssl_fingerprint_title)
sslFingerprintTitle.text = stringProvider.getString(R.string.ssl_fingerprint_hash, unrecognizedFingerprint.hashType.toString())
val sslFingerprint = layout.findViewById<TextView>(R.id.ssl_fingerprint)
sslFingerprint.text = unrecognizedFingerprint.displayableHexRepr
val sslUserId = layout.findViewById<TextView>(R.id.ssl_user_id)
if (userId != null) {
sslUserId.text = stringProvider.getString(R.string.generic_label_and_value,
stringProvider.getString(R.string.username),
userId)
} else {
sslUserId.text = stringProvider.getString(R.string.generic_label_and_value,
stringProvider.getString(R.string.hs_url),
homeServerUrl)
}
val sslExpl = layout.findViewById<TextView>(R.id.ssl_explanation)
if (existing) {
if (homeServerConnectionConfigHasFingerprints) {
sslExpl.text = stringProvider.getString(R.string.ssl_expected_existing_expl)
} else {
sslExpl.text = stringProvider.getString(R.string.ssl_unexpected_existing_expl)
}
} else {
sslExpl.text = stringProvider.getString(R.string.ssl_cert_new_account_expl)
}
builder.setView(layout)
builder.setTitle(R.string.ssl_could_not_verify)
builder.setPositiveButton(R.string.ssl_trust) { _, _ ->
callback.onAccept()
}
if (existing) {
builder.setNegativeButton(R.string.ssl_remain_offline) { _, _ ->
if (userId != null) {
var f = ignoredFingerprints[userId]
if (f == null) {
f = HashSet()
ignoredFingerprints[userId] = f
}
f.add(unrecognizedFingerprint)
}
callback.onIgnore()
}
builder.setNeutralButton(R.string.ssl_logout_account) { _, _ -> callback.onReject() }
} else {
builder.setNegativeButton(R.string.cancel) { _, _ -> callback.onReject() }
}
builder.setOnDismissListener {
Timber.d("Dismissed!")
openDialogIds.remove(dialogId)
}
builder.show()
openDialogIds.add(dialogId)
}
interface Callback {
/**
* The certificate was explicitly accepted
*/
fun onAccept()
/**
* The warning was ignored by the user
*/
fun onIgnore()
/**
* The unknown certificate was explicitly rejected
*/
fun onReject()
}
}

View File

@ -26,6 +26,8 @@ import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import javax.inject.Inject
import javax.net.ssl.SSLException
import javax.net.ssl.SSLPeerUnverifiedException
interface ErrorFormatter {
fun toHumanReadable(throwable: Throwable?): String
@ -41,13 +43,17 @@ class DefaultErrorFormatter @Inject constructor(
is IdentityServiceError -> identityServerError(throwable)
is Failure.NetworkConnection -> {
when (throwable.ioException) {
is SocketTimeoutException ->
is SocketTimeoutException ->
stringProvider.getString(R.string.error_network_timeout)
is UnknownHostException ->
is UnknownHostException ->
// Invalid homeserver?
// TODO Check network state, airplane mode, etc.
stringProvider.getString(R.string.login_error_unknown_host)
else ->
is SSLPeerUnverifiedException ->
stringProvider.getString(R.string.login_error_ssl_peer_unverified)
is SSLException ->
stringProvider.getString(R.string.login_error_ssl_other)
else ->
stringProvider.getString(R.string.error_no_network)
}
}

View File

@ -55,7 +55,10 @@ import im.vector.riotx.core.di.HasVectorInjector
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.di.VectorComponent
import im.vector.riotx.core.dialogs.DialogLocker
import im.vector.riotx.core.dialogs.UnrecognizedCertificateDialog
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.vectorComponent
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs
@ -231,7 +234,30 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
is GlobalError.ConsentNotGivenError ->
consentNotGivenHelper.displayDialog(globalError.consentUri,
activeSessionHolder.getActiveSession().sessionParams.homeServerHost ?: "")
}
is GlobalError.CertificateError ->
handleCertificateError(globalError)
}.exhaustive
}
private fun handleCertificateError(certificateError: GlobalError.CertificateError) {
vectorComponent()
.unrecognizedCertificateDialog()
.show(this,
certificateError.fingerprint,
object : UnrecognizedCertificateDialog.Callback {
override fun onAccept() {
// TODO Support certificate error once logged
}
override fun onIgnore() {
// TODO Support certificate error once logged
}
override fun onReject() {
// TODO Support certificate error once logged
}
}
)
}
protected open fun handleInvalidToken(globalError: GlobalError.InvalidToken) {

View File

@ -44,6 +44,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.dialogs.UnrecognizedCertificateDialog
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.features.navigation.Navigator
import io.reactivex.android.schedulers.AndroidSchedulers
@ -69,6 +70,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
protected lateinit var navigator: Navigator
protected lateinit var errorFormatter: ErrorFormatter
protected lateinit var unrecognizedCertificateDialog: UnrecognizedCertificateDialog
private var progress: ProgressDialog? = null
@ -92,6 +94,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
navigator = screenComponent.navigator()
errorFormatter = screenComponent.errorFormatter()
unrecognizedCertificateDialog = screenComponent.unrecognizedCertificateDialog()
viewModelFactory = screenComponent.viewModelFactory()
childFragmentManager.fragmentFactory = screenComponent.fragmentFactory()
injectWith(injector())

View File

@ -27,6 +27,7 @@ import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.UnrecognizedCertificateDialog
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.OnBackPressed
import im.vector.riotx.core.platform.VectorBaseFragment
@ -72,7 +73,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
override fun showFailure(throwable: Throwable) {
when (throwable) {
is Failure.ServerError -> {
is Failure.ServerError ->
if (throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {
AlertDialog.Builder(requireActivity())
@ -83,11 +84,34 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
} else {
onError(throwable)
}
}
else -> onError(throwable)
is Failure.UnrecognizedCertificateFailure ->
showUnrecognizedCertificateFailure(throwable)
else ->
onError(throwable)
}
}
private fun showUnrecognizedCertificateFailure(failure: Failure.UnrecognizedCertificateFailure) {
// Ask the user to accept the certificate
unrecognizedCertificateDialog.show(requireActivity(),
failure.fingerprint,
failure.url,
object : UnrecognizedCertificateDialog.Callback {
override fun onAccept() {
// User accept the certificate
loginViewModel.handle(LoginAction.UserAcceptCertificate(failure.fingerprint))
}
override fun onIgnore() {
// Cannot happen in this case
}
override fun onReject() {
// Nothing to do in this case
}
})
}
open fun onError(throwable: Throwable) {
super.showFailure(throwable)
}

View File

@ -18,6 +18,7 @@ package im.vector.riotx.features.login
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
import im.vector.matrix.android.internal.network.ssl.Fingerprint
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class LoginAction : VectorViewModelAction {
@ -61,4 +62,6 @@ sealed class LoginAction : VectorViewModelAction {
data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction()
data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction()
data class UserAcceptCertificate(val fingerprint: Fingerprint) : LoginAction()
}

View File

@ -133,15 +133,14 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
}
}
}
is LoginViewEvents.OutdatedHomeserver ->
is LoginViewEvents.OutdatedHomeserver -> {
AlertDialog.Builder(this)
.setTitle(R.string.login_error_outdated_homeserver_title)
.setMessage(R.string.login_error_outdated_homeserver_content)
.setPositiveButton(R.string.ok, null)
.show()
is LoginViewEvents.Failure ->
// This is handled by the Fragments
Unit
}
is LoginViewEvents.OpenServerSelection ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginServerSelectionFragment::class.java,
@ -195,7 +194,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginViewEvents.msisdn),
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption)
}
is LoginViewEvents.Failure,
is LoginViewEvents.Loading ->
// This is handled by the Fragments
Unit
}.exhaustive
}
private fun updateWithState(loginViewState: LoginViewState) {

View File

@ -87,6 +87,8 @@ class LoginViewModel @AssistedInject constructor(
}
}
private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null
val currentThreePid: String?
get() = registrationWizard?.currentThreePid
@ -118,10 +120,18 @@ class LoginViewModel @AssistedInject constructor(
is LoginAction.RegisterAction -> handleRegisterAction(action)
is LoginAction.ResetAction -> handleResetAction(action)
is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action)
is LoginAction.UserAcceptCertificate -> handleUserAcceptCertificate(action)
is LoginAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
}.exhaustive
}
private fun handleUserAcceptCertificate(action: LoginAction.UserAcceptCertificate) {
// It happen when we get the login flow, so alter the homeserver config and retrieve again the login flow
currentHomeServerConnectionConfig
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
?.let { getLoginFlow(it) }
}
private fun handleLoginWithToken(action: LoginAction.LoginWithToken) {
val safeLoginWizard = loginWizard
@ -486,7 +496,8 @@ class LoginViewModel @AssistedInject constructor(
)
}
authenticationService.getWellKnownData(action.username, object : MatrixCallback<WellknownResult> {
// TODO Handle certificate error in this case. Direct login is deactivated now, so we will handle that later
authenticationService.getWellKnownData(action.username, null, object : MatrixCallback<WellknownResult> {
override fun onSuccess(data: WellknownResult) {
when (data) {
is WellknownResult.Prompt ->
@ -649,67 +660,74 @@ class LoginViewModel @AssistedInject constructor(
// This is invalid
_viewEvents.post(LoginViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
} else {
currentTask?.cancel()
currentTask = null
authenticationService.cancelPendingLoginOrRegistration()
getLoginFlow(homeServerConnectionConfig)
}
}
setState {
copy(
asyncHomeServerLoginFlowRequest = Loading()
)
private fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig) {
currentHomeServerConnectionConfig = homeServerConnectionConfig
currentTask?.cancel()
currentTask = null
authenticationService.cancelPendingLoginOrRegistration()
setState {
copy(
asyncHomeServerLoginFlowRequest = Loading()
)
}
currentTask = authenticationService.getLoginFlow(homeServerConnectionConfig, object : MatrixCallback<LoginFlowResult> {
override fun onFailure(failure: Throwable) {
_viewEvents.post(LoginViewEvents.Failure(failure))
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized
)
}
}
currentTask = authenticationService.getLoginFlow(homeServerConnectionConfig, object : MatrixCallback<LoginFlowResult> {
override fun onFailure(failure: Throwable) {
_viewEvents.post(LoginViewEvents.Failure(failure))
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized
)
}
}
override fun onSuccess(data: LoginFlowResult) {
when (data) {
is LoginFlowResult.Success -> {
val loginMode = when {
// SSO login is taken first
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) {
notSupported()
} else {
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized,
homeServerUrl = data.homeServerUrl,
loginMode = loginMode,
loginModeSupportedTypes = data.supportedLoginTypes.toList()
)
}
}
override fun onSuccess(data: LoginFlowResult) {
when (data) {
is LoginFlowResult.Success -> {
val loginMode = when {
// SSO login is taken first
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}
is LoginFlowResult.OutdatedHomeserver -> {
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) {
notSupported()
} else {
// FIXME We should post a view event here normally?
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized,
homeServerUrl = data.homeServerUrl,
loginMode = loginMode,
loginModeSupportedTypes = data.supportedLoginTypes.toList()
)
}
}
}
}
private fun notSupported() {
// Notify the UI
_viewEvents.post(LoginViewEvents.OutdatedHomeserver)
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized
)
is LoginFlowResult.OutdatedHomeserver -> {
notSupported()
}
}
})
}
}
private fun notSupported() {
// Notify the UI
_viewEvents.post(LoginViewEvents.OutdatedHomeserver)
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized
)
}
}
})
}
override fun onCleared() {

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingLeft="?dialogPreferredPadding"
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding"
android:paddingRight="?dialogPreferredPadding">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/ssl_cert_not_trust" />
<TextView
android:id="@+id/ssl_explanation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/ssl_cert_new_account_expl" />
<TextView
android:id="@+id/ssl_user_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
tools:text="Home Server URL: http://matrix.org" />
<TextView
android:id="@+id/ssl_fingerprint_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/ssl_fingerprint_hash" />
<TextView
android:id="@+id/ssl_fingerprint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
tools:text="07 89 7A A0 30 82 99 95 E6 17 5D 1F 34 5D 8D 0C 67 82 63 1C 1F 57 20 75 42 91 F7 8B 28 03 54 A2" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/ssl_only_accept" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -298,6 +298,8 @@
<string name="login_error_unknown_host">This URL is not reachable, please check it</string>
<string name="login_error_no_homeserver_found">This is not a valid Matrix server address</string>
<string name="login_error_homeserver_not_found">Cannot reach a homeserver at this URL, please check it</string>
<string name="login_error_ssl_peer_unverified">"SSL Error: the peer's identity has not been verified."</string>
<string name="login_error_ssl_other">"SSL Error."</string>
<string name="login_error_ssl_handshake">Your device is using an outdated TLS security protocol, vulnerable to attack, for your security you will not be able to connect</string>
<string name="login_mobile_device">Mobile</string>