From fb7a727b9d40b8fcf213528d64e6761e9268b9e1 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Sat, 19 Feb 2022 16:08:55 +0100 Subject: Implement profile encryption using KeyMaster library --- main/build.gradle.kts | 3 + main/src/main/AndroidManifest.xml | 1 - .../main/java/de/blinkt/openvpn/core/LogItem.java | 17 +- .../de/blinkt/openvpn/core/ProfileManager.java | 235 ++++++++++++--------- main/src/main/res/values/strings.xml | 1 + .../de/blinkt/openvpn/core/ProfileEncryption.java | 37 ++++ main/src/ui/AndroidManifest.xml | 4 + .../de/blinkt/openvpn/core/ProfileEncryption.kt | 63 ++++++ main/src/ui/res/xml/general_settings.xml | 5 + 9 files changed, 264 insertions(+), 102 deletions(-) create mode 100644 main/src/skeleton/java/de/blinkt/openvpn/core/ProfileEncryption.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/core/ProfileEncryption.kt diff --git a/main/build.gradle.kts b/main/build.gradle.kts index 41a69ed9..1cd491eb 100644 --- a/main/build.gradle.kts +++ b/main/build.gradle.kts @@ -28,6 +28,7 @@ android { } } + testOptions.unitTests.isIncludeAndroidResources = true externalNativeBuild { @@ -188,6 +189,8 @@ dependencies { dependencies.add("uiImplementation", "androidx.webkit:webkit:1.4.0") dependencies.add("uiImplementation", "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1") dependencies.add("uiImplementation", "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1") + dependencies.add("uiImplementation","androidx.security:security-crypto:1.0.0") + testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21") testImplementation("junit:junit:4.13.2") diff --git a/main/src/main/AndroidManifest.xml b/main/src/main/AndroidManifest.xml index 6cfbfffa..61d96c4b 100644 --- a/main/src/main/AndroidManifest.xml +++ b/main/src/main/AndroidManifest.xml @@ -14,7 +14,6 @@ - diff --git a/main/src/main/java/de/blinkt/openvpn/core/LogItem.java b/main/src/main/java/de/blinkt/openvpn/core/LogItem.java index 823343a2..c61cbc44 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/LogItem.java +++ b/main/src/main/java/de/blinkt/openvpn/core/LogItem.java @@ -10,6 +10,7 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; +import android.content.res.Resources; import android.os.Parcel; import android.os.Parcelable; @@ -251,12 +252,16 @@ public class LogItem implements Parcelable { return mMessage; } else { if (c != null) { - if (mRessourceId == R.string.mobile_info) - return getMobileInfoString(c); - if (mArgs == null) - return c.getString(mRessourceId); - else - return c.getString(mRessourceId, mArgs); + try { + if (mRessourceId == R.string.mobile_info) + return getMobileInfoString(c); + if (mArgs == null) + return c.getString(mRessourceId); + else + return c.getString(mRessourceId, mArgs); + } catch (Resources.NotFoundException re) { + return getString(null); + } } else { String str = String.format(Locale.ENGLISH, "Log (no context) resid %d", mRessourceId); if (mArgs != null) diff --git a/main/src/main/java/de/blinkt/openvpn/core/ProfileManager.java b/main/src/main/java/de/blinkt/openvpn/core/ProfileManager.java index 4cf98da2..06822817 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/ProfileManager.java +++ b/main/src/main/java/de/blinkt/openvpn/core/ProfileManager.java @@ -10,15 +10,18 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.security.GeneralSecurityException; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Set; -import java.util.UUID; import de.blinkt.openvpn.VpnProfile; @@ -30,9 +33,12 @@ public class ProfileManager { private static ProfileManager instance; private static VpnProfile mLastConnectedVpn = null; - private HashMap profiles = new HashMap<>(); private static VpnProfile tmpprofile = null; + private HashMap profiles = new HashMap<>(); + + private ProfileManager() { + } private static VpnProfile get(String key) { if (tmpprofile != null && tmpprofile.getUUIDString().equals(key)) @@ -41,16 +47,12 @@ public class ProfileManager { if (instance == null) return null; return instance.profiles.get(key); - - } - - - private ProfileManager() { } private static void checkInstance(Context context) { if (instance == null) { instance = new ProfileManager(); + ProfileEncryption.initMasterCryptAlias(); instance.loadVPNList(context); } } @@ -65,7 +67,6 @@ public class ProfileManager { Editor prefsedit = prefs.edit(); prefsedit.putString(LAST_CONNECTED_PROFILE, null); prefsedit.apply(); - } /** @@ -78,7 +79,6 @@ public class ProfileManager { prefsedit.putString(LAST_CONNECTED_PROFILE, connectedProfile.getUUIDString()); prefsedit.apply(); mLastConnectedVpn = connectedProfile; - } /** @@ -94,6 +94,122 @@ public class ProfileManager { return null; } + public static void setTemporaryProfile(Context c, VpnProfile tmp) { + tmp.mTemporaryProfile = true; + ProfileManager.tmpprofile = tmp; + saveProfile(c, tmp); + } + + public static boolean isTempProfile() { + return mLastConnectedVpn != null && mLastConnectedVpn == tmpprofile; + } + + public static void saveProfile(Context context, VpnProfile profile) { + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(context); + boolean preferEncryption = prefs.getBoolean("preferencryption", true); + + profile.mVersion += 1; + ObjectOutputStream vpnFile; + + String filename = profile.getUUID().toString(); + + if (profile.mTemporaryProfile) + filename = TEMPORARY_PROFILE_FILENAME; + + File encryptedFileOld = context.getFileStreamPath(filename + ".cpold"); + + if (encryptedFileOld.exists()) + { + encryptedFileOld.delete(); + } + + String deleteIfExists; + try { + FileOutputStream vpnFileOut; + if (preferEncryption && ProfileEncryption.encryptionEnabled()) { + File encryptedFile = context.getFileStreamPath(filename + ".cp"); + + if (encryptedFile.exists()) + { + if (!encryptedFile.renameTo(encryptedFileOld)) + { + VpnStatus.logInfo("Cannot rename " + encryptedFile); + } + } + vpnFileOut = ProfileEncryption.getEncryptedVpOutput(context, encryptedFile); + deleteIfExists = filename + ".vp"; + if (encryptedFileOld.exists()) { + encryptedFileOld.delete(); + } + } + else { + vpnFileOut = context.openFileOutput(filename + ".vp", Activity.MODE_PRIVATE); + deleteIfExists = filename + ".cp"; + } + + vpnFile = new ObjectOutputStream(vpnFileOut); + + vpnFile.writeObject(profile); + vpnFile.flush(); + vpnFile.close(); + + File delete = context.getFileStreamPath(deleteIfExists); + if (delete.exists()) + { + //noinspection ResultOfMethodCallIgnored + delete.delete(); + } + + + } catch (IOException | GeneralSecurityException e) { + VpnStatus.logException("saving VPN profile", e); + throw new RuntimeException(e); + } + } + + public static VpnProfile get(Context context, String profileUUID) { + return get(context, profileUUID, 0, 10); + } + + public static VpnProfile get(Context context, String profileUUID, int version, int tries) { + checkInstance(context); + VpnProfile profile = get(profileUUID); + int tried = 0; + while ((profile == null || profile.mVersion < version) && (tried++ < tries)) { + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + } + instance.loadVPNList(context); + profile = get(profileUUID); + } + + if (tried > 5) { + int ver = profile == null ? -1 : profile.mVersion; + VpnStatus.logError(String.format(Locale.US, "Used x %d tries to get current version (%d/%d) of the profile", tried, ver, version)); + } + return profile; + } + + public static VpnProfile getLastConnectedVpn() { + return mLastConnectedVpn; + } + + public static VpnProfile getAlwaysOnVPN(Context context) { + checkInstance(context); + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(context); + + String uuid = prefs.getString("alwaysOnVpn", null); + return get(uuid); + + } + + public static void updateLRU(Context c, VpnProfile profile) { + profile.mLastUsed = System.currentTimeMillis(); + // LRU does not change the profile, no need for the service to refresh + if (profile != tmpprofile) + saveProfile(c, profile); + } public Collection getProfiles() { return profiles.values(); @@ -119,46 +235,12 @@ public class ProfileManager { int counter = sharedprefs.getInt("counter", 0); editor.putInt("counter", counter + 1); editor.apply(); - } public void addProfile(VpnProfile profile) { profiles.put(profile.getUUID().toString(), profile); - - } - - public static void setTemporaryProfile(Context c, VpnProfile tmp) { - tmp.mTemporaryProfile = true; - ProfileManager.tmpprofile = tmp; - saveProfile(c, tmp); - } - - public static boolean isTempProfile() { - return mLastConnectedVpn != null && mLastConnectedVpn == tmpprofile; - } - - public static void saveProfile(Context context, VpnProfile profile) { - - profile.mVersion += 1; - ObjectOutputStream vpnFile; - - String filename = profile.getUUID().toString() + ".vp"; - if (profile.mTemporaryProfile) - filename = TEMPORARY_PROFILE_FILENAME + ".vp"; - - try { - vpnFile = new ObjectOutputStream(context.openFileOutput(filename, Activity.MODE_PRIVATE)); - - vpnFile.writeObject(profile); - vpnFile.flush(); - vpnFile.close(); - } catch (IOException e) { - VpnStatus.logException("saving VPN profile", e); - throw new RuntimeException(e); - } } - private void loadVPNList(Context context) { profiles = new HashMap<>(); SharedPreferences listpref = Preferences.getSharedPreferencesMulti(PREFS_NAME, context); @@ -170,9 +252,20 @@ public class ProfileManager { vlist.add(TEMPORARY_PROFILE_FILENAME); for (String vpnentry : vlist) { - ObjectInputStream vpnfile=null; + ObjectInputStream vpnfile = null; try { - vpnfile = new ObjectInputStream(context.openFileInput(vpnentry + ".vp")); + FileInputStream vpInput; + File encryptedPath = context.getFileStreamPath(vpnentry + ".cp"); + File encryptedPathOld = context.getFileStreamPath(vpnentry + ".cpold"); + + if (encryptedPath.exists()) { + vpInput = ProfileEncryption.getEncryptedVpInput(context, encryptedPath); + } else if (encryptedPathOld.exists()) { + vpInput = ProfileEncryption.getEncryptedVpInput(context, encryptedPathOld); + } else { + vpInput = context.openFileInput(vpnentry + ".vp"); + } + vpnfile = new ObjectInputStream(vpInput); VpnProfile vp = ((VpnProfile) vpnfile.readObject()); // Sanity check @@ -187,11 +280,11 @@ public class ProfileManager { } - } catch (IOException | ClassNotFoundException e) { + } catch (IOException | ClassNotFoundException | GeneralSecurityException e) { if (!vpnentry.equals(TEMPORARY_PROFILE_FILENAME)) VpnStatus.logException("Loading VPN List", e); } finally { - if (vpnfile!=null) { + if (vpnfile != null) { try { vpnfile.close(); } catch (IOException e) { @@ -202,7 +295,6 @@ public class ProfileManager { } } - public void removeProfile(Context context, VpnProfile profile) { String vpnentry = profile.getUUID().toString(); profiles.remove(vpnentry); @@ -212,51 +304,4 @@ public class ProfileManager { mLastConnectedVpn = null; } - - public static VpnProfile get(Context context, String profileUUID) { - return get(context, profileUUID, 0, 10); - } - - public static VpnProfile get(Context context, String profileUUID, int version, int tries) { - checkInstance(context); - VpnProfile profile = get(profileUUID); - int tried = 0; - while ((profile == null || profile.mVersion < version) && (tried++ < tries)) { - try { - Thread.sleep(100); - } catch (InterruptedException ignored) { - } - instance.loadVPNList(context); - profile = get(profileUUID); - int ver = profile == null ? -1 : profile.mVersion; - } - - if (tried > 5) - - { - int ver = profile == null ? -1 : profile.mVersion; - VpnStatus.logError(String.format(Locale.US, "Used x %d tries to get current version (%d/%d) of the profile", tried, ver, version)); - } - return profile; - } - - public static VpnProfile getLastConnectedVpn() { - return mLastConnectedVpn; - } - - public static VpnProfile getAlwaysOnVPN(Context context) { - checkInstance(context); - SharedPreferences prefs = Preferences.getDefaultSharedPreferences(context); - - String uuid = prefs.getString("alwaysOnVpn", null); - return get(uuid); - - } - - public static void updateLRU(Context c, VpnProfile profile) { - profile.mLastUsed = System.currentTimeMillis(); - // LRU does not change the profile, no need for the service to refresh - if (profile != tmpprofile) - saveProfile(c, profile); - } } diff --git a/main/src/main/res/values/strings.xml b/main/src/main/res/values/strings.xml index b02c2259..da32ec9e 100755 --- a/main/src/main/res/values/strings.xml +++ b/main/src/main/res/values/strings.xml @@ -504,5 +504,6 @@ Allow community contributed translations Allows the app to be translated with translations contributed by the community. Requires a restart of the app to activate. TLS Security Profile + Try to encrypt profiles on storage (if supported by Android OS) diff --git a/main/src/skeleton/java/de/blinkt/openvpn/core/ProfileEncryption.java b/main/src/skeleton/java/de/blinkt/openvpn/core/ProfileEncryption.java new file mode 100644 index 00000000..c526a69f --- /dev/null +++ b/main/src/skeleton/java/de/blinkt/openvpn/core/ProfileEncryption.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012-2022 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.content.Context; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; + +/* Dummy class that supports no encryption */ +class ProfileEncryption { + static void initMasterCryptAlias() + { + + } + + static boolean encryptionEnabled() + { + return false; + } + + static FileInputStream getEncryptedVpInput(Context context, File file) throws GeneralSecurityException, IOException { + throw new GeneralSecurityException("encryption of file not supported in this build"); + } + + static FileOutputStream getEncryptedVpOutput(Context context, File file) throws GeneralSecurityException, IOException { + throw new GeneralSecurityException("encryption of file not supported in this build"); + } + + +} diff --git a/main/src/ui/AndroidManifest.xml b/main/src/ui/AndroidManifest.xml index b3bd8ecf..21241f0a 100644 --- a/main/src/ui/AndroidManifest.xml +++ b/main/src/ui/AndroidManifest.xml @@ -14,6 +14,10 @@ android:name="android.hardware.touchscreen" android:required="false" /> + + + + +