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}