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