TrustKeysActivity.java

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