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