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