TrustKeysActivity.java

  1package eu.siacs.conversations.ui;
  2
  3import android.content.Intent;
  4import android.os.Bundle;
  5import android.view.View;
  6import android.view.View.OnClickListener;
  7import android.widget.Button;
  8import android.widget.CompoundButton;
  9import android.widget.LinearLayout;
 10import android.widget.TextView;
 11import android.widget.Toast;
 12
 13import org.whispersystems.libaxolotl.IdentityKey;
 14
 15import java.util.ArrayList;
 16import java.util.HashMap;
 17import java.util.List;
 18import java.util.Map;
 19import java.util.Set;
 20
 21import eu.siacs.conversations.R;
 22import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 23import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 24import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
 25import eu.siacs.conversations.entities.Account;
 26import eu.siacs.conversations.entities.Conversation;
 27import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 28import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 29import eu.siacs.conversations.xmpp.jid.Jid;
 30
 31public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdated {
 32	private List<Jid> contactJids;
 33
 34	private Account mAccount;
 35	private Conversation mConversation;
 36	private TextView keyErrorMessage;
 37	private LinearLayout keyErrorMessageCard;
 38	private TextView ownKeysTitle;
 39	private LinearLayout ownKeys;
 40	private LinearLayout ownKeysCard;
 41	private LinearLayout foreignKeys;
 42	private Button mSaveButton;
 43	private Button mCancelButton;
 44
 45	private AxolotlService.FetchStatus lastFetchReport = AxolotlService.FetchStatus.SUCCESS;
 46
 47	private final Map<String, Boolean> ownKeysToTrust = new HashMap<>();
 48	private final Map<Jid,Map<String, Boolean>> foreignKeysToTrust = new HashMap<>();
 49
 50	private final OnClickListener mSaveButtonListener = new OnClickListener() {
 51		@Override
 52		public void onClick(View v) {
 53			commitTrusts();
 54			finishOk();
 55		}
 56	};
 57
 58	private final OnClickListener mCancelButtonListener = new OnClickListener() {
 59		@Override
 60		public void onClick(View v) {
 61			setResult(RESULT_CANCELED);
 62			finish();
 63		}
 64	};
 65
 66	@Override
 67	protected void refreshUiReal() {
 68		invalidateOptionsMenu();
 69		populateView();
 70	}
 71
 72	@Override
 73	protected void onCreate(final Bundle savedInstanceState) {
 74		super.onCreate(savedInstanceState);
 75		setContentView(R.layout.activity_trust_keys);
 76		this.contactJids = new ArrayList<>();
 77		for(String jid : getIntent().getStringArrayExtra("contacts")) {
 78			try {
 79				this.contactJids.add(Jid.fromString(jid));
 80			} catch (InvalidJidException e) {
 81				e.printStackTrace();
 82			}
 83		}
 84
 85		keyErrorMessageCard = (LinearLayout) findViewById(R.id.key_error_message_card);
 86		keyErrorMessage = (TextView) findViewById(R.id.key_error_message);
 87		ownKeysTitle = (TextView) findViewById(R.id.own_keys_title);
 88		ownKeys = (LinearLayout) findViewById(R.id.own_keys_details);
 89		ownKeysCard = (LinearLayout) findViewById(R.id.own_keys_card);
 90		foreignKeys = (LinearLayout) findViewById(R.id.foreign_keys);
 91		mCancelButton = (Button) findViewById(R.id.cancel_button);
 92		mCancelButton.setOnClickListener(mCancelButtonListener);
 93		mSaveButton = (Button) findViewById(R.id.save_button);
 94		mSaveButton.setOnClickListener(mSaveButtonListener);
 95
 96
 97		if (getActionBar() != null) {
 98			getActionBar().setHomeButtonEnabled(true);
 99			getActionBar().setDisplayHomeAsUpEnabled(true);
100		}
101	}
102
103	private void populateView() {
104		setTitle(getString(R.string.trust_omemo_fingerprints));
105		ownKeys.removeAllViews();
106		foreignKeys.removeAllViews();
107		boolean hasOwnKeys = false;
108		boolean hasForeignKeys = false;
109		for(final String fingerprint : ownKeysToTrust.keySet()) {
110			hasOwnKeys = true;
111			addFingerprintRowWithListeners(ownKeys, mAccount, fingerprint, false,
112					FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint)), false,
113					new CompoundButton.OnCheckedChangeListener() {
114						@Override
115						public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
116							ownKeysToTrust.put(fingerprint, isChecked);
117							// own fingerprints have no impact on locked status.
118						}
119					},
120					null,
121					null
122			);
123		}
124
125		synchronized (this.foreignKeysToTrust) {
126			for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
127				hasForeignKeys = true;
128				final LinearLayout layout = (LinearLayout) getLayoutInflater().inflate(R.layout.keys_card, foreignKeys, false);
129				final Jid jid = entry.getKey();
130				final TextView header = (TextView) layout.findViewById(R.id.foreign_keys_title);
131				final LinearLayout keysContainer = (LinearLayout) layout.findViewById(R.id.foreign_keys_details);
132				final TextView informNoKeys = (TextView) layout.findViewById(R.id.no_keys_to_accept);
133				header.setText(jid.toString());
134				final Map<String, Boolean> fingerprints = entry.getValue();
135				for (final String fingerprint : fingerprints.keySet()) {
136					addFingerprintRowWithListeners(keysContainer, mAccount, fingerprint, false,
137							FingerprintStatus.createActive(fingerprints.get(fingerprint)), false,
138							new CompoundButton.OnCheckedChangeListener() {
139								@Override
140								public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
141									fingerprints.put(fingerprint, isChecked);
142									lockOrUnlockAsNeeded();
143								}
144							},
145							null,
146							null
147					);
148				}
149				if (fingerprints.size() == 0) {
150					informNoKeys.setVisibility(View.VISIBLE);
151					informNoKeys.setText(getString(R.string.no_keys_just_confirm,mAccount.getRoster().getContact(jid).getDisplayName()));
152				} else {
153					informNoKeys.setVisibility(View.GONE);
154				}
155				foreignKeys.addView(layout);
156			}
157		}
158
159		ownKeysTitle.setText(mAccount.getJid().toBareJid().toString());
160		ownKeysCard.setVisibility(hasOwnKeys ? View.VISIBLE : View.GONE);
161		foreignKeys.setVisibility(hasForeignKeys ? View.VISIBLE : View.GONE);
162		if(hasPendingKeyFetches()) {
163			setFetching();
164			lock();
165		} else {
166			if (!hasForeignKeys && hasNoOtherTrustedKeys()) {
167				keyErrorMessageCard.setVisibility(View.VISIBLE);
168				if (lastFetchReport == AxolotlService.FetchStatus.ERROR
169						|| mAccount.getAxolotlService().fetchMapHasErrors(contactJids)) {
170					keyErrorMessage.setText(R.string.error_no_keys_to_trust_server_error);
171				} else {
172					keyErrorMessage.setText(R.string.error_no_keys_to_trust);
173				}
174				ownKeys.removeAllViews();
175				ownKeysCard.setVisibility(View.GONE);
176				foreignKeys.removeAllViews();
177				foreignKeys.setVisibility(View.GONE);
178			}
179			lockOrUnlockAsNeeded();
180			setDone();
181		}
182	}
183
184	private boolean reloadFingerprints() {
185		List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
186		ownKeysToTrust.clear();
187		AxolotlService service = this.mAccount.getAxolotlService();
188		Set<IdentityKey> ownKeysSet = service.getKeysWithTrust(FingerprintStatus.createActiveUndecided());
189		for(final IdentityKey identityKey : ownKeysSet) {
190			if(!ownKeysToTrust.containsKey(identityKey)) {
191				ownKeysToTrust.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
192			}
193		}
194		synchronized (this.foreignKeysToTrust) {
195			foreignKeysToTrust.clear();
196			for (Jid jid : contactJids) {
197				Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), jid);
198				if (hasNoOtherTrustedKeys(jid) && ownKeysSet.size() == 0) {
199					foreignKeysSet.addAll(service.getKeysWithTrust(FingerprintStatus.createActive(false), jid));
200				}
201				Map<String, Boolean> foreignFingerprints = new HashMap<>();
202				for (final IdentityKey identityKey : foreignKeysSet) {
203					if (!foreignFingerprints.containsKey(identityKey)) {
204						foreignFingerprints.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
205					}
206				}
207				if (foreignFingerprints.size() > 0 || !acceptedTargets.contains(jid)) {
208					foreignKeysToTrust.put(jid, foreignFingerprints);
209				}
210			}
211		}
212		return ownKeysSet.size() + foreignKeysToTrust.size() > 0;
213	}
214
215	@Override
216	public void onBackendConnected() {
217		Intent intent = getIntent();
218		this.mAccount = extractAccount(intent);
219		if (this.mAccount != null && intent != null) {
220			String uuid = intent.getStringExtra("conversation");
221			this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
222			reloadFingerprints();
223			populateView();
224		}
225	}
226
227	private boolean hasNoOtherTrustedKeys() {
228		return mAccount == null || mAccount.getAxolotlService().anyTargetHasNoTrustedKeys(contactJids);
229	}
230
231	private boolean hasNoOtherTrustedKeys(Jid contact) {
232		return mAccount == null || mAccount.getAxolotlService().getNumTrustedKeys(contact) == 0;
233	}
234
235	private boolean hasPendingKeyFetches() {
236		return mAccount != null && mAccount.getAxolotlService().hasPendingKeyFetches(mAccount, contactJids);
237	}
238
239
240	@Override
241	public void onKeyStatusUpdated(final AxolotlService.FetchStatus report) {
242		if (report != null) {
243			lastFetchReport = report;
244			runOnUiThread(new Runnable() {
245				@Override
246				public void run() {
247					switch (report) {
248						case ERROR:
249							Toast.makeText(TrustKeysActivity.this,R.string.error_fetching_omemo_key,Toast.LENGTH_SHORT).show();
250							break;
251						case SUCCESS_VERIFIED:
252							Toast.makeText(TrustKeysActivity.this,R.string.verified_omemo_key_with_certificate,Toast.LENGTH_LONG).show();
253							break;
254					}
255				}
256			});
257
258		}
259		boolean keysToTrust = reloadFingerprints();
260		if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) {
261			refreshUi();
262		} else {
263			runOnUiThread(new Runnable() {
264				@Override
265				public void run() {
266					finishOk();
267				}
268			});
269
270		}
271	}
272
273	private void finishOk() {
274		Intent data = new Intent();
275		data.putExtra("choice", getIntent().getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID));
276		setResult(RESULT_OK, data);
277		finish();
278	}
279
280	private void commitTrusts() {
281		for(final String fingerprint :ownKeysToTrust.keySet()) {
282			mAccount.getAxolotlService().setFingerprintTrust(
283					fingerprint,
284					FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint)));
285		}
286		List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
287		synchronized (this.foreignKeysToTrust) {
288			for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
289				Jid jid = entry.getKey();
290				Map<String, Boolean> value = entry.getValue();
291				if (!acceptedTargets.contains(jid)) {
292					acceptedTargets.add(jid);
293				}
294				for (final String fingerprint : value.keySet()) {
295					mAccount.getAxolotlService().setFingerprintTrust(
296							fingerprint,
297							FingerprintStatus.createActive(value.get(fingerprint)));
298				}
299			}
300		}
301		if (mConversation != null && mConversation.getMode() == Conversation.MODE_MULTI) {
302			mConversation.setAcceptedCryptoTargets(acceptedTargets);
303			xmppConnectionService.updateConversation(mConversation);
304		}
305	}
306
307	private void unlock() {
308		mSaveButton.setEnabled(true);
309		mSaveButton.setTextColor(getPrimaryTextColor());
310	}
311
312	private void lock() {
313		mSaveButton.setEnabled(false);
314		mSaveButton.setTextColor(getSecondaryTextColor());
315	}
316
317	private void lockOrUnlockAsNeeded() {
318		synchronized (this.foreignKeysToTrust) {
319			for (Jid jid : contactJids) {
320				Map<String, Boolean> fingerprints = foreignKeysToTrust.get(jid);
321				if (hasNoOtherTrustedKeys(jid) && (fingerprints == null || !fingerprints.values().contains(true))) {
322					lock();
323					return;
324				}
325			}
326		}
327		unlock();
328
329	}
330
331	private void setDone() {
332		mSaveButton.setText(getString(R.string.done));
333	}
334
335	private void setFetching() {
336		mSaveButton.setText(getString(R.string.fetching_keys));
337	}
338}