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