Forráskód Böngészése

Merge pull request #3485 from nextcloud/bugfix/noid/inviniteLoadingAfterLogin

Add fixes and changes to push handling
Marcel Hibbe 1 éve
szülő
commit
e86599e817

+ 1 - 4
app/src/generic/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java

@@ -38,9 +38,6 @@ public class ClosedInterfaceImpl implements ClosedInterface {
 
     @Override
     public void setUpPushTokenRegistration() {
-        // no push notifications for generic build flavour :(
-        // If you want to develop push notifications without google play services, here is a good place to start...
-        // Also have a look at app/src/gplay/AndroidManifest.xml to see how to include a service that handles push
-        // notifications.
+        // no push notifications for generic build variant
     }
 }

+ 11 - 8
app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt

@@ -28,20 +28,24 @@ import androidx.work.OneTimeWorkRequest
 import androidx.work.WorkManager
 import androidx.work.Worker
 import androidx.work.WorkerParameters
+import autodagger.AutoInjector
 import com.google.android.gms.tasks.OnCompleteListener
 import com.google.firebase.messaging.FirebaseMessaging
+import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.utils.preferences.AppPreferences
 import javax.inject.Inject
 
+@AutoInjector(NextcloudTalkApplication::class)
 class GetFirebasePushTokenWorker(val context: Context, workerParameters: WorkerParameters) :
     Worker(context, workerParameters) {
 
-    @JvmField
     @Inject
-    var appPreferences: AppPreferences? = null
+    lateinit var appPreferences: AppPreferences
 
     @SuppressLint("LongLogTag")
     override fun doWork(): Result {
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+
         FirebaseMessaging.getInstance().token.addOnCompleteListener(
             OnCompleteListener { task ->
                 if (!task.isSuccessful) {
@@ -49,14 +53,13 @@ class GetFirebasePushTokenWorker(val context: Context, workerParameters: WorkerP
                     return@OnCompleteListener
                 }
 
-                val token = task.result
+                val pushToken = task.result
+                Log.d(TAG, "Fetched firebase push token is: $pushToken")
 
-                appPreferences?.pushToken = token
+                appPreferences.pushToken = pushToken
 
                 val data: Data =
-                    Data.Builder()
-                        .putString(PushRegistrationWorker.ORIGIN, "GetFirebasePushTokenWorker")
-                        .build()
+                    Data.Builder().putString(PushRegistrationWorker.ORIGIN, "GetFirebasePushTokenWorker").build()
                 val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
                     .setInputData(data)
                     .build()
@@ -68,6 +71,6 @@ class GetFirebasePushTokenWorker(val context: Context, workerParameters: WorkerP
     }
 
     companion object {
-        const val TAG = "GetFirebasePushTokenWorker"
+        private val TAG = GetFirebasePushTokenWorker::class.simpleName
     }
 }

+ 2 - 4
app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt

@@ -79,10 +79,8 @@ class NCFirebaseMessagingService : FirebaseMessagingService() {
 
         appPreferences.pushToken = token
 
-        val data: Data = Data.Builder().putString(
-            PushRegistrationWorker.ORIGIN,
-            "NCFirebaseMessagingService#onNewToken"
-        ).build()
+        val data: Data =
+            Data.Builder().putString(PushRegistrationWorker.ORIGIN, "NCFirebaseMessagingService#onNewToken").build()
         val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
             .setInputData(data)
             .build()

+ 17 - 52
app/src/gplay/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt

@@ -24,7 +24,7 @@
 package com.nextcloud.talk.utils
 
 import android.content.Intent
-import androidx.work.Data
+import android.util.Log
 import androidx.work.ExistingPeriodicWorkPolicy
 import androidx.work.OneTimeWorkRequest
 import androidx.work.PeriodicWorkRequest
@@ -36,7 +36,6 @@ import com.google.android.gms.security.ProviderInstaller
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.interfaces.ClosedInterface
 import com.nextcloud.talk.jobs.GetFirebasePushTokenWorker
-import com.nextcloud.talk.jobs.PushRegistrationWorker
 import java.util.concurrent.TimeUnit
 
 @AutoInjector(NextcloudTalkApplication::class)
@@ -65,77 +64,43 @@ class ClosedInterfaceImpl : ClosedInterface, ProviderInstaller.ProviderInstallLi
         val api = GoogleApiAvailability.getInstance()
         val code =
             NextcloudTalkApplication.sharedApplication?.let {
-                api.isGooglePlayServicesAvailable(
-                    it.applicationContext
-                )
+                api.isGooglePlayServicesAvailable(it.applicationContext)
             }
-        return code == ConnectionResult.SUCCESS
+        return if (code == ConnectionResult.SUCCESS) {
+            true
+        } else {
+            Log.w(TAG, "GooglePlayServices are not available. Code:$code")
+            false
+        }
     }
 
     override fun setUpPushTokenRegistration() {
-        registerLocalToken()
-        setUpPeriodicLocalTokenRegistration()
-        setUpPeriodicTokenRefreshFromFCM()
-    }
-
-    private fun registerLocalToken() {
-        val data: Data = Data.Builder().putString(
-            PushRegistrationWorker.ORIGIN,
-            "ClosedInterfaceImpl#registerLocalToken"
-        )
-            .build()
-        val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
-            .setInputData(data)
-            .build()
-        WorkManager.getInstance().enqueue(pushRegistrationWork)
-    }
-
-    private fun setUpPeriodicLocalTokenRegistration() {
-        val data: Data = Data.Builder().putString(
-            PushRegistrationWorker.ORIGIN,
-            "ClosedInterfaceImpl#setUpPeriodicLocalTokenRegistration"
-        )
-            .build()
-
-        val periodicTokenRegistration = PeriodicWorkRequest.Builder(
-            PushRegistrationWorker::class.java,
-            DAILY,
-            TimeUnit.HOURS,
-            FLEX_INTERVAL,
-            TimeUnit.HOURS
-        )
-            .setInputData(data)
-            .build()
+        val firebasePushTokenWorker = OneTimeWorkRequest.Builder(GetFirebasePushTokenWorker::class.java).build()
+        WorkManager.getInstance().enqueue(firebasePushTokenWorker)
 
-        WorkManager.getInstance()
-            .enqueueUniquePeriodicWork(
-                "periodicTokenRegistration",
-                ExistingPeriodicWorkPolicy.REPLACE,
-                periodicTokenRegistration
-            )
+        setUpPeriodicTokenRefreshFromFCM()
     }
 
     private fun setUpPeriodicTokenRefreshFromFCM() {
         val periodicTokenRefreshFromFCM = PeriodicWorkRequest.Builder(
             GetFirebasePushTokenWorker::class.java,
-            MONTHLY,
-            TimeUnit.DAYS,
+            DAILY,
+            TimeUnit.HOURS,
             FLEX_INTERVAL,
-            TimeUnit.DAYS
-        )
-            .build()
+            TimeUnit.HOURS
+        ).build()
 
         WorkManager.getInstance()
             .enqueueUniquePeriodicWork(
                 "periodicTokenRefreshFromFCM",
-                ExistingPeriodicWorkPolicy.REPLACE,
+                ExistingPeriodicWorkPolicy.UPDATE,
                 periodicTokenRefreshFromFCM
             )
     }
 
     companion object {
+        private val TAG = ClosedInterfaceImpl::class.java.simpleName
         const val DAILY: Long = 24
-        const val MONTHLY: Long = 30
         const val FLEX_INTERVAL: Long = 10
     }
 }

+ 9 - 19
app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt

@@ -49,7 +49,6 @@ import com.nextcloud.talk.databinding.ActivityAccountVerificationBinding
 import com.nextcloud.talk.events.EventStatus
 import com.nextcloud.talk.jobs.AccountRemovalWorker
 import com.nextcloud.talk.jobs.CapabilitiesWorker
-import com.nextcloud.talk.jobs.PushRegistrationWorker
 import com.nextcloud.talk.jobs.SignalingSettingsWorker
 import com.nextcloud.talk.jobs.WebsocketConnectionsWorker
 import com.nextcloud.talk.models.json.capabilities.Capabilities
@@ -277,8 +276,9 @@ class AccountVerificationActivity : BaseActivity() {
                 override fun onSuccess(user: User) {
                     internalAccountId = user.id!!
                     if (ClosedInterfaceImpl().isGooglePlayServicesAvailable) {
-                        registerForPush()
+                        ClosedInterfaceImpl().setUpPushTokenRegistration()
                     } else {
+                        Log.w(TAG, "Skipping push registration.")
                         runOnUiThread {
                             binding.progressText.text =
                                 """ ${binding.progressText.text}
@@ -357,21 +357,10 @@ class AccountVerificationActivity : BaseActivity() {
             })
     }
 
-    private fun registerForPush() {
-        val data =
-            Data.Builder()
-                .putString(PushRegistrationWorker.ORIGIN, "AccountVerificationActivity#registerForPush")
-                .build()
-        val pushRegistrationWork =
-            OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
-                .setInputData(data)
-                .build()
-        WorkManager.getInstance().enqueue(pushRegistrationWork)
-    }
-
     @SuppressLint("SetTextI18n")
     @Subscribe(threadMode = ThreadMode.BACKGROUND)
     fun onMessageEvent(eventStatus: EventStatus) {
+        Log.d(TAG, "caught EventStatus of type " + eventStatus.eventType.toString())
         if (eventStatus.eventType == EventStatus.EventType.PUSH_REGISTRATION) {
             if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) {
                 runOnUiThread {
@@ -415,11 +404,11 @@ class AccountVerificationActivity : BaseActivity() {
             Data.Builder()
                 .putLong(KEY_INTERNAL_USER_ID, internalAccountId)
                 .build()
-        val pushNotificationWork =
+        val capabilitiesWork =
             OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java)
                 .setInputData(userData)
                 .build()
-        WorkManager.getInstance().enqueue(pushNotificationWork)
+        WorkManager.getInstance().enqueue(capabilitiesWork)
     }
 
     private fun fetchAndStoreExternalSignalingSettings() {
@@ -427,19 +416,18 @@ class AccountVerificationActivity : BaseActivity() {
             Data.Builder()
                 .putLong(KEY_INTERNAL_USER_ID, internalAccountId)
                 .build()
-        val signalingSettings = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java)
+        val signalingSettingsWorker = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java)
             .setInputData(userData)
             .build()
         val websocketConnectionsWorker = OneTimeWorkRequest.Builder(WebsocketConnectionsWorker::class.java).build()
 
         WorkManager.getInstance(applicationContext!!)
-            .beginWith(signalingSettings)
+            .beginWith(signalingSettingsWorker)
             .then(websocketConnectionsWorker)
             .enqueue()
     }
 
     private fun proceedWithLogin() {
-        Log.d(TAG, "proceedWithLogin...")
         cookieManager.cookieStore.removeAll()
 
         if (userManager.users.blockingGet().size == 1 ||
@@ -466,6 +454,8 @@ class AccountVerificationActivity : BaseActivity() {
                 Log.e(TAG, "failed to set active user")
                 Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
             }
+        } else {
+            Log.d(TAG, "continuing proceedWithLogin was skipped for this user")
         }
     }
 

+ 8 - 1
app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt

@@ -32,6 +32,7 @@ import android.os.Bundle
 import android.provider.ContactsContract
 import android.text.TextUtils
 import android.util.Log
+import android.widget.Toast
 import androidx.activity.OnBackPressedCallback
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.LifecycleOwner
@@ -52,6 +53,7 @@ import com.nextcloud.talk.lock.LockedActivity
 import com.nextcloud.talk.models.json.conversations.RoomOverall
 import com.nextcloud.talk.users.UserManager
 import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.ClosedInterfaceImpl
 import com.nextcloud.talk.utils.SecurityUtils
 import com.nextcloud.talk.utils.bundle.BundleKeys
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
@@ -78,7 +80,6 @@ class MainActivity : BaseActivity(), ActionBarProvider {
         }
     }
 
-    @Suppress("Detekt.TooGenericExceptionCaught")
     override fun onCreate(savedInstanceState: Bundle?) {
         Log.d(TAG, "onCreate: Activity: " + System.identityHashCode(this).toString())
 
@@ -280,6 +281,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
 
                 override fun onSuccess(users: List<User>) {
                     if (users.isNotEmpty()) {
+                        ClosedInterfaceImpl().setUpPushTokenRegistration()
                         runOnUiThread {
                             openConversationList()
                         }
@@ -292,6 +294,11 @@ class MainActivity : BaseActivity(), ActionBarProvider {
 
                 override fun onError(e: Throwable) {
                     Log.e(TAG, "Error loading existing users", e)
+                    Toast.makeText(
+                        context,
+                        context.resources.getString(R.string.nc_common_error_sorry),
+                        Toast.LENGTH_SHORT
+                    ).show()
                 }
             })
         }

+ 2 - 1
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -57,6 +57,7 @@ import java.util.Map;
 
 import androidx.annotation.Nullable;
 import io.reactivex.Observable;
+import kotlin.Unit;
 import okhttp3.MultipartBody;
 import okhttp3.RequestBody;
 import okhttp3.ResponseBody;
@@ -333,7 +334,7 @@ public interface NcApi {
 
     @FormUrlEncoded
     @POST
-    Observable<Void> registerDeviceForNotificationsWithPushProxy(@Url String url,
+    Observable<Unit> registerDeviceForNotificationsWithPushProxy(@Url String url,
                                                                  @FieldMap Map<String, String> fields);
 
 

+ 0 - 2
app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt

@@ -107,7 +107,6 @@ import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog
 import com.nextcloud.talk.ui.dialog.FilterConversationFragment
 import com.nextcloud.talk.users.UserManager
 import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.ClosedInterfaceImpl
 import com.nextcloud.talk.utils.FileUtils
 import com.nextcloud.talk.utils.Mimetype
 import com.nextcloud.talk.utils.ParticipantPermissions
@@ -254,7 +253,6 @@ class ConversationsListActivity :
 
         showShareToScreen = hasActivityActionSendIntent()
 
-        ClosedInterfaceImpl().setUpPushTokenRegistration()
         if (!eventBus.isRegistered(this)) {
             eventBus.register(this)
         }

+ 1 - 1
app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java

@@ -64,7 +64,7 @@ public class PushRegistrationWorker extends Worker {
     @Override
     public Result doWork() {
         NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
-        if(new ClosedInterfaceImpl().isGooglePlayServicesAvailable()){
+        if (new ClosedInterfaceImpl().isGooglePlayServicesAvailable()) {
             Data data = getInputData();
             String origin = data.getString("origin");
             Log.d(TAG, "PushRegistrationWorker called via " + origin);

+ 0 - 432
app/src/main/java/com/nextcloud/talk/utils/PushUtils.java

@@ -1,432 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Andy Scherzinger
- * @author Marcel Hibbe
- * @author Mario Danic
- * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
- * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
- * Copyright (C) 2017 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.nextcloud.talk.utils;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.Base64;
-import android.util.Log;
-
-import com.nextcloud.talk.R;
-import com.nextcloud.talk.api.NcApi;
-import com.nextcloud.talk.application.NextcloudTalkApplication;
-import com.nextcloud.talk.data.user.model.User;
-import com.nextcloud.talk.events.EventStatus;
-import com.nextcloud.talk.models.SignatureVerification;
-import com.nextcloud.talk.models.json.push.PushConfigurationState;
-import com.nextcloud.talk.models.json.push.PushRegistrationOverall;
-import com.nextcloud.talk.users.UserManager;
-import com.nextcloud.talk.utils.preferences.AppPreferences;
-
-import org.greenrobot.eventbus.EventBus;
-
-import java.io.File;
-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;
-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;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import javax.inject.Inject;
-
-import autodagger.AutoInjector;
-import io.reactivex.Observer;
-import io.reactivex.SingleObserver;
-import io.reactivex.annotations.NonNull;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.schedulers.Schedulers;
-
-@AutoInjector(NextcloudTalkApplication.class)
-public class PushUtils {
-    private static final String TAG = "PushUtils";
-
-    @Inject
-    UserManager userManager;
-
-    @Inject
-    AppPreferences appPreferences;
-
-    @Inject
-    EventBus eventBus;
-
-    private final File publicKeyFile;
-    private final File privateKeyFile;
-
-    private final String proxyServer;
-
-    public PushUtils() {
-        NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
-
-        String keyPath = NextcloudTalkApplication
-            .Companion
-            .getSharedApplication()
-            .getDir("PushKeystore", Context.MODE_PRIVATE)
-            .getAbsolutePath();
-        publicKeyFile = new File(keyPath, "push_key.pub");
-        privateKeyFile = new File(keyPath, "push_key.priv");
-        proxyServer = NextcloudTalkApplication
-            .Companion
-            .getSharedApplication()
-            .getResources().
-            getString(R.string.nc_push_server_url);
-    }
-
-    public SignatureVerification verifySignature(byte[] signatureBytes, byte[] subjectBytes) {
-        SignatureVerification signatureVerification = new SignatureVerification();
-        signatureVerification.setSignatureValid(false);
-
-        List<User> users = userManager.getUsers().blockingGet();
-        try {
-            Signature signature = Signature.getInstance("SHA512withRSA");
-            if (users != null && users.size() > 0) {
-                PublicKey publicKey;
-                for (User user : users) {
-                    if (user.getPushConfigurationState() != null) {
-                        publicKey = (PublicKey) readKeyFromString(true,
-                                                                  user.getPushConfigurationState().getUserPublicKey());
-                        signature.initVerify(publicKey);
-                        signature.update(subjectBytes);
-                        if (signature.verify(signatureBytes)) {
-                            signatureVerification.setSignatureValid(true);
-                            signatureVerification.setUser(user);
-                            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 int saveKeyToFile(Key key, String path) {
-        byte[] encoded = key.getEncoded();
-
-        try {
-            if (!new File(path).exists()) {
-                if (!new File(path).createNewFile()) {
-                    return -1;
-                }
-            }
-
-            try (FileOutputStream keyFileOutputStream = new FileOutputStream(path)) {
-                keyFileOutputStream.write(encoded);
-                return 0;
-            }
-        } catch (FileNotFoundException e) {
-            Log.d(TAG, "Failed to save key to file");
-        } catch (IOException e) {
-            Log.d(TAG, "Failed to save key to file via IOException");
-        }
-
-        return -1;
-    }
-
-    private String generateSHA512Hash(String pushToken) {
-        MessageDigest messageDigest = null;
-        try {
-            messageDigest = MessageDigest.getInstance("SHA-512");
-            messageDigest.update(pushToken.getBytes());
-            return bytesToHex(messageDigest.digest());
-        } catch (NoSuchAlgorithmException e) {
-            Log.d(TAG, "SHA-512 algorithm not supported");
-        }
-        return "";
-    }
-
-    private String bytesToHex(byte[] bytes) {
-        StringBuilder result = new StringBuilder();
-        for (byte individualByte : bytes) {
-            result.append(Integer.toString((individualByte & 0xff) + 0x100, 16)
-                              .substring(1));
-        }
-        return result.toString();
-    }
-
-    public int generateRsa2048KeyPair() {
-        if (!publicKeyFile.exists() && !privateKeyFile.exists()) {
-
-            KeyPairGenerator keyGen = null;
-            try {
-                keyGen = KeyPairGenerator.getInstance("RSA");
-                keyGen.initialize(2048);
-
-                KeyPair pair = keyGen.generateKeyPair();
-                int statusPrivate = saveKeyToFile(pair.getPrivate(), privateKeyFile.getAbsolutePath());
-                int statusPublic = saveKeyToFile(pair.getPublic(), publicKeyFile.getAbsolutePath());
-
-                if (statusPrivate == 0 && statusPublic == 0) {
-                    // all went well
-                    return 0;
-                } else {
-                    return -2;
-                }
-
-            } catch (NoSuchAlgorithmException e) {
-                Log.d(TAG, "RSA algorithm not supported");
-            }
-        } else {
-            // We already have the key
-            return -1;
-        }
-
-        // we failed to generate the key
-        return -2;
-    }
-
-    public void pushRegistrationToServer(NcApi ncApi) {
-        String token = appPreferences.getPushToken();
-
-        if (!TextUtils.isEmpty(token)) {
-            String pushTokenHash = generateSHA512Hash(token).toLowerCase();
-            PublicKey devicePublicKey = (PublicKey) readKeyFromFile(true);
-            if (devicePublicKey != null) {
-                byte[] devicePublicKeyBytes = Base64.encode(devicePublicKey.getEncoded(), Base64.NO_WRAP);
-                String devicePublicKeyBase64 = new String(devicePublicKeyBytes);
-                devicePublicKeyBase64 = devicePublicKeyBase64.replaceAll("(.{64})", "$1\n");
-
-                devicePublicKeyBase64 =
-                    "-----BEGIN PUBLIC KEY-----\n"
-                        + devicePublicKeyBase64
-                        + "\n-----END PUBLIC KEY-----\n";
-
-                List<User> users = userManager.getUsers().blockingGet();
-
-                for (User user : users) {
-                    if (!user.getScheduledForDeletion()) {
-                        Map<String, String> nextcloudRegisterPushMap = new HashMap<>();
-                        nextcloudRegisterPushMap.put("format", "json");
-                        nextcloudRegisterPushMap.put("pushTokenHash", pushTokenHash);
-                        nextcloudRegisterPushMap.put("devicePublicKey", devicePublicKeyBase64);
-                        nextcloudRegisterPushMap.put("proxyServer", proxyServer);
-
-                        registerDeviceWithNextcloud(ncApi, nextcloudRegisterPushMap, token, user);
-                    }
-                }
-
-            }
-        } else {
-            Log.e(TAG, "push token was empty when trying to register at nextcloud server");
-        }
-    }
-
-    private void registerDeviceWithNextcloud(NcApi ncApi,
-                                             Map<String, String> nextcloudRegisterPushMap,
-                                             String token,
-                                             User user) {
-        String credentials = ApiUtils.getCredentials(user.getUsername(), user.getToken());
-
-        ncApi.registerDeviceForNotificationsWithNextcloud(
-                credentials,
-                ApiUtils.getUrlNextcloudPush(user.getBaseUrl()),
-                nextcloudRegisterPushMap)
-            .subscribe(new Observer<PushRegistrationOverall>() {
-                @Override
-                public void onSubscribe(@NonNull Disposable d) {
-                    // unused atm
-                }
-
-                @Override
-                public void onNext(@NonNull PushRegistrationOverall pushRegistrationOverall) {
-                    Log.d(TAG, "pushTokenHash successfully registered at nextcloud server.");
-
-                    Map<String, String> proxyMap = new HashMap<>();
-                    proxyMap.put("pushToken", token);
-                    proxyMap.put("deviceIdentifier",
-                                 pushRegistrationOverall.getOcs().getData().getDeviceIdentifier());
-                    proxyMap.put("deviceIdentifierSignature",
-                                 pushRegistrationOverall.getOcs().getData().getSignature());
-                    proxyMap.put("userPublicKey",
-                                 pushRegistrationOverall.getOcs().getData().getPublicKey());
-
-                    registerDeviceWithPushProxy(ncApi, proxyMap, user);
-                }
-
-                @Override
-                public void onError(@NonNull Throwable e) {
-                    eventBus.post(new EventStatus(user.getId(),
-                                                  EventStatus.EventType.PUSH_REGISTRATION, false));
-                }
-
-                @Override
-                public void onComplete() {
-                    // unused atm
-                }
-            });
-    }
-
-    private void registerDeviceWithPushProxy(NcApi ncApi, Map<String, String> proxyMap, User user) {
-        ncApi.registerDeviceForNotificationsWithPushProxy(ApiUtils.getUrlPushProxy(), proxyMap)
-            .subscribeOn(Schedulers.io())
-            .subscribe(new Observer<Void>() {
-                @Override
-                public void onSubscribe(@NonNull Disposable d) {
-                    // unused atm
-                }
-
-                @Override
-                public void onNext(@NonNull Void aVoid) {
-                    try {
-                        Log.d(TAG, "pushToken successfully registered at pushproxy.");
-                        updatePushStateForUser(proxyMap, user);
-                    } catch (IOException e) {
-                        Log.e(TAG, "IOException while updating user", e);
-                    }
-                }
-
-                @Override
-                public void onError(@NonNull Throwable e) {
-                    eventBus.post(new EventStatus(user.getId(),
-                                                  EventStatus.EventType.PUSH_REGISTRATION, false));
-                }
-
-                @Override
-                public void onComplete() {
-                    // unused atm
-                }
-            });
-    }
-
-    private void updatePushStateForUser(Map<String, String> proxyMap, User user) throws IOException {
-        PushConfigurationState pushConfigurationState = new PushConfigurationState();
-        pushConfigurationState.setPushToken(proxyMap.get("pushToken"));
-        pushConfigurationState.setDeviceIdentifier(proxyMap.get("deviceIdentifier"));
-        pushConfigurationState.setDeviceIdentifierSignature(proxyMap.get("deviceIdentifierSignature"));
-        pushConfigurationState.setUserPublicKey(proxyMap.get("userPublicKey"));
-        pushConfigurationState.setUsesRegularPass(Boolean.FALSE);
-
-        if (user.getId() != null) {
-            userManager.updatePushState(user.getId(), pushConfigurationState).subscribe(new SingleObserver<Integer>() {
-                @Override
-                public void onSubscribe(Disposable d) {
-                    // unused atm
-                }
-
-                @Override
-                public void onSuccess(Integer integer) {
-                    eventBus.post(new EventStatus(UserIdUtils.INSTANCE.getIdForUser(user),
-                                                  EventStatus.EventType.PUSH_REGISTRATION,
-                                                  true));
-                }
-
-                @Override
-                public void onError(Throwable e) {
-                    eventBus.post(new EventStatus(UserIdUtils.INSTANCE.getIdForUser(user),
-                                                  EventStatus.EventType.PUSH_REGISTRATION,
-                                                  false));
-                }
-            });
-        } else {
-            Log.e(TAG, "failed to update updatePushStateForUser. user.getId() was null");
-        }
-
-    }
-
-    private Key readKeyFromString(boolean readPublicKey, String keyString) {
-        if (readPublicKey) {
-            keyString = keyString.replaceAll("\\n", "").replace("-----BEGIN PUBLIC KEY-----",
-                                                                "").replace("-----END PUBLIC KEY-----", "");
-        } else {
-            keyString = 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(keyString, Base64.DEFAULT));
-                return keyFactory.generatePublic(keySpec);
-            } else {
-                PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(keyString, 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;
-    }
-
-    public Key readKeyFromFile(boolean readPublicKey) {
-        String path;
-
-        if (readPublicKey) {
-            path = publicKeyFile.getAbsolutePath();
-        } else {
-            path = privateKeyFile.getAbsolutePath();
-        }
-
-        try (FileInputStream fileInputStream = new FileInputStream(path)) {
-            byte[] bytes = new byte[fileInputStream.available()];
-            fileInputStream.read(bytes);
-
-            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
-
-            if (readPublicKey) {
-                X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
-                return keyFactory.generatePublic(keySpec);
-            } else {
-                PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
-                return keyFactory.generatePrivate(keySpec);
-            }
-
-        } catch (FileNotFoundException e) {
-            Log.d(TAG, "Failed to find path while reading the Key");
-        } catch (IOException e) {
-            Log.d(TAG, "IOException while reading the key");
-        } catch (InvalidKeySpecException e) {
-            Log.d(TAG, "InvalidKeySpecException while reading the key");
-        } catch (NoSuchAlgorithmException e) {
-            Log.d(TAG, "RSA algorithm not supported");
-        }
-
-        return null;
-    }
-}

+ 400 - 0
app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt

@@ -0,0 +1,400 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * @author Marcel Hibbe
+ * @author Mario Danic
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ * Copyright (C) 2017 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.nextcloud.talk.utils
+
+import android.content.Context
+import android.util.Base64
+import android.util.Log
+import autodagger.AutoInjector
+import com.nextcloud.talk.R
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.events.EventStatus
+import com.nextcloud.talk.models.SignatureVerification
+import com.nextcloud.talk.models.json.push.PushConfigurationState
+import com.nextcloud.talk.models.json.push.PushRegistrationOverall
+import com.nextcloud.talk.users.UserManager
+import com.nextcloud.talk.utils.UserIdUtils.getIdForUser
+import com.nextcloud.talk.utils.preferences.AppPreferences
+import io.reactivex.Observer
+import io.reactivex.SingleObserver
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import org.greenrobot.eventbus.EventBus
+import java.io.File
+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.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
+import java.util.Locale
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class PushUtils {
+    @JvmField
+    @Inject
+    var userManager: UserManager? = null
+
+    @Inject
+    lateinit var appPreferences: AppPreferences
+
+    @JvmField
+    @Inject
+    var eventBus: EventBus? = null
+    private val publicKeyFile: File
+    private val privateKeyFile: File
+    private val proxyServer: String
+
+    init {
+        sharedApplication!!.componentApplication.inject(this)
+        val keyPath = sharedApplication!!
+            .getDir("PushKeystore", Context.MODE_PRIVATE)
+            .absolutePath
+        publicKeyFile = File(keyPath, "push_key.pub")
+        privateKeyFile = File(keyPath, "push_key.priv")
+        proxyServer = sharedApplication!!
+            .resources.getString(R.string.nc_push_server_url)
+    }
+
+    fun verifySignature(signatureBytes: ByteArray?, subjectBytes: ByteArray?): SignatureVerification {
+        val signatureVerification = SignatureVerification()
+        signatureVerification.signatureValid = false
+        val users = userManager!!.users.blockingGet()
+        try {
+            val signature = Signature.getInstance("SHA512withRSA")
+            if (users != null && users.size > 0) {
+                var publicKey: PublicKey?
+                for (user in users) {
+                    if (user.pushConfigurationState != null) {
+                        publicKey = readKeyFromString(
+                            true,
+                            user.pushConfigurationState!!.userPublicKey
+                        ) as PublicKey?
+                        signature.initVerify(publicKey)
+                        signature.update(subjectBytes)
+                        if (signature.verify(signatureBytes)) {
+                            signatureVerification.signatureValid = true
+                            signatureVerification.user = user
+                            return signatureVerification
+                        }
+                    }
+                }
+            }
+        } catch (e: NoSuchAlgorithmException) {
+            Log.d(TAG, "No such algorithm")
+        } catch (e: InvalidKeyException) {
+            Log.d(TAG, "Invalid key while trying to verify")
+        } catch (e: SignatureException) {
+            Log.d(TAG, "Signature exception while trying to verify")
+        }
+        return signatureVerification
+    }
+
+    private fun saveKeyToFile(key: Key, path: String): Int {
+        val encoded = key.encoded
+        try {
+            if (!File(path).exists()) {
+                if (!File(path).createNewFile()) {
+                    return -1
+                }
+            }
+            FileOutputStream(path).use { keyFileOutputStream ->
+                keyFileOutputStream.write(encoded)
+                return 0
+            }
+        } catch (e: FileNotFoundException) {
+            Log.d(TAG, "Failed to save key to file")
+        } catch (e: IOException) {
+            Log.d(TAG, "Failed to save key to file via IOException")
+        }
+        return -1
+    }
+
+    private fun generateSHA512Hash(pushToken: String): String {
+        var messageDigest: MessageDigest? = null
+        try {
+            messageDigest = MessageDigest.getInstance("SHA-512")
+            messageDigest.update(pushToken.toByteArray())
+            return bytesToHex(messageDigest.digest())
+        } catch (e: NoSuchAlgorithmException) {
+            Log.d(TAG, "SHA-512 algorithm not supported")
+        }
+        return ""
+    }
+
+    private fun bytesToHex(bytes: ByteArray): String {
+        val result = StringBuilder()
+        for (individualByte in bytes) {
+            result.append(
+                Integer.toString((individualByte.toInt() and 0xff) + 0x100, 16)
+                    .substring(1)
+            )
+        }
+        return result.toString()
+    }
+
+    fun generateRsa2048KeyPair(): Int {
+        if (!publicKeyFile.exists() && !privateKeyFile.exists()) {
+            var keyGen: KeyPairGenerator? = null
+            try {
+                keyGen = KeyPairGenerator.getInstance("RSA")
+                keyGen.initialize(2048)
+                val pair = keyGen.generateKeyPair()
+                val statusPrivate = saveKeyToFile(pair.private, privateKeyFile.absolutePath)
+                val statusPublic = saveKeyToFile(pair.public, publicKeyFile.absolutePath)
+                return if (statusPrivate == 0 && statusPublic == 0) {
+                    // all went well
+                    0
+                } else {
+                    -2
+                }
+            } catch (e: NoSuchAlgorithmException) {
+                Log.d(TAG, "RSA algorithm not supported")
+            }
+        } else {
+            // We already have the key
+            return -1
+        }
+
+        // we failed to generate the key
+        return -2
+    }
+
+    fun pushRegistrationToServer(ncApi: NcApi) {
+        val pushToken = appPreferences.pushToken
+
+        if (pushToken.isNotEmpty()) {
+            Log.d(TAG, "pushRegistrationToServer will be done with pushToken: $pushToken")
+            val pushTokenHash = generateSHA512Hash(pushToken).lowercase(Locale.getDefault())
+            val devicePublicKey = readKeyFromFile(true) as PublicKey?
+            if (devicePublicKey != null) {
+                val devicePublicKeyBytes = Base64.encode(devicePublicKey.encoded, Base64.NO_WRAP)
+                var devicePublicKeyBase64 = String(devicePublicKeyBytes)
+                devicePublicKeyBase64 = devicePublicKeyBase64.replace("(.{64})".toRegex(), "$1\n")
+                devicePublicKeyBase64 = "-----BEGIN PUBLIC KEY-----\n$devicePublicKeyBase64\n-----END PUBLIC KEY-----"
+
+                val users = userManager!!.users.blockingGet()
+                for (user in users) {
+                    if (!user.scheduledForDeletion) {
+                        val nextcloudRegisterPushMap: MutableMap<String, String> = HashMap()
+                        nextcloudRegisterPushMap["format"] = "json"
+                        nextcloudRegisterPushMap["pushTokenHash"] = pushTokenHash
+                        nextcloudRegisterPushMap["devicePublicKey"] = devicePublicKeyBase64
+                        nextcloudRegisterPushMap["proxyServer"] = proxyServer
+                        registerDeviceWithNextcloud(ncApi, nextcloudRegisterPushMap, pushToken, user)
+                    }
+                }
+            }
+        } else {
+            Log.e(TAG, "push token was empty when trying to register at server")
+        }
+    }
+
+    private fun registerDeviceWithNextcloud(
+        ncApi: NcApi,
+        nextcloudRegisterPushMap: Map<String, String>,
+        token: String,
+        user: User
+    ) {
+        val credentials = ApiUtils.getCredentials(user.username, user.token)
+        ncApi.registerDeviceForNotificationsWithNextcloud(
+            credentials,
+            ApiUtils.getUrlNextcloudPush(user.baseUrl),
+            nextcloudRegisterPushMap
+        )
+            .subscribe(object : Observer<PushRegistrationOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(pushRegistrationOverall: PushRegistrationOverall) {
+                    Log.d(TAG, "pushTokenHash successfully registered at nextcloud server.")
+                    val proxyMap: MutableMap<String, String?> = HashMap()
+                    proxyMap["pushToken"] = token
+                    proxyMap["deviceIdentifier"] = pushRegistrationOverall.ocs!!.data!!.deviceIdentifier
+                    proxyMap["deviceIdentifierSignature"] = pushRegistrationOverall.ocs!!.data!!.signature
+                    proxyMap["userPublicKey"] = pushRegistrationOverall.ocs!!.data!!.publicKey
+                    registerDeviceWithPushProxy(ncApi, proxyMap, user)
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, "Failed to register device with nextcloud", e)
+                    eventBus!!.post(EventStatus(user.id!!, EventStatus.EventType.PUSH_REGISTRATION, false))
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    private fun registerDeviceWithPushProxy(ncApi: NcApi, proxyMap: Map<String, String?>, user: User) {
+        ncApi.registerDeviceForNotificationsWithPushProxy(ApiUtils.getUrlPushProxy(), proxyMap)
+            .subscribeOn(Schedulers.io())
+            .subscribe(object : Observer<Unit> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(t: Unit) {
+                    try {
+                        Log.d(TAG, "pushToken successfully registered at pushproxy.")
+                        updatePushStateForUser(proxyMap, user)
+                    } catch (e: IOException) {
+                        Log.e(TAG, "IOException while updating user", e)
+                    }
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, "Failed to register device with pushproxy", e)
+                    eventBus!!.post(EventStatus(user.id!!, EventStatus.EventType.PUSH_REGISTRATION, false))
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    @Throws(IOException::class)
+    private fun updatePushStateForUser(proxyMap: Map<String, String?>, user: User) {
+        val pushConfigurationState = PushConfigurationState()
+        pushConfigurationState.pushToken = proxyMap["pushToken"]
+        pushConfigurationState.deviceIdentifier = proxyMap["deviceIdentifier"]
+        pushConfigurationState.deviceIdentifierSignature = proxyMap["deviceIdentifierSignature"]
+        pushConfigurationState.userPublicKey = proxyMap["userPublicKey"]
+        pushConfigurationState.usesRegularPass = java.lang.Boolean.FALSE
+        if (user.id != null) {
+            userManager!!.updatePushState(user.id!!, pushConfigurationState).subscribe(object : SingleObserver<Int?> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onSuccess(integer: Int) {
+                    eventBus!!.post(
+                        EventStatus(
+                            getIdForUser(user),
+                            EventStatus.EventType.PUSH_REGISTRATION,
+                            true
+                        )
+                    )
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, "update push state for user failed", e)
+                    eventBus!!.post(
+                        EventStatus(
+                            getIdForUser(user),
+                            EventStatus.EventType.PUSH_REGISTRATION,
+                            false
+                        )
+                    )
+                }
+            })
+        } else {
+            Log.e(TAG, "failed to update updatePushStateForUser. user.getId() was null")
+        }
+    }
+
+    private fun readKeyFromString(readPublicKey: Boolean, keyString: String?): Key? {
+        var keyString = keyString
+        keyString = if (readPublicKey) {
+            keyString!!.replace("\\n".toRegex(), "").replace(
+                "-----BEGIN PUBLIC KEY-----",
+                ""
+            ).replace("-----END PUBLIC KEY-----", "")
+        } else {
+            keyString!!.replace("\\n".toRegex(), "").replace(
+                "-----BEGIN PRIVATE KEY-----",
+                ""
+            ).replace("-----END PRIVATE KEY-----", "")
+        }
+        var keyFactory: KeyFactory? = null
+        try {
+            keyFactory = KeyFactory.getInstance("RSA")
+            return if (readPublicKey) {
+                val keySpec = X509EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT))
+                keyFactory.generatePublic(keySpec)
+            } else {
+                val keySpec = PKCS8EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT))
+                keyFactory.generatePrivate(keySpec)
+            }
+        } catch (e: NoSuchAlgorithmException) {
+            Log.d(TAG, "No such algorithm while reading key from string")
+        } catch (e: InvalidKeySpecException) {
+            Log.d(TAG, "Invalid key spec while reading key from string")
+        }
+        return null
+    }
+
+    fun readKeyFromFile(readPublicKey: Boolean): Key? {
+        val path: String
+        path = if (readPublicKey) {
+            publicKeyFile.absolutePath
+        } else {
+            privateKeyFile.absolutePath
+        }
+        try {
+            FileInputStream(path).use { fileInputStream ->
+                val bytes = ByteArray(fileInputStream.available())
+                fileInputStream.read(bytes)
+                val keyFactory = KeyFactory.getInstance("RSA")
+                return if (readPublicKey) {
+                    val keySpec = X509EncodedKeySpec(bytes)
+                    keyFactory.generatePublic(keySpec)
+                } else {
+                    val keySpec = PKCS8EncodedKeySpec(bytes)
+                    keyFactory.generatePrivate(keySpec)
+                }
+            }
+        } catch (e: FileNotFoundException) {
+            Log.d(TAG, "Failed to find path while reading the Key")
+        } catch (e: IOException) {
+            Log.d(TAG, "IOException while reading the key")
+        } catch (e: InvalidKeySpecException) {
+            Log.d(TAG, "InvalidKeySpecException while reading the key")
+        } catch (e: NoSuchAlgorithmException) {
+            Log.d(TAG, "RSA algorithm not supported")
+        }
+        return null
+    }
+
+    companion object {
+        private const val TAG = "PushUtils"
+    }
+}

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 85 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 84 warnings</span>