TrustKeysActivity.java

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