summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArne Schwabe <arne@rfc2549.org>2022-02-19 16:08:55 +0100
committerArne Schwabe <arne@rfc2549.org>2022-05-04 19:21:56 +0200
commitfb7a727b9d40b8fcf213528d64e6761e9268b9e1 (patch)
treed3be85209223316dae54c73318b45f2416717dec
parent6d364856a35661e7dad414d38dc34c8cbd8b5985 (diff)
Implement profile encryption using KeyMaster library
-rw-r--r--main/build.gradle.kts3
-rw-r--r--main/src/main/AndroidManifest.xml1
-rw-r--r--main/src/main/java/de/blinkt/openvpn/core/LogItem.java17
-rw-r--r--main/src/main/java/de/blinkt/openvpn/core/ProfileManager.java235
-rwxr-xr-xmain/src/main/res/values/strings.xml1
-rw-r--r--main/src/skeleton/java/de/blinkt/openvpn/core/ProfileEncryption.java37
-rw-r--r--main/src/ui/AndroidManifest.xml4
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/core/ProfileEncryption.kt63
-rw-r--r--main/src/ui/res/xml/general_settings.xml5
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"