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