TrustKeysActivity.java

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