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