Parcourir la source

Merge pull request #2372 from nextcloud/improve-notifications

Fixes #2315 and other issues
Andy Scherzinger il y a 7 ans
Parent
commit
b66c5975fb

+ 11 - 0
src/generic/java/com/owncloud/android/utils/PushUtils.java

@@ -23,8 +23,11 @@ package com.owncloud.android.utils;
 import android.content.Context;
 
 import com.owncloud.android.MainApp;
+import com.owncloud.android.datamodel.SignatureVerification;
 import com.owncloud.android.db.PreferenceManager;
 
+import java.security.Key;
+
 public class PushUtils {
     public static final String KEY_PUSH = "push";
 
@@ -37,4 +40,12 @@ public class PushUtils {
         PreferenceManager.setKeysReInit(context);
     }
 
+    public static Key readKeyFromFile(boolean readPublicKey) {
+        return null;
+    }
+
+    public SignatureVerification verifySignature(Context context, byte[] signatureBytes, byte[] subjectBytes) {
+        return null;
+    }
+
 }

+ 0 - 1
src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseInstanceIDService.java

@@ -39,4 +39,3 @@ public class NCFirebaseInstanceIDService extends FirebaseInstanceIdService {
         }
     }
 }
-

+ 16 - 39
src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java

@@ -19,51 +19,28 @@
  */
 package com.owncloud.android.services.firebase;
 
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.BitmapFactory;
-import android.media.RingtoneManager;
-import android.support.v4.app.NotificationCompat;
-
+import com.evernote.android.job.JobRequest;
+import com.evernote.android.job.util.support.PersistableBundleCompat;
 import com.google.firebase.messaging.FirebaseMessagingService;
 import com.google.firebase.messaging.RemoteMessage;
-import com.owncloud.android.MainApp;
-import com.owncloud.android.R;
-import com.owncloud.android.ui.activity.NotificationsActivity;
-import com.owncloud.android.ui.notifications.NotificationUtils;
-import com.owncloud.android.utils.ThemeUtils;
+import com.owncloud.android.jobs.NotificationJob;
 
 public class NCFirebaseMessagingService extends FirebaseMessagingService {
 
     @Override
     public void onMessageReceived(RemoteMessage remoteMessage) {
-        super.onMessageReceived(remoteMessage);
-
-        sendNotification(MainApp.getAppContext().getString(R.string.new_notification_received));
-    }
-
-    private void sendNotification(String contentTitle) {
-        Intent intent = new Intent(this, NotificationsActivity.class);
-        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
-
-        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
-                .setSmallIcon(R.drawable.notification_icon)
-                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notification_icon))
-                .setColor(ThemeUtils.primaryColor())
-                .setContentTitle(contentTitle)
-                .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
-                .setAutoCancel(true)
-                .setContentIntent(pendingIntent);
-
-        if ((android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O)) {
-            notificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_PUSH);
+        if (remoteMessage.getData() != null) {
+            PersistableBundleCompat persistableBundleCompat = new PersistableBundleCompat();
+            persistableBundleCompat.putString(NotificationJob.KEY_NOTIFICATION_SUBJECT, remoteMessage.getData().get
+                    (NotificationJob.KEY_NOTIFICATION_SUBJECT));
+            persistableBundleCompat.putString(NotificationJob.KEY_NOTIFICATION_SIGNATURE, remoteMessage.getData().get
+                    (NotificationJob.KEY_NOTIFICATION_SIGNATURE));
+            new JobRequest.Builder(NotificationJob.TAG)
+                    .addExtras(persistableBundleCompat)
+                    .setUpdateCurrent(false)
+                    .startNow()
+                    .build()
+                    .schedule();
         }
-
-        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
-
-        notificationManager.notify(0, notificationBuilder.build());
     }
-}
+}

+ 85 - 8
src/gplay/java/com/owncloud/android/utils/PushUtils.java

@@ -2,7 +2,7 @@
  * Nextcloud Android client application
  *
  * @author Mario Danic
- * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -34,6 +34,7 @@ import com.owncloud.android.R;
 import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.PushConfigurationState;
+import com.owncloud.android.datamodel.SignatureVerification;
 import com.owncloud.android.db.PreferenceManager;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.OwnCloudClient;
@@ -55,6 +56,7 @@ import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.security.InvalidKeyException;
 import java.security.Key;
 import java.security.KeyFactory;
 import java.security.KeyPair;
@@ -62,6 +64,8 @@ import java.security.KeyPairGenerator;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.security.spec.X509EncodedKeySpec;
@@ -69,14 +73,12 @@ import java.util.Locale;
 
 public class PushUtils {
 
+    public static final String KEY_PUSH = "push";
     private static final String TAG = "PushUtils";
     private static final String KEYPAIR_FOLDER = "nc-keypair";
     private static final String KEYPAIR_FILE_NAME = "push_key";
     private static final String KEYPAIR_PRIV_EXTENSION = ".priv";
     private static final String KEYPAIR_PUB_EXTENSION = ".pub";
-
-    public static final String KEY_PUSH = "push";
-
     private static ArbitraryDataProvider arbitraryDataProvider;
 
     public static String generateSHA512Hash(String pushToken) {
@@ -99,10 +101,10 @@ public class PushUtils {
         }
         return result.toString();
     }
-    
+
     private static int generateRsa2048KeyPair() {
         migratePushKeys();
-        String keyPath = MainApp.getAppContext().getFilesDir().getAbsolutePath() + File.separator + 
+        String keyPath = MainApp.getAppContext().getFilesDir().getAbsolutePath() + File.separator +
                 MainApp.getDataFolder() + File.separator + KEYPAIR_FOLDER;
 
         String privateKeyPath = keyPath + File.separator + KEYPAIR_FILE_NAME + KEYPAIR_PRIV_EXTENSION;
@@ -230,7 +232,7 @@ public class PushUtils {
                                             publicKey,
                                             context.getResources().getString(R.string.push_server_url));
 
-                            RemoteOperationResult remoteOperationResult = 
+                            RemoteOperationResult remoteOperationResult =
                                     registerAccountDeviceForNotificationsOperation.execute(mClient);
 
                             if (remoteOperationResult.isSuccess()) {
@@ -274,7 +276,7 @@ public class PushUtils {
     }
 
     public static Key readKeyFromFile(boolean readPublicKey) {
-        String keyPath = MainApp.getAppContext().getFilesDir().getAbsolutePath() + File.separator + 
+        String keyPath = MainApp.getAppContext().getFilesDir().getAbsolutePath() + File.separator +
                 MainApp.getDataFolder() + File.separator + KEYPAIR_FOLDER;
 
         String privateKeyPath = keyPath + File.separator + KEYPAIR_FILE_NAME + KEYPAIR_PRIV_EXTENSION;
@@ -397,4 +399,79 @@ public class PushUtils {
             }
         }
     }
+
+    public SignatureVerification verifySignature(Context context, byte[] signatureBytes, byte[] subjectBytes) {
+        Signature signature = null;
+        PublicKey publicKey;
+        SignatureVerification signatureVerification = new SignatureVerification();
+        signatureVerification.setSignatureValid(false);
+
+        Account[] accounts = AccountUtils.getAccounts(context);
+
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(context.getContentResolver());
+        String arbitraryValue;
+        Gson gson = new Gson();
+        PushConfigurationState pushArbitraryData;
+
+
+        try {
+            signature = Signature.getInstance("SHA512withRSA");
+            if (accounts.length > 0) {
+                for (Account account : accounts) {
+                    if (!TextUtils.isEmpty(arbitraryValue = arbitraryDataProvider.getValue(account, KEY_PUSH))) {
+                        pushArbitraryData = gson.fromJson(arbitraryValue, PushConfigurationState.class);
+                        if (!pushArbitraryData.isShouldBeDeleted()) {
+                            publicKey = (PublicKey) readKeyFromString(true, pushArbitraryData.getUserPublicKey());
+                            signature.initVerify(publicKey);
+                            signature.update(subjectBytes);
+                            if (signature.verify(signatureBytes)) {
+                                signatureVerification.setSignatureValid(true);
+                                signatureVerification.setAccount(account);
+                                return signatureVerification;
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (NoSuchAlgorithmException e) {
+            Log.d(TAG, "No such algorithm");
+        } catch (InvalidKeyException e) {
+            Log.d(TAG, "Invalid key while trying to verify");
+        } catch (SignatureException e) {
+            Log.d(TAG, "Signature exception while trying to verify");
+        }
+
+        return signatureVerification;
+    }
+
+    private Key readKeyFromString(boolean readPublicKey, String keyString) {
+        String modifiedKey;
+        if (readPublicKey) {
+            modifiedKey = keyString.replaceAll("\\n", "").replace("-----BEGIN PUBLIC KEY-----",
+                    "").replace("-----END PUBLIC KEY-----", "");
+        } else {
+            modifiedKey = keyString.replaceAll("\\n", "").replace("-----BEGIN PRIVATE KEY-----",
+                    "").replace("-----END PRIVATE KEY-----", "");
+        }
+
+        KeyFactory keyFactory = null;
+        try {
+            keyFactory = KeyFactory.getInstance("RSA");
+            if (readPublicKey) {
+                X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.decode(modifiedKey, Base64.DEFAULT));
+                return keyFactory.generatePublic(keySpec);
+            } else {
+                PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(modifiedKey, Base64.DEFAULT));
+                return keyFactory.generatePrivate(keySpec);
+            }
+
+        } catch (NoSuchAlgorithmException e) {
+            Log.d("TAG", "No such algorithm while reading key from string");
+        } catch (InvalidKeySpecException e) {
+            Log.d("TAG", "Invalid key spec while reading key from string");
+        }
+
+        return null;
+    }
+
 }

+ 75 - 0
src/main/java/com/owncloud/android/datamodel/DecryptedPushMessage.java

@@ -0,0 +1,75 @@
+/*
+ * Nextcloud application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * 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 com.owncloud.android.datamodel;
+
+import org.parceler.Parcel;
+
+@Parcel public class DecryptedPushMessage {
+    public String app;
+
+    public String type;
+
+    public String subject;
+
+    public String id;
+
+    public DecryptedPushMessage(String app, String type, String subject, String id) {
+        this.app = app;
+        this.type = type;
+        this.subject = subject;
+        this.id = id;
+    }
+
+    public DecryptedPushMessage() {
+    }
+
+    public String getApp() {
+        return app;
+    }
+
+    public void setApp(String app) {
+        this.app = app;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getSubject() {
+        return subject;
+    }
+
+    public void setSubject(String subject) {
+        this.subject = subject;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+}

+ 54 - 0
src/main/java/com/owncloud/android/datamodel/SignatureVerification.java

@@ -0,0 +1,54 @@
+/*
+ * Nextcloud application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * 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 com.owncloud.android.datamodel;
+
+import android.accounts.Account;
+
+import org.parceler.Parcel;
+
+@Parcel
+public class SignatureVerification {
+    public boolean signatureValid;
+    public Account account;
+
+    public SignatureVerification() {
+    }
+
+    public SignatureVerification(boolean signatureValid, Account account) {
+        this.signatureValid = signatureValid;
+        this.account = account;
+    }
+
+    public boolean isSignatureValid() {
+        return signatureValid;
+    }
+
+    public void setSignatureValid(boolean signatureValid) {
+        this.signatureValid = signatureValid;
+    }
+
+    public Account getAccount() {
+        return account;
+    }
+
+    public void setAccount(Account account) {
+        this.account = account;
+    }
+}

+ 2 - 0
src/main/java/com/owncloud/android/jobs/NCJobCreator.java

@@ -41,6 +41,8 @@ public class NCJobCreator implements JobCreator {
                 return new FilesSyncJob();
             case OfflineSyncJob.TAG:
                 return new OfflineSyncJob();
+            case NotificationJob.TAG:
+                return new NotificationJob();
             default:
                 return null;
         }

+ 140 - 0
src/main/java/com/owncloud/android/jobs/NotificationJob.java

@@ -0,0 +1,140 @@
+/*
+ * Nextcloud application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * 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 com.owncloud.android.jobs;
+
+import android.accounts.Account;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.media.RingtoneManager;
+import android.support.annotation.NonNull;
+import android.support.v4.app.NotificationCompat;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+
+import com.evernote.android.job.Job;
+import com.evernote.android.job.util.support.PersistableBundleCompat;
+import com.google.gson.Gson;
+import com.owncloud.android.R;
+import com.owncloud.android.datamodel.DecryptedPushMessage;
+import com.owncloud.android.datamodel.SignatureVerification;
+import com.owncloud.android.ui.activity.NotificationsActivity;
+import com.owncloud.android.ui.notifications.NotificationUtils;
+import com.owncloud.android.utils.PushUtils;
+import com.owncloud.android.utils.ThemeUtils;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
+
+public class NotificationJob extends Job {
+    public static final String TAG = "NotificationJob";
+
+    public static final String KEY_NOTIFICATION_SUBJECT = "subject";
+    public static final String KEY_NOTIFICATION_SIGNATURE = "signature";
+    public static final String KEY_NOTIFICATION_ACCOUNT = "KEY_NOTIFICATION_ACCOUNT";
+
+    @NonNull
+    @Override
+    protected Result onRunJob(Params params) {
+
+        Context context = getContext();
+        PersistableBundleCompat persistableBundleCompat = getParams().getExtras();
+        String subject = persistableBundleCompat.getString(KEY_NOTIFICATION_SUBJECT, "");
+        String signature = persistableBundleCompat.getString(KEY_NOTIFICATION_SIGNATURE, "");
+
+        if (!TextUtils.isEmpty(subject) && !TextUtils.isEmpty(signature)) {
+
+            try {
+                byte[] base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT);
+                byte[] base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT);
+                PushUtils pushUtils = new PushUtils();
+                PrivateKey privateKey = (PrivateKey) PushUtils.readKeyFromFile(false);
+
+                try {
+                    SignatureVerification signatureVerification = pushUtils.verifySignature(context,
+                            base64DecodedSignature, base64DecodedSubject);
+
+                    if (signatureVerification.isSignatureValid()) {
+                        Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding");
+                        cipher.init(Cipher.DECRYPT_MODE, privateKey);
+                        byte[] decryptedSubject = cipher.doFinal(base64DecodedSubject);
+
+                        Gson gson = new Gson();
+                        DecryptedPushMessage decryptedPushMessage = gson.fromJson(new String(decryptedSubject),
+                                DecryptedPushMessage.class);
+
+                        // We ignore Spreed messages for now
+                        if (!decryptedPushMessage.getApp().equals("spreed")) {
+                            sendNotification(decryptedPushMessage.getSubject(), signatureVerification.getAccount());
+                        }
+
+                    }
+                } catch (NoSuchAlgorithmException e1) {
+                    Log.d(TAG, "No proper algorithm to decrypt the message " + e1.getLocalizedMessage());
+                } catch (NoSuchPaddingException e1) {
+                    Log.d(TAG, "No proper padding to decrypt the message " + e1.getLocalizedMessage());
+                } catch (InvalidKeyException e1) {
+                    Log.d(TAG, "Invalid private key " + e1.getLocalizedMessage());
+                }
+            } catch (Exception exception) {
+                Log.d(TAG, "Something went very wrong" + exception.getLocalizedMessage());
+            }
+        }
+        return Result.SUCCESS;
+    }
+
+    private void sendNotification(String contentTitle, Account account) {
+        Context context = getContext();
+        Intent intent = new Intent(getContext(), NotificationsActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        intent.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name);
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT);
+
+
+        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context)
+                .setSmallIcon(R.drawable.notification_icon)
+                .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.notification_icon))
+                .setColor(ThemeUtils.primaryColor())
+                .setShowWhen(true)
+                .setSubText(account.name)
+                .setContentTitle(contentTitle)
+                .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
+                .setAutoCancel(true)
+                .setContentIntent(pendingIntent);
+
+        if ((android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O)) {
+            notificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_PUSH);
+        }
+
+        NotificationManager notificationManager = (NotificationManager)
+                context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+        notificationManager.notify(0, notificationBuilder.build());
+    }
+
+}

+ 11 - 0
src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.java

@@ -46,6 +46,7 @@ import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
+import com.owncloud.android.jobs.NotificationJob;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
@@ -117,6 +118,16 @@ public class NotificationsActivity extends FileActivity {
         setContentView(R.layout.notifications_layout);
         unbinder = ButterKnife.bind(this);
 
+        String account;
+        Account currentAccount;
+        if (getIntent() != null && getIntent().getExtras() != null &&
+                (account = getIntent().getExtras().getString(NotificationJob.KEY_NOTIFICATION_ACCOUNT)) != null &&
+                (currentAccount = AccountUtils.getCurrentOwnCloudAccount(getApplicationContext())) != null &&
+                !account.equalsIgnoreCase(currentAccount.name)) {
+            AccountUtils.setCurrentOwnCloudAccount(getApplicationContext(), account);
+            setAccount(AccountUtils.getCurrentOwnCloudAccount(this));
+        }
+
         // setup toolbar
         setupToolbar();
 

+ 11 - 0
src/versionDev/java/com/owncloud/android/utils/PushUtils.java

@@ -23,8 +23,11 @@ package com.owncloud.android.utils;
 import android.content.Context;
 
 import com.owncloud.android.MainApp;
+import com.owncloud.android.datamodel.SignatureVerification;
 import com.owncloud.android.db.PreferenceManager;
 
+import java.security.Key;
+
 public class PushUtils {
     public static final String KEY_PUSH = "push";
 
@@ -36,4 +39,12 @@ public class PushUtils {
         Context context = MainApp.getAppContext();
         PreferenceManager.setKeysReInit(context);
     }
+
+    public static Key readKeyFromFile(boolean readPublicKey) {
+        return null;
+    }
+
+    public SignatureVerification verifySignature(Context context, byte[] signatureBytes, byte[] subjectBytes) {
+        return null;
+    }
 }