diff options
author | Arne Schwabe <arne@rfc2549.org> | 2022-02-19 16:08:55 +0100 |
---|---|---|
committer | Arne Schwabe <arne@rfc2549.org> | 2022-05-04 19:21:56 +0200 |
commit | fb7a727b9d40b8fcf213528d64e6761e9268b9e1 (patch) | |
tree | d3be85209223316dae54c73318b45f2416717dec | |
parent | 6d364856a35661e7dad414d38dc34c8cbd8b5985 (diff) |
Implement profile encryption using KeyMaster library
-rw-r--r-- | main/build.gradle.kts | 3 | ||||
-rw-r--r-- | main/src/main/AndroidManifest.xml | 1 | ||||
-rw-r--r-- | main/src/main/java/de/blinkt/openvpn/core/LogItem.java | 17 | ||||
-rw-r--r-- | main/src/main/java/de/blinkt/openvpn/core/ProfileManager.java | 235 | ||||
-rwxr-xr-x | main/src/main/res/values/strings.xml | 1 | ||||
-rw-r--r-- | main/src/skeleton/java/de/blinkt/openvpn/core/ProfileEncryption.java | 37 | ||||
-rw-r--r-- | main/src/ui/AndroidManifest.xml | 4 | ||||
-rw-r--r-- | main/src/ui/java/de/blinkt/openvpn/core/ProfileEncryption.kt | 63 | ||||
-rw-r--r-- | main/src/ui/res/xml/general_settings.xml | 5 |
9 files changed, 264 insertions, 102 deletions
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 @@ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> - <!-- <queries>--> <!-- <intent>--> <!-- <action android:name="android.intent.action.GET_CONTENT" />--> 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<String, VpnProfile> profiles = new HashMap<>(); private static VpnProfile tmpprofile = null; + private HashMap<String, VpnProfile> 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<VpnProfile> 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 @@ <string name="allow_translations_title">Allow community contributed translations</string> <string name="allow_translations_summary">Allows the app to be translated with translations contributed by the community. Requires a restart of the app to activate.</string> <string name="tls_profile">TLS Security Profile</string> + <string name="encrypt_profiles">Try to encrypt profiles on storage (if supported by Android OS)</string> </resources> 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" /> + <!-- This library wants SDK version 23 but we do runtime checks to not use it before + API 23 --> + <uses-sdk tools:overrideLibrary="androidx.security"/> + <application android:banner="@mipmap/banner_tv"> <activity android:exported="true" diff --git a/main/src/ui/java/de/blinkt/openvpn/core/ProfileEncryption.kt b/main/src/ui/java/de/blinkt/openvpn/core/ProfileEncryption.kt new file mode 100644 index 00000000..ad22460f --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/core/ProfileEncryption.kt @@ -0,0 +1,63 @@ +/* + * 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 android.os.Build +import androidx.security.crypto.EncryptedFile +import androidx.security.crypto.MasterKeys +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.security.GeneralSecurityException + +internal class ProfileEncryption { + + companion object { + @JvmStatic + fun encryptionEnabled(): Boolean { + return mMasterKeyAlias != null + } + + private var mMasterKeyAlias: String? = null + @JvmStatic + fun initMasterCryptAlias() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return + try { + mMasterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) + } catch (e: GeneralSecurityException) { + VpnStatus.logException("Could not initialise file encryption key.", e) + } catch (e: IOException) { + VpnStatus.logException("Could not initialise file encryption key.", e) + } + } + + @JvmStatic + @Throws(GeneralSecurityException::class, IOException::class) + fun getEncryptedVpInput(context: Context, file: File): FileInputStream { + val encryptedFile = EncryptedFile.Builder( + file, + context, + mMasterKeyAlias!!, + EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB + ).build() + return encryptedFile.openFileInput() + } + + @JvmStatic + @Throws(GeneralSecurityException::class, IOException::class) + fun getEncryptedVpOutput(context: Context, file: File): FileOutputStream { + val encryptedFile = EncryptedFile.Builder( + file, + context, + mMasterKeyAlias!!, + EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB + ).build() + return encryptedFile.openFileOutput() + } + } +}
\ No newline at end of file diff --git a/main/src/ui/res/xml/general_settings.xml b/main/src/ui/res/xml/general_settings.xml index 4f294a1a..55eedb94 100644 --- a/main/src/ui/res/xml/general_settings.xml +++ b/main/src/ui/res/xml/general_settings.xml @@ -42,6 +42,11 @@ android:summary="@string/onbootrestartsummary" android:title="@string/onbootrestart"/> + <CheckBoxPreference + android:defaultValue="true" + android:key="preferencryption" + android:title="@string/encrypt_profiles"/> + <Preference android:key="clearapi" android:persistent="false" |