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