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