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