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