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