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