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