From a54f3ed1a28d85f862df0060d3a77ad6e0103fb1 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 18 Dec 2025 20:21:52 +0100 Subject: [PATCH 1/3] require authorization for relays management --- src/main/AndroidManifest.xml | 20 +- .../securesms/ConversationListActivity.java | 23 +- .../securesms/InstantOnboardingActivity.java | 6 +- .../securesms/NewConversationActivity.java | 7 +- .../securesms/WebViewActivity.java | 2 +- .../securesms/WelcomeActivity.java | 39 +- .../contacts/NewContactActivity.java | 7 +- .../AdvancedPreferenceFragment.java | 8 +- .../thoughtcrime/securesms/qr/QrActivity.java | 32 +- .../securesms/qr/QrCodeHandler.java | 374 ++++++++++-------- .../securesms/relay/RelayListActivity.java | 39 +- .../securesms/util/LongClickCopySpan.java | 10 +- 12 files changed, 304 insertions(+), 263 deletions(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 77c5f6e3e..5daa74438 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -209,16 +209,7 @@ - - - - - - - - + android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"> + android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"> + android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"> + android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"> startBackupTransfer(finalQrRaw)) - .setNegativeButton(R.string.cancel, null) - .setCancelable(false) - .show(); - break; - - case DcContext.DC_QR_BACKUP_TOO_NEW: - new AlertDialog.Builder(this) - .setTitle(R.string.multidevice_receiver_title) - .setMessage(R.string.multidevice_receiver_needs_update) - .setPositiveButton(R.string.ok, null) - .show(); - break; - - default: - new AlertDialog.Builder(this) - .setMessage(R.string.qraccount_qr_code_cannot_be_used) - .setPositiveButton(R.string.ok, null) - .show(); - break; + if (!new QrCodeHandler(this).handleBackupQr(qrRaw)) { + new AlertDialog.Builder(this) + .setMessage(R.string.qraccount_qr_code_cannot_be_used) + .setPositiveButton(R.string.ok, null) + .show(); } } else if (requestCode == PICK_BACKUP) { Uri uri = (data != null ? data.getData() : null); diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/NewContactActivity.java b/src/main/java/org/thoughtcrime/securesms/contacts/NewContactActivity.java index 494c8655a..c3459619a 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/NewContactActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/NewContactActivity.java @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.qr.QrCodeHandler; import org.thoughtcrime.securesms.util.ViewUtil; +import chat.delta.rpc.types.SecurejoinSource; import chat.delta.rpc.types.SecurejoinUiPath; public class NewContactActivity extends PassphraseRequiredActionBarActivity @@ -104,10 +105,10 @@ public class NewContactActivity extends PassphraseRequiredActionBarActivity @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode == IntentIntegrator.REQUEST_CODE) { - IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK && requestCode == IntentIntegrator.REQUEST_CODE) { + IntentResult scanResult = IntentIntegrator.parseActivityResult(resultCode, data); QrCodeHandler qrCodeHandler = new QrCodeHandler(this); - qrCodeHandler.onScanPerformed(scanResult, SecurejoinUiPath.NewContact); + qrCodeHandler.handleOnlySecureJoinQr(scanResult.getContents(), SecurejoinSource.Scan, SecurejoinUiPath.NewContact); } } } diff --git a/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java b/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java index 8fe732761..0c73ca8ab 100644 --- a/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java @@ -17,7 +17,6 @@ import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.EditText; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -34,8 +33,8 @@ import org.thoughtcrime.securesms.relay.RelayListActivity; import org.thoughtcrime.securesms.StatsSending; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.proxy.ProxySettingsActivity; -import org.thoughtcrime.securesms.util.IntentUtils; import org.thoughtcrime.securesms.util.Prefs; +import org.thoughtcrime.securesms.util.ScreenLockUtil; import org.thoughtcrime.securesms.util.StreamUtil; import org.thoughtcrime.securesms.util.Util; @@ -223,7 +222,10 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment Preference relayListBtn = this.findPreference("pref_relay_list_button"); if (relayListBtn != null) { relayListBtn.setOnPreferenceClickListener(((preference) -> { - openRelayListActivity(); + boolean result = ScreenLockUtil.applyScreenLock(requireActivity(), getString(R.string.transports), getString(R.string.enter_system_secret_to_continue), REQUEST_CODE_CONFIRM_CREDENTIALS_ACCOUNT); + if (!result) { + openRelayListActivity(); + } return true; })); } diff --git a/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java b/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java index 1b62f61e1..a9f08134c 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.qr; import android.Manifest; import android.app.Activity; -import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; @@ -27,11 +26,10 @@ import com.google.zxing.MultiFormatReader; import com.google.zxing.NotFoundException; import com.google.zxing.RGBLuminanceSource; import com.google.zxing.Result; +import com.google.zxing.client.android.Intents; import com.google.zxing.common.HybridBinarizer; import org.thoughtcrime.securesms.BaseActionBarActivity; -import org.thoughtcrime.securesms.ConversationListActivity; -import org.thoughtcrime.securesms.NewConversationActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.contacts.NewContactActivity; @@ -44,9 +42,6 @@ import org.thoughtcrime.securesms.util.ViewUtil; import java.io.FileNotFoundException; import java.io.InputStream; -import chat.delta.rpc.types.SecurejoinSource; -import chat.delta.rpc.types.SecurejoinUiPath; - public class QrActivity extends BaseActionBarActivity implements View.OnClickListener { private final static String TAG = QrActivity.class.getSimpleName(); @@ -154,8 +149,7 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis } else if (itemId == R.id.load_from_image) { AttachmentManager.selectImage(this, REQUEST_CODE_IMAGE); } else if (itemId == R.id.paste) { - QrCodeHandler qrCodeHandler = new QrCodeHandler(this); - qrCodeHandler.handleQrData(Util.getTextFromClipboard(this), SecurejoinSource.Clipboard, getUiPath()); + setQrResult(Util.getTextFromClipboard(this)); } return false; @@ -203,10 +197,10 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis RGBLuminanceSource source = new RGBLuminanceSource(width, height, pixels); BinaryBitmap bBitmap = new BinaryBitmap(new HybridBinarizer(source)); MultiFormatReader reader = new MultiFormatReader(); + try { Result result = reader.decode(bBitmap); - QrCodeHandler qrCodeHandler = new QrCodeHandler(this); - qrCodeHandler.handleQrData(result.getText(), SecurejoinSource.ImageLoaded, getUiPath()); + setQrResult(result.getText()); } catch (NotFoundException e) { Log.e(TAG, "decode exception", e); Toast.makeText(this, getString(R.string.qrscan_failed), Toast.LENGTH_LONG).show(); @@ -219,19 +213,11 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis } } - private SecurejoinUiPath getUiPath() { - SecurejoinUiPath uiPath = null; - ComponentName caller = this.getCallingActivity(); - if (caller != null) { - if (caller.getClassName().equals(NewConversationActivity.class.getName())) { - uiPath = SecurejoinUiPath.NewContact; - } else if (caller.getClassName().equals(ConversationListActivity.class.getName()) - // RoutingActivity is an alias for ConversationListActivity - || caller.getClassName().endsWith(".RoutingActivity")) { - uiPath = SecurejoinUiPath.QrIcon; - } - } - return uiPath; + private void setQrResult(String qrData) { + Intent intent = new Intent(); + intent.putExtra(Intents.Scan.RESULT, qrData); + setResult(RESULT_OK, intent); + finish(); } @Override diff --git a/src/main/java/org/thoughtcrime/securesms/qr/QrCodeHandler.java b/src/main/java/org/thoughtcrime/securesms/qr/QrCodeHandler.java index 84f3c9fd6..8e5900386 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/QrCodeHandler.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/QrCodeHandler.java @@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.connect.AccountManager; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.relay.RelayListActivity; import org.thoughtcrime.securesms.util.IntentUtils; +import org.thoughtcrime.securesms.util.ScreenLockUtil; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.views.ProgressDialog; @@ -53,118 +54,182 @@ public class QrCodeHandler { accId = dcContext.getAccountId(); } - public void onScanPerformed(IntentResult scanResult, SecurejoinUiPath uipath) { - if (scanResult == null || scanResult.getFormatName() == null) { - return; // aborted - } - - handleQrData(scanResult.getContents(), SecurejoinSource.Scan, uipath); + /** Process only QR about getting in contact or joining chats */ + public void handleOnlySecureJoinQr(String rawString, SecurejoinSource source, SecurejoinUiPath uiPath) { + final DcLot qrParsed = dcContext.checkQr(rawString); + if (!handleSecureJoinQr(qrParsed, rawString, source, uiPath)) { + handleDefault(new AlertDialog.Builder(activity), rawString, qrParsed); } + } - public void handleQrData(String rawString, SecurejoinSource source, SecurejoinUiPath uiPath) { + private boolean handleSecureJoinQr(DcLot qrParsed, String rawString, SecurejoinSource source, SecurejoinUiPath uiPath) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + String name = dcContext.getContact(qrParsed.getId()).getDisplayName(); + switch (qrParsed.getState()) { + case DcContext.DC_QR_ASK_VERIFYCONTACT: + case DcContext.DC_QR_ASK_VERIFYGROUP: + case DcContext.DC_QR_ASK_JOIN_BROADCAST: + showVerifyContactOrGroup(builder, rawString, qrParsed, name, source, uiPath); + break; + + case DcContext.DC_QR_WITHDRAW_VERIFYCONTACT: + case DcContext.DC_QR_WITHDRAW_VERIFYGROUP: + case DcContext.DC_QR_WITHDRAW_JOINBROADCAST: + String message = qrParsed.getState() == DcContext.DC_QR_WITHDRAW_VERIFYCONTACT ? activity.getString(R.string.withdraw_verifycontact_explain) + : qrParsed.getState() == DcContext.DC_QR_WITHDRAW_VERIFYCONTACT ? activity.getString(R.string.withdraw_verifygroup_explain, qrParsed.getText1()) + : activity.getString(R.string.withdraw_joinbroadcast_explain, qrParsed.getText1()); + builder.setTitle(R.string.qrshow_title); + builder.setMessage(message); + builder.setNeutralButton(R.string.reset, (dialog, which) -> { + dcContext.setConfigFromQr(rawString); + }); + builder.setPositiveButton(R.string.ok, null); + Util.redButton(builder.show(), AlertDialog.BUTTON_NEUTRAL); + return true; + + case DcContext.DC_QR_REVIVE_VERIFYCONTACT: + case DcContext.DC_QR_REVIVE_VERIFYGROUP: + case DcContext.DC_QR_REVIVE_JOINBROADCAST: + builder.setTitle(R.string.qrshow_title); + builder.setMessage(activity.getString(R.string.revive_verifycontact_explain)); + builder.setNeutralButton(R.string.revive_qr_code, (dialog, which) -> { + dcContext.setConfigFromQr(rawString); + }); + builder.setPositiveButton(R.string.ok, null); + break; + + case DcContext.DC_QR_FPR_WITHOUT_ADDR: + showVerifyFingerprintWithoutAddress(builder, qrParsed); + break; + + case DcContext.DC_QR_FPR_MISMATCH: + showFingerPrintError(builder, name); + break; + + case DcContext.DC_QR_FPR_OK: + case DcContext.DC_QR_ADDR: + showFingerprintOrQrSuccess(builder, qrParsed, name); + break; + + default: + return false; + } + builder.create().show(); + return true; + } + + /** Process only QR about adding relays/profiles (DCACCOUNT: / DCLOGIN:) */ + public void handleOnlyAddRelayQr(String rawString) { + final DcLot qrParsed = dcContext.checkQr(rawString); + if (!handleAddRelayQr(qrParsed, rawString)) { + handleDefault(new AlertDialog.Builder(activity), rawString, qrParsed); + } + } + + private boolean handleAddRelayQr(DcLot qrParsed, String rawString) { + switch (qrParsed.getState()) { + case DcContext.DC_QR_ACCOUNT: + case DcContext.DC_QR_LOGIN: AlertDialog.Builder builder = new AlertDialog.Builder(activity); - final DcLot qrParsed = dcContext.checkQr(rawString); - String name = dcContext.getContact(qrParsed.getId()).getDisplayName(); - switch (qrParsed.getState()) { - case DcContext.DC_QR_ASK_VERIFYCONTACT: - case DcContext.DC_QR_ASK_VERIFYGROUP: - case DcContext.DC_QR_ASK_JOIN_BROADCAST: - showVerifyContactOrGroup(activity, builder, rawString, qrParsed, name, source, uiPath); - break; - - case DcContext.DC_QR_FPR_WITHOUT_ADDR: - showVerifyFingerprintWithoutAddress(builder, qrParsed); - break; - - case DcContext.DC_QR_FPR_MISMATCH: - showFingerPrintError(builder, name); - break; - - case DcContext.DC_QR_FPR_OK: - case DcContext.DC_QR_ADDR: - showFingerprintOrQrSuccess(builder, qrParsed, name); - break; - - case DcContext.DC_QR_URL: - showQrUrl(builder, qrParsed); - break; - - case DcContext.DC_QR_ACCOUNT: - case DcContext.DC_QR_LOGIN: - final String scope = qrParsed.getText1(); - setAddTransportDialog(activity, builder, rawString, scope); - builder.setNegativeButton(R.string.cancel, null); - builder.setCancelable(false); - break; - - case DcContext.DC_QR_BACKUP2: - builder.setTitle(R.string.multidevice_receiver_title); - builder.setMessage(activity.getString(R.string.multidevice_receiver_scanning_ask) + "\n\n" + activity.getString(R.string.multidevice_same_network_hint)); - builder.setPositiveButton(R.string.perm_continue, (dialog, which) -> { - AccountManager.getInstance().addAccountFromSecondDevice(activity, rawString); - }); - builder.setNegativeButton(R.string.cancel, null); - builder.setCancelable(false); - - AlertDialog alertDialog = builder.create(); - alertDialog.show(); - BackupTransferActivity.appendSSID(activity, alertDialog.findViewById(android.R.id.message)); - return; - - case DcContext.DC_QR_BACKUP_TOO_NEW: - builder.setTitle(R.string.multidevice_receiver_title); - builder.setMessage(activity.getString(R.string.multidevice_receiver_needs_update)); - builder.setNegativeButton(R.string.ok, null); - break; - - case DcContext.DC_QR_PROXY: - builder.setTitle(R.string.proxy_use_proxy); - builder.setMessage(activity.getString(R.string.proxy_use_proxy_confirm, qrParsed.getText1())); - builder.setPositiveButton(R.string.proxy_use_proxy, (dlg, btn) -> { - dcContext.setConfigFromQr(rawString); - dcContext.restartIo(); - showDoneToast(activity); - }); - if (rawString.toLowerCase().startsWith("http")) { - builder.setNeutralButton(R.string.open, (d, b) -> IntentUtils.showInBrowser(activity, rawString)); - } - builder.setNegativeButton(R.string.cancel, null); - builder.setCancelable(false); - break; - - case DcContext.DC_QR_WITHDRAW_VERIFYCONTACT: - case DcContext.DC_QR_WITHDRAW_VERIFYGROUP: - case DcContext.DC_QR_WITHDRAW_JOINBROADCAST: - String message = qrParsed.getState() == DcContext.DC_QR_WITHDRAW_VERIFYCONTACT ? activity.getString(R.string.withdraw_verifycontact_explain) - : qrParsed.getState() == DcContext.DC_QR_WITHDRAW_VERIFYCONTACT ? activity.getString(R.string.withdraw_verifygroup_explain, qrParsed.getText1()) - : activity.getString(R.string.withdraw_joinbroadcast_explain, qrParsed.getText1()); - builder.setTitle(R.string.qrshow_title); - builder.setMessage(message); - builder.setNeutralButton(R.string.reset, (dialog, which) -> { - dcContext.setConfigFromQr(rawString); - }); - builder.setPositiveButton(R.string.ok, null); - AlertDialog withdrawDialog = builder.show(); - Util.redButton(withdrawDialog, AlertDialog.BUTTON_NEUTRAL); - return; - - case DcContext.DC_QR_REVIVE_VERIFYCONTACT: - case DcContext.DC_QR_REVIVE_VERIFYGROUP: - case DcContext.DC_QR_REVIVE_JOINBROADCAST: - builder.setTitle(R.string.qrshow_title); - builder.setMessage(activity.getString(R.string.revive_verifycontact_explain)); - builder.setNeutralButton(R.string.revive_qr_code, (dialog, which) -> { - dcContext.setConfigFromQr(rawString); - }); - builder.setPositiveButton(R.string.ok, null); - break; - - default: - handleDefault(builder, rawString, qrParsed); - break; - } + builder.setTitle(R.string.confirm_add_transport); + builder.setMessage(qrParsed.getText1()); + builder.setPositiveButton(R.string.ok, (d, w) -> { + if (activity instanceof RelayListActivity) { + // no need to protect with lock, RelayListActivity is already protected + addRelay(rawString); + } else { + boolean result = ScreenLockUtil.applyScreenLock(activity, activity.getString(R.string.add_transport), activity.getString(R.string.enter_system_secret_to_continue), ScreenLockUtil.REQUEST_CODE_CONFIRM_CREDENTIALS); + if (!result) { + addRelay(rawString); + } + } + }); + builder.setNegativeButton(R.string.cancel, null); + builder.setCancelable(false); builder.create().show(); + return true; + default: + return false; } + } + + /** Process a proxy QR, returns true if a dialog was shown, false if the QR is not a proxy QR */ + public boolean handleProxyQr(String rawString) { + return handleProxyQr(dcContext.checkQr(rawString), rawString); + } + + private boolean handleProxyQr(DcLot qrParsed, String rawString) { + if (qrParsed.getState() == DcContext.DC_QR_PROXY) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(R.string.proxy_use_proxy); + builder.setMessage(activity.getString(R.string.proxy_use_proxy_confirm, qrParsed.getText1())); + builder.setPositiveButton(R.string.proxy_use_proxy, (dlg, btn) -> { + dcContext.setConfigFromQr(rawString); + dcContext.restartIo(); + showDoneToast(); + }); + if (rawString.toLowerCase().startsWith("http")) { + builder.setNeutralButton(R.string.open, (d, b) -> IntentUtils.showInBrowser(activity, rawString)); + } + builder.setNegativeButton(R.string.cancel, null); + builder.setCancelable(false); + builder.create().show(); + return true; + } + return false; + } + + /** Process a backup QR, returns true if a dialog was shown, false if the QR is not a backup QR */ + public boolean handleBackupQr(String rawString) { + return handleBackupQr(dcContext.checkQr(rawString), rawString); + } + + private boolean handleBackupQr(DcLot qrParsed, String rawString) { + switch (qrParsed.getState()) { + case DcContext.DC_QR_BACKUP2: + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(R.string.multidevice_receiver_title); + builder.setMessage(activity.getString(R.string.multidevice_receiver_scanning_ask) + "\n\n" + activity.getString(R.string.multidevice_same_network_hint)); + builder.setPositiveButton(R.string.perm_continue, (dialog, which) -> { + AccountManager.getInstance().addAccountFromSecondDevice(activity, rawString); + }); + builder.setNegativeButton(R.string.cancel, null); + builder.setCancelable(false); + + AlertDialog alertDialog = builder.create(); + alertDialog.show(); + BackupTransferActivity.appendSSID(activity, alertDialog.findViewById(android.R.id.message)); + return true; + + case DcContext.DC_QR_BACKUP_TOO_NEW: + new AlertDialog.Builder(activity) + .setTitle(R.string.multidevice_receiver_title) + .setMessage(activity.getString(R.string.multidevice_receiver_needs_update)) + .setNegativeButton(R.string.ok, null) + .create().show(); + return true; + + default: + return false; + } + } + + /** Handle any kind of QR showing an AlertDialog adapted to the QR type. */ + public void handleQrData(String rawString, SecurejoinSource source, SecurejoinUiPath uiPath) { + final DcLot qrParsed = dcContext.checkQr(rawString); + if (handleSecureJoinQr(qrParsed, rawString, source, uiPath) + || handleAddRelayQr(qrParsed, rawString) + || handleProxyQr(qrParsed, rawString) + || handleBackupQr(qrParsed, rawString)) return; + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + if (qrParsed.getState() == DcContext.DC_QR_URL) { + showQrUrl(builder, qrParsed); + } else { + handleDefault(builder, rawString, qrParsed); + } + builder.create().show(); + } private void handleDefault(AlertDialog.Builder builder, String qrRawString, DcLot qrParsed) { String msg; @@ -187,7 +252,7 @@ public class QrCodeHandler { builder.setPositiveButton(android.R.string.ok, null); builder.setNeutralButton(R.string.menu_copy_to_clipboard, (dialog, which) -> { Util.writeTextToClipboard(activity, scannedText); - showDoneToast(activity); + showDoneToast(); }); } @@ -199,11 +264,11 @@ public class QrCodeHandler { builder.setNegativeButton(android.R.string.cancel, null); builder.setNeutralButton(R.string.menu_copy_to_clipboard, (dialog, which) -> { Util.writeTextToClipboard(activity, url); - showDoneToast(activity); + showDoneToast(); }); } - private void showDoneToast(Activity activity) { + private void showDoneToast() { Toast.makeText(activity, activity.getString(R.string.done), Toast.LENGTH_SHORT).show(); } @@ -232,12 +297,11 @@ public class QrCodeHandler { builder.setPositiveButton(android.R.string.ok, null); builder.setNeutralButton(R.string.menu_copy_to_clipboard, (dialog, which) -> { Util.writeTextToClipboard(activity, qrParsed.getText1()); - showDoneToast(activity); + showDoneToast(); }); } - private void showVerifyContactOrGroup(Activity activity, - AlertDialog.Builder builder, + private void showVerifyContactOrGroup(AlertDialog.Builder builder, String qrRawString, DcLot qrParsed, String name, @@ -275,51 +339,47 @@ public class QrCodeHandler { builder.setNegativeButton(android.R.string.cancel, null); } - private void setAddTransportDialog(Activity activity, AlertDialog.Builder builder, String qrData, String transportName) { - builder.setTitle(R.string.confirm_add_transport); - builder.setMessage(transportName); - builder.setPositiveButton(R.string.ok, (d, w) -> { - ProgressDialog progressDialog = new ProgressDialog(activity); - progressDialog.setMessage(activity.getResources().getString(R.string.one_moment)); - progressDialog.setCanceledOnTouchOutside(false); - progressDialog.setCancelable(false); - String cancel = activity.getResources().getString(android.R.string.cancel); - progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, cancel, (d2, w2) -> { - dcContext.stopOngoingProcess(); - }); - progressDialog.show(); + public void addRelay(String qrData) { + ProgressDialog progressDialog = new ProgressDialog(activity); + progressDialog.setMessage(activity.getResources().getString(R.string.one_moment)); + progressDialog.setCanceledOnTouchOutside(false); + progressDialog.setCancelable(false); + String cancel = activity.getResources().getString(android.R.string.cancel); + progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, cancel, (d, w) -> { + dcContext.stopOngoingProcess(); + }); + progressDialog.show(); - Util.runOnAnyBackgroundThread(() -> { - String error = null; - try { - rpc.addTransportFromQr(accId, qrData); - } catch (RpcException e) { - Log.w(TAG, e); - error = e.getMessage(); - } - final String finalError = error; - Util.runOnMain(() -> { - if (!progressDialog.isShowing()) return; // canceled dialog, nothing to do - if (finalError != null) { - new AlertDialog.Builder(activity) - .setTitle(R.string.error) - .setMessage(finalError) - .setPositiveButton(R.string.ok, null) - .show(); - } else { - showDoneToast(activity); - if (!(activity instanceof RelayListActivity)) { - activity.startActivity(new Intent(activity, RelayListActivity.class)); - } - } - try { - progressDialog.dismiss(); - } catch (IllegalArgumentException e) { - // see https://stackoverflow.com/a/5102572/4557005 - Log.w(TAG, e); - } - }); - }); + Util.runOnAnyBackgroundThread(() -> { + String error = null; + try { + rpc.addTransportFromQr(accId, qrData); + } catch (RpcException e) { + Log.w(TAG, e); + error = e.getMessage(); + } + final String finalError = error; + Util.runOnMain(() -> { + if (!progressDialog.isShowing()) return; // canceled dialog, nothing to do + if (finalError != null) { + new AlertDialog.Builder(activity) + .setTitle(R.string.error) + .setMessage(finalError) + .setPositiveButton(R.string.ok, null) + .show(); + } else { + showDoneToast(); + if (!(activity instanceof RelayListActivity)) { + activity.startActivity(new Intent(activity, RelayListActivity.class)); + } + } + try { + progressDialog.dismiss(); + } catch (IllegalArgumentException e) { + // see https://stackoverflow.com/a/5102572/4557005 + Log.w(TAG, e); + } }); + }); } } diff --git a/src/main/java/org/thoughtcrime/securesms/relay/RelayListActivity.java b/src/main/java/org/thoughtcrime/securesms/relay/RelayListActivity.java index bb15e4dfc..a5b45990d 100644 --- a/src/main/java/org/thoughtcrime/securesms/relay/RelayListActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/relay/RelayListActivity.java @@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.qr.QrActivity; import org.thoughtcrime.securesms.qr.QrCodeHandler; +import org.thoughtcrime.securesms.util.ScreenLockUtil; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; @@ -32,8 +33,6 @@ import java.util.List; import chat.delta.rpc.Rpc; import chat.delta.rpc.RpcException; import chat.delta.rpc.types.EnteredLoginParam; -import chat.delta.rpc.types.SecurejoinSource; -import chat.delta.rpc.types.SecurejoinUiPath; public class RelayListActivity extends BaseActionBarActivity implements RelayListAdapter.OnRelayClickListener, DcEventCenter.DcEventDelegate { @@ -45,6 +44,9 @@ public class RelayListActivity extends BaseActionBarActivity private Rpc rpc; private int accId; + /** QR provided via Intent extras needs to be saved to pass it to QrCodeHandler when authorization finishes */ + private String qrData = null; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -67,6 +69,15 @@ public class RelayListActivity extends BaseActionBarActivity // Apply insets to prevent fab from being covered by system bars ViewUtil.applyWindowInsetsAsMargin(fabAdd); + qrData = getIntent().getStringExtra(EXTRA_QR_DATA); + if (qrData != null) { + // when the activity is opened with a QR data, we need to ask for authorization first + boolean result = ScreenLockUtil.applyScreenLock(this, getString(R.string.add_transport), getString(R.string.enter_system_secret_to_continue), ScreenLockUtil.REQUEST_CODE_CONFIRM_CREDENTIALS); + if (!result) { + new QrCodeHandler(this).handleOnlyAddRelayQr(qrData); + } + } + fabAdd.setOnClickListener(v -> { new IntentIntegrator(this).setCaptureActivity(QrActivity.class).addExtra(QrActivity.EXTRA_SCAN_RELAY, true).initiateScan(); }); @@ -86,12 +97,6 @@ public class RelayListActivity extends BaseActionBarActivity DcEventCenter eventCenter = DcHelper.getEventCenter(this); eventCenter.addObserver(DcContext.DC_EVENT_CONFIGURE_PROGRESS, this); - - String qrdata = getIntent().getStringExtra(EXTRA_QR_DATA); - if (qrdata != null) { - QrCodeHandler qrCodeHandler = new QrCodeHandler(this); - qrCodeHandler.handleQrData(qrdata, SecurejoinSource.Unknown, SecurejoinUiPath.Unknown); - } } @Override @@ -172,10 +177,22 @@ public class RelayListActivity extends BaseActionBarActivity @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); + if (resultCode != RESULT_OK) { + // if user canceled unlocking, then finish + if (requestCode == ScreenLockUtil.REQUEST_CODE_CONFIRM_CREDENTIALS) finish(); + return; + } + + QrCodeHandler qrCodeHandler = new QrCodeHandler(this); if (requestCode == IntentIntegrator.REQUEST_CODE) { - IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data); - QrCodeHandler qrCodeHandler = new QrCodeHandler(this); - qrCodeHandler.onScanPerformed(scanResult, SecurejoinUiPath.Unknown); + IntentResult scanResult = IntentIntegrator.parseActivityResult(resultCode, data); + qrCodeHandler.handleOnlyAddRelayQr(scanResult.getContents()); + } else if (requestCode == ScreenLockUtil.REQUEST_CODE_CONFIRM_CREDENTIALS) { + // user authorized, then proceed to handle the QR data + if (qrData != null) { + qrCodeHandler.handleOnlyAddRelayQr(qrData); + qrData = null; + } } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/LongClickCopySpan.java b/src/main/java/org/thoughtcrime/securesms/util/LongClickCopySpan.java index 606bedbeb..594081204 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/LongClickCopySpan.java +++ b/src/main/java/org/thoughtcrime/securesms/util/LongClickCopySpan.java @@ -82,15 +82,11 @@ public class LongClickCopySpan extends ClickableSpan { } } else if (Util.isInviteURL(url)) { QrCodeHandler qrCodeHandler = new QrCodeHandler((Activity) widget.getContext()); - qrCodeHandler.handleQrData(url, SecurejoinSource.InternalLink, null); + qrCodeHandler.handleOnlySecureJoinQr(url, SecurejoinSource.InternalLink, null); } else { Activity activity = (Activity) widget.getContext(); - DcContext dcContext = DcHelper.getContext(activity); - if (dcContext.checkQr(url).getState() == DcContext.DC_QR_PROXY) { - QrCodeHandler qrCodeHandler = new QrCodeHandler(activity); - qrCodeHandler.handleQrData(url, null, null); - } else { - IntentUtils.showInBrowser(widget.getContext(), url); + if (!new QrCodeHandler(activity).handleProxyQr(url)) { + IntentUtils.showInBrowser(activity, url); } } } From 5310c41694b5305c95c44c783efcbf5bd8ffa2c1 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 18 Dec 2025 22:08:26 +0100 Subject: [PATCH 2/3] protect profile deletion --- .../securesms/ConversationListActivity.java | 33 +++++++++++++++++++ .../AccountSelectionListFragment.java | 31 +++++------------ .../securesms/connect/AccountManager.java | 4 +-- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationListActivity.java b/src/main/java/org/thoughtcrime/securesms/ConversationListActivity.java index 2b980ff42..dd202b3ad 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationListActivity.java @@ -96,6 +96,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit public static final String CLEAR_NOTIFICATIONS = "clear_notifications"; public static final String ACCOUNT_ID_EXTRA = "account_id"; public static final String FROM_WELCOME = "from_welcome"; + private static final int REQUEST_CODE_CONFIRM_CREDENTIALS_DELETE_PROFILE = ScreenLockUtil.REQUEST_CODE_CONFIRM_CREDENTIALS+1; private ConversationListFragment conversationListFragment; public TextView title; @@ -109,6 +110,8 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit /** used to store temporarily scanned QR to pass it back to QrCodeHandler when ScreenLockUtil is used */ private String qrData = null; + /** used to store temporarily profile ID to delete after authorization is granted via ScreenLockUtil */ + private int deleteProfileId = 0; @Override protected void onPreCreate() { @@ -570,6 +573,30 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit startActivity(Intent.createChooser(intent, getString(R.string.chat_share_with_title))); } + public void onDeleteProfile(int profileId) { + deleteProfileId = profileId; + boolean result = ScreenLockUtil.applyScreenLock(this, getString(R.string.delete_account), getString(R.string.enter_system_secret_to_continue), REQUEST_CODE_CONFIRM_CREDENTIALS_DELETE_PROFILE); + if (!result) { + deleteProfile(profileId); + } + } + + private void deleteProfile(int profileId) { + DcAccounts accounts = DcHelper.getAccounts(this); + boolean selected = profileId == accounts.getSelectedAccount().getAccountId(); + DcHelper.getNotificationCenter(this).removeAllNotifications(profileId); + accounts.removeAccount(profileId); + if (selected) { + DcContext selAcc = accounts.getSelectedAccount(); + AccountManager.getInstance().switchAccountAndStartActivity(this, selAcc.isOk()? selAcc.getAccountId() : 0); + } else { + AccountManager.getInstance().showSwitchAccountMenu(this); + } + + // title update needed to show "Delta Chat" in case there is only one profile left + refreshTitle(); + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); @@ -590,6 +617,12 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit qrData = null; } break; + case REQUEST_CODE_CONFIRM_CREDENTIALS_DELETE_PROFILE: + if (deleteProfileId != 0) { + deleteProfile(deleteProfileId); + deleteProfileId = 0; + } + break; default: break; } diff --git a/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java b/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java index 012807a31..c5e4bcf0b 100644 --- a/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java @@ -47,9 +47,15 @@ import chat.delta.rpc.RpcException; public class AccountSelectionListFragment extends DialogFragment implements DcEventCenter.DcEventDelegate { private static final String TAG = AccountSelectionListFragment.class.getSimpleName(); + private final ConversationListActivity activity; private RecyclerView recyclerView; private AccountSelectionListAdapter adapter; + public AccountSelectionListFragment(ConversationListActivity activity) { + super(); + this.activity = activity; + } + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { @@ -132,7 +138,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv private void onContextItemSelected(MenuItem item, int accountId) { int itemId = item.getItemId(); if (itemId == R.id.delete) { - onDeleteAccount(accountId); + onDeleteProfile(accountId); } else if (itemId == R.id.menu_mute_notifications) { onToggleMute(accountId); } else if (itemId == R.id.menu_set_tag) { @@ -167,8 +173,6 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv } private void onSetTag(int accountId) { - Activity activity = getActivity(); - if (activity == null) return; AccountSelectionListFragment.this.dismiss(); DcContext dcContext = DcHelper.getAccounts(activity).getAccount(accountId); @@ -190,10 +194,8 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv .show(); } - private void onDeleteAccount(int accountId) { - Activity activity = getActivity(); + private void onDeleteProfile(int accountId) { AccountSelectionListFragment.this.dismiss(); - if (activity == null) return; DcAccounts accounts = DcHelper.getAccounts(activity); Rpc rpc = DcHelper.getRpc(activity); @@ -229,22 +231,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv .setTitle(R.string.delete_account) .setView(dialogView) .setNegativeButton(R.string.cancel, (d, which) -> AccountManager.getInstance().showSwitchAccountMenu(activity)) - .setPositiveButton(R.string.delete, (d2, which2) -> { - boolean selected = accountId == accounts.getSelectedAccount().getAccountId(); - DcHelper.getNotificationCenter(activity).removeAllNotifications(accountId); - accounts.removeAccount(accountId); - if (selected) { - DcContext selAcc = accounts.getSelectedAccount(); - AccountManager.getInstance().switchAccountAndStartActivity(activity, selAcc.isOk()? selAcc.getAccountId() : 0); - } else { - AccountManager.getInstance().showSwitchAccountMenu(activity); - } - - // title update needed to show "Delta Chat" in case there is only one profile left - if (activity instanceof ConversationListActivity) { - ((ConversationListActivity)activity).refreshTitle(); - } - }) + .setPositiveButton(R.string.delete, (d2, w2) -> activity.onDeleteProfile(accountId)) .show(); Util.redPositiveButton(dialog); } diff --git a/src/main/java/org/thoughtcrime/securesms/connect/AccountManager.java b/src/main/java/org/thoughtcrime/securesms/connect/AccountManager.java index 7c5268dcb..5763089d6 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/AccountManager.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/AccountManager.java @@ -147,8 +147,8 @@ public class AccountManager { // ui - public void showSwitchAccountMenu(Activity activity) { - AccountSelectionListFragment dialog = new AccountSelectionListFragment(); + public void showSwitchAccountMenu(ConversationListActivity activity) { + AccountSelectionListFragment dialog = new AccountSelectionListFragment(activity); dialog.show(((FragmentActivity) activity).getSupportFragmentManager(), null); } From 56c00a7c43b38e8deee88cb9146dce4e22f91084 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 18 Dec 2025 22:11:54 +0100 Subject: [PATCH 3/3] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e890a223b..c53255540 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased * Remove deprecated "real-time apps" switch +* Protect profile deletion and relays management with system lock/pin ## v2.34.0 2025-12