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