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