diff options
Diffstat (limited to 'app/src/main/java/se/leap/bitmaskclient/base/utils')
10 files changed, 1217 insertions, 0 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/Cmd.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/Cmd.java new file mode 100644 index 00000000..affceacf --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/Cmd.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2019 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 <http://www.gnu.org/licenses/>. + */ + +package se.leap.bitmaskclient.base.utils; + +import androidx.annotation.WorkerThread; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; + +public class Cmd { + + private static final String TAG = Cmd.class.getSimpleName(); + + @WorkerThread + public static int runBlockingCmd(String[] cmds, StringBuilder log) throws Exception { + return runCmd(cmds, log, true); + } + + @WorkerThread + private static int runCmd(String[] cmds, StringBuilder log, + boolean waitFor) throws Exception { + + int exitCode = -1; + Process proc = Runtime.getRuntime().exec("sh"); + OutputStreamWriter out = new OutputStreamWriter(proc.getOutputStream()); + + try { + for (String cmd : cmds) { + out.write(cmd); + out.write("\n"); + } + + out.flush(); + out.write("exit\n"); + out.flush(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + out.close(); + } + + if (waitFor) { + // Consume the "stdout" + InputStreamReader reader = new InputStreamReader(proc.getInputStream()); + readToLogString(reader, log); + + // Consume the "stderr" + reader = new InputStreamReader(proc.getErrorStream()); + readToLogString(reader, log); + + try { + exitCode = proc.waitFor(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + return exitCode; + } + + private static void readToLogString(InputStreamReader reader, StringBuilder log) throws IOException { + final char buf[] = new char[10]; + int read = 0; + try { + while ((read = reader.read(buf)) != -1) { + if (log != null) + log.append(buf, 0, read); + } + } catch (IOException e) { + reader.close(); + throw new IOException(e); + } + reader.close(); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java new file mode 100644 index 00000000..4248072a --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java @@ -0,0 +1,230 @@ +/** + * Copyright (c) 2013 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 <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.base.utils; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; +import android.os.Looper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import org.json.JSONException; +import org.json.JSONObject; +import org.spongycastle.util.encoders.Base64; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Calendar; + +import se.leap.bitmaskclient.BuildConfig; +import se.leap.bitmaskclient.providersetup.ProviderAPI; +import se.leap.bitmaskclient.R; + +import static se.leap.bitmaskclient.base.models.Constants.DEFAULT_BITMASK; + +/** + * Stores constants, and implements auxiliary methods used across all Bitmask Android classes. + * Wraps BuildConfigFields for to support easier unit testing + * + * @author parmegv + * @author MeanderingCode + */ +public class ConfigHelper { + final public static String NG_1024 = + "eeaf0ab9adb38dd69c33f80afa8fc5e86072618775ff3c0b9ea2314c9c256576d674df7496ea81d3383b4813d692c6e0e0d5d8e250b98be48e495c1d6089dad15dc7d7b46154d6b6ce8ef4ad69b15d4982559b297bcf1885c529f566660e57ec68edbc3c05726cc02fd4cbf4976eaa9afd5138fe8376435b9fc61d2fc0eb06e3"; + final public static BigInteger G = new BigInteger("2"); + + public static boolean checkErroneousDownload(String downloadedString) { + try { + if (downloadedString == null || downloadedString.isEmpty() || new JSONObject(downloadedString).has(ProviderAPI.ERRORS) || new JSONObject(downloadedString).has(ProviderAPI.BACKEND_ERROR_KEY)) { + return true; + } else { + return false; + } + } catch (NullPointerException | JSONException e) { + return false; + } + } + + /** + * Treat the input as the MSB representation of a number, + * and lop off leading zero elements. For efficiency, the + * input is simply returned if no leading zeroes are found. + * + * @param in array to be trimmed + */ + public static byte[] trim(byte[] in) { + if (in.length == 0 || in[0] != 0) + return in; + + int len = in.length; + int i = 1; + while (in[i] == 0 && i < len) + ++i; + byte[] ret = new byte[len - i]; + System.arraycopy(in, i, ret, 0, len - i); + return ret; + } + + public static X509Certificate parseX509CertificateFromString(String certificateString) { + java.security.cert.Certificate certificate = null; + CertificateFactory cf; + try { + cf = CertificateFactory.getInstance("X.509"); + + certificateString = certificateString.replaceFirst("-----BEGIN CERTIFICATE-----", "").replaceFirst("-----END CERTIFICATE-----", "").trim(); + byte[] cert_bytes = Base64.decode(certificateString); + InputStream caInput = new ByteArrayInputStream(cert_bytes); + try { + certificate = cf.generateCertificate(caInput); + System.out.println("ca=" + ((X509Certificate) certificate).getSubjectDN()); + } finally { + caInput.close(); + } + } catch (NullPointerException | CertificateException | IOException | IllegalArgumentException e) { + return null; + } + return (X509Certificate) certificate; + } + + public static RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) { + RSAPrivateKey key; + try { + KeyFactory kf; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + kf = KeyFactory.getInstance("RSA", "BC"); + } else { + kf = KeyFactory.getInstance("RSA"); + } + rsaKeyString = rsaKeyString.replaceFirst("-----BEGIN RSA PRIVATE KEY-----", "").replaceFirst("-----END RSA PRIVATE KEY-----", ""); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString)); + key = (RSAPrivateKey) kf.generatePrivate(keySpec); + } catch (InvalidKeySpecException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return null; + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return null; + } catch (NullPointerException e) { + e.printStackTrace(); + return null; + } catch (NoSuchProviderException e) { + e.printStackTrace(); + return null; + } + + return key; + } + + private static String byteArrayToHex(byte[] input) { + int readBytes = input.length; + StringBuffer hexData = new StringBuffer(); + int onebyte; + for (int i = 0; i < readBytes; i++) { + onebyte = ((0x000000ff & input[i]) | 0xffffff00); + hexData.append(Integer.toHexString(onebyte).substring(6)); + } + return hexData.toString(); + } + + /** + * Calculates the hexadecimal representation of a sha256/sha1 fingerprint of a certificate + * + * @param certificate + * @param encoding + * @return + * @throws NoSuchAlgorithmException + * @throws CertificateEncodingException + */ + @NonNull + public static String getFingerprintFromCertificate(X509Certificate certificate, String encoding) throws NoSuchAlgorithmException, CertificateEncodingException /*, UnsupportedEncodingException*/ { + byte[] byteArray = MessageDigest.getInstance(encoding).digest(certificate.getEncoded()); + return byteArrayToHex(byteArray); + } + + public static void ensureNotOnMainThread(@NonNull Context context) throws IllegalStateException{ + Looper looper = Looper.myLooper(); + if (looper != null && looper == context.getMainLooper()) { + throw new IllegalStateException( + "calling this from your main thread can lead to deadlock"); + } + } + + public static boolean isDefaultBitmask() { + return BuildConfig.FLAVOR_branding.equals(DEFAULT_BITMASK); + } + + public static boolean preferAnonymousUsage() { + return BuildConfig.priotize_anonymous_usage; + } + + public static int getCurrentTimezone() { + return Calendar.getInstance().get(Calendar.ZONE_OFFSET) / 3600000; + } + + public static String getProviderFormattedString(Resources resources, @StringRes int resourceId) { + String appName = resources.getString(R.string.app_name); + return resources.getString(resourceId, appName); + } + + public static boolean stringEqual(@Nullable String string1, @Nullable String string2) { + return (string1 == null && string2 == null) || + (string1 != null && string1.equals(string2)); + } + + public static String getApkFileName() { + try { + return BuildConfig.update_apk_url.substring(BuildConfig.update_apk_url.lastIndexOf("/")); + } catch (Exception e) { + return null; + } + } + + public static String getVersionFileName() { + try { + return BuildConfig.version_file_url.substring(BuildConfig.version_file_url.lastIndexOf("/")); + } catch (Exception e) { + return null; + } + } + + public static String getSignatureFileName() { + try { + return BuildConfig.signature_url.substring(BuildConfig.signature_url.lastIndexOf("/")); + } catch (Exception e) { + return null; + } + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java new file mode 100644 index 00000000..0476bf12 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java @@ -0,0 +1,29 @@ +package se.leap.bitmaskclient.base.utils; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Contains helper methods related to date manipulation. + * + * @author Janak + */ +public class DateHelper { + private static final String DATE_PATTERN = "dd/MM/yyyy"; + private static final int ONE_DAY = 86400000; //1000*60*60*24 + + public static long getDateDiffToCurrentDateInDays(String startDate) throws ParseException { + SimpleDateFormat sdf = new SimpleDateFormat(DATE_PATTERN, Locale.US); + Date lastDate = sdf.parse(startDate); + Date currentDate = new Date(); + return (currentDate.getTime() - lastDate.getTime()) / ONE_DAY; + } + + public static String getCurrentDateString() { + SimpleDateFormat sdf = new SimpleDateFormat(DATE_PATTERN, Locale.US); + Date lastDate = new Date(); + return sdf.format(lastDate); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java new file mode 100644 index 00000000..eb1c255c --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java @@ -0,0 +1,46 @@ +package se.leap.bitmaskclient.base.utils; + +import android.content.Context; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * Created by cyberta on 18.03.18. + */ + +public class FileHelper { + public static File createFile(File dir, String fileName) { + return new File(dir, fileName); + } + + public static void persistFile(File file, String content) throws IOException { + FileWriter writer = new FileWriter(file); + writer.write(content); + writer.close(); + } + + public static String readPublicKey(Context context) { + { + InputStream inputStream; + try { + inputStream = context.getAssets().open("public.pgp"); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } catch (IOException errabi) { + return null; + } + } + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/IPAddress.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/IPAddress.java new file mode 100644 index 00000000..377617a4 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/IPAddress.java @@ -0,0 +1,102 @@ +package se.leap.bitmaskclient.base.utils; + +/* + * Copyright (C) 2006-2008 Alfresco Software Limited. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ + +import java.util.StringTokenizer; + +/** + * TCP/IP Address Utility Class + * + * @author gkspencer + */ +public class IPAddress { + + + /** + * Convert a TCP/IP address string into a byte array + * + * @param addr String + * @return byte[] + */ + public static byte[] asBytes(String addr) { + + // Convert the TCP/IP address string to an integer value + int ipInt = parseNumericAddress(addr); + if (ipInt == 0) + return null; + + // Convert to bytes + byte[] ipByts = new byte[4]; + + ipByts[3] = (byte) (ipInt & 0xFF); + ipByts[2] = (byte) ((ipInt >> 8) & 0xFF); + ipByts[1] = (byte) ((ipInt >> 16) & 0xFF); + ipByts[0] = (byte) ((ipInt >> 24) & 0xFF); + + // Return the TCP/IP bytes + return ipByts; + } + /** + * Check if the specified address is a valid numeric TCP/IP address and return as an integer value + * + * @param ipaddr String + * @return int + */ + private static int parseNumericAddress(String ipaddr) { + + // Check if the string is valid + if (ipaddr == null || ipaddr.length() < 7 || ipaddr.length() > 15) + return 0; + + // Check the address string, should be n.n.n.n format + StringTokenizer token = new StringTokenizer(ipaddr,"."); + if (token.countTokens() != 4) + return 0; + + int ipInt = 0; + while (token.hasMoreTokens()) { + + // Get the current token and convert to an integer value + String ipNum = token.nextToken(); + + try { + // Validate the current address part + int ipVal = Integer.valueOf(ipNum).intValue(); + if (ipVal < 0 || ipVal > 255) + return 0; + + // Add to the integer address + ipInt = (ipInt << 8) + ipVal; + } + catch (NumberFormatException ex) { + return 0; + } + } + + // Return the integer address + return ipInt; + } +}
\ No newline at end of file diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java new file mode 100644 index 00000000..77189dff --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java @@ -0,0 +1,21 @@ +package se.leap.bitmaskclient.base.utils; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; + +/** + * Created by cyberta on 18.03.18. + */ + +public class InputStreamHelper { + //allows us to mock FileInputStream + public static InputStream getInputStreamFrom(String filePath) throws FileNotFoundException { + return new FileInputStream(filePath); + } + + public static String loadInputStreamAsString(InputStream is) { + java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/KeyStoreHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/KeyStoreHelper.java new file mode 100644 index 00000000..b0b28993 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/KeyStoreHelper.java @@ -0,0 +1,78 @@ +package se.leap.bitmaskclient.base.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +/** + * Created by cyberta on 18.03.18. + */ + +public class KeyStoreHelper { + private static KeyStore trustedKeystore; + + /** + * Adds a new X509 certificate given its input stream and its provider name + * + * @param provider used to store the certificate in the keystore + * @param inputStream from which X509 certificate must be generated. + */ + public static void addTrustedCertificate(String provider, InputStream inputStream) { + CertificateFactory cf; + try { + cf = CertificateFactory.getInstance("X.509"); + X509Certificate cert = + (X509Certificate) cf.generateCertificate(inputStream); + trustedKeystore.setCertificateEntry(provider, cert); + } catch (CertificateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (KeyStoreException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * Adds a new X509 certificate given in its string from and using its provider name + * + * @param provider used to store the certificate in the keystore + * @param certificate + */ + public static void addTrustedCertificate(String provider, String certificate) { + + try { + X509Certificate cert = ConfigHelper.parseX509CertificateFromString(certificate); + if (trustedKeystore == null) { + trustedKeystore = KeyStore.getInstance("BKS"); + trustedKeystore.load(null); + } + trustedKeystore.setCertificateEntry(provider, cert); + } catch (KeyStoreException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (CertificateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * @return class wide keystore + */ + public static KeyStore getKeystore() { + return trustedKeystore; + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/PRNGFixes.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PRNGFixes.java new file mode 100644 index 00000000..41b8cf35 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/PRNGFixes.java @@ -0,0 +1,330 @@ +package se.leap.bitmaskclient.base.utils; + +/* + * This software is provided 'as-is', without any express or implied + * warranty. In no event will Google be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, as long as the origin is not misrepresented. + * + * Source: http://android-developers.blogspot.de/2013/08/some-securerandom-thoughts.html + */ + +import android.os.*; +import android.os.Process; +import android.util.*; + +import java.io.*; +import java.security.*; +import java.security.Provider; + +/** + * Fixes for the output of the default PRNG having low entropy. + * <p/> + * The fixes need to be applied via {@link #apply()} before any use of Java + * Cryptography Architecture primitives. A good place to invoke them is in the + * application's {@code onCreate}. + */ +public final class PRNGFixes { + + private static final int VERSION_CODE_JELLY_BEAN = 16; + private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; + private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = + getBuildFingerprintAndDeviceSerial(); + + /** + * Hidden constructor to prevent instantiation. + */ + private PRNGFixes() { + } + + /** + * Applies all fixes. + * + * @throws SecurityException if a fix is needed but could not be applied. + */ + public static void apply() { + applyOpenSSLFix(); + installLinuxPRNGSecureRandom(); + } + + /** + * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the + * fix is not needed. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void applyOpenSSLFix() throws SecurityException { + if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) + || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { + // No need to apply the fix + return; + } + + try { + // Mix in the device- and invocation-specific seed. + Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_seed", byte[].class) + .invoke(null, generateSeed()); + + // Mix output of Linux PRNG into OpenSSL's PRNG + int bytesRead = (Integer) Class.forName( + "org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_load_file", String.class, long.class) + .invoke(null, "/dev/urandom", 1024); + if (bytesRead != 1024) { + throw new IOException( + "Unexpected number of bytes read from Linux PRNG: " + + bytesRead); + } + } catch (Exception e) { + throw new SecurityException("Failed to seed OpenSSL PRNG", e); + } + } + + /** + * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the + * default. Does nothing if the implementation is already the default or if + * there is not need to install the implementation. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void installLinuxPRNGSecureRandom() + throws SecurityException { + if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { + // No need to apply the fix + return; + } + + // Install a Linux PRNG-based SecureRandom implementation as the + // default, if not yet installed. + Provider[] secureRandomProviders = + Security.getProviders("SecureRandom.SHA1PRNG"); + if ((secureRandomProviders == null) + || (secureRandomProviders.length < 1) + || (!LinuxPRNGSecureRandomProvider.class.equals( + secureRandomProviders[0].getClass()))) { + Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); + } + + // Assert that new SecureRandom() and + // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed + // by the Linux PRNG-based SecureRandom implementation. + SecureRandom rng1 = new SecureRandom(); + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng1.getProvider().getClass())) { + throw new SecurityException( + "new SecureRandom() backed by wrong Provider: " + + rng1.getProvider().getClass()); + } + + SecureRandom rng2; + try { + rng2 = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng2.getProvider().getClass())) { + throw new SecurityException( + "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" + + " Provider: " + rng2.getProvider().getClass()); + } + } + + /** + * {@code Provider} of {@code SecureRandom} engines which pass through + * all requests to the Linux PRNG. + */ + private static class LinuxPRNGSecureRandomProvider extends Provider { + + public LinuxPRNGSecureRandomProvider() { + super("LinuxPRNG", + 1.0, + "A Linux-specific random number provider that uses" + + " /dev/urandom"); + // Although /dev/urandom is not a SHA-1 PRNG, some apps + // explicitly request a SHA1PRNG SecureRandom and we thus need to + // prevent them from getting the default implementation whose output + // may have low entropy. + put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); + put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); + } + } + + /** + * {@link SecureRandomSpi} which passes all requests to the Linux PRNG + * ({@code /dev/urandom}). + */ + public static class LinuxPRNGSecureRandom extends SecureRandomSpi { + + /* + * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed + * are passed through to the Linux PRNG (/dev/urandom). Instances of + * this class seed themselves by mixing in the current time, PID, UID, + * build fingerprint, and hardware serial number (where available) into + * Linux PRNG. + * + * Concurrency: Read requests to the underlying Linux PRNG are + * serialized (on sLock) to ensure that multiple threads do not get + * duplicated PRNG output. + */ + + private static final File URANDOM_FILE = new File("/dev/urandom"); + + private static final Object sLock = new Object(); + + /** + * Input stream for reading from Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static DataInputStream sUrandomIn; + + /** + * Output stream for writing to Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static OutputStream sUrandomOut; + + /** + * Whether this engine instance has been seeded. This is needed because + * each instance needs to seed itself if the client does not explicitly + * seed it. + */ + private boolean mSeeded; + + @Override + protected void engineSetSeed(byte[] bytes) { + try { + OutputStream out; + synchronized (sLock) { + out = getUrandomOutputStream(); + } + out.write(bytes); + out.flush(); + } catch (IOException e) { + // On a small fraction of devices /dev/urandom is not writable. + // Log and ignore. + Log.w(PRNGFixes.class.getSimpleName(), + "Failed to mix seed into " + URANDOM_FILE); + } finally { + mSeeded = true; + } + } + + @Override + protected void engineNextBytes(byte[] bytes) { + if (!mSeeded) { + // Mix in the device- and invocation-specific seed. + engineSetSeed(generateSeed()); + } + + try { + DataInputStream in; + synchronized (sLock) { + in = getUrandomInputStream(); + } + synchronized (in) { + in.readFully(bytes); + } + } catch (IOException e) { + throw new SecurityException( + "Failed to read from " + URANDOM_FILE, e); + } + } + + @Override + protected byte[] engineGenerateSeed(int size) { + byte[] seed = new byte[size]; + engineNextBytes(seed); + return seed; + } + + private DataInputStream getUrandomInputStream() { + synchronized (sLock) { + if (sUrandomIn == null) { + // NOTE: Consider inserting a BufferedInputStream between + // DataInputStream and FileInputStream if you need higher + // PRNG output performance and can live with future PRNG + // output being pulled into this process prematurely. + try { + sUrandomIn = new DataInputStream( + new FileInputStream(URANDOM_FILE)); + } catch (IOException e) { + throw new SecurityException("Failed to open " + + URANDOM_FILE + " for reading", e); + } + } + return sUrandomIn; + } + } + + private OutputStream getUrandomOutputStream() throws IOException { + synchronized (sLock) { + if (sUrandomOut == null) { + sUrandomOut = new FileOutputStream(URANDOM_FILE); + } + return sUrandomOut; + } + } + } + + /** + * Generates a device- and invocation-specific seed to be mixed into the + * Linux PRNG. + */ + private static byte[] generateSeed() { + try { + ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); + DataOutputStream seedBufferOut = + new DataOutputStream(seedBuffer); + seedBufferOut.writeLong(System.currentTimeMillis()); + seedBufferOut.writeLong(System.nanoTime()); + seedBufferOut.writeInt(Process.myPid()); + seedBufferOut.writeInt(Process.myUid()); + seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); + seedBufferOut.close(); + return seedBuffer.toByteArray(); + } catch (IOException e) { + throw new SecurityException("Failed to generate seed", e); + } + } + + /** + * Gets the hardware serial number of this device. + * + * @return serial number or {@code null} if not available. + */ + private static String getDeviceSerialNumber() { + // We're using the Reflection API because Build.SERIAL is only available + // since API Level 9 (Gingerbread, Android 2.3). + try { + return (String) Build.class.getField("SERIAL").get(null); + } catch (Exception ignored) { + return null; + } + } + + private static byte[] getBuildFingerprintAndDeviceSerial() { + StringBuilder result = new StringBuilder(); + String fingerprint = Build.FINGERPRINT; + if (fingerprint != null) { + result.append(fingerprint); + } + String serial = getDeviceSerialNumber(); + if (serial != null) { + result.append(serial); + } + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported"); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java new file mode 100644 index 00000000..d31c7a20 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java @@ -0,0 +1,273 @@ +package se.leap.bitmaskclient.base.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashSet; +import java.util.Set; + +import de.blinkt.openvpn.VpnProfile; +import se.leap.bitmaskclient.base.models.Provider; + +import static android.content.Context.MODE_PRIVATE; +import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_BLUETOOTH; +import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_USB; +import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_WIFI; +import static se.leap.bitmaskclient.base.models.Constants.ALWAYS_ON_SHOW_DIALOG; +import static se.leap.bitmaskclient.base.models.Constants.DEFAULT_SHARED_PREFS_BATTERY_SAVER; +import static se.leap.bitmaskclient.base.models.Constants.EXCLUDED_APPS; +import static se.leap.bitmaskclient.base.models.Constants.LAST_UPDATE_CHECK; +import static se.leap.bitmaskclient.base.models.Constants.LAST_USED_PROFILE; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_CONFIGURED; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_EIP_DEFINITION; +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.Constants.RESTART_ON_UPDATE; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.base.models.Constants.SHOW_EXPERIMENTAL; +import static se.leap.bitmaskclient.base.models.Constants.USE_IPv6_FIREWALL; +import static se.leap.bitmaskclient.base.models.Constants.USE_PLUGGABLE_TRANSPORTS; + +/** + * Created by cyberta on 18.03.18. + */ + +public class PreferenceHelper { + + public static Provider getSavedProviderFromSharedPreferences(@NonNull SharedPreferences preferences) { + Provider provider = new Provider(); + try { + provider.setMainUrl(new URL(preferences.getString(Provider.MAIN_URL, ""))); + provider.setProviderIp(preferences.getString(Provider.PROVIDER_IP, "")); + provider.setProviderApiIp(preferences.getString(Provider.PROVIDER_API_IP, "")); + provider.setGeoipUrl(preferences.getString(Provider.GEOIP_URL, "")); + provider.define(new JSONObject(preferences.getString(Provider.KEY, ""))); + provider.setCaCert(preferences.getString(Provider.CA_CERT, "")); + provider.setVpnCertificate(preferences.getString(PROVIDER_VPN_CERTIFICATE, "")); + provider.setPrivateKey(preferences.getString(PROVIDER_PRIVATE_KEY, "")); + provider.setEipServiceJson(new JSONObject(preferences.getString(PROVIDER_EIP_DEFINITION, ""))); + } catch (MalformedURLException | JSONException e) { + e.printStackTrace(); + } + + return provider; + } + + public static String getFromPersistedProvider(String toFetch, String providerDomain, SharedPreferences preferences) { + return preferences.getString(toFetch + "." + providerDomain, ""); + } + + // TODO: replace commit with apply after refactoring EIP + //FIXME: don't save private keys in shared preferences! use the keystore + public static void storeProviderInPreferences(SharedPreferences preferences, Provider provider) { + preferences.edit().putBoolean(PROVIDER_CONFIGURED, true). + putString(Provider.PROVIDER_IP, provider.getProviderIp()). + putString(Provider.GEOIP_URL, provider.getGeoipUrl().toString()). + putString(Provider.PROVIDER_API_IP, provider.getProviderApiIp()). + putString(Provider.MAIN_URL, provider.getMainUrlString()). + putString(Provider.KEY, provider.getDefinitionString()). + putString(Provider.CA_CERT, provider.getCaCert()). + putString(PROVIDER_EIP_DEFINITION, provider.getEipServiceJsonString()). + putString(PROVIDER_PRIVATE_KEY, provider.getPrivateKey()). + putString(PROVIDER_VPN_CERTIFICATE, provider.getVpnCertificate()). + commit(); + + String providerDomain = provider.getDomain(); + preferences.edit().putBoolean(PROVIDER_CONFIGURED, true). + putString(Provider.PROVIDER_IP + "." + providerDomain, provider.getProviderIp()). + putString(Provider.PROVIDER_API_IP + "." + providerDomain, provider.getProviderApiIp()). + putString(Provider.MAIN_URL + "." + providerDomain, provider.getMainUrlString()). + putString(Provider.GEOIP_URL + "." + providerDomain, provider.getGeoipUrl().toString()). + putString(Provider.KEY + "." + providerDomain, provider.getDefinitionString()). + putString(Provider.CA_CERT + "." + providerDomain, provider.getCaCert()). + putString(PROVIDER_EIP_DEFINITION + "." + providerDomain, provider.getEipServiceJsonString()). + apply(); + } + + /** + * Sets the profile that is connected (to connect if the service restarts) + */ + public static void setLastUsedVpnProfile(Context context, VpnProfile connectedProfile) { + SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + SharedPreferences.Editor prefsedit = prefs.edit(); + prefsedit.putString(LAST_USED_PROFILE, connectedProfile.toJson()); + prefsedit.apply(); + } + + /** + * Returns the profile that was last connected (to connect if the service restarts) + */ + public static VpnProfile getLastConnectedVpnProfile(Context context) { + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + String lastConnectedProfileJson = preferences.getString(LAST_USED_PROFILE, null); + return VpnProfile.fromJson(lastConnectedProfileJson); + } + + public static void deleteProviderDetailsFromPreferences(@NonNull SharedPreferences preferences, String providerDomain) { + preferences.edit(). + remove(Provider.KEY + "." + providerDomain). + remove(Provider.CA_CERT + "." + providerDomain). + remove(Provider.PROVIDER_IP + "." + providerDomain). + remove(Provider.PROVIDER_API_IP + "." + providerDomain). + remove(Provider.MAIN_URL + "." + providerDomain). + remove(Provider.GEOIP_URL + "." + providerDomain). + remove(PROVIDER_EIP_DEFINITION + "." + providerDomain). + remove(PROVIDER_PRIVATE_KEY + "." + providerDomain). + remove(PROVIDER_VPN_CERTIFICATE + "." + providerDomain). + apply(); + } + + public static void setLastAppUpdateCheck(Context context) { + putLong(context, LAST_UPDATE_CHECK, System.currentTimeMillis()); + } + + public static long getLastAppUpdateCheck(Context context) { + return getLong(context, LAST_UPDATE_CHECK, 0); + } + + public static void restartOnUpdate(Context context, boolean isEnabled) { + putBoolean(context, RESTART_ON_UPDATE, isEnabled); + } + + public static boolean getRestartOnUpdate(Context context) { + return getBoolean(context, RESTART_ON_UPDATE, false); + } + + public static boolean getUsePluggableTransports(Context context) { + return getBoolean(context, USE_PLUGGABLE_TRANSPORTS, false); + } + + public static void usePluggableTransports(Context context, boolean isEnabled) { + putBoolean(context, USE_PLUGGABLE_TRANSPORTS, isEnabled); + } + + public static void saveBattery(Context context, boolean isEnabled) { + putBoolean(context, DEFAULT_SHARED_PREFS_BATTERY_SAVER, isEnabled); + } + + public static boolean getSaveBattery(Context context) { + return getBoolean(context, DEFAULT_SHARED_PREFS_BATTERY_SAVER, false); + } + + public static void allowUsbTethering(Context context, boolean isEnabled) { + putBoolean(context, ALLOW_TETHERING_USB, isEnabled); + } + + public static boolean isUsbTetheringAllowed(Context context) { + return getBoolean(context, ALLOW_TETHERING_USB, false); + } + + public static void allowWifiTethering(Context context, boolean isEnabled) { + putBoolean(context, ALLOW_TETHERING_WIFI, isEnabled); + } + + public static boolean isWifiTetheringAllowed(Context context) { + return getBoolean(context, ALLOW_TETHERING_WIFI, false); + } + + public static void allowBluetoothTethering(Context context, boolean isEnabled) { + putBoolean(context, ALLOW_TETHERING_BLUETOOTH, isEnabled); + } + + public static boolean isBluetoothTetheringAllowed(Context context) { + return getBoolean(context, ALLOW_TETHERING_BLUETOOTH, false); + } + + public static void setShowExperimentalFeatures(Context context, boolean show) { + putBoolean(context, SHOW_EXPERIMENTAL, show); + } + + public static boolean showExperimentalFeatures(Context context) { + return getBoolean(context, SHOW_EXPERIMENTAL, false); + } + + public static void setUseIPv6Firewall(Context context, boolean useFirewall) { + putBoolean(context, USE_IPv6_FIREWALL, useFirewall); + } + + public static boolean useIpv6Firewall(Context context) { + return getBoolean(context, USE_IPv6_FIREWALL, false); + } + + public static void saveShowAlwaysOnDialog(Context context, boolean showAlwaysOnDialog) { + putBoolean(context, ALWAYS_ON_SHOW_DIALOG, showAlwaysOnDialog); + } + + public static boolean getShowAlwaysOnDialog(Context context) { + return getBoolean(context, ALWAYS_ON_SHOW_DIALOG, true); + } + + public static JSONObject getEipDefinitionFromPreferences(SharedPreferences preferences) { + JSONObject result = new JSONObject(); + try { + String eipDefinitionString = preferences.getString(PROVIDER_EIP_DEFINITION, ""); + if (!eipDefinitionString.isEmpty()) { + result = new JSONObject(eipDefinitionString); + } + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return result; + } + + public static void setExcludedApps(Context context, Set<String> apps) { + SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + SharedPreferences.Editor prefsedit = prefs.edit(); + prefsedit.putStringSet(EXCLUDED_APPS, apps); + prefsedit.apply(); + } + + public static Set<String> getExcludedApps(Context context) { + if (context == null) { + return null; + } + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + return preferences.getStringSet(EXCLUDED_APPS, new HashSet<>()); + } + + public static long getLong(Context context, String key, long defValue) { + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + return preferences.getLong(key, defValue); + } + + public static void putLong(Context context, String key, long value) { + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + preferences.edit().putLong(key, value).apply(); + } + + public static String getString(Context context, String key, String defValue) { + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + return preferences.getString(key, defValue); + } + + public static void putString(Context context, String key, String value) { + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + preferences.edit().putString(key, value).apply(); + } + + public static Boolean getBoolean(Context context, String key, Boolean defValue) { + if (context == null) { + return false; + } + + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + return preferences.getBoolean(key, defValue); + } + + public static void putBoolean(Context context, String key, Boolean value) { + if (context == null) { + return; + } + + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + preferences.edit().putBoolean(key, value).apply(); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java new file mode 100644 index 00000000..23ca40e5 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java @@ -0,0 +1,17 @@ +package se.leap.bitmaskclient.base.utils; + +import android.content.Context; + +import androidx.annotation.DimenRes; + +/** + * Created by cyberta on 29.06.18. + */ + +public class ViewHelper { + + public static int convertDimensionToPx(Context context, @DimenRes int dimension) { + return context.getResources().getDimensionPixelSize(dimension); + } + +} |