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