]> git.ipfire.org Git - thirdparty/strongswan.git/commitdiff
android: Add facility to request a password from a user via notification
authorTobias Brunner <tobias@strongswan.org>
Fri, 27 Mar 2020 09:10:43 +0000 (10:10 +0100)
committerTobias Brunner <tobias@strongswan.org>
Fri, 30 Oct 2020 14:34:07 +0000 (15:34 +0100)
Due to the use of CompletableFuture this requires at least Android 7 (API
level 24), so we'll have to use the existing dialog on older versions.

src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java
src/frontends/android/app/src/main/res/values-de/strings.xml
src/frontends/android/app/src/main/res/values-pl/strings.xml
src/frontends/android/app/src/main/res/values-ru/strings.xml
src/frontends/android/app/src/main/res/values-ua/strings.xml
src/frontends/android/app/src/main/res/values-zh-rCN/strings.xml
src/frontends/android/app/src/main/res/values-zh-rTW/strings.xml
src/frontends/android/app/src/main/res/values/strings.xml

index da6ea637412efdae740fd9b397f2a95bc227db11..bee99cf103d4f85d4a36ed55c105998fa86d8476 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012-2018 Tobias Brunner
+ * Copyright (C) 2012-2020 Tobias Brunner
  * Copyright (C) 2012 Giuliano Grassi
  * Copyright (C) 2012 Ralf Sager
  * HSR Hochschule fuer Technik Rapperswil
@@ -23,9 +23,11 @@ import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
@@ -50,6 +52,7 @@ import org.strongswan.android.logic.VpnStateService.State;
 import org.strongswan.android.logic.imc.ImcState;
 import org.strongswan.android.logic.imc.RemediationInstruction;
 import org.strongswan.android.ui.MainActivity;
+import org.strongswan.android.ui.VpnLoginActivity;
 import org.strongswan.android.ui.VpnProfileControlActivity;
 import org.strongswan.android.utils.Constants;
 import org.strongswan.android.utils.IPRange;
@@ -73,9 +76,16 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 import java.util.SortedSet;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
+import androidx.annotation.RequiresApi;
 import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
 import androidx.core.content.ContextCompat;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import androidx.preference.PreferenceManager;
 
 public class CharonVpnService extends VpnService implements Runnable, VpnStateService.VpnStateListener
@@ -84,9 +94,11 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
        private static final String VPN_SERVICE_ACTION = "android.net.VpnService";
        public static final String DISCONNECT_ACTION = "org.strongswan.android.CharonVpnService.DISCONNECT";
        private static final String NOTIFICATION_CHANNEL = "org.strongswan.android.CharonVpnService.VPN_STATE_NOTIFICATION";
+       private static final String PASSWORD_CHANNEL = "org.strongswan.android.CharonVpnService.VPN_PASSWORD_NOTIFICATION";
        public static final String LOG_FILE = "charon.log";
        public static final String KEY_IS_RETRY = "retry";
        public static final int VPN_STATE_NOTIFICATION_ID = 1;
+       public static final int PASSWORD_NOTIFICATION_ID = 2;
 
        private String mLogFile;
        private String mAppDir;
@@ -101,6 +113,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
        private volatile boolean mIsDisconnecting;
        private volatile boolean mShowNotification;
        private BuilderAdapter mBuilderAdapter = new BuilderAdapter();
+       private PasswordPrompt mPasswordPrompt = new PasswordPrompt();
        private Handler mHandler;
        private VpnStateService mService;
        private final Object mServiceLock = new Object();
@@ -202,7 +215,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
                bindService(new Intent(this, VpnStateService.class),
                                        mServiceConnection, Service.BIND_AUTO_CREATE);
 
-               createNotificationChannel();
+               createNotificationChannels();
        }
 
        @Override
@@ -286,6 +299,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
                                                mIsDisconnecting = false;
 
                                                SimpleFetcher.enable();
+                                               mPasswordPrompt.enable();
                                                addNotification();
                                                mBuilderAdapter.setProfile(mCurrentProfile);
                                                if (initializeCharon(mBuilderAdapter, mLogFile, mAppDir, mCurrentProfile.getVpnType().has(VpnTypeFeature.BYOD),
@@ -356,6 +370,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
                                setState(State.DISCONNECTING);
                                mIsDisconnecting = true;
                                SimpleFetcher.disable();
+                               mPasswordPrompt.disable();
                                deinitializeCharon();
                                Log.i(TAG, "charon stopped");
                                mCurrentProfile = null;
@@ -404,17 +419,25 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
        /**
         * Create a notification channel for Android 8+
         */
-       private void createNotificationChannel()
+       private void createNotificationChannels()
        {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
                {
+                       NotificationManager notificationManager = getSystemService(NotificationManager.class);
                        NotificationChannel channel;
+
                        channel = new NotificationChannel(NOTIFICATION_CHANNEL, getString(R.string.permanent_notification_name),
                                                                                          NotificationManager.IMPORTANCE_LOW);
                        channel.setDescription(getString(R.string.permanent_notification_description));
                        channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
                        channel.setShowBadge(false);
-                       NotificationManager notificationManager = getSystemService(NotificationManager.class);
+                       notificationManager.createNotificationChannel(channel);
+
+                       channel = new NotificationChannel(PASSWORD_CHANNEL, getString(R.string.password_notification_name),
+                                                                                         NotificationManager.IMPORTANCE_HIGH);
+                       channel.setDescription(getString(R.string.password_notification_description));
+                       channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
+                       channel.setShowBadge(false);
                        notificationManager.createNotificationChannel(channel);
                }
        }
@@ -1358,6 +1381,128 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
                }
        }
 
+       private class PasswordPrompt
+       {
+               private int PASSWORD_TIMEOUT = 20;
+               private Object mLock = new Object();
+               private CompletableFuture<String> mPassword;
+               private boolean mDisabled;
+               private BroadcastReceiver mPasswordHandler = new BroadcastReceiver()
+               {
+                       @RequiresApi(api = Build.VERSION_CODES.N)
+                       @Override
+                       public void onReceive(Context context, Intent intent)
+                       {
+                               String password = intent.getStringExtra(VpnProfileDataSource.KEY_PASSWORD);
+                               synchronized (mLock)
+                               {
+                                       if (mPassword != null)
+                                       {
+                                               mPassword.complete(password);
+                                               hideNotification();
+                                       }
+                               }
+                       }
+               };
+
+               public void enable()
+               {
+                       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+                       {
+                               IntentFilter passwordFilter = new IntentFilter(Constants.VPN_PASSWORD_ENTERED);
+                               LocalBroadcastManager.getInstance(CharonVpnService.this).registerReceiver(mPasswordHandler, passwordFilter);
+                       }
+                       synchronized (mLock)
+                       {
+                               mDisabled = false;
+                       }
+               }
+
+               public void disable()
+               {
+                       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+                       {
+                               LocalBroadcastManager.getInstance(CharonVpnService.this).unregisterReceiver(mPasswordHandler);
+                       }
+                       synchronized (mLock)
+                       {
+                               mDisabled = true;
+                               if (mPassword != null)
+                               {
+                                       hideNotification();
+                                       mPassword.cancel(true);
+                                       mPassword = null;
+                               }
+                       }
+               }
+
+               private void hideNotification()
+               {
+                       NotificationManagerCompat manager = NotificationManagerCompat.from(CharonVpnService.this);
+                       manager.cancel(PASSWORD_NOTIFICATION_ID);
+               }
+
+               @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+               private Notification buildNotification(boolean publicVersion)
+               {
+                       CharonVpnService service = CharonVpnService.this;
+
+                       Intent intent = new Intent(service, VpnLoginActivity.class);
+                       intent.putExtra(VpnProfileDataSource.KEY_USERNAME, mCurrentProfile.getUsername());
+                       PendingIntent pending = PendingIntent.getActivity(service, 0, intent,
+                               PendingIntent.FLAG_UPDATE_CURRENT);
+
+                       NotificationCompat.Builder builder =
+                               new NotificationCompat.Builder(service, PASSWORD_CHANNEL)
+                                       .setSmallIcon(R.drawable.ic_notification_warning)
+                                       .setColor(ContextCompat.getColor(service, R.color.warning_text))
+                                       .setContentTitle(getString(R.string.password_notification_prompt))
+                                       .setTimeoutAfter(PASSWORD_TIMEOUT * 1000)
+                                       .setWhen(System.currentTimeMillis() + PASSWORD_TIMEOUT * 1000)
+                                       .setUsesChronometer(true)
+                                       .setPriority(NotificationCompat.PRIORITY_HIGH)
+                                       .setCategory(NotificationCompat.CATEGORY_STATUS)
+                                       .setFullScreenIntent(pending, true);
+
+                       if (!publicVersion)
+                       {
+                               builder.setContentText(mCurrentProfile.getName());
+                               builder.setPublicVersion(buildNotification(true));
+                       }
+
+                       Notification notification = builder.build();
+                       /* hack because even though the documentation says setChronometerCountDown() should exist,
+                        * it currently doesn't, maybe comes with an update to AndroidX */
+                       notification.extras.putBoolean("android.chronometerCountDown", true);
+                       return notification;
+               }
+
+               @RequiresApi(api = Build.VERSION_CODES.N)
+               public String getPassword()
+               {
+                       synchronized (mLock)
+                       {
+                               if (mDisabled)
+                               {
+                                       return null;
+                               }
+                               mPassword = new CompletableFuture<>();
+                       }
+
+                       NotificationManagerCompat manager = NotificationManagerCompat.from(CharonVpnService.this);
+                       manager.notify(PASSWORD_NOTIFICATION_ID, buildNotification(false));
+                       try
+                       {
+                               return mPassword.get(PASSWORD_TIMEOUT, TimeUnit.SECONDS);
+                       }
+                       catch (ExecutionException|InterruptedException|TimeoutException e)
+                       {
+                               hideNotification();
+                       }
+                       return null;
+               }
+       }
+
        /**
         * Function called via JNI to determine information about the Android version.
         */
index e833d25621e5cdad74674a2e25ae8550a8dbfd4c..687befe8a1c917f0d13c7d7f90f9b079c8c8ce16 100644 (file)
@@ -31,6 +31,9 @@
     <string name="strongswan_shortcut">strongSwan-Verknüpfung</string>
     <string name="permanent_notification_name">VPN Verbindungsstatus</string>
     <string name="permanent_notification_description">Zeigt Informationen zum Verbindungsstatus der VPN Verbindung und dient als permanente Notification dazu, den VPN Dienst im Hintergrund am Laufen zu halten.</string>
+    <string name="password_notification_name">VPN Passwort</string>
+    <string name="password_notification_description">Wird verwendet, um vom Benutzer ein Passwort zu verlangen.</string>
+    <string name="password_notification_prompt">Password benötigt</string>
 
     <!-- Settings -->
     <string name="pref_title">Einstellungen</string>
index 791ee1380b7139a8e60caad595610ff1568e89f6..dbb06f9aff075b3a3b482354f409a8b1f4cf9db4 100644 (file)
@@ -31,6 +31,9 @@
     <string name="strongswan_shortcut">Skrót strongSwan</string>
     <string name="permanent_notification_name">VPN connection state</string>
     <string name="permanent_notification_description">Provides information about the VPN connection state and serves as permanent notification to keep the VPN service running in the background.</string>
+    <string name="password_notification_name">VPN password</string>
+    <string name="password_notification_description">Used to request a password from the user.</string>
+    <string name="password_notification_prompt">Password required</string>
 
     <!-- Settings -->
     <string name="pref_title">Settings</string>
index e4421bf162843b8e6bb4ca9d7672e648fe513316..f4925f5ec254bca00ea6e852448cadb03ff86a27 100644 (file)
@@ -28,6 +28,9 @@
     <string name="strongswan_shortcut">Ссылка на strongSwan</string>
     <string name="permanent_notification_name">VPN connection state</string>
     <string name="permanent_notification_description">Provides information about the VPN connection state and serves as permanent notification to keep the VPN service running in the background.</string>
+    <string name="password_notification_name">VPN password</string>
+    <string name="password_notification_description">Used to request a password from the user.</string>
+    <string name="password_notification_prompt">Password required</string>
 
     <!-- Settings -->
     <string name="pref_title">Settings</string>
index 5a6e3aa61b5d237ee4eda181a35a84e911faf4a9..45ded7841c1683e78cbe23ed92e61712f1b71b8d 100644 (file)
@@ -29,6 +29,9 @@
     <string name="strongswan_shortcut">strongSwan посилання</string>
     <string name="permanent_notification_name">VPN connection state</string>
     <string name="permanent_notification_description">Provides information about the VPN connection state and serves as permanent notification to keep the VPN service running in the background.</string>
+    <string name="password_notification_name">VPN password</string>
+    <string name="password_notification_description">Used to request a password from the user.</string>
+    <string name="password_notification_prompt">Password required</string>
 
     <!-- Settings -->
     <string name="pref_title">Settings</string>
index efb27552a536d12e25ce7a402d004324701f4fb9..cdb6f79c3b5d48bd6ed7488761fc18572ae9797f 100644 (file)
@@ -28,6 +28,9 @@
     <string name="strongswan_shortcut">strongSwan快捷方式</string>
     <string name="permanent_notification_name">VPN connection state</string>
     <string name="permanent_notification_description">Provides information about the VPN connection state and serves as permanent notification to keep the VPN service running in the background.</string>
+    <string name="password_notification_name">VPN password</string>
+    <string name="password_notification_description">Used to request a password from the user.</string>
+    <string name="password_notification_prompt">Password required</string>
 
     <!-- Settings -->
     <string name="pref_title">Settings</string>
index ba04ad43ff5c66a2030a325a6434f8d48f5a1126..51759fc50aad181b446dbedd4683dd46672f9f5c 100644 (file)
@@ -28,6 +28,9 @@
     <string name="strongswan_shortcut">strongSwan快速選單</string>
     <string name="permanent_notification_name">VPN connection state</string>
     <string name="permanent_notification_description">Provides information about the VPN connection state and serves as permanent notification to keep the VPN service running in the background.</string>
+    <string name="password_notification_name">VPN password</string>
+    <string name="password_notification_description">Used to request a password from the user.</string>
+    <string name="password_notification_prompt">Password required</string>
 
     <!-- Settings -->
     <string name="pref_title">Settings</string>
index 3d01d4f7c7fc0a494e48baf4b5facf023a55242c..22d496bb4ca630f7ab1bd6c0f8acff2e12a25ed7 100644 (file)
@@ -31,6 +31,9 @@
     <string name="strongswan_shortcut">strongSwan shortcut</string>
     <string name="permanent_notification_name">VPN connection state</string>
     <string name="permanent_notification_description">Provides information about the VPN connection state and serves as permanent notification to keep the VPN service running in the background.</string>
+    <string name="password_notification_name">VPN password</string>
+    <string name="password_notification_description">Used to request a password from the user.</string>
+    <string name="password_notification_prompt">Password required</string>
 
     <!-- Settings -->
     <string name="pref_title">Settings</string>