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