Create LegacySessionImporter class and import credentials - tested OK

This commit is contained in:
Benoit Marty 2020-06-18 21:53:04 +02:00
parent 7247b4471a
commit a0a8f95d37
20 changed files with 2074 additions and 6 deletions

View file

@ -23,6 +23,7 @@ import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.legacy.LegacySessionImporter
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
@ -41,6 +42,7 @@ import javax.inject.Inject
*/
class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) {
@Inject internal lateinit var legacySessionImporter: LegacySessionImporter
@Inject internal lateinit var authenticationService: AuthenticationService
@Inject internal lateinit var userAgentHolder: UserAgentHolder
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
@ -62,6 +64,10 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
return authenticationService
}
fun legacySessionImporter(): LegacySessionImporter {
return legacySessionImporter
}
companion object {
private lateinit var instance: Matrix

View file

@ -34,10 +34,10 @@ data class HomeServerConnectionConfig(
val homeServerUri: Uri,
val identityServerUri: Uri? = null,
val antiVirusServerUri: Uri? = null,
val allowedFingerprints: MutableList<Fingerprint> = ArrayList(),
val allowedFingerprints: List<Fingerprint> = emptyList(),
val shouldPin: Boolean = false,
val tlsVersions: MutableList<TlsVersion>? = null,
val tlsCipherSuites: MutableList<CipherSuite>? = null,
val tlsVersions: List<TlsVersion>? = null,
val tlsCipherSuites: List<CipherSuite>? = null,
val shouldAcceptTlsExtensions: Boolean = true,
val allowHttpExtension: Boolean = false,
val forceUsageTlsVersions: Boolean = false

View file

@ -0,0 +1,25 @@
/*
* 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.api.legacy
interface LegacySessionImporter {
/**
* Will eventually import a session created by the legacy app.
*/
fun process()
}

View file

@ -21,6 +21,7 @@ import dagger.Binds
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.legacy.LegacySessionImporter
import im.vector.matrix.android.internal.auth.db.AuthRealmMigration
import im.vector.matrix.android.internal.auth.db.AuthRealmModule
import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore
@ -29,6 +30,7 @@ import im.vector.matrix.android.internal.auth.login.DefaultDirectLoginTask
import im.vector.matrix.android.internal.auth.login.DirectLoginTask
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.AuthDatabase
import im.vector.matrix.android.internal.legacy.DefaultLegacySessionImporter
import im.vector.matrix.android.internal.wellknown.WellknownModule
import io.realm.RealmConfiguration
import java.io.File
@ -61,6 +63,9 @@ internal abstract class AuthModule {
}
}
@Binds
abstract fun bindLegacySessionImporter(service: DefaultLegacySessionImporter): LegacySessionImporter
@Binds
abstract fun bindSessionParamsStore(store: RealmSessionParamsStore): SessionParamsStore

View file

@ -121,8 +121,10 @@ internal abstract class CryptoModule {
.apply {
realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
}
.name("crypto_store.realm")
// Add `_x` because of the name clash with legacy Riot
.name("crypto_store_x.realm")
.modules(RealmCryptoStoreModule())
// TODO Cleanup the migration
.schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION)
.migration(realmCryptoStoreMigration)
.build()

View file

@ -0,0 +1,114 @@
/*
* 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
import android.content.Context
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.DiscoveryInformation
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.data.WellKnownBaseConfig
import im.vector.matrix.android.api.legacy.LegacySessionImporter
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.legacy.riot.LoginStorage
import im.vector.matrix.android.internal.network.ssl.Fingerprint
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
internal class DefaultLegacySessionImporter @Inject constructor(
context: Context,
private val sessionParamsStore: SessionParamsStore
) : LegacySessionImporter {
private val loginStorage = LoginStorage(context)
override fun process() {
Timber.d("Migration: Importing legacy session")
val list = loginStorage.credentialsList
Timber.d("Migration: found ${list.size} session(s).")
val firstConfig = list.firstOrNull() ?: return
Timber.d("Migration: importing a session")
@Suppress("DEPRECATION")
val sessionParams = SessionParams(
credentials = Credentials(
userId = firstConfig.credentials.userId,
accessToken = firstConfig.credentials.accessToken,
refreshToken = firstConfig.credentials.refreshToken,
homeServer = firstConfig.credentials.homeServer,
deviceId = firstConfig.credentials.deviceId,
discoveryInformation = firstConfig.credentials.wellKnown?.let { wellKnown ->
// Note credentials.wellKnown is not serialized in the LoginStorage, so this code is a bit useless...
if (wellKnown.homeServer?.baseURL != null
|| wellKnown.identityServer?.baseURL != null) {
DiscoveryInformation(
homeServer = wellKnown.homeServer?.baseURL?.let { WellKnownBaseConfig(baseURL = it) },
identityServer = wellKnown.identityServer?.baseURL?.let { WellKnownBaseConfig(baseURL = it) }
)
} else {
null
}
}
),
homeServerConnectionConfig = HomeServerConnectionConfig(
homeServerUri = firstConfig.homeserverUri,
identityServerUri = firstConfig.identityServerUri,
antiVirusServerUri = firstConfig.antiVirusServerUri,
allowedFingerprints = firstConfig.allowedFingerprints.map {
Fingerprint(
bytes = it.bytes,
hashType = when (it.type) {
im.vector.matrix.android.internal.legacy.riot.Fingerprint.HashType.SHA1,
null -> Fingerprint.HashType.SHA1
im.vector.matrix.android.internal.legacy.riot.Fingerprint.HashType.SHA256 -> Fingerprint.HashType.SHA256
}
)
},
shouldPin = firstConfig.shouldPin(),
tlsVersions = firstConfig.acceptedTlsVersions,
tlsCipherSuites = firstConfig.acceptedTlsCipherSuites,
shouldAcceptTlsExtensions = firstConfig.shouldAcceptTlsExtensions(),
allowHttpExtension = false, // TODO
forceUsageTlsVersions = firstConfig.forceUsageOfTlsVersions()
),
// If token is not valid, this boolean will be updated later
isTokenValid = true
)
Timber.d("Migration: save session")
GlobalScope.launch {
sessionParamsStore.save(sessionParams)
}
Timber.d("Migration: clear legacy session")
// Delete to avoid doing this several times
loginStorage.clear()
// TODO Crypto DB
// TODO Remove
Thread.sleep(5000)
}
}

View file

@ -0,0 +1,284 @@
/*
* 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

@ -0,0 +1,112 @@
/*
* 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.text.TextUtils;
import org.jetbrains.annotations.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
/*
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
*/
/**
* The user's credentials.
*/
public class Credentials {
public String userId;
// This is the server name and not a URI, e.g. "matrix.org". Spec says it's now deprecated
@Deprecated
public String homeServer;
public String accessToken;
public String refreshToken;
public String deviceId;
// Optional data that may contain info to override home server and/or identity server
public WellKnown wellKnown;
public JSONObject toJson() throws JSONException {
JSONObject json = new JSONObject();
json.put("user_id", userId);
json.put("home_server", homeServer);
json.put("access_token", accessToken);
json.put("refresh_token", TextUtils.isEmpty(refreshToken) ? JSONObject.NULL : refreshToken);
json.put("device_id", deviceId);
return json;
}
public static Credentials fromJson(JSONObject obj) throws JSONException {
Credentials creds = new Credentials();
creds.userId = obj.getString("user_id");
creds.homeServer = obj.getString("home_server");
creds.accessToken = obj.getString("access_token");
if (obj.has("device_id")) {
creds.deviceId = obj.getString("device_id");
}
// refresh_token is mandatory
if (obj.has("refresh_token")) {
try {
creds.refreshToken = obj.getString("refresh_token");
} catch (Exception e) {
creds.refreshToken = null;
}
} else {
throw new RuntimeException("refresh_token is required.");
}
return creds;
}
@Override
public String toString() {
return "Credentials{" +
"userId='" + userId + '\'' +
", homeServer='" + homeServer + '\'' +
", refreshToken.length='" + (refreshToken != null ? refreshToken.length() : "null") + '\'' +
", accessToken.length='" + (accessToken != null ? accessToken.length() : "null") + '\'' +
'}';
}
@Nullable
public String getUserId() {
return userId;
}
@Nullable
public String getHomeServer() {
return homeServer;
}
@Nullable
public String getAccessToken() {
return accessToken;
}
@Nullable
public String getDeviceId() {
return deviceId;
}
}

View file

@ -0,0 +1,134 @@
/*
* 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.Base64;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
/*
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
*/
/**
* Represents a X509 Certificate fingerprint.
*/
public class Fingerprint {
public enum HashType {
SHA1,
SHA256
}
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() {
return mHashType;
}
public byte[] getBytes() {
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));
obj.put("hash_type", mHashType.toString());
return obj;
}
public static Fingerprint fromJson(JSONObject obj) throws JSONException {
String hashTypeStr = obj.getString("hash_type");
byte[] fingerprintBytes = Base64.decode(obj.getString("bytes"), Base64.DEFAULT);
final HashType hashType;
if ("SHA256".equalsIgnoreCase(hashTypeStr)) {
hashType = HashType.SHA256;
} else if ("SHA1".equalsIgnoreCase(hashTypeStr)) {
hashType = HashType.SHA1;
} else {
throw new JSONException("Unrecognized hash type: " + hashTypeStr);
}
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;
if (o == null || getClass() != o.getClass()) return false;
Fingerprint that = (Fingerprint) o;
if (!Arrays.equals(mBytes, that.mBytes)) return false;
return mHashType == that.mHashType;
}
@Override
public int hashCode() {
int result = mBytes != null ? Arrays.hashCode(mBytes) : 0;
result = 31 * result + (mHashType != null ? mHashType.hashCode() : 0);
return result;
}
}

View file

@ -0,0 +1,676 @@
/*
* 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.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.ArrayList;
import java.util.List;
import okhttp3.CipherSuite;
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
*/
/**
* Represents how to connect to a specific Homeserver, may include credentials to use.
*/
public class HomeServerConnectionConfig {
// the home server URI
private Uri mHomeServerUri;
// the jitsi server URI. Can be null
@Nullable
private Uri mJitsiServerUri;
// the identity server URI. Can be null
@Nullable
private Uri mIdentityServerUri;
// the anti-virus server URI
private Uri mAntiVirusServerUri;
// allowed fingerprints
private List<Fingerprint> mAllowedFingerprints = new ArrayList<>();
// the credentials
private Credentials mCredentials;
// tell whether we should reject X509 certs that were issued by trusts CAs and only trustcerts with matching fingerprints.
private boolean mPin;
// the accepted TLS versions
private List<TlsVersion> mTlsVersions;
// the accepted TLS cipher suites
private List<CipherSuite> mTlsCipherSuites;
// should accept TLS extensions
private boolean mShouldAcceptTlsExtensions = true;
// Force usage of TLS versions
private boolean mForceUsageTlsVersions;
// the proxy hostname
private String mProxyHostname;
// the proxy port
private int mProxyPort = -1;
/**
* Private constructor. Please use the Builder
*/
private HomeServerConnectionConfig() {
// Private constructor
}
/**
* Update the home server URI.
*
* @param uri the new HS uri
*/
public void setHomeserverUri(Uri uri) {
mHomeServerUri = uri;
}
/**
* @return the home server uri
*/
public Uri getHomeserverUri() {
return mHomeServerUri;
}
/**
* @return the jitsi server uri
*/
public Uri getJitsiServerUri() {
return mJitsiServerUri;
}
/**
* @return the identity server uri, or null if not defined
*/
@Nullable
public Uri getIdentityServerUri() {
return mIdentityServerUri;
}
/**
* @return the anti-virus server uri
*/
public Uri getAntiVirusServerUri() {
if (null != mAntiVirusServerUri) {
return mAntiVirusServerUri;
}
// Else consider the HS uri by default.
return mHomeServerUri;
}
/**
* @return the allowed fingerprints.
*/
public List<Fingerprint> getAllowedFingerprints() {
return mAllowedFingerprints;
}
/**
* @return the credentials
*/
public Credentials getCredentials() {
return mCredentials;
}
/**
* Update the credentials.
*
* @param credentials the new credentials
*/
public void setCredentials(Credentials credentials) {
mCredentials = credentials;
// Override home server url and/or identity server url if provided
if (credentials.wellKnown != null) {
if (credentials.wellKnown.homeServer != null) {
String homeServerUrl = credentials.wellKnown.homeServer.baseURL;
if (!TextUtils.isEmpty(homeServerUrl)) {
// remove trailing "/"
if (homeServerUrl.endsWith("/")) {
homeServerUrl = homeServerUrl.substring(0, homeServerUrl.length() - 1);
}
Timber.d("Overriding homeserver url to " + homeServerUrl);
mHomeServerUri = Uri.parse(homeServerUrl);
}
}
if (credentials.wellKnown.identityServer != null) {
String identityServerUrl = credentials.wellKnown.identityServer.baseURL;
if (!TextUtils.isEmpty(identityServerUrl)) {
// remove trailing "/"
if (identityServerUrl.endsWith("/")) {
identityServerUrl = identityServerUrl.substring(0, identityServerUrl.length() - 1);
}
Timber.d("Overriding identity server url to " + identityServerUrl);
mIdentityServerUri = Uri.parse(identityServerUrl);
}
}
if (credentials.wellKnown.jitsiServer != null) {
String jitsiServerUrl = credentials.wellKnown.jitsiServer.preferredDomain;
if (!TextUtils.isEmpty(jitsiServerUrl)) {
// add trailing "/"
if (!jitsiServerUrl.endsWith("/")) {
jitsiServerUrl =jitsiServerUrl + "/";
}
Timber.d("Overriding jitsi server url to " + jitsiServerUrl);
mJitsiServerUri = Uri.parse(jitsiServerUrl);
}
}
}
}
/**
* @return whether we should reject X509 certs that were issued by trusts CAs and only trust
* certs with matching fingerprints.
*/
public boolean shouldPin() {
return mPin;
}
/**
* TLS versions accepted for TLS connections with the home server.
*/
@Nullable
public List<TlsVersion> getAcceptedTlsVersions() {
return mTlsVersions;
}
/**
* TLS cipher suites accepted for TLS connections with the home server.
*/
@Nullable
public List<CipherSuite> getAcceptedTlsCipherSuites() {
return mTlsCipherSuites;
}
/**
* @return whether we should accept TLS extensions.
*/
public boolean shouldAcceptTlsExtensions() {
return mShouldAcceptTlsExtensions;
}
/**
* @return true if the usage of TlsVersions has to be forced
*/
public boolean forceUsageOfTlsVersions() {
return mForceUsageTlsVersions;
}
/**
* @return proxy config if available
*/
@Nullable
public Proxy getProxyConfig() {
if (mProxyHostname == null || mProxyHostname.length() == 0 || mProxyPort == -1) {
return null;
}
return new Proxy(Proxy.Type.HTTP,
new InetSocketAddress(mProxyHostname, mProxyPort));
}
@Override
public String toString() {
return "HomeserverConnectionConfig{" +
"mHomeServerUri=" + mHomeServerUri +
", mJitsiServerUri=" + mJitsiServerUri +
", mIdentityServerUri=" + mIdentityServerUri +
", mAntiVirusServerUri=" + mAntiVirusServerUri +
", mAllowedFingerprints size=" + mAllowedFingerprints.size() +
", mCredentials=" + mCredentials +
", mPin=" + mPin +
", mShouldAcceptTlsExtensions=" + mShouldAcceptTlsExtensions +
", mProxyHostname=" + (null == mProxyHostname ? "" : mProxyHostname) +
", mProxyPort=" + (-1 == mProxyPort ? "" : mProxyPort) +
", mTlsVersions=" + (null == mTlsVersions ? "" : mTlsVersions.size()) +
", mTlsCipherSuites=" + (null == mTlsCipherSuites ? "" : mTlsCipherSuites.size()) +
'}';
}
/**
* Convert the object instance into a JSon object
*
* @return the JSon representation
* @throws JSONException the JSON conversion failure reason
*/
public JSONObject toJson() throws JSONException {
JSONObject json = new JSONObject();
json.put("home_server_url", mHomeServerUri.toString());
Uri jitsiServerUri = getJitsiServerUri();
if (jitsiServerUri != null) {
json.put("jitsi_server_url", jitsiServerUri.toString());
}
Uri identityServerUri = getIdentityServerUri();
if (identityServerUri != null) {
json.put("identity_server_url", identityServerUri.toString());
}
if (mAntiVirusServerUri != null) {
json.put("antivirus_server_url", mAntiVirusServerUri.toString());
}
json.put("pin", mPin);
if (mCredentials != null) json.put("credentials", mCredentials.toJson());
if (mAllowedFingerprints != null) {
List<JSONObject> fingerprints = new ArrayList<>(mAllowedFingerprints.size());
for (Fingerprint fingerprint : mAllowedFingerprints) {
fingerprints.add(fingerprint.toJson());
}
json.put("fingerprints", new JSONArray(fingerprints));
}
json.put("tls_extensions", mShouldAcceptTlsExtensions);
if (mTlsVersions != null) {
List<String> tlsVersions = new ArrayList<>(mTlsVersions.size());
for (TlsVersion tlsVersion : mTlsVersions) {
tlsVersions.add(tlsVersion.javaName());
}
json.put("tls_versions", new JSONArray(tlsVersions));
}
json.put("force_usage_of_tls_versions", mForceUsageTlsVersions);
if (mTlsCipherSuites != null) {
List<String> tlsCipherSuites = new ArrayList<>(mTlsCipherSuites.size());
for (CipherSuite tlsCipherSuite : mTlsCipherSuites) {
tlsCipherSuites.add(tlsCipherSuite.javaName());
}
json.put("tls_cipher_suites", new JSONArray(tlsCipherSuites));
}
if (mProxyPort != -1) {
json.put("proxy_port", mProxyPort);
}
if (mProxyHostname != null && mProxyHostname.length() > 0) {
json.put("proxy_hostname", mProxyHostname);
}
return json;
}
/**
* Create an object instance from the json object.
*
* @param jsonObject the json object
* @return a HomeServerConnectionConfig instance
* @throws JSONException the conversion failure reason
*/
public static HomeServerConnectionConfig fromJson(JSONObject jsonObject) throws JSONException {
JSONObject credentialsObj = jsonObject.optJSONObject("credentials");
Credentials creds = credentialsObj != null ? Credentials.fromJson(credentialsObj) : null;
Builder builder = new Builder()
.withHomeServerUri(Uri.parse(jsonObject.getString("home_server_url")))
.withJitsiServerUri(jsonObject.has("jitsi_server_url") ? Uri.parse(jsonObject.getString("jitsi_server_url")) : null)
.withIdentityServerUri(jsonObject.has("identity_server_url") ? Uri.parse(jsonObject.getString("identity_server_url")) : null)
.withCredentials(creds)
.withPin(jsonObject.optBoolean("pin", false));
JSONArray fingerprintArray = jsonObject.optJSONArray("fingerprints");
if (fingerprintArray != null) {
for (int i = 0; i < fingerprintArray.length(); i++) {
builder.addAllowedFingerPrint(Fingerprint.fromJson(fingerprintArray.getJSONObject(i)));
}
}
// Set the anti-virus server uri if any
if (jsonObject.has("antivirus_server_url")) {
builder.withAntiVirusServerUri(Uri.parse(jsonObject.getString("antivirus_server_url")));
}
builder.withShouldAcceptTlsExtensions(jsonObject.optBoolean("tls_extensions", true));
// Set the TLS versions if any
if (jsonObject.has("tls_versions")) {
JSONArray tlsVersionsArray = jsonObject.optJSONArray("tls_versions");
if (tlsVersionsArray != null) {
for (int i = 0; i < tlsVersionsArray.length(); i++) {
builder.addAcceptedTlsVersion(TlsVersion.forJavaName(tlsVersionsArray.getString(i)));
}
}
}
builder.forceUsageOfTlsVersions(jsonObject.optBoolean("force_usage_of_tls_versions", false));
// Set the TLS cipher suites if any
if (jsonObject.has("tls_cipher_suites")) {
JSONArray tlsCipherSuitesArray = jsonObject.optJSONArray("tls_cipher_suites");
if (tlsCipherSuitesArray != null) {
for (int i = 0; i < tlsCipherSuitesArray.length(); i++) {
builder.addAcceptedTlsCipherSuite(CipherSuite.forJavaName(tlsCipherSuitesArray.getString(i)));
}
}
}
// Set the proxy options right if any
if (jsonObject.has("proxy_hostname") && jsonObject.has("proxy_port")) {
builder.withProxy(jsonObject.getString("proxy_hostname"), jsonObject.getInt("proxy_port"));
}
return builder.build();
}
/**
* Builder
*/
public static class Builder {
private HomeServerConnectionConfig mHomeServerConnectionConfig;
/**
* Builder constructor
*/
public Builder() {
mHomeServerConnectionConfig = new HomeServerConnectionConfig();
}
/**
* create a Builder from an existing HomeServerConnectionConfig
*/
public Builder(HomeServerConnectionConfig from) {
try {
mHomeServerConnectionConfig = HomeServerConnectionConfig.fromJson(from.toJson());
} catch (JSONException e) {
// Should not happen
throw new RuntimeException("Unable to create a HomeServerConnectionConfig", e);
}
}
/**
* @param homeServerUri The URI to use to connect to the homeserver. Cannot be null
* @return this builder
*/
public Builder withHomeServerUri(final Uri homeServerUri) {
if (homeServerUri == null || (!"http".equals(homeServerUri.getScheme()) && !"https".equals(homeServerUri.getScheme()))) {
throw new RuntimeException("Invalid home server URI: " + homeServerUri);
}
// remove trailing /
if (homeServerUri.toString().endsWith("/")) {
try {
String url = homeServerUri.toString();
mHomeServerConnectionConfig.mHomeServerUri = Uri.parse(url.substring(0, url.length() - 1));
} catch (Exception e) {
throw new RuntimeException("Invalid home server URI: " + homeServerUri);
}
} else {
mHomeServerConnectionConfig.mHomeServerUri = homeServerUri;
}
return this;
}
/**
* @param jitsiServerUri The URI to use to manage identity. Can be null
* @return this builder
*/
public Builder withJitsiServerUri(@Nullable final Uri jitsiServerUri) {
if (jitsiServerUri != null
&& !jitsiServerUri.toString().isEmpty()
&& !"http".equals(jitsiServerUri.getScheme())
&& !"https".equals(jitsiServerUri.getScheme())) {
throw new RuntimeException("Invalid jitsi server URI: " + jitsiServerUri);
}
// add trailing /
if ((null != jitsiServerUri) && !jitsiServerUri.toString().endsWith("/")) {
try {
String url = jitsiServerUri.toString();
mHomeServerConnectionConfig.mJitsiServerUri = Uri.parse(url + "/");
} catch (Exception e) {
throw new RuntimeException("Invalid jitsi server URI: " + jitsiServerUri);
}
} else {
if (jitsiServerUri != null && jitsiServerUri.toString().isEmpty()) {
mHomeServerConnectionConfig.mJitsiServerUri = null;
} else {
mHomeServerConnectionConfig.mJitsiServerUri = jitsiServerUri;
}
}
return this;
}
/**
* @param identityServerUri The URI to use to manage identity. Can be null
* @return this builder
*/
public Builder withIdentityServerUri(@Nullable final Uri identityServerUri) {
if (identityServerUri != null
&& !identityServerUri.toString().isEmpty()
&& !"http".equals(identityServerUri.getScheme())
&& !"https".equals(identityServerUri.getScheme())) {
throw new RuntimeException("Invalid identity server URI: " + identityServerUri);
}
// remove trailing /
if ((null != identityServerUri) && identityServerUri.toString().endsWith("/")) {
try {
String url = identityServerUri.toString();
mHomeServerConnectionConfig.mIdentityServerUri = Uri.parse(url.substring(0, url.length() - 1));
} catch (Exception e) {
throw new RuntimeException("Invalid identity server URI: " + identityServerUri);
}
} else {
if (identityServerUri != null && identityServerUri.toString().isEmpty()) {
mHomeServerConnectionConfig.mIdentityServerUri = null;
} else {
mHomeServerConnectionConfig.mIdentityServerUri = identityServerUri;
}
}
return this;
}
/**
* @param credentials The credentials to use, if needed. Can be null.
* @return this builder
*/
public Builder withCredentials(@Nullable Credentials credentials) {
mHomeServerConnectionConfig.mCredentials = credentials;
return this;
}
/**
* @param allowedFingerprint If using SSL, allow server certs that match this fingerprint.
* @return this builder
*/
public Builder addAllowedFingerPrint(@Nullable Fingerprint allowedFingerprint) {
if (allowedFingerprint != null) {
mHomeServerConnectionConfig.mAllowedFingerprints.add(allowedFingerprint);
}
return this;
}
/**
* @param pin If true only allow certs matching given fingerprints, otherwise fallback to
* standard X509 checks.
* @return this builder
*/
public Builder withPin(boolean pin) {
mHomeServerConnectionConfig.mPin = pin;
return this;
}
/**
* @param shouldAcceptTlsExtension
* @return this builder
*/
public Builder withShouldAcceptTlsExtensions(boolean shouldAcceptTlsExtension) {
mHomeServerConnectionConfig.mShouldAcceptTlsExtensions = shouldAcceptTlsExtension;
return this;
}
/**
* Add an accepted TLS version for TLS connections with the home server.
*
* @param tlsVersion the tls version to add to the set of TLS versions accepted.
* @return this builder
*/
public Builder addAcceptedTlsVersion(@NonNull TlsVersion tlsVersion) {
if (mHomeServerConnectionConfig.mTlsVersions == null) {
mHomeServerConnectionConfig.mTlsVersions = new ArrayList<>();
}
mHomeServerConnectionConfig.mTlsVersions.add(tlsVersion);
return this;
}
/**
* Force the usage of TlsVersion. This can be usefull for device on Android version < 20
*
* @param forceUsageOfTlsVersions set to true to force the usage of specified TlsVersions (with {@link #addAcceptedTlsVersion(TlsVersion)}
* @return this builder
*/
public Builder forceUsageOfTlsVersions(boolean forceUsageOfTlsVersions) {
mHomeServerConnectionConfig.mForceUsageTlsVersions = forceUsageOfTlsVersions;
return this;
}
/**
* Add a TLS cipher suite to the list of accepted TLS connections with the home server.
*
* @param tlsCipherSuite the tls cipher suite to add.
* @return this builder
*/
public Builder addAcceptedTlsCipherSuite(@NonNull CipherSuite tlsCipherSuite) {
if (mHomeServerConnectionConfig.mTlsCipherSuites == null) {
mHomeServerConnectionConfig.mTlsCipherSuites = new ArrayList<>();
}
mHomeServerConnectionConfig.mTlsCipherSuites.add(tlsCipherSuite);
return this;
}
/**
* Update the anti-virus server URI.
*
* @param antivirusServerUri the new anti-virus uri. Can be null
* @return this builder
*/
public Builder withAntiVirusServerUri(@Nullable Uri antivirusServerUri) {
if ((null != antivirusServerUri) && (!"http".equals(antivirusServerUri.getScheme()) && !"https".equals(antivirusServerUri.getScheme()))) {
throw new RuntimeException("Invalid antivirus server URI: " + antivirusServerUri);
}
mHomeServerConnectionConfig.mAntiVirusServerUri = antivirusServerUri;
return this;
}
/**
* Convenient method to limit the TLS versions and cipher suites for this Builder
* Ref:
* - https://www.ssi.gouv.fr/uploads/2017/02/security-recommendations-for-tls_v1.1.pdf
* - https://developer.android.com/reference/javax/net/ssl/SSLEngine
*
* @param tlsLimitations true to use Tls limitations
* @param enableCompatibilityMode set to true for Android < 20
* @return this builder
*/
public Builder withTlsLimitations(boolean tlsLimitations, boolean enableCompatibilityMode) {
if (tlsLimitations) {
withShouldAcceptTlsExtensions(false);
// Tls versions
addAcceptedTlsVersion(TlsVersion.TLS_1_2);
addAcceptedTlsVersion(TlsVersion.TLS_1_3);
forceUsageOfTlsVersions(enableCompatibilityMode);
// Cipher suites
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256);
if (enableCompatibilityMode) {
// Adopt some preceding cipher suites for Android < 20 to be able to negotiate
// a TLS session.
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
}
}
return this;
}
/**
* @param proxyHostname Proxy Hostname
* @param proxyPort Proxy Port
* @return this builder
*/
public Builder withProxy(@Nullable String proxyHostname, int proxyPort) {
mHomeServerConnectionConfig.mProxyHostname = proxyHostname;
mHomeServerConnectionConfig.mProxyPort = proxyPort;
return this;
}
/**
* @return the {@link HomeServerConnectionConfig}
*/
public HomeServerConnectionConfig build() {
// Check mandatory parameters
if (mHomeServerConnectionConfig.mHomeServerUri == null) {
throw new RuntimeException("Home server URI not set");
}
return mHomeServerConnectionConfig;
}
}
}

View file

@ -0,0 +1,206 @@
/*
* 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.content.Context;
import android.content.SharedPreferences;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
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
*/
/**
* Stores login credentials in SharedPreferences.
*/
public class LoginStorage {
private static final String PREFS_LOGIN = "Vector.LoginStorage";
// multi accounts + home server config
private static final String PREFS_KEY_CONNECTION_CONFIGS = "PREFS_KEY_CONNECTION_CONFIGS";
private final Context mContext;
public LoginStorage(Context appContext) {
mContext = appContext.getApplicationContext();
}
/**
* @return the list of home server configurations.
*/
public List<HomeServerConnectionConfig> getCredentialsList() {
SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE);
String connectionConfigsString = prefs.getString(PREFS_KEY_CONNECTION_CONFIGS, null);
Timber.d("Got connection json: ");
if (connectionConfigsString == null) {
return new ArrayList<>();
}
try {
JSONArray connectionConfigsStrings = new JSONArray(connectionConfigsString);
List<HomeServerConnectionConfig> configList = new ArrayList<>(
connectionConfigsStrings.length()
);
for (int i = 0; i < connectionConfigsStrings.length(); i++) {
configList.add(
HomeServerConnectionConfig.fromJson(connectionConfigsStrings.getJSONObject(i))
);
}
return configList;
} catch (JSONException e) {
Timber.e(e, "Failed to deserialize accounts");
throw new RuntimeException("Failed to deserialize accounts");
}
}
/**
* Add a credentials to the credentials list
*
* @param config the home server config to add.
*/
public void addCredentials(HomeServerConnectionConfig config) {
if (null != config && config.getCredentials() != null) {
SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
List<HomeServerConnectionConfig> configs = getCredentialsList();
configs.add(config);
List<JSONObject> serialized = new ArrayList<>(configs.size());
try {
for (HomeServerConnectionConfig c : configs) {
serialized.add(c.toJson());
}
} catch (JSONException e) {
throw new RuntimeException("Failed to serialize connection config");
}
String ser = new JSONArray(serialized).toString();
Timber.d("Storing " + serialized.size() + " credentials");
editor.putString(PREFS_KEY_CONNECTION_CONFIGS, ser);
editor.apply();
}
}
/**
* Remove the credentials from credentials list
*
* @param config the credentials to remove
*/
public void removeCredentials(HomeServerConnectionConfig config) {
if (null != config && config.getCredentials() != null) {
Timber.d("Removing account: " + config.getCredentials().userId);
SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
List<HomeServerConnectionConfig> configs = getCredentialsList();
List<JSONObject> serialized = new ArrayList<>(configs.size());
boolean found = false;
try {
for (HomeServerConnectionConfig c : configs) {
if (c.getCredentials().userId.equals(config.getCredentials().userId)) {
found = true;
} else {
serialized.add(c.toJson());
}
}
} catch (JSONException e) {
throw new RuntimeException("Failed to serialize connection config");
}
if (!found) return;
String ser = new JSONArray(serialized).toString();
Timber.d("Storing " + serialized.size() + " credentials");
editor.putString(PREFS_KEY_CONNECTION_CONFIGS, ser);
editor.apply();
}
}
/**
* Replace the credential from credentials list, based on credentials.userId.
* If it does not match an existing credential it does *not* insert the new credentials.
*
* @param config the credentials to insert
*/
public void replaceCredentials(HomeServerConnectionConfig config) {
if (null != config && config.getCredentials() != null) {
SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
List<HomeServerConnectionConfig> configs = getCredentialsList();
List<JSONObject> serialized = new ArrayList<>(configs.size());
boolean found = false;
try {
for (HomeServerConnectionConfig c : configs) {
if (c.getCredentials().userId.equals(config.getCredentials().userId)) {
serialized.add(config.toJson());
found = true;
} else {
serialized.add(c.toJson());
}
}
} catch (JSONException e) {
throw new RuntimeException("Failed to serialize connection config");
}
if (!found) return;
String ser = new JSONArray(serialized).toString();
Timber.d("Storing " + serialized.size() + " credentials");
editor.putString(PREFS_KEY_CONNECTION_CONFIGS, ser);
editor.apply();
}
}
/**
* Clear the stored values
*/
public void clear() {
SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.remove(PREFS_KEY_CONNECTION_CONFIGS);
//Need to commit now because called before forcing an app restart
editor.commit();
}
}

View file

@ -0,0 +1,107 @@
/*
* 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

@ -0,0 +1,135 @@
/*
* 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

@ -0,0 +1,47 @@
/*
* 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

@ -0,0 +1,97 @@
/*
* 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 com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/*
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
*/
/**
* https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
* <pre>
* {
* "m.homeserver": {
* "base_url": "https://matrix.org"
* },
* "m.identity_server": {
* "base_url": "https://vector.im"
* }
* "m.integrations": {
* "managers": [
* {
* "api_url": "https://integrations.example.org",
* "ui_url": "https://integrations.example.org/ui"
* },
* {
* "api_url": "https://bots.example.org"
* }
* ]
* }
* "im.vector.riot.jitsi": {
* "preferredDomain": "https://jitsi.riot.im/"
* }
* }
* </pre>
*/
@JsonClass(generateAdapter = true)
class WellKnown {
@JvmField
@Json(name = "m.homeserver")
var homeServer: WellKnownBaseConfig? = null
@JvmField
@Json(name = "m.identity_server")
var identityServer: WellKnownBaseConfig? = null
@JvmField
@Json(name = "m.integrations")
var integrations: Map<String, *>? = null
/**
* Returns the list of integration managers proposed
*/
fun getIntegrationManagers(): List<WellKnownManagerConfig> {
val managers = ArrayList<WellKnownManagerConfig>()
integrations?.get("managers")?.let {
(it as? ArrayList<*>)?.let { configs ->
configs.forEach { config ->
(config as? Map<*, *>)?.let { map ->
val apiUrl = map["api_url"] as? String
val uiUrl = map["ui_url"] as? String ?: apiUrl
if (apiUrl != null
&& apiUrl.startsWith("https://")
&& uiUrl!!.startsWith("https://")) {
managers.add(WellKnownManagerConfig(
apiUrl = apiUrl,
uiUrl = uiUrl
))
}
}
}
}
}
return managers
}
@JvmField
@Json(name = "im.vector.riot.jitsi")
var jitsiServer: WellKnownPreferredConfig? = null
}

View file

@ -0,0 +1,40 @@
/*
* 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 com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/*
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
*/
/**
* https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
* <pre>
* {
* "base_url": "https://vector.im"
* }
* </pre>
*/
@JsonClass(generateAdapter = true)
class WellKnownBaseConfig {
@JvmField
@Json(name = "base_url")
var baseURL: String? = null
}

View file

@ -0,0 +1,25 @@
/*
* 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
/*
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
*/
data class WellKnownManagerConfig(
val apiUrl : String,
val uiUrl: String
)

View file

@ -0,0 +1,40 @@
/*
* 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 com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/*
* IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
*/
/**
* https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
* <pre>
* {
* "preferredDomain": "https://jitsi.riot.im/"
* }
* </pre>
*/
@JsonClass(generateAdapter = true)
class WellKnownPreferredConfig {
@JvmField
@Json(name = "preferredDomain")
var preferredDomain: String? = null
}

View file

@ -37,6 +37,7 @@ import com.github.piasy.biv.loader.glide.GlideImageLoader
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.legacy.LegacySessionImporter
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.DaggerVectorComponent
import im.vector.riotx.core.di.HasVectorInjector
@ -57,15 +58,15 @@ import im.vector.riotx.features.version.VersionProvider
import im.vector.riotx.push.fcm.FcmHelper
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.concurrent.Executors
import java.util.Date
import java.util.Locale
import java.util.concurrent.Executors
import javax.inject.Inject
class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider {
lateinit var appContext: Context
// font thread handler
@Inject lateinit var legacySessionImporter: LegacySessionImporter
@Inject lateinit var authenticationService: AuthenticationService
@Inject lateinit var vectorConfiguration: VectorConfiguration
@Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
@ -84,6 +85,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
lateinit var vectorComponent: VectorComponent
// font thread handler
private var fontThreadHandler: Handler? = null
override fun onCreate() {
@ -121,6 +123,10 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
emojiCompatWrapper.init(fontRequest)
notificationUtils.createNotificationChannels()
// It can takes time, but do we care?
legacySessionImporter.process()
if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession)

View file

@ -25,6 +25,7 @@ import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.legacy.LegacySessionImporter
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.core.error.DefaultErrorFormatter
import im.vector.riotx.core.error.ErrorFormatter
@ -64,6 +65,12 @@ abstract class VectorModule {
return activeSessionHolder.getActiveSession()
}
@Provides
@JvmStatic
fun providesLegacySessionImporter(matrix: Matrix): LegacySessionImporter {
return matrix.legacySessionImporter()
}
@Provides
@JvmStatic
fun providesAuthenticationService(matrix: Matrix): AuthenticationService {