1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.os.SystemClock;
6import android.util.Log;
7import android.util.Pair;
8
9import org.json.JSONException;
10import org.json.JSONObject;
11
12import java.util.ArrayList;
13import java.util.Collection;
14import java.util.HashMap;
15import java.util.HashSet;
16import java.util.List;
17import java.util.Map;
18import java.util.Set;
19import java.util.concurrent.CopyOnWriteArrayList;
20import java.util.concurrent.CopyOnWriteArraySet;
21
22import eu.siacs.conversations.Config;
23import eu.siacs.conversations.R;
24import eu.siacs.conversations.crypto.PgpDecryptionService;
25import eu.siacs.conversations.crypto.axolotl.AxolotlService;
26import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
27import eu.siacs.conversations.services.AvatarService;
28import eu.siacs.conversations.services.XmppConnectionService;
29import eu.siacs.conversations.utils.UIHelper;
30import eu.siacs.conversations.utils.XmppUri;
31import eu.siacs.conversations.xmpp.XmppConnection;
32import rocks.xmpp.addr.Jid;
33
34public class Account extends AbstractEntity implements AvatarService.Avatarable {
35
36 public static final String TABLENAME = "accounts";
37
38 public static final String USERNAME = "username";
39 public static final String SERVER = "server";
40 public static final String PASSWORD = "password";
41 public static final String OPTIONS = "options";
42 public static final String ROSTERVERSION = "rosterversion";
43 public static final String KEYS = "keys";
44 public static final String AVATAR = "avatar";
45 public static final String DISPLAY_NAME = "display_name";
46 public static final String HOSTNAME = "hostname";
47 public static final String PORT = "port";
48 public static final String STATUS = "status";
49 public static final String STATUS_MESSAGE = "status_message";
50 public static final String RESOURCE = "resource";
51
52 public static final String PINNED_MECHANISM_KEY = "pinned_mechanism";
53 public static final String PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
54
55 public static final int OPTION_USETLS = 0;
56 public static final int OPTION_DISABLED = 1;
57 public static final int OPTION_REGISTER = 2;
58 public static final int OPTION_USECOMPRESSION = 3;
59 public static final int OPTION_MAGIC_CREATE = 4;
60 public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5;
61 public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6;
62 public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7;
63 public static final int OPTION_UNVERIFIED = 8;
64 private static final String KEY_PGP_SIGNATURE = "pgp_signature";
65 private static final String KEY_PGP_ID = "pgp_id";
66 public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>();
67 protected final JSONObject keys;
68 private final Roster roster = new Roster(this);
69 private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
70 public final Set<Conversation> pendingConferenceJoins = new HashSet<>();
71 public final Set<Conversation> pendingConferenceLeaves = new HashSet<>();
72 public final Set<Conversation> inProgressConferenceJoins = new HashSet<>();
73 public final Set<Conversation> inProgressConferencePings = new HashSet<>();
74 protected Jid jid;
75 protected String password;
76 protected int options = 0;
77 protected State status = State.OFFLINE;
78 private State lastErrorStatus = State.OFFLINE;
79 protected String resource;
80 protected String avatar;
81 protected String hostname = null;
82 protected int port = 5222;
83 protected boolean online = false;
84 private String rosterVersion;
85 private String displayName = null;
86 private AxolotlService axolotlService = null;
87 private PgpDecryptionService pgpDecryptionService = null;
88 private XmppConnection xmppConnection = null;
89 private long mEndGracePeriod = 0L;
90 private final Map<Jid, Bookmark> bookmarks = new HashMap<>();
91 private Presence.Status presenceStatus = Presence.Status.ONLINE;
92 private String presenceStatusMessage = null;
93
94 public Account(final Jid jid, final String password) {
95 this(java.util.UUID.randomUUID().toString(), jid,
96 password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null);
97 }
98
99 private Account(final String uuid, final Jid jid,
100 final String password, final int options, final String rosterVersion, final String keys,
101 final String avatar, String displayName, String hostname, int port,
102 final Presence.Status status, String statusMessage) {
103 this.uuid = uuid;
104 this.jid = jid;
105 this.password = password;
106 this.options = options;
107 this.rosterVersion = rosterVersion;
108 JSONObject tmp;
109 try {
110 tmp = new JSONObject(keys);
111 } catch (JSONException e) {
112 tmp = new JSONObject();
113 }
114 this.keys = tmp;
115 this.avatar = avatar;
116 this.displayName = displayName;
117 this.hostname = hostname;
118 this.port = port;
119 this.presenceStatus = status;
120 this.presenceStatusMessage = statusMessage;
121 }
122
123 public static Account fromCursor(final Cursor cursor) {
124 final Jid jid;
125 try {
126 String resource = cursor.getString(cursor.getColumnIndex(RESOURCE));
127 jid = Jid.of(
128 cursor.getString(cursor.getColumnIndex(USERNAME)),
129 cursor.getString(cursor.getColumnIndex(SERVER)),
130 resource == null || resource.trim().isEmpty() ? null : resource);
131 } catch (final IllegalArgumentException ignored) {
132 Log.d(Config.LOGTAG, cursor.getString(cursor.getColumnIndex(USERNAME)) + "@" + cursor.getString(cursor.getColumnIndex(SERVER)));
133 throw new AssertionError(ignored);
134 }
135 return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
136 jid,
137 cursor.getString(cursor.getColumnIndex(PASSWORD)),
138 cursor.getInt(cursor.getColumnIndex(OPTIONS)),
139 cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
140 cursor.getString(cursor.getColumnIndex(KEYS)),
141 cursor.getString(cursor.getColumnIndex(AVATAR)),
142 cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)),
143 cursor.getString(cursor.getColumnIndex(HOSTNAME)),
144 cursor.getInt(cursor.getColumnIndex(PORT)),
145 Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))),
146 cursor.getString(cursor.getColumnIndex(STATUS_MESSAGE)));
147 }
148
149 public boolean httpUploadAvailable(long filesize) {
150 return xmppConnection != null && (xmppConnection.getFeatures().httpUpload(filesize) || xmppConnection.getFeatures().p1S3FileTransfer());
151 }
152
153 public boolean httpUploadAvailable() {
154 return isOptionSet(OPTION_HTTP_UPLOAD_AVAILABLE) || httpUploadAvailable(0);
155 }
156
157 public String getDisplayName() {
158 return displayName;
159 }
160
161 public void setDisplayName(String displayName) {
162 this.displayName = displayName;
163 }
164
165 public XmppConnection.Identity getServerIdentity() {
166 if (xmppConnection == null) {
167 return XmppConnection.Identity.UNKNOWN;
168 } else {
169 return xmppConnection.getServerIdentity();
170 }
171 }
172
173 public Contact getSelfContact() {
174 return getRoster().getContact(jid);
175 }
176
177 public boolean hasPendingPgpIntent(Conversation conversation) {
178 return pgpDecryptionService != null && pgpDecryptionService.hasPendingIntent(conversation);
179 }
180
181 public boolean isPgpDecryptionServiceConnected() {
182 return pgpDecryptionService != null && pgpDecryptionService.isConnected();
183 }
184
185 public boolean setShowErrorNotification(boolean newValue) {
186 boolean oldValue = showErrorNotification();
187 setKey("show_error", Boolean.toString(newValue));
188 return newValue != oldValue;
189 }
190
191 public boolean showErrorNotification() {
192 String key = getKey("show_error");
193 return key == null || Boolean.parseBoolean(key);
194 }
195
196 public boolean isEnabled() {
197 return !isOptionSet(Account.OPTION_DISABLED);
198 }
199
200 public boolean isOptionSet(final int option) {
201 return ((options & (1 << option)) != 0);
202 }
203
204 public boolean setOption(final int option, final boolean value) {
205 final int before = this.options;
206 if (value) {
207 this.options |= 1 << option;
208 } else {
209 this.options &= ~(1 << option);
210 }
211 return before != this.options;
212 }
213
214 public String getUsername() {
215 return jid.getEscapedLocal();
216 }
217
218 public boolean setJid(final Jid next) {
219 final Jid previousFull = this.jid;
220 final Jid prev = this.jid != null ? this.jid.asBareJid() : null;
221 final boolean changed = prev == null || (next != null && !prev.equals(next.asBareJid()));
222 if (changed) {
223 final AxolotlService oldAxolotlService = this.axolotlService;
224 if (oldAxolotlService != null) {
225 oldAxolotlService.destroy();
226 this.jid = next;
227 this.axolotlService = oldAxolotlService.makeNew();
228 }
229 }
230 this.jid = next;
231 return next != null && !next.equals(previousFull);
232 }
233
234 public String getServer() {
235 return jid.getDomain();
236 }
237
238 public String getPassword() {
239 return password;
240 }
241
242 public void setPassword(final String password) {
243 this.password = password;
244 }
245
246 public String getHostname() {
247 return this.hostname == null ? "" : this.hostname;
248 }
249
250 public void setHostname(String hostname) {
251 this.hostname = hostname;
252 }
253
254 public boolean isOnion() {
255 final String server = getServer();
256 return server != null && server.endsWith(".onion");
257 }
258
259 public int getPort() {
260 return this.port;
261 }
262
263 public void setPort(int port) {
264 this.port = port;
265 }
266
267 public State getStatus() {
268 if (isOptionSet(OPTION_DISABLED)) {
269 return State.DISABLED;
270 } else {
271 return this.status;
272 }
273 }
274
275 public State getLastErrorStatus() {
276 return this.lastErrorStatus;
277 }
278
279 public void setStatus(final State status) {
280 this.status = status;
281 if (status.isError || status == State.ONLINE) {
282 this.lastErrorStatus = status;
283 }
284 }
285
286 public State getTrueStatus() {
287 return this.status;
288 }
289
290 public boolean errorStatus() {
291 return getStatus().isError();
292 }
293
294 public boolean hasErrorStatus() {
295 return getXmppConnection() != null
296 && (getStatus().isError() || getStatus() == State.CONNECTING)
297 && getXmppConnection().getAttempt() >= 3;
298 }
299
300 public Presence.Status getPresenceStatus() {
301 return this.presenceStatus;
302 }
303
304 public void setPresenceStatus(Presence.Status status) {
305 this.presenceStatus = status;
306 }
307
308 public String getPresenceStatusMessage() {
309 return this.presenceStatusMessage;
310 }
311
312 public void setPresenceStatusMessage(String message) {
313 this.presenceStatusMessage = message;
314 }
315
316 public String getResource() {
317 return jid.getResource();
318 }
319
320 public void setResource(final String resource) {
321 this.jid = this.jid.withResource(resource);
322 }
323
324 public Jid getJid() {
325 return jid;
326 }
327
328 public JSONObject getKeys() {
329 return keys;
330 }
331
332 public String getKey(final String name) {
333 synchronized (this.keys) {
334 return this.keys.optString(name, null);
335 }
336 }
337
338 public int getKeyAsInt(final String name, int defaultValue) {
339 String key = getKey(name);
340 try {
341 return key == null ? defaultValue : Integer.parseInt(key);
342 } catch (NumberFormatException e) {
343 return defaultValue;
344 }
345 }
346
347 public boolean setKey(final String keyName, final String keyValue) {
348 synchronized (this.keys) {
349 try {
350 this.keys.put(keyName, keyValue);
351 return true;
352 } catch (final JSONException e) {
353 return false;
354 }
355 }
356 }
357
358 public boolean setPrivateKeyAlias(String alias) {
359 return setKey("private_key_alias", alias);
360 }
361
362 public String getPrivateKeyAlias() {
363 return getKey("private_key_alias");
364 }
365
366 @Override
367 public ContentValues getContentValues() {
368 final ContentValues values = new ContentValues();
369 values.put(UUID, uuid);
370 values.put(USERNAME, jid.getLocal());
371 values.put(SERVER, jid.getDomain());
372 values.put(PASSWORD, password);
373 values.put(OPTIONS, options);
374 synchronized (this.keys) {
375 values.put(KEYS, this.keys.toString());
376 }
377 values.put(ROSTERVERSION, rosterVersion);
378 values.put(AVATAR, avatar);
379 values.put(DISPLAY_NAME, displayName);
380 values.put(HOSTNAME, hostname);
381 values.put(PORT, port);
382 values.put(STATUS, presenceStatus.toShowString());
383 values.put(STATUS_MESSAGE, presenceStatusMessage);
384 values.put(RESOURCE, jid.getResource());
385 return values;
386 }
387
388 public AxolotlService getAxolotlService() {
389 return axolotlService;
390 }
391
392 public void initAccountServices(final XmppConnectionService context) {
393 this.axolotlService = new AxolotlService(this, context);
394 this.pgpDecryptionService = new PgpDecryptionService(context);
395 if (xmppConnection != null) {
396 xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
397 }
398 }
399
400 public PgpDecryptionService getPgpDecryptionService() {
401 return this.pgpDecryptionService;
402 }
403
404 public XmppConnection getXmppConnection() {
405 return this.xmppConnection;
406 }
407
408 public void setXmppConnection(final XmppConnection connection) {
409 this.xmppConnection = connection;
410 }
411
412 public String getRosterVersion() {
413 if (this.rosterVersion == null) {
414 return "";
415 } else {
416 return this.rosterVersion;
417 }
418 }
419
420 public void setRosterVersion(final String version) {
421 this.rosterVersion = version;
422 }
423
424 public int countPresences() {
425 return this.getSelfContact().getPresences().size();
426 }
427
428 public String getPgpSignature() {
429 return getKey(KEY_PGP_SIGNATURE);
430 }
431
432 public boolean setPgpSignature(String signature) {
433 return setKey(KEY_PGP_SIGNATURE, signature);
434 }
435
436 public boolean unsetPgpSignature() {
437 synchronized (this.keys) {
438 return keys.remove(KEY_PGP_SIGNATURE) != null;
439 }
440 }
441
442 public long getPgpId() {
443 synchronized (this.keys) {
444 if (keys.has(KEY_PGP_ID)) {
445 try {
446 return keys.getLong(KEY_PGP_ID);
447 } catch (JSONException e) {
448 return 0;
449 }
450 } else {
451 return 0;
452 }
453 }
454 }
455
456 public boolean setPgpSignId(long pgpID) {
457 synchronized (this.keys) {
458 try {
459 if (pgpID == 0) {
460 keys.remove(KEY_PGP_ID);
461 } else {
462 keys.put(KEY_PGP_ID, pgpID);
463 }
464 } catch (JSONException e) {
465 return false;
466 }
467 return true;
468 }
469 }
470
471 public Roster getRoster() {
472 return this.roster;
473 }
474
475 public Collection<Bookmark> getBookmarks() {
476 return this.bookmarks.values();
477 }
478
479 public void setBookmarks(Map<Jid, Bookmark> bookmarks) {
480 synchronized (this.bookmarks) {
481 this.bookmarks.clear();
482 this.bookmarks.putAll(bookmarks);
483 }
484 }
485
486 public void putBookmark(Bookmark bookmark) {
487 synchronized (this.bookmarks) {
488 this.bookmarks.put(bookmark.getJid(), bookmark);
489 }
490 }
491
492 public void removeBookmark(Bookmark bookmark) {
493 synchronized (this.bookmarks) {
494 this.bookmarks.remove(bookmark.getJid());
495 }
496 }
497
498 public void removeBookmark(Jid jid) {
499 synchronized (this.bookmarks) {
500 this.bookmarks.remove(jid);
501 }
502 }
503
504 public Set<Jid> getBookmarkedJids() {
505 synchronized (this.bookmarks) {
506 return new HashSet<>(this.bookmarks.keySet());
507 }
508 }
509
510 public boolean hasBookmarkFor(final Jid jid) {
511 synchronized (this.bookmarks) {
512 return this.bookmarks.containsKey(jid.asBareJid());
513 }
514 }
515
516 Bookmark getBookmark(final Jid jid) {
517 synchronized (this.bookmarks) {
518 return this.bookmarks.get(jid.asBareJid());
519 }
520 }
521
522 public boolean setAvatar(final String filename) {
523 if (this.avatar != null && this.avatar.equals(filename)) {
524 return false;
525 } else {
526 this.avatar = filename;
527 return true;
528 }
529 }
530
531 public String getAvatar() {
532 return this.avatar;
533 }
534
535 public void activateGracePeriod(final long duration) {
536 if (duration > 0) {
537 this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
538 }
539 }
540
541 public void deactivateGracePeriod() {
542 this.mEndGracePeriod = 0L;
543 }
544
545 public boolean inGracePeriod() {
546 return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
547 }
548
549 public String getShareableUri() {
550 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
551 String uri = "xmpp:" + this.getJid().asBareJid().toEscapedString();
552 if (fingerprints.size() > 0) {
553 return XmppUri.getFingerprintUri(uri, fingerprints, ';');
554 } else {
555 return uri;
556 }
557 }
558
559 public String getShareableLink() {
560 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
561 String uri = "https://conversations.im/i/" + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString());
562 if (fingerprints.size() > 0) {
563 return XmppUri.getFingerprintUri(uri, fingerprints, '&');
564 } else {
565 return uri;
566 }
567 }
568
569 private List<XmppUri.Fingerprint> getFingerprints() {
570 ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
571 if (axolotlService == null) {
572 return fingerprints;
573 }
574 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, axolotlService.getOwnFingerprint().substring(2), axolotlService.getOwnDeviceId()));
575 for (XmppAxolotlSession session : axolotlService.findOwnSessions()) {
576 if (session.getTrust().isVerified() && session.getTrust().isActive()) {
577 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, session.getFingerprint().substring(2).replaceAll("\\s", ""), session.getRemoteAddress().getDeviceId()));
578 }
579 }
580 return fingerprints;
581 }
582
583 public boolean isBlocked(final ListItem contact) {
584 final Jid jid = contact.getJid();
585 return jid != null && (blocklist.contains(jid.asBareJid()) || blocklist.contains(Jid.ofDomain(jid.getDomain())));
586 }
587
588 public boolean isBlocked(final Jid jid) {
589 return jid != null && blocklist.contains(jid.asBareJid());
590 }
591
592 public Collection<Jid> getBlocklist() {
593 return this.blocklist;
594 }
595
596 public void clearBlocklist() {
597 getBlocklist().clear();
598 }
599
600 public boolean isOnlineAndConnected() {
601 return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
602 }
603
604 @Override
605 public int getAvatarBackgroundColor() {
606 return UIHelper.getColorForName(jid.asBareJid().toString());
607 }
608
609 public enum State {
610 DISABLED(false, false),
611 OFFLINE(false),
612 CONNECTING(false),
613 ONLINE(false),
614 NO_INTERNET(false),
615 UNAUTHORIZED,
616 SERVER_NOT_FOUND,
617 REGISTRATION_SUCCESSFUL(false),
618 REGISTRATION_FAILED(true, false),
619 REGISTRATION_WEB(true, false),
620 REGISTRATION_CONFLICT(true, false),
621 REGISTRATION_NOT_SUPPORTED(true, false),
622 REGISTRATION_PLEASE_WAIT(true, false),
623 REGISTRATION_INVALID_TOKEN(true,false),
624 REGISTRATION_PASSWORD_TOO_WEAK(true, false),
625 TLS_ERROR,
626 INCOMPATIBLE_SERVER,
627 TOR_NOT_AVAILABLE,
628 DOWNGRADE_ATTACK,
629 SESSION_FAILURE,
630 BIND_FAILURE,
631 HOST_UNKNOWN,
632 STREAM_ERROR,
633 STREAM_OPENING_ERROR,
634 POLICY_VIOLATION,
635 PAYMENT_REQUIRED,
636 MISSING_INTERNET_PERMISSION(false);
637
638 private final boolean isError;
639 private final boolean attemptReconnect;
640
641 State(final boolean isError) {
642 this(isError, true);
643 }
644
645 State(final boolean isError, final boolean reconnect) {
646 this.isError = isError;
647 this.attemptReconnect = reconnect;
648 }
649
650 State() {
651 this(true, true);
652 }
653
654 public boolean isError() {
655 return this.isError;
656 }
657
658 public boolean isAttemptReconnect() {
659 return this.attemptReconnect;
660 }
661
662 public int getReadableId() {
663 switch (this) {
664 case DISABLED:
665 return R.string.account_status_disabled;
666 case ONLINE:
667 return R.string.account_status_online;
668 case CONNECTING:
669 return R.string.account_status_connecting;
670 case OFFLINE:
671 return R.string.account_status_offline;
672 case UNAUTHORIZED:
673 return R.string.account_status_unauthorized;
674 case SERVER_NOT_FOUND:
675 return R.string.account_status_not_found;
676 case NO_INTERNET:
677 return R.string.account_status_no_internet;
678 case REGISTRATION_FAILED:
679 return R.string.account_status_regis_fail;
680 case REGISTRATION_WEB:
681 return R.string.account_status_regis_web;
682 case REGISTRATION_CONFLICT:
683 return R.string.account_status_regis_conflict;
684 case REGISTRATION_SUCCESSFUL:
685 return R.string.account_status_regis_success;
686 case REGISTRATION_NOT_SUPPORTED:
687 return R.string.account_status_regis_not_sup;
688 case REGISTRATION_INVALID_TOKEN:
689 return R.string.account_status_regis_invalid_token;
690 case TLS_ERROR:
691 return R.string.account_status_tls_error;
692 case INCOMPATIBLE_SERVER:
693 return R.string.account_status_incompatible_server;
694 case TOR_NOT_AVAILABLE:
695 return R.string.account_status_tor_unavailable;
696 case BIND_FAILURE:
697 return R.string.account_status_bind_failure;
698 case SESSION_FAILURE:
699 return R.string.session_failure;
700 case DOWNGRADE_ATTACK:
701 return R.string.sasl_downgrade;
702 case HOST_UNKNOWN:
703 return R.string.account_status_host_unknown;
704 case POLICY_VIOLATION:
705 return R.string.account_status_policy_violation;
706 case REGISTRATION_PLEASE_WAIT:
707 return R.string.registration_please_wait;
708 case REGISTRATION_PASSWORD_TOO_WEAK:
709 return R.string.registration_password_too_weak;
710 case STREAM_ERROR:
711 return R.string.account_status_stream_error;
712 case STREAM_OPENING_ERROR:
713 return R.string.account_status_stream_opening_error;
714 case PAYMENT_REQUIRED:
715 return R.string.payment_required;
716 case MISSING_INTERNET_PERMISSION:
717 return R.string.missing_internet_permission;
718 default:
719 return R.string.account_status_unknown;
720 }
721 }
722 }
723}