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 19ca5448bbe..88506eb1c75 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 66b3876e24a..6882ea9ba1d 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,15 +53,13 @@ class GetFirebasePushTokenWorker(val context: Context, workerParameters: WorkerP return@OnCompleteListener } - val token = task.result - Log.d(TAG, "Fetched push token is: $token") + 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() 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 866a54037ff..413b2e79702 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 @@ -77,13 +77,10 @@ class NCFirebaseMessagingService : FirebaseMessagingService() { super.onNewToken(token) Log.d(TAG, "onNewToken. token = $token") - // appPreferences.pushToken = token // TODO revert this! just for testing, do not set the token. - // seems this sometimes somehow also happens in reality + 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 60b55a5ce26..f7562336d75 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,78 +64,43 @@ class ClosedInterfaceImpl : ClosedInterface, ProviderInstaller.ProviderInstallLi val api = GoogleApiAvailability.getInstance() val code = NextcloudTalkApplication.sharedApplication?.let { - api.isGooglePlayServicesAvailable( - it.applicationContext - ) + api.isGooglePlayServicesAvailable(it.applicationContext) } - // TODO: check if this always works! - 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 b3e251004f4..ceb6ae1b441 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,18 +357,6 @@ 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) { 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 5c107f91712..e5b308935f7 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt @@ -52,6 +52,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 +79,6 @@ class MainActivity : BaseActivity(), ActionBarProvider { } } - @Suppress("Detekt.TooGenericExceptionCaught") override fun onCreate(savedInstanceState: Bundle?) { Log.d(TAG, "onCreate: Activity: " + System.identityHashCode(this).toString()) @@ -280,6 +280,7 @@ class MainActivity : BaseActivity(), ActionBarProvider { override fun onSuccess(users: List) { if (users.isNotEmpty()) { + ClosedInterfaceImpl().setUpPushTokenRegistration() runOnUiThread { openConversationList() } 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 cacc4c0b4d5..674443ee691 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 d1392ba43a8..9ee0e5c41e4 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/utils/PushUtils.java b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java deleted file mode 100644 index 30f8419abdf..00000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java +++ /dev/null @@ -1,448 +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 pushToken = appPreferences.getPushToken(); - - // TODO: ???? if token is empty, execute GetFirebasePushTokenWorker and retrieve token. - // When worker finished, continue here... - - if (!TextUtils.isEmpty(pushToken)) { - Log.d(TAG, "pushRegistrationToServer will be done with pushToken: " + pushToken); - - String pushTokenHash = generateSHA512Hash(pushToken).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, pushToken, user); - } - } - - } - } else { - // TODO: when token is empty, registerDeviceWithNextcloud is NOT executed which would execute: - // registerDeviceWithPushProxy - // updatePushStateForUser - // eventBus.post(new EventStatus(...EventStatus.EventType.PUSH_REGISTRATION, true)); - // AccountVerificationActivity -> onMessageEvent(eventStatus: EventStatus) PUSH_REGISTRATION - // fetchAndStoreCapabilities()... - // fetchAndStoreExternalSignalingSettings()... - // proceedWithLogin() - // start ConversationsListActivity - // - // --> this can cause the infinite loading spinner after login! - 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 00000000000..a2c9f0957d9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt @@ -0,0 +1,412 @@ +/* + * 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 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 + + @JvmField + @Inject + var appPreferences: AppPreferences? = null + + @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 (!TextUtils.isEmpty(pushToken)) { + 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 { + // when token is empty, registerDeviceWithNextcloud is NOT executed which would execute: + // registerDeviceWithPushProxy + // updatePushStateForUser + // eventBus.post(new EventStatus(...EventStatus.EventType.PUSH_REGISTRATION, true)); + // AccountVerificationActivity -> onMessageEvent(eventStatus: EventStatus) PUSH_REGISTRATION + // fetchAndStoreCapabilities()... + // fetchAndStoreExternalSignalingSettings()... + // proceedWithLogin() + // start ConversationsListActivity + // + // --> this can cause the infinite loading spinner after login! + Log.e(TAG, "push token was empty when trying to register at nextcloud 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) { + 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" + } +}