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