/** * Copyright (c) 2018 LEAP Encryption Access Project and contributers * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package se.leap.bitmaskclient.providersetup; import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid; import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY; import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD; import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_HASHES; import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_LAST_SEEN; import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_LAST_UPDATED; import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY; import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE; import static se.leap.bitmaskclient.base.models.Provider.CA_CERT; import static se.leap.bitmaskclient.base.models.Provider.GEOIP_URL; import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_API_IP; import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_IP; import static se.leap.bitmaskclient.base.utils.CertificateHelper.getFingerprintFromCertificate; import static se.leap.bitmaskclient.base.utils.ConfigHelper.getDomainFromMainURL; import static se.leap.bitmaskclient.base.utils.PreferenceHelper.deleteProviderDetailsFromPreferences; import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getFromPersistedProvider; import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getLongFromPersistedProvider; import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getStringSetFromPersistedProvider; import static se.leap.bitmaskclient.base.utils.PrivateKeyHelper.ED_25519_KEY_BEGIN; import static se.leap.bitmaskclient.base.utils.PrivateKeyHelper.ED_25519_KEY_END; import static se.leap.bitmaskclient.base.utils.PrivateKeyHelper.RSA_KEY_BEGIN; import static se.leap.bitmaskclient.base.utils.PrivateKeyHelper.RSA_KEY_END; import static se.leap.bitmaskclient.base.utils.PrivateKeyHelper.parsePrivateKeyFromString; import android.content.Intent; import android.content.res.Resources; import android.os.Bundle; import android.util.Base64; import org.json.JSONException; import org.json.JSONObject; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; import java.util.ArrayList; import java.util.Set; import java.util.concurrent.TimeoutException; import se.leap.bitmaskclient.base.models.Provider; import se.leap.bitmaskclient.base.utils.ConfigHelper; import se.leap.bitmaskclient.base.utils.PreferenceHelper; import se.leap.bitmaskclient.base.utils.PrivateKeyHelper; /** * Implements the logic of the http api calls. The methods of this class needs to be called from * a background thread. */ public abstract class ProviderApiManagerBase { private final static String TAG = ProviderApiManagerBase.class.getName(); public interface ProviderApiServiceCallback { void broadcastEvent(Intent intent); boolean startTorService() throws InterruptedException, IllegalStateException, TimeoutException; void stopTorService(); int getTorHttpTunnelPort(); boolean hasNetworkConnection(); void saveProvider(Provider p); } protected final ProviderApiServiceCallback serviceCallback; protected Resources resources; protected ProviderApiEventSender eventSender; protected ProviderApiTorHandler torHandler; ProviderApiManagerBase(Resources resources, ProviderApiServiceCallback callback) { this.resources = resources; this.serviceCallback = callback; this.eventSender = new ProviderApiEventSender(resources, serviceCallback); this.torHandler = new ProviderApiTorHandler(callback); } void resetProviderDetails(Provider provider) { provider.reset(); deleteProviderDetailsFromPreferences(provider.getDomain()); } protected boolean isValidJson(String jsonString) { try { new JSONObject(jsonString); return true; } catch(JSONException e) { return false; } catch(NullPointerException e) { e.printStackTrace(); return false; } } protected boolean validCertificate(Provider provider, String certString) { boolean result = false; if (!ConfigHelper.checkErroneousDownload(certString)) { ArrayList certificates = ConfigHelper.parseX509CertificatesFromString(certString); try { if (certificates != null) { if (certificates.size() == 1) { JSONObject providerJson = provider.getDefinition(); String fingerprint = providerJson.getString(Provider.CA_CERT_FINGERPRINT); String encoding = fingerprint.split(":")[0]; String expectedFingerprint = fingerprint.split(":")[1]; String realFingerprint = getFingerprintFromCertificate(certificates.get(0), encoding); result = realFingerprint.trim().equalsIgnoreCase(expectedFingerprint.trim()); } else { // otherwise we assume the provider is transitioning the CA certs and thus shipping multiple CA certs // in that case we don't do cert pinning result = true; } } else { result = false; } } catch (JSONException | NoSuchAlgorithmException | CertificateEncodingException e) { result = false; } } return result; } protected void getPersistedProviderUpdates(Provider provider) { String providerDomain = getDomainFromMainURL(provider.getMainUrlString()); if (hasUpdatedProviderDetails(providerDomain)) { provider.setCaCert(getPersistedProviderCA(providerDomain)); provider.define(getPersistedProviderDefinition(providerDomain)); provider.setPrivateKeyString(getPersistedPrivateKey(providerDomain)); provider.setVpnCertificate(getPersistedVPNCertificate(providerDomain)); provider.setProviderApiIp(getPersistedProviderApiIp(providerDomain)); provider.setProviderIp(getPersistedProviderIp(providerDomain)); provider.setGeoipUrl(getPersistedGeoIp(providerDomain)); // TODO: do we really need to persist the Geoip URL?? provider.setLastMotdSeen(getPersistedMotdLastSeen(providerDomain)); provider.setMotdLastSeenHashes(getPersistedMotdHashes(providerDomain)); provider.setLastMotdUpdate(getPersistedMotdLastUpdate(providerDomain)); provider.setMotdJson(getPersistedMotd(providerDomain)); } } protected String getPersistedPrivateKey(String providerDomain) { return getFromPersistedProvider(PROVIDER_PRIVATE_KEY, providerDomain); } protected String getPersistedVPNCertificate(String providerDomain) { return getFromPersistedProvider(PROVIDER_VPN_CERTIFICATE, providerDomain); } protected JSONObject getPersistedProviderDefinition(String providerDomain) { try { return new JSONObject(getFromPersistedProvider(Provider.KEY, providerDomain)); } catch (JSONException e) { e.printStackTrace(); return new JSONObject(); } } protected String getPersistedProviderCA(String providerDomain) { return getFromPersistedProvider(CA_CERT, providerDomain); } protected String getPersistedProviderApiIp(String providerDomain) { return getFromPersistedProvider(PROVIDER_API_IP, providerDomain); } protected String getPersistedProviderIp(String providerDomain) { return getFromPersistedProvider(PROVIDER_IP, providerDomain); } protected String getPersistedGeoIp(String providerDomain) { return getFromPersistedProvider(GEOIP_URL, providerDomain); } protected JSONObject getPersistedMotd(String providerDomain) { try { return new JSONObject(getFromPersistedProvider(PROVIDER_MOTD, providerDomain)); } catch (JSONException e) { return new JSONObject(); } } protected long getPersistedMotdLastSeen(String providerDomain) { return getLongFromPersistedProvider(PROVIDER_MOTD_LAST_SEEN, providerDomain); } protected long getPersistedMotdLastUpdate(String providerDomain) { return getLongFromPersistedProvider(PROVIDER_MOTD_LAST_UPDATED, providerDomain); } protected Set getPersistedMotdHashes(String providerDomain) { return getStringSetFromPersistedProvider(PROVIDER_MOTD_HASHES, providerDomain); } protected boolean hasUpdatedProviderDetails(String domain) { return PreferenceHelper.hasKey(Provider.KEY + "." + domain) && PreferenceHelper.hasKey(CA_CERT + "." + domain); } protected Bundle loadCertificate(Provider provider, String certString) { Bundle result = new Bundle(); if (certString == null) { eventSender.setErrorResult(result, vpn_certificate_is_invalid, null); return result; } try { // API returns concatenated cert & key. Split them for OpenVPN options String certificateString = null, keyString = null; String[] certAndKey = certString.split("(?<=-\n)"); for (int i = 0; i < certAndKey.length - 1; i++) { if (certAndKey[i].contains("KEY")) { keyString = certAndKey[i++] + certAndKey[i]; } else if (certAndKey[i].contains("CERTIFICATE")) { certificateString = certAndKey[i++] + certAndKey[i]; } } PrivateKey key = parsePrivateKeyFromString(keyString); keyString = Base64.encodeToString(key.getEncoded(), Base64.DEFAULT); if (key instanceof RSAPrivateKey) { provider.setPrivateKeyString(RSA_KEY_BEGIN + keyString + RSA_KEY_END); } else { provider.setPrivateKeyString(ED_25519_KEY_BEGIN + keyString + ED_25519_KEY_END); } ArrayList certificates = ConfigHelper.parseX509CertificatesFromString(certificateString); certificates.get(0).checkValidity(); certificateString = Base64.encodeToString(certificates.get(0).getEncoded(), Base64.DEFAULT); provider.setVpnCertificate( "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----"); result.putBoolean(BROADCAST_RESULT_KEY, true); } catch (CertificateException | NullPointerException e) { e.printStackTrace(); eventSender.setErrorResult(result, vpn_certificate_is_invalid, null); } return result; } }