TrustKeysActivity.java

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