From 119310ecc7a1c63b6f1a5d4eb9a1bb1f6631340d Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Fri, 24 Nov 2023 16:25:12 +0100 Subject: [PATCH 1/2] Add fixes and changes to push handling - Fix injection in GetFirebasePushTokenWorker. injection was not setup correctly in GetFirebasePushTokenWorker so the appPreferences were null. This resulted in the invinite loading screen during account setup if somehow onNewToken did not set the token. - avoid to register push on every load of ConversationList. - call GetFirebasePushTokenWorker instead of PushRegistrationWorker to make sure the firebase token is set(if onNewToken somehow fails to set it). Other than that, only call PushRegistrationWorker directly in NCFirebaseMessagingService as there the token is set. - add logging - trigger GetFirebasePushTokenWorker daily with periodical worker (instead monthly), and combine this with PushRegistrationWorker as this is defined inside GetFirebasePushTokenWorker Signed-off-by: Marcel Hibbe --- .../talk/utils/ClosedInterfaceImpl.java | 5 +- .../talk/jobs/GetFirebasePushTokenWorker.kt | 19 +- .../firebase/NCFirebaseMessagingService.kt | 6 +- .../talk/utils/ClosedInterfaceImpl.kt | 69 +-- .../account/AccountVerificationActivity.kt | 28 +- .../nextcloud/talk/activities/MainActivity.kt | 9 +- .../java/com/nextcloud/talk/api/NcApi.java | 3 +- .../ConversationsListActivity.kt | 2 - .../talk/jobs/PushRegistrationWorker.java | 2 +- .../com/nextcloud/talk/utils/PushUtils.java | 432 ------------------ .../com/nextcloud/talk/utils/PushUtils.kt | 400 ++++++++++++++++ 11 files changed, 451 insertions(+), 524 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/utils/PushUtils.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt diff --git a/app/src/generic/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java b/app/src/generic/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java index 19ca5448bb..88506eb1c7 100644 --- a/app/src/generic/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java +++ b/app/src/generic/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java @@ -38,9 +38,6 @@ public boolean isGooglePlayServicesAvailable() { @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 } } diff --git a/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt b/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt index afc7c7bb78..a2320217e5 100644 --- a/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt +++ b/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 } } diff --git a/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt b/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt index 04f61b0521..413b2e7970 100644 --- a/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt +++ b/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() diff --git a/app/src/gplay/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt b/app/src/gplay/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt index 7c24a019e7..f7562336d7 100644 --- a/app/src/gplay/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt +++ b/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 } } diff --git a/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt index 4230a6511c..bcbf04dfb0 100644 --- a/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt +++ b/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") } } diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index 5c107f9171..ce631df93f 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/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) { 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() } }) } diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index cacc4c0b4d..674443ee69 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -57,6 +57,7 @@ import androidx.annotation.Nullable; import io.reactivex.Observable; +import kotlin.Unit; import okhttp3.MultipartBody; import okhttp3.RequestBody; import okhttp3.ResponseBody; @@ -333,7 +334,7 @@ Observable unregisterDeviceForNotificationsWithNextcloud( @FormUrlEncoded @POST - Observable registerDeviceForNotificationsWithPushProxy(@Url String url, + Observable registerDeviceForNotificationsWithPushProxy(@Url String url, @FieldMap Map fields); diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index d1392ba43a..9ee0e5c41e 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/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) } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java index 2fe18bb2a9..7869a6e7f0 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java +++ b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java @@ -64,7 +64,7 @@ public PushRegistrationWorker(@NonNull Context context, @NonNull WorkerParameter @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); diff --git a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java deleted file mode 100644 index 2890acfeaf..0000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Andy Scherzinger - * @author Marcel Hibbe - * @author Mario Danic - * Copyright (C) 2022 Andy Scherzinger - * Copyright (C) 2022 Marcel Hibbe - * Copyright (C) 2017 Mario Danic - * - * 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 . - */ - -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 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 users = userManager.getUsers().blockingGet(); - - for (User user : users) { - if (!user.getScheduledForDeletion()) { - Map 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 nextcloudRegisterPushMap, - String token, - User user) { - String credentials = ApiUtils.getCredentials(user.getUsername(), user.getToken()); - - ncApi.registerDeviceForNotificationsWithNextcloud( - credentials, - ApiUtils.getUrlNextcloudPush(user.getBaseUrl()), - nextcloudRegisterPushMap) - .subscribe(new Observer() { - @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 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 proxyMap, User user) { - ncApi.registerDeviceForNotificationsWithPushProxy(ApiUtils.getUrlPushProxy(), proxyMap) - .subscribeOn(Schedulers.io()) - .subscribe(new Observer() { - @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 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() { - @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; - } -} diff --git a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt new file mode 100644 index 0000000000..5e7dbe4e1a --- /dev/null +++ b/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 + * Copyright (C) 2022 Marcel Hibbe + * Copyright (C) 2017 Mario Danic + * + * 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 . + */ +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 = 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, + token: String, + user: User + ) { + val credentials = ApiUtils.getCredentials(user.username, user.token) + ncApi.registerDeviceForNotificationsWithNextcloud( + credentials, + ApiUtils.getUrlNextcloudPush(user.baseUrl), + nextcloudRegisterPushMap + ) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(pushRegistrationOverall: PushRegistrationOverall) { + Log.d(TAG, "pushTokenHash successfully registered at nextcloud server.") + val proxyMap: MutableMap = 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, user: User) { + ncApi.registerDeviceForNotificationsWithPushProxy(ApiUtils.getUrlPushProxy(), proxyMap) + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + 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, 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 { + 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" + } +} From 173582ad0ed4212eb3e2b3956e7cf7fc238af334 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Nov 2023 20:14:08 +0000 Subject: [PATCH 2/2] Analysis: update lint results to reflect reduced error/warning count Signed-off-by: github-actions --- scripts/analysis/lint-results.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt index bf24c5db3f..4bc2fbc3bd 100644 --- a/scripts/analysis/lint-results.txt +++ b/scripts/analysis/lint-results.txt @@ -1,2 +1,2 @@ DO NOT TOUCH; GENERATED BY DRONE - Lint Report: 85 warnings + Lint Report: 84 warnings