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		if (this.mAccount == null) {
165			return;
166		}
167
168		setTitle(getString(R.string.trust_omemo_fingerprints));
169		binding.ownKeysDetails.removeAllViews();
170		binding.foreignKeys.removeAllViews();
171		boolean hasOwnKeys = false;
172		boolean hasForeignKeys = false;
173		for (final String fingerprint : ownKeysToTrust.keySet()) {
174			hasOwnKeys = true;
175			addFingerprintRowWithListeners(binding.ownKeysDetails, mAccount, fingerprint, false,
176					FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint)), false, false,
177					(buttonView, isChecked) -> {
178						ownKeysToTrust.put(fingerprint, isChecked);
179						// own fingerprints have no impact on locked status.
180					}
181			);
182		}
183
184		synchronized (this.foreignKeysToTrust) {
185			for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
186				hasForeignKeys = true;
187				KeysCardBinding keysCardBinding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.keys_card, binding.foreignKeys, false);
188				final Jid jid = entry.getKey();
189				keysCardBinding.foreignKeysTitle.setText(IrregularUnicodeDetector.style(this, jid));
190				keysCardBinding.foreignKeysTitle.setOnClickListener(v -> switchToContactDetails(mAccount.getRoster().getContact(jid)));
191				final Map<String, Boolean> fingerprints = entry.getValue();
192				for (final String fingerprint : fingerprints.keySet()) {
193					addFingerprintRowWithListeners(keysCardBinding.foreignKeysDetails, mAccount, fingerprint, false,
194							FingerprintStatus.createActive(fingerprints.get(fingerprint)), false, false,
195							(buttonView, isChecked) -> {
196								fingerprints.put(fingerprint, isChecked);
197								lockOrUnlockAsNeeded();
198							}
199					);
200				}
201				if (fingerprints.isEmpty()) {
202					keysCardBinding.noKeysToAccept.setVisibility(View.VISIBLE);
203					if (hasNoOtherTrustedKeys(jid)) {
204						if (!mAccount.getRoster().getContact(jid).mutualPresenceSubscription()) {
205							keysCardBinding.noKeysToAccept.setText(R.string.error_no_keys_to_trust_presence);
206						} else {
207							keysCardBinding.noKeysToAccept.setText(R.string.error_no_keys_to_trust_server_error);
208						}
209					} else {
210						keysCardBinding.noKeysToAccept.setText(getString(R.string.no_keys_just_confirm, mAccount.getRoster().getContact(jid).getDisplayName()));
211					}
212				} else {
213					keysCardBinding.noKeysToAccept.setVisibility(View.GONE);
214				}
215				binding.foreignKeys.addView(keysCardBinding.foreignKeysCard);
216			}
217		}
218
219		if ((hasOwnKeys || foreignActuallyHasKeys()) && isCameraFeatureAvailable() && mUseCameraHintShown.compareAndSet(false, true)) {
220			showCameraToast();
221		}
222
223		binding.ownKeysTitle.setText(mAccount.getJid().asBareJid().toEscapedString());
224		binding.ownKeysCard.setVisibility(hasOwnKeys ? View.VISIBLE : View.GONE);
225		binding.foreignKeys.setVisibility(hasForeignKeys ? View.VISIBLE : View.GONE);
226		if (hasPendingKeyFetches()) {
227			setFetching();
228			lock();
229		} else {
230			if (!hasForeignKeys && hasNoOtherTrustedKeys()) {
231				binding.keyErrorMessageCard.setVisibility(View.VISIBLE);
232				boolean lastReportWasError = lastFetchReport == AxolotlService.FetchStatus.ERROR;
233				boolean errorFetchingBundle = mAccount.getAxolotlService().fetchMapHasErrors(contactJids);
234				boolean errorFetchingDeviceList = mAccount.getAxolotlService().hasErrorFetchingDeviceList(contactJids);
235				boolean anyWithoutMutualPresenceSubscription = anyWithoutMutualPresenceSubscription(contactJids);
236				if (errorFetchingDeviceList) {
237					binding.keyErrorMessage.setVisibility(View.VISIBLE);
238					binding.keyErrorMessage.setText(R.string.error_trustkey_device_list);
239				} else if (errorFetchingBundle || lastReportWasError) {
240					binding.keyErrorMessage.setVisibility(View.VISIBLE);
241					binding.keyErrorMessage.setText(R.string.error_trustkey_bundle);
242				} else {
243					binding.keyErrorMessage.setVisibility(View.GONE);
244				}
245				this.binding.keyErrorHintMutual.setVisibility(anyWithoutMutualPresenceSubscription ? View.VISIBLE : View.GONE);
246				Contact contact = mAccount.getRoster().getContact(contactJids.get(0));
247				binding.keyErrorGeneral.setText(getString(R.string.error_trustkey_general, getString(R.string.app_name), contact.getDisplayName()));
248				binding.ownKeysDetails.removeAllViews();
249				if (OmemoSetting.isAlways()) {
250					binding.disableButton.setVisibility(View.GONE);
251				} else {
252					binding.disableButton.setVisibility(View.VISIBLE);
253					binding.disableButton.setOnClickListener(this::disableEncryptionDialog);
254				}
255				binding.ownKeysCard.setVisibility(View.GONE);
256				binding.foreignKeys.removeAllViews();
257				binding.foreignKeys.setVisibility(View.GONE);
258			}
259			lockOrUnlockAsNeeded();
260			setDone();
261		}
262	}
263
264	private void disableEncryptionDialog(final View view) {
265		final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
266		builder.setTitle(R.string.disable_encryption);
267		builder.setMessage(R.string.disable_encryption_message);
268		builder.setPositiveButton(R.string.disable_now, (dialog, which) -> {
269			mConversation.setNextEncryption(Message.ENCRYPTION_NONE);
270			xmppConnectionService.updateConversation(mConversation);
271			finishOk(true);
272		});
273		builder.setNegativeButton(R.string.cancel, null);
274		builder.create().show();
275	}
276
277	private boolean anyWithoutMutualPresenceSubscription(List<Jid> contactJids) {
278		for (Jid jid : contactJids) {
279			if (!mAccount.getRoster().getContact(jid).mutualPresenceSubscription()) {
280				return true;
281			}
282		}
283		return false;
284	}
285
286	private boolean foreignActuallyHasKeys() {
287		synchronized (this.foreignKeysToTrust) {
288			for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
289				if (!entry.getValue().isEmpty()) {
290					return true;
291				}
292			}
293		}
294		return false;
295	}
296
297	private boolean reloadFingerprints() {
298		List<Jid> acceptedTargets = mConversation == null ? new ArrayList<>() : mConversation.getAcceptedCryptoTargets();
299		ownKeysToTrust.clear();
300		if (this.mAccount == null) {
301			return false;
302		}
303		AxolotlService service = this.mAccount.getAxolotlService();
304		Set<IdentityKey> ownKeysSet = service.getKeysWithTrust(FingerprintStatus.createActiveUndecided());
305		for (final IdentityKey identityKey : ownKeysSet) {
306			final String fingerprint = CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize());
307			if (!ownKeysToTrust.containsKey(fingerprint)) {
308				ownKeysToTrust.put(fingerprint, false);
309			}
310		}
311		synchronized (this.foreignKeysToTrust) {
312			foreignKeysToTrust.clear();
313			for (Jid jid : contactJids) {
314				Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), jid);
315				if (hasNoOtherTrustedKeys(jid) && ownKeysSet.isEmpty()) {
316					foreignKeysSet.addAll(service.getKeysWithTrust(FingerprintStatus.createActive(false), jid));
317				}
318				Map<String, Boolean> foreignFingerprints = new HashMap<>();
319				for (final IdentityKey identityKey : foreignKeysSet) {
320					final String fingerprint = CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize());
321					if (!foreignFingerprints.containsKey(fingerprint)) {
322						foreignFingerprints.put(fingerprint, false);
323					}
324				}
325				if (!foreignFingerprints.isEmpty() || !acceptedTargets.contains(jid)) {
326					foreignKeysToTrust.put(jid, foreignFingerprints);
327				}
328			}
329		}
330		return ownKeysSet.size() + foreignKeysToTrust.size() > 0;
331	}
332
333	public void onBackendConnected() {
334		Intent intent = getIntent();
335		this.mAccount = extractAccount(intent);
336		if (this.mAccount != null && intent != null) {
337			String uuid = intent.getStringExtra("conversation");
338			this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
339			if (this.mPendingFingerprintVerificationUri != null) {
340				processFingerprintVerification(this.mPendingFingerprintVerificationUri);
341				this.mPendingFingerprintVerificationUri = null;
342			} else {
343				final boolean keysToTrust = reloadFingerprints();
344				if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) {
345					populateView();
346					invalidateOptionsMenu();
347				} else {
348					finishOk(false);
349				}
350			}
351		}
352	}
353
354	private boolean hasNoOtherTrustedKeys() {
355		return mAccount == null || mAccount.getAxolotlService().anyTargetHasNoTrustedKeys(contactJids);
356	}
357
358	private boolean hasNoOtherTrustedKeys(Jid contact) {
359		return mAccount == null || mAccount.getAxolotlService().getNumTrustedKeys(contact) == 0;
360	}
361
362	private boolean hasPendingKeyFetches() {
363		return mAccount != null && mAccount.getAxolotlService().hasPendingKeyFetches(contactJids);
364	}
365
366
367	@Override
368	public void onKeyStatusUpdated(final AxolotlService.FetchStatus report) {
369		final boolean keysToTrust = reloadFingerprints();
370		if (report != null) {
371			lastFetchReport = report;
372			runOnUiThread(() -> {
373				if (mUseCameraHintToast != null && !keysToTrust) {
374					mUseCameraHintToast.cancel();
375				}
376				switch (report) {
377					case ERROR:
378						Toast.makeText(TrustKeysActivity.this, R.string.error_fetching_omemo_key, Toast.LENGTH_SHORT).show();
379						break;
380					case SUCCESS_TRUSTED:
381						Toast.makeText(TrustKeysActivity.this, R.string.blindly_trusted_omemo_keys, Toast.LENGTH_LONG).show();
382						break;
383					case SUCCESS_VERIFIED:
384						Toast.makeText(TrustKeysActivity.this,
385								Config.X509_VERIFICATION ? R.string.verified_omemo_key_with_certificate : R.string.all_omemo_keys_have_been_verified,
386								Toast.LENGTH_LONG).show();
387						break;
388				}
389			});
390
391		}
392		if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) {
393			refreshUi();
394		} else {
395			runOnUiThread(() -> finishOk(false));
396
397		}
398	}
399
400	private void finishOk(boolean disabled) {
401		Intent data = new Intent();
402		data.putExtra("choice", getIntent().getIntExtra("choice", ConversationFragment.ATTACHMENT_CHOICE_INVALID));
403		data.putExtra("disabled", disabled);
404		setResult(RESULT_OK, data);
405		finish();
406	}
407
408	private void commitTrusts() {
409		for (final String fingerprint : ownKeysToTrust.keySet()) {
410			mAccount.getAxolotlService().setFingerprintTrust(
411					fingerprint,
412					FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint)));
413		}
414		List<Jid> acceptedTargets = mConversation == null ? new ArrayList<>() : mConversation.getAcceptedCryptoTargets();
415		synchronized (this.foreignKeysToTrust) {
416			for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
417				Jid jid = entry.getKey();
418				Map<String, Boolean> value = entry.getValue();
419				if (!acceptedTargets.contains(jid)) {
420					acceptedTargets.add(jid);
421				}
422				for (final String fingerprint : value.keySet()) {
423					mAccount.getAxolotlService().setFingerprintTrust(
424							fingerprint,
425							FingerprintStatus.createActive(value.get(fingerprint)));
426				}
427			}
428		}
429		if (mConversation != null && mConversation.getMode() == Conversation.MODE_MULTI) {
430			mConversation.setAcceptedCryptoTargets(acceptedTargets);
431			xmppConnectionService.updateConversation(mConversation);
432		}
433	}
434
435	private void unlock() {
436		binding.saveButton.setEnabled(true);
437	}
438
439	private void lock() {
440		binding.saveButton.setEnabled(false);
441	}
442
443	private void lockOrUnlockAsNeeded() {
444		synchronized (this.foreignKeysToTrust) {
445			for (Jid jid : contactJids) {
446				Map<String, Boolean> fingerprints = foreignKeysToTrust.get(jid);
447				if (hasNoOtherTrustedKeys(jid) && (fingerprints == null || !fingerprints.containsValue(true))) {
448					lock();
449					return;
450				}
451			}
452		}
453		unlock();
454
455	}
456
457	private void setDone() {
458		binding.saveButton.setText(getString(R.string.done));
459	}
460
461	private void setFetching() {
462		binding.saveButton.setText(getString(R.string.fetching_keys));
463	}
464}