1package eu.siacs.conversations.ui;
  2
  3import android.content.Intent;
  4import android.os.Bundle;
  5import android.util.Log;
  6import android.view.Gravity;
  7import android.view.Menu;
  8import android.view.MenuItem;
  9import android.view.View;
 10import android.view.View.OnClickListener;
 11import android.widget.Toast;
 12import androidx.appcompat.app.ActionBar;
 13import androidx.databinding.DataBindingUtil;
 14import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 15import eu.siacs.conversations.Config;
 16import eu.siacs.conversations.R;
 17import eu.siacs.conversations.crypto.OmemoSetting;
 18import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 19import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 20import eu.siacs.conversations.databinding.ActivityTrustKeysBinding;
 21import eu.siacs.conversations.databinding.KeysCardBinding;
 22import eu.siacs.conversations.entities.Account;
 23import eu.siacs.conversations.entities.Contact;
 24import eu.siacs.conversations.entities.Conversation;
 25import eu.siacs.conversations.entities.Message;
 26import eu.siacs.conversations.utils.CryptoHelper;
 27import eu.siacs.conversations.utils.IrregularUnicodeDetector;
 28import eu.siacs.conversations.utils.XmppUri;
 29import eu.siacs.conversations.xmpp.Jid;
 30import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 31import java.util.ArrayList;
 32import java.util.HashMap;
 33import java.util.List;
 34import java.util.Map;
 35import java.util.Set;
 36import java.util.concurrent.atomic.AtomicBoolean;
 37import org.whispersystems.libsignal.IdentityKey;
 38
 39public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdated {
 40    private final Map<String, Boolean> ownKeysToTrust = new HashMap<>();
 41    private final Map<Jid, Map<String, Boolean>> foreignKeysToTrust = new HashMap<>();
 42    private final OnClickListener mCancelButtonListener =
 43            v -> {
 44                setResult(RESULT_CANCELED);
 45                finish();
 46            };
 47    private List<Jid> contactJids;
 48    private Account mAccount;
 49    private Conversation mConversation;
 50    private final OnClickListener mSaveButtonListener =
 51            v -> {
 52                commitTrusts();
 53                finishOk(false);
 54            };
 55    private final AtomicBoolean mUseCameraHintShown = new AtomicBoolean(false);
 56    private AxolotlService.FetchStatus lastFetchReport = AxolotlService.FetchStatus.SUCCESS;
 57    private Toast mUseCameraHintToast = null;
 58    private ActivityTrustKeysBinding binding;
 59
 60    @Override
 61    protected void refreshUiReal() {
 62        invalidateOptionsMenu();
 63        populateView();
 64    }
 65
 66    @Override
 67    protected void onCreate(final Bundle savedInstanceState) {
 68        super.onCreate(savedInstanceState);
 69        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_trust_keys);
 70        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
 71        this.contactJids = new ArrayList<>();
 72        final var intent = getIntent();
 73        final String[] contacts = intent == null ? null : intent.getStringArrayExtra("contacts");
 74        for (final String jid : (contacts == null ? new String[0] : contacts)) {
 75            try {
 76                this.contactJids.add(Jid.of(jid));
 77            } catch (final IllegalArgumentException ignored) {
 78            }
 79        }
 80
 81        binding.cancelButton.setOnClickListener(mCancelButtonListener);
 82        binding.saveButton.setOnClickListener(mSaveButtonListener);
 83
 84        setSupportActionBar(binding.toolbar);
 85        configureActionBar(getSupportActionBar());
 86
 87        if (savedInstanceState != null) {
 88            mUseCameraHintShown.set(savedInstanceState.getBoolean("camera_hint_shown", false));
 89        }
 90    }
 91
 92    @Override
 93    public void onSaveInstanceState(Bundle savedInstanceState) {
 94        savedInstanceState.putBoolean("camera_hint_shown", mUseCameraHintShown.get());
 95        super.onSaveInstanceState(savedInstanceState);
 96    }
 97
 98    @Override
 99    public boolean onCreateOptionsMenu(Menu menu) {
100        getMenuInflater().inflate(R.menu.trust_keys, menu);
101        MenuItem scanQrCode = menu.findItem(R.id.action_scan_qr_code);
102        scanQrCode.setVisible(
103                (!ownKeysToTrust.isEmpty() || foreignActuallyHasKeys())
104                        && isCameraFeatureAvailable());
105        return super.onCreateOptionsMenu(menu);
106    }
107
108    private void showCameraToast() {
109        mUseCameraHintToast =
110                Toast.makeText(this, R.string.use_camera_icon_to_scan_barcode, Toast.LENGTH_LONG);
111        ActionBar actionBar = getSupportActionBar();
112        mUseCameraHintToast.setGravity(
113                Gravity.TOP | Gravity.END, 0, actionBar == null ? 0 : actionBar.getHeight());
114        mUseCameraHintToast.show();
115    }
116
117    @Override
118    public boolean onOptionsItemSelected(MenuItem item) {
119        switch (item.getItemId()) {
120            case R.id.action_scan_qr_code:
121                if (hasPendingKeyFetches()) {
122                    Toast.makeText(
123                                    this,
124                                    R.string.please_wait_for_keys_to_be_fetched,
125                                    Toast.LENGTH_SHORT)
126                            .show();
127                } else {
128                    ScanActivity.scan(this);
129                    // new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE"));
130                    return true;
131                }
132        }
133        return super.onOptionsItemSelected(item);
134    }
135
136    @Override
137    protected void onStop() {
138        super.onStop();
139        if (mUseCameraHintToast != null) {
140            mUseCameraHintToast.cancel();
141        }
142    }
143
144    @Override
145    protected void processFingerprintVerification(XmppUri uri) {
146        if (mConversation != null
147                && mAccount != null
148                && uri.hasFingerprints()
149                && mAccount.getAxolotlService()
150                        .getCryptoTargets(mConversation)
151                        .contains(uri.getJid())) {
152            boolean performedVerification =
153                    xmppConnectionService.verifyFingerprints(
154                            mAccount.getRoster().getContact(uri.getJid()), uri.getFingerprints());
155            boolean keys = reloadFingerprints();
156            if (performedVerification
157                    && !keys
158                    && !hasNoOtherTrustedKeys()
159                    && !hasPendingKeyFetches()) {
160                Toast.makeText(this, R.string.all_omemo_keys_have_been_verified, Toast.LENGTH_SHORT)
161                        .show();
162                finishOk(false);
163                return;
164            } else if (performedVerification) {
165                Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
166            }
167        } else {
168            reloadFingerprints();
169            Log.d(
170                    Config.LOGTAG,
171                    "xmpp uri was: "
172                            + uri.getJid()
173                            + " has Fingerprints: "
174                            + uri.hasFingerprints());
175            Toast.makeText(
176                            this,
177                            R.string.barcode_does_not_contain_fingerprints_for_this_chat,
178                            Toast.LENGTH_SHORT)
179                    .show();
180        }
181        populateView();
182    }
183
184    private void populateView() {
185        setTitle(getString(R.string.trust_omemo_fingerprints));
186        binding.ownKeysDetails.removeAllViews();
187        binding.foreignKeys.removeAllViews();
188        boolean hasOwnKeys = false;
189        boolean hasForeignKeys = false;
190        for (final String fingerprint : ownKeysToTrust.keySet()) {
191            hasOwnKeys = true;
192            addFingerprintRowWithListeners(
193                    binding.ownKeysDetails,
194                    mAccount,
195                    fingerprint,
196                    false,
197                    FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint)),
198                    false,
199                    false,
200                    (buttonView, isChecked) -> {
201                        ownKeysToTrust.put(fingerprint, isChecked);
202                        // own fingerprints have no impact on locked status.
203                    });
204        }
205
206        synchronized (this.foreignKeysToTrust) {
207            for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
208                hasForeignKeys = true;
209                KeysCardBinding keysCardBinding =
210                        DataBindingUtil.inflate(
211                                getLayoutInflater(),
212                                R.layout.keys_card,
213                                binding.foreignKeys,
214                                false);
215                final Jid jid = entry.getKey();
216                keysCardBinding.foreignKeysTitle.setText(IrregularUnicodeDetector.style(this, jid));
217                keysCardBinding.foreignKeysTitle.setOnClickListener(
218                        v -> switchToContactDetails(mAccount.getRoster().getContact(jid)));
219                final Map<String, Boolean> fingerprints = entry.getValue();
220                for (final String fingerprint : fingerprints.keySet()) {
221                    addFingerprintRowWithListeners(
222                            keysCardBinding.foreignKeysDetails,
223                            mAccount,
224                            fingerprint,
225                            false,
226                            FingerprintStatus.createActive(fingerprints.get(fingerprint)),
227                            false,
228                            false,
229                            (buttonView, isChecked) -> {
230                                fingerprints.put(fingerprint, isChecked);
231                                lockOrUnlockAsNeeded();
232                            });
233                }
234                if (fingerprints.isEmpty()) {
235                    keysCardBinding.noKeysToAccept.setVisibility(View.VISIBLE);
236                    if (hasNoOtherTrustedKeys(jid)) {
237                        if (!mAccount.getRoster().getContact(jid).mutualPresenceSubscription()) {
238                            keysCardBinding.noKeysToAccept.setText(
239                                    R.string.error_no_keys_to_trust_presence);
240                        } else {
241                            keysCardBinding.noKeysToAccept.setText(
242                                    R.string.error_no_keys_to_trust_server_error);
243                        }
244                    } else {
245                        keysCardBinding.noKeysToAccept.setText(
246                                getString(
247                                        R.string.no_keys_just_confirm,
248                                        mAccount.getRoster().getContact(jid).getDisplayName()));
249                    }
250                } else {
251                    keysCardBinding.noKeysToAccept.setVisibility(View.GONE);
252                }
253                binding.foreignKeys.addView(keysCardBinding.foreignKeysCard);
254            }
255        }
256
257        if ((hasOwnKeys || foreignActuallyHasKeys())
258                && isCameraFeatureAvailable()
259                && mUseCameraHintShown.compareAndSet(false, true)) {
260            showCameraToast();
261        }
262
263        binding.ownKeysTitle.setText(mAccount.getJid().asBareJid().toString());
264        binding.ownKeysCard.setVisibility(hasOwnKeys ? View.VISIBLE : View.GONE);
265        binding.foreignKeys.setVisibility(hasForeignKeys ? View.VISIBLE : View.GONE);
266        if (hasPendingKeyFetches()) {
267            setFetching();
268            lock();
269        } else {
270            if (!hasForeignKeys && hasNoOtherTrustedKeys()) {
271                binding.keyErrorMessageCard.setVisibility(View.VISIBLE);
272                boolean lastReportWasError = lastFetchReport == AxolotlService.FetchStatus.ERROR;
273                boolean errorFetchingBundle =
274                        mAccount.getAxolotlService().fetchMapHasErrors(contactJids);
275                boolean errorFetchingDeviceList =
276                        mAccount.getAxolotlService().hasErrorFetchingDeviceList(contactJids);
277                boolean anyWithoutMutualPresenceSubscription =
278                        anyWithoutMutualPresenceSubscription(contactJids);
279                if (errorFetchingDeviceList) {
280                    binding.keyErrorMessage.setVisibility(View.VISIBLE);
281                    binding.keyErrorMessage.setText(R.string.error_trustkey_device_list);
282                } else if (errorFetchingBundle || lastReportWasError) {
283                    binding.keyErrorMessage.setVisibility(View.VISIBLE);
284                    binding.keyErrorMessage.setText(R.string.error_trustkey_bundle);
285                } else {
286                    binding.keyErrorMessage.setVisibility(View.GONE);
287                }
288                this.binding.keyErrorHintMutual.setVisibility(
289                        anyWithoutMutualPresenceSubscription ? View.VISIBLE : View.GONE);
290                Contact contact = mAccount.getRoster().getContact(contactJids.get(0));
291                binding.keyErrorGeneral.setText(
292                        getString(
293                                R.string.error_trustkey_general,
294                                getString(R.string.app_name),
295                                contact.getDisplayName()));
296                binding.ownKeysDetails.removeAllViews();
297                if (OmemoSetting.isAlways()) {
298                    binding.disableButton.setVisibility(View.GONE);
299                } else {
300                    binding.disableButton.setVisibility(View.VISIBLE);
301                    binding.disableButton.setOnClickListener(this::disableEncryptionDialog);
302                }
303                binding.ownKeysCard.setVisibility(View.GONE);
304                binding.foreignKeys.removeAllViews();
305                binding.foreignKeys.setVisibility(View.GONE);
306            }
307            lockOrUnlockAsNeeded();
308            setDone();
309        }
310    }
311
312    private void disableEncryptionDialog(final View view) {
313        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
314        builder.setTitle(R.string.disable_encryption);
315        builder.setMessage(R.string.disable_encryption_message);
316        builder.setPositiveButton(
317                R.string.disable_now,
318                (dialog, which) -> {
319                    mConversation.setNextEncryption(Message.ENCRYPTION_NONE);
320                    xmppConnectionService.updateConversation(mConversation);
321                    finishOk(true);
322                });
323        builder.setNegativeButton(R.string.cancel, null);
324        builder.create().show();
325    }
326
327    private boolean anyWithoutMutualPresenceSubscription(List<Jid> contactJids) {
328        for (Jid jid : contactJids) {
329            if (!mAccount.getRoster().getContact(jid).mutualPresenceSubscription()) {
330                return true;
331            }
332        }
333        return false;
334    }
335
336    private boolean foreignActuallyHasKeys() {
337        synchronized (this.foreignKeysToTrust) {
338            for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
339                if (!entry.getValue().isEmpty()) {
340                    return true;
341                }
342            }
343        }
344        return false;
345    }
346
347    private boolean reloadFingerprints() {
348        List<Jid> acceptedTargets =
349                mConversation == null
350                        ? new ArrayList<>()
351                        : mConversation.getAcceptedCryptoTargets();
352        ownKeysToTrust.clear();
353        if (this.mAccount == null) {
354            return false;
355        }
356        AxolotlService service = this.mAccount.getAxolotlService();
357        Set<IdentityKey> ownKeysSet =
358                service.getKeysWithTrust(FingerprintStatus.createActiveUndecided());
359        for (final IdentityKey identityKey : ownKeysSet) {
360            final String fingerprint =
361                    CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize());
362            if (!ownKeysToTrust.containsKey(fingerprint)) {
363                ownKeysToTrust.put(fingerprint, false);
364            }
365        }
366        synchronized (this.foreignKeysToTrust) {
367            foreignKeysToTrust.clear();
368            for (Jid jid : contactJids) {
369                Set<IdentityKey> foreignKeysSet =
370                        service.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), jid);
371                if (hasNoOtherTrustedKeys(jid) && ownKeysSet.isEmpty()) {
372                    foreignKeysSet.addAll(
373                            service.getKeysWithTrust(FingerprintStatus.createActive(false), jid));
374                }
375                Map<String, Boolean> foreignFingerprints = new HashMap<>();
376                for (final IdentityKey identityKey : foreignKeysSet) {
377                    final String fingerprint =
378                            CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize());
379                    if (!foreignFingerprints.containsKey(fingerprint)) {
380                        foreignFingerprints.put(fingerprint, false);
381                    }
382                }
383                if (!foreignFingerprints.isEmpty() || !acceptedTargets.contains(jid)) {
384                    foreignKeysToTrust.put(jid, foreignFingerprints);
385                }
386            }
387        }
388        return ownKeysSet.size() + foreignKeysToTrust.size() > 0;
389    }
390
391    public void onBackendConnected() {
392        Intent intent = getIntent();
393        this.mAccount = extractAccount(intent);
394        if (this.mAccount != null && intent != null) {
395            String uuid = intent.getStringExtra("conversation");
396            this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
397            if (this.mPendingFingerprintVerificationUri != null) {
398                processFingerprintVerification(this.mPendingFingerprintVerificationUri);
399                this.mPendingFingerprintVerificationUri = null;
400            } else {
401                final boolean keysToTrust = reloadFingerprints();
402                if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) {
403                    populateView();
404                    invalidateOptionsMenu();
405                } else {
406                    finishOk(false);
407                }
408            }
409        }
410    }
411
412    private boolean hasNoOtherTrustedKeys() {
413        return mAccount == null
414                || mAccount.getAxolotlService().anyTargetHasNoTrustedKeys(contactJids);
415    }
416
417    private boolean hasNoOtherTrustedKeys(Jid contact) {
418        return mAccount == null || mAccount.getAxolotlService().getNumTrustedKeys(contact) == 0;
419    }
420
421    private boolean hasPendingKeyFetches() {
422        return mAccount != null && mAccount.getAxolotlService().hasPendingKeyFetches(contactJids);
423    }
424
425    @Override
426    public void onKeyStatusUpdated(final AxolotlService.FetchStatus report) {
427        final boolean keysToTrust = reloadFingerprints();
428        if (report != null) {
429            lastFetchReport = report;
430            runOnUiThread(
431                    () -> {
432                        if (mUseCameraHintToast != null && !keysToTrust) {
433                            mUseCameraHintToast.cancel();
434                        }
435                        switch (report) {
436                            case ERROR:
437                                Toast.makeText(
438                                                TrustKeysActivity.this,
439                                                R.string.error_fetching_omemo_key,
440                                                Toast.LENGTH_SHORT)
441                                        .show();
442                                break;
443                            case SUCCESS_TRUSTED:
444                                Toast.makeText(
445                                                TrustKeysActivity.this,
446                                                R.string.blindly_trusted_omemo_keys,
447                                                Toast.LENGTH_LONG)
448                                        .show();
449                                break;
450                            case SUCCESS_VERIFIED:
451                                Toast.makeText(
452                                                TrustKeysActivity.this,
453                                                Config.X509_VERIFICATION
454                                                        ? R.string
455                                                                .verified_omemo_key_with_certificate
456                                                        : R.string
457                                                                .all_omemo_keys_have_been_verified,
458                                                Toast.LENGTH_LONG)
459                                        .show();
460                                break;
461                        }
462                    });
463        }
464        if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) {
465            refreshUi();
466        } else {
467            runOnUiThread(() -> finishOk(false));
468        }
469    }
470
471    private void finishOk(boolean disabled) {
472        Intent data = new Intent();
473        data.putExtra(
474                "choice",
475                getIntent().getIntExtra("choice", ConversationFragment.ATTACHMENT_CHOICE_INVALID));
476        data.putExtra("disabled", disabled);
477        setResult(RESULT_OK, data);
478        finish();
479    }
480
481    private void commitTrusts() {
482        for (final String fingerprint : ownKeysToTrust.keySet()) {
483            mAccount.getAxolotlService()
484                    .setFingerprintTrust(
485                            fingerprint,
486                            FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint)));
487        }
488        List<Jid> acceptedTargets =
489                mConversation == null
490                        ? new ArrayList<>()
491                        : mConversation.getAcceptedCryptoTargets();
492        synchronized (this.foreignKeysToTrust) {
493            for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
494                Jid jid = entry.getKey();
495                Map<String, Boolean> value = entry.getValue();
496                if (!acceptedTargets.contains(jid)) {
497                    acceptedTargets.add(jid);
498                }
499                for (final String fingerprint : value.keySet()) {
500                    mAccount.getAxolotlService()
501                            .setFingerprintTrust(
502                                    fingerprint,
503                                    FingerprintStatus.createActive(value.get(fingerprint)));
504                }
505            }
506        }
507        if (mConversation != null && mConversation.getMode() == Conversation.MODE_MULTI) {
508            mConversation.setAcceptedCryptoTargets(acceptedTargets);
509            xmppConnectionService.updateConversation(mConversation);
510        }
511    }
512
513    private void unlock() {
514        binding.saveButton.setEnabled(true);
515    }
516
517    private void lock() {
518        binding.saveButton.setEnabled(false);
519    }
520
521    private void lockOrUnlockAsNeeded() {
522        synchronized (this.foreignKeysToTrust) {
523            for (Jid jid : contactJids) {
524                Map<String, Boolean> fingerprints = foreignKeysToTrust.get(jid);
525                if (hasNoOtherTrustedKeys(jid)
526                        && (fingerprints == null || !fingerprints.containsValue(true))) {
527                    lock();
528                    return;
529                }
530            }
531        }
532        unlock();
533    }
534
535    private void setDone() {
536        binding.saveButton.setText(getString(R.string.done));
537    }
538
539    private void setFetching() {
540        binding.saveButton.setText(getString(R.string.fetching_keys));
541    }
542}