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

Add fixes and changes to push handling
This commit is contained in:
Marcel Hibbe 2023-12-01 14:14:36 +01:00 committed by GitHub
commit e86599e817
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 452 additions and 525 deletions

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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()

View file

@ -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()
val firebasePushTokenWorker = OneTimeWorkRequest.Builder(GetFirebasePushTokenWorker::class.java).build()
WorkManager.getInstance().enqueue(firebasePushTokenWorker)
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()
WorkManager.getInstance()
.enqueueUniquePeriodicWork(
"periodicTokenRegistration",
ExistingPeriodicWorkPolicy.REPLACE,
periodicTokenRegistration
)
}
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
}
}

View file

@ -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")
}
}

View file

@ -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()
}
})
}

View file

@ -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);

View file

@ -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)
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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"
}
}

View file

@ -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>