Add missing dependencies to feature app
This commit is contained in:
parent
d51a33ec3c
commit
72d002571d
5 changed files with 382 additions and 0 deletions
|
@ -66,8 +66,10 @@ android {
|
|||
dependencies {
|
||||
implementation(projects.core.ui.compose.designsystem)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.mail.common)
|
||||
|
||||
implementation(projects.feature.onboarding)
|
||||
implementation(projects.feature.account.setup)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.timber)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,13 @@ package app.k9mail.feature.preview
|
|||
|
||||
import app.k9mail.core.common.oauth.OAuthConfigurationFactory
|
||||
import app.k9mail.feature.account.setup.featureAccountSetupModule
|
||||
import app.k9mail.feature.preview.auth.AndroidKeyStoreDirectoryProvider
|
||||
import app.k9mail.feature.preview.auth.AppOAuthConfigurationFactory
|
||||
import app.k9mail.feature.preview.auth.DefaultTrustedSocketFactory
|
||||
import com.fsck.k9.mail.ssl.KeyStoreDirectoryProvider
|
||||
import com.fsck.k9.mail.ssl.LocalKeyStore
|
||||
import com.fsck.k9.mail.ssl.TrustManagerFactory
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
|
@ -16,5 +22,10 @@ val featureModule: Module = module {
|
|||
|
||||
single<OAuthConfigurationFactory> { AppOAuthConfigurationFactory() }
|
||||
|
||||
factory<KeyStoreDirectoryProvider> { AndroidKeyStoreDirectoryProvider(context = get()) }
|
||||
single { LocalKeyStore(directoryProvider = get()) }
|
||||
single { TrustManagerFactory.createInstance(get()) }
|
||||
single<TrustedSocketFactory> { DefaultTrustedSocketFactory(get(), get()) }
|
||||
|
||||
includes(featureAccountSetupModule)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package app.k9mail.feature.preview.auth
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.mail.ssl.KeyStoreDirectoryProvider
|
||||
import java.io.File
|
||||
|
||||
internal class AndroidKeyStoreDirectoryProvider(private val context: Context) : KeyStoreDirectoryProvider {
|
||||
override fun getDirectory(): File {
|
||||
return context.getDir("KeyStore", Context.MODE_PRIVATE)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
package app.k9mail.feature.preview.auth;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.SSLCertificateSocketFactory;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.ssl.TrustManagerFactory;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.SNIHostName;
|
||||
import javax.net.ssl.SNIServerName;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public class DefaultTrustedSocketFactory implements TrustedSocketFactory {
|
||||
private static final String[] ENABLED_CIPHERS;
|
||||
private static final String[] ENABLED_PROTOCOLS;
|
||||
|
||||
private static final String[] DISALLOWED_CIPHERS = {
|
||||
"SSL_RSA_WITH_DES_CBC_SHA",
|
||||
"SSL_DHE_RSA_WITH_DES_CBC_SHA",
|
||||
"SSL_DHE_DSS_WITH_DES_CBC_SHA",
|
||||
"SSL_RSA_EXPORT_WITH_RC4_40_MD5",
|
||||
"SSL_RSA_EXPORT_WITH_DES40_CBC_SHA",
|
||||
"SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA",
|
||||
"SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA",
|
||||
"SSL_RSA_WITH_3DES_EDE_CBC_SHA",
|
||||
"SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
|
||||
"SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA",
|
||||
"TLS_ECDHE_RSA_WITH_RC4_128_SHA",
|
||||
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
|
||||
"TLS_ECDH_RSA_WITH_RC4_128_SHA",
|
||||
"TLS_ECDH_ECDSA_WITH_RC4_128_SHA",
|
||||
"SSL_RSA_WITH_RC4_128_SHA",
|
||||
"SSL_RSA_WITH_RC4_128_MD5",
|
||||
"TLS_ECDH_RSA_WITH_NULL_SHA",
|
||||
"TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA",
|
||||
"TLS_ECDH_anon_WITH_NULL_SHA",
|
||||
"TLS_ECDH_anon_WITH_RC4_128_SHA",
|
||||
"TLS_RSA_WITH_NULL_SHA256"
|
||||
};
|
||||
|
||||
private static final String[] DISALLOWED_PROTOCOLS = {
|
||||
"SSLv3"
|
||||
};
|
||||
|
||||
static {
|
||||
String[] enabledCiphers = null;
|
||||
String[] supportedProtocols = null;
|
||||
|
||||
try {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, null, null);
|
||||
SSLSocketFactory sf = sslContext.getSocketFactory();
|
||||
SSLSocket sock = (SSLSocket) sf.createSocket();
|
||||
enabledCiphers = sock.getEnabledCipherSuites();
|
||||
|
||||
/*
|
||||
* Retrieve all supported protocols, not just the (default) enabled
|
||||
* ones. TLSv1.1 & TLSv1.2 are supported on API levels 16+, but are
|
||||
* only enabled by default on API levels 20+.
|
||||
*/
|
||||
supportedProtocols = sock.getSupportedProtocols();
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "Error getting information about available SSL/TLS ciphers and protocols");
|
||||
}
|
||||
|
||||
ENABLED_CIPHERS = (enabledCiphers == null) ? null : remove(enabledCiphers, DISALLOWED_CIPHERS);
|
||||
ENABLED_PROTOCOLS = (supportedProtocols == null) ? null : remove(supportedProtocols, DISALLOWED_PROTOCOLS);
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final TrustManagerFactory trustManagerFactory;
|
||||
|
||||
public DefaultTrustedSocketFactory(Context context, TrustManagerFactory trustManagerFactory) {
|
||||
this.context = context;
|
||||
this.trustManagerFactory = trustManagerFactory;
|
||||
}
|
||||
|
||||
protected static String[] remove(String[] enabled, String[] disallowed) {
|
||||
List<String> items = new ArrayList<>();
|
||||
Collections.addAll(items, enabled);
|
||||
|
||||
if (disallowed != null) {
|
||||
for (String item : disallowed) {
|
||||
items.remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
return items.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public Socket createSocket(Socket socket, String host, int port, String clientCertificateAlias)
|
||||
throws NoSuchAlgorithmException, KeyManagementException, MessagingException, IOException {
|
||||
|
||||
TrustManager[] trustManagers = new TrustManager[] { trustManagerFactory.getTrustManagerForDomain(host, port) };
|
||||
KeyManager[] keyManagers = null;
|
||||
if (!TextUtils.isEmpty(clientCertificateAlias)) {
|
||||
keyManagers = new KeyManager[] { new KeyChainKeyManager(context, clientCertificateAlias) };
|
||||
}
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(keyManagers, trustManagers, null);
|
||||
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
|
||||
Socket trustedSocket;
|
||||
if (socket == null) {
|
||||
trustedSocket = socketFactory.createSocket();
|
||||
} else {
|
||||
trustedSocket = socketFactory.createSocket(socket, host, port, true);
|
||||
}
|
||||
|
||||
SSLSocket sslSocket = (SSLSocket) trustedSocket;
|
||||
|
||||
hardenSocket(sslSocket);
|
||||
|
||||
setSniHost(socketFactory, sslSocket, host);
|
||||
|
||||
return trustedSocket;
|
||||
}
|
||||
|
||||
private static void hardenSocket(SSLSocket sock) {
|
||||
if (ENABLED_CIPHERS != null) {
|
||||
sock.setEnabledCipherSuites(ENABLED_CIPHERS);
|
||||
}
|
||||
if (ENABLED_PROTOCOLS != null) {
|
||||
sock.setEnabledProtocols(ENABLED_PROTOCOLS);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setSniHost(SSLSocketFactory factory, SSLSocket socket, String hostname) {
|
||||
if (factory instanceof SSLCertificateSocketFactory) {
|
||||
SSLCertificateSocketFactory sslCertificateSocketFactory = (SSLCertificateSocketFactory) factory;
|
||||
sslCertificateSocketFactory.setHostname(socket, hostname);
|
||||
} else if (Build.VERSION.SDK_INT >= 24) {
|
||||
SSLParameters sslParameters = socket.getSSLParameters();
|
||||
List<SNIServerName> sniServerNames = Collections.singletonList(new SNIHostName(hostname));
|
||||
sslParameters.setServerNames(sniServerNames);
|
||||
socket.setSSLParameters(sslParameters);
|
||||
} else {
|
||||
setHostnameViaReflection(socket, hostname);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setHostnameViaReflection(SSLSocket socket, String hostname) {
|
||||
try {
|
||||
socket.getClass().getMethod("setHostname", String.class).invoke(socket, hostname);
|
||||
} catch (Throwable e) {
|
||||
Timber.e(e, "Could not call SSLSocket#setHostname(String) method ");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
|
||||
package app.k9mail.feature.preview.auth;
|
||||
|
||||
|
||||
import java.net.Socket;
|
||||
import java.security.Principal;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import android.content.Context;
|
||||
import android.security.KeyChain;
|
||||
import android.security.KeyChainException;
|
||||
|
||||
import com.fsck.k9.mail.CertificateValidationException;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.X509ExtendedKeyManager;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static com.fsck.k9.mail.CertificateValidationException.Reason;
|
||||
import static com.fsck.k9.mail.CertificateValidationException.Reason.RetrievalFailure;
|
||||
|
||||
|
||||
/**
|
||||
* For client certificate authentication! Provide private keys and certificates
|
||||
* during the TLS handshake using the Android 4.0 KeyChain API.
|
||||
*/
|
||||
class KeyChainKeyManager extends X509ExtendedKeyManager {
|
||||
private final String mAlias;
|
||||
private final X509Certificate[] mChain;
|
||||
private final PrivateKey mPrivateKey;
|
||||
|
||||
|
||||
/**
|
||||
* @param alias Must not be null nor empty
|
||||
* @throws MessagingException
|
||||
* Indicates an error in retrieving the certificate for the alias
|
||||
* (likely because the alias is invalid or the certificate was deleted)
|
||||
*/
|
||||
public KeyChainKeyManager(Context context, String alias) throws MessagingException {
|
||||
mAlias = alias;
|
||||
|
||||
try {
|
||||
mChain = fetchCertificateChain(context, alias);
|
||||
mPrivateKey = fetchPrivateKey(context, alias);
|
||||
} catch (KeyChainException e) {
|
||||
// The certificate was possibly deleted. Notify user of error.
|
||||
throw new CertificateValidationException(e.getMessage(), RetrievalFailure, alias);
|
||||
} catch (InterruptedException e) {
|
||||
throw new CertificateValidationException(e.getMessage(), RetrievalFailure, alias);
|
||||
}
|
||||
}
|
||||
|
||||
private X509Certificate[] fetchCertificateChain(Context context, String alias)
|
||||
throws KeyChainException, InterruptedException, MessagingException {
|
||||
|
||||
X509Certificate[] chain = KeyChain.getCertificateChain(context, alias);
|
||||
if (chain == null || chain.length == 0) {
|
||||
throw new MessagingException("No certificate chain found for: " + alias);
|
||||
}
|
||||
try {
|
||||
for (X509Certificate certificate : chain) {
|
||||
certificate.checkValidity();
|
||||
}
|
||||
} catch (CertificateException e) {
|
||||
throw new CertificateValidationException(e.getMessage(), Reason.Expired, alias);
|
||||
}
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
private PrivateKey fetchPrivateKey(Context context, String alias) throws KeyChainException,
|
||||
InterruptedException, MessagingException {
|
||||
|
||||
PrivateKey privateKey = KeyChain.getPrivateKey(context, alias);
|
||||
if (privateKey == null) {
|
||||
throw new MessagingException("No private key found for: " + alias);
|
||||
}
|
||||
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
|
||||
return chooseAlias(keyTypes, issuers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getCertificateChain(String alias) {
|
||||
return (mAlias.equals(alias) ? mChain : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey getPrivateKey(String alias) {
|
||||
return (mAlias.equals(alias) ? mPrivateKey : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
|
||||
return chooseAlias(new String[] { keyType }, issuers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getClientAliases(String keyType, Principal[] issuers) {
|
||||
final String al = chooseAlias(new String[] { keyType }, issuers);
|
||||
return (al == null ? null : new String[] { al });
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getServerAliases(String keyType, Principal[] issuers) {
|
||||
final String al = chooseAlias(new String[] { keyType }, issuers);
|
||||
return (al == null ? null : new String[] { al });
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseEngineClientAlias(String[] keyTypes, Principal[] issuers, SSLEngine engine) {
|
||||
return chooseAlias(keyTypes, issuers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
|
||||
return chooseAlias(new String[] { keyType }, issuers);
|
||||
}
|
||||
|
||||
private String chooseAlias(String[] keyTypes, Principal[] issuers) {
|
||||
if (keyTypes == null || keyTypes.length == 0) {
|
||||
return null;
|
||||
}
|
||||
final X509Certificate cert = mChain[0];
|
||||
final String certKeyAlg = cert.getPublicKey().getAlgorithm();
|
||||
final String certSigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
|
||||
for (String keyAlgorithm : keyTypes) {
|
||||
if (keyAlgorithm == null) {
|
||||
continue;
|
||||
}
|
||||
final String sigAlgorithm;
|
||||
// handle cases like EC_EC and EC_RSA
|
||||
int index = keyAlgorithm.indexOf('_');
|
||||
if (index == -1) {
|
||||
sigAlgorithm = null;
|
||||
} else {
|
||||
sigAlgorithm = keyAlgorithm.substring(index + 1);
|
||||
keyAlgorithm = keyAlgorithm.substring(0, index);
|
||||
}
|
||||
// key algorithm does not match
|
||||
if (!certKeyAlg.equals(keyAlgorithm)) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* TODO find a more reliable test for signature
|
||||
* algorithm. Unfortunately value varies with
|
||||
* provider. For example for "EC" it could be
|
||||
* "SHA1WithECDSA" or simply "ECDSA".
|
||||
*/
|
||||
// sig algorithm does not match
|
||||
if (sigAlgorithm != null && certSigAlg != null
|
||||
&& !certSigAlg.contains(sigAlgorithm)) {
|
||||
continue;
|
||||
}
|
||||
// no issuers to match
|
||||
if (issuers == null || issuers.length == 0) {
|
||||
return mAlias;
|
||||
}
|
||||
List<Principal> issuersList = Arrays.asList(issuers);
|
||||
// check that a certificate in the chain was issued by one of the specified issuers
|
||||
for (X509Certificate certFromChain : mChain) {
|
||||
/*
|
||||
* Note use of X500Principal from
|
||||
* getIssuerX500Principal as opposed to Principal
|
||||
* from getIssuerDN. Principal.equals test does
|
||||
* not work in the case where
|
||||
* xcertFromChain.getIssuerDN is a bouncycastle
|
||||
* org.bouncycastle.jce.X509Principal.
|
||||
*/
|
||||
X500Principal issuerFromChain = certFromChain.getIssuerX500Principal();
|
||||
if (issuersList.contains(issuerFromChain)) {
|
||||
return mAlias;
|
||||
}
|
||||
}
|
||||
Timber.w("Client certificate %s not issued by any of the requested issuers", mAlias);
|
||||
return null;
|
||||
}
|
||||
Timber.w("Client certificate %s does not match any of the requested key types", mAlias);
|
||||
return null;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue