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