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 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 protected String rosterVersion;
233 protected State status = State.OFFLINE;
234 protected final JSONObject keys;
235 protected String avatar;
236 protected String displayName = null;
237 protected String hostname = null;
238 protected int port = 5222;
239 protected boolean online = false;
240 private OtrService mOtrService = null;
241 private AxolotlService axolotlService = null;
242 private PgpDecryptionService pgpDecryptionService = null;
243 private XmppConnection xmppConnection = null;
244 private long mEndGracePeriod = 0L;
245 private String otrFingerprint;
246 private final Roster roster = new Roster(this);
247 private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
248 private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
249 private Presence.Status presenceStatus = Presence.Status.ONLINE;
250 private String presenceStatusMessage = null;
251
252 public Account(final Jid jid, final String password) {
253 this(java.util.UUID.randomUUID().toString(), jid,
254 password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null);
255 }
256
257 private Account(final String uuid, final Jid jid,
258 final String password, final int options, final String rosterVersion, final String keys,
259 final String avatar, String displayName, String hostname, int port,
260 final Presence.Status status, String statusMessage) {
261 this.uuid = uuid;
262 this.jid = jid;
263 if (jid.isBareJid()) {
264 this.setResource("mobile");
265 }
266 this.password = password;
267 this.options = options;
268 this.rosterVersion = rosterVersion;
269 JSONObject tmp;
270 try {
271 tmp = new JSONObject(keys);
272 } catch(JSONException e) {
273 tmp = new JSONObject();
274 }
275 this.keys = tmp;
276 this.avatar = avatar;
277 this.displayName = displayName;
278 this.hostname = hostname;
279 this.port = port;
280 this.presenceStatus = status;
281 this.presenceStatusMessage = statusMessage;
282 }
283
284 public static Account fromCursor(final Cursor cursor) {
285 Jid jid = null;
286 try {
287 jid = Jid.fromParts(cursor.getString(cursor.getColumnIndex(USERNAME)),
288 cursor.getString(cursor.getColumnIndex(SERVER)), "mobile");
289 } catch (final InvalidJidException ignored) {
290 }
291 return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
292 jid,
293 cursor.getString(cursor.getColumnIndex(PASSWORD)),
294 cursor.getInt(cursor.getColumnIndex(OPTIONS)),
295 cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
296 cursor.getString(cursor.getColumnIndex(KEYS)),
297 cursor.getString(cursor.getColumnIndex(AVATAR)),
298 cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)),
299 cursor.getString(cursor.getColumnIndex(HOSTNAME)),
300 cursor.getInt(cursor.getColumnIndex(PORT)),
301 Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))),
302 cursor.getString(cursor.getColumnIndex(STATUS_MESSAGE)));
303 }
304
305 public boolean isOptionSet(final int option) {
306 return ((options & (1 << option)) != 0);
307 }
308
309 public boolean setOption(final int option, final boolean value) {
310 final int before = this.options;
311 if (value) {
312 this.options |= 1 << option;
313 } else {
314 this.options &= ~(1 << option);
315 }
316 return before != this.options;
317 }
318
319 public String getUsername() {
320 return jid.getLocalpart();
321 }
322
323 public boolean setJid(final Jid next) {
324 final Jid prev = this.jid != null ? this.jid.toBareJid() : null;
325 final boolean changed = prev == null || (next != null && !prev.equals(next.toBareJid()));
326 if (changed) {
327 final AxolotlService oldAxolotlService = this.axolotlService;
328 if (oldAxolotlService != null) {
329 oldAxolotlService.destroy();
330 this.jid = next;
331 this.axolotlService = oldAxolotlService.makeNew();
332 }
333 }
334 this.jid = next;
335 return changed;
336 }
337
338 public Jid getServer() {
339 return jid.toDomainJid();
340 }
341
342 public String getPassword() {
343 return password;
344 }
345
346 public void setPassword(final String password) {
347 this.password = password;
348 }
349
350 public void setHostname(String hostname) {
351 this.hostname = hostname;
352 }
353
354 public String getHostname() {
355 return this.hostname == null ? "" : this.hostname;
356 }
357
358 public boolean isOnion() {
359 final Jid server = getServer();
360 return server != null && server.toString().toLowerCase().endsWith(".onion");
361 }
362
363 public void setPort(int port) {
364 this.port = port;
365 }
366
367 public int getPort() {
368 return this.port;
369 }
370
371 public State getStatus() {
372 if (isOptionSet(OPTION_DISABLED)) {
373 return State.DISABLED;
374 } else {
375 return this.status;
376 }
377 }
378
379 public State getTrueStatus() {
380 return this.status;
381 }
382
383 public void setStatus(final State status) {
384 this.status = status;
385 }
386
387 public boolean errorStatus() {
388 return getStatus().isError();
389 }
390
391 public boolean hasErrorStatus() {
392 return getXmppConnection() != null
393 && (getStatus().isError() || getStatus() == State.CONNECTING)
394 && getXmppConnection().getAttempt() >= 3;
395 }
396
397 public void setPresenceStatus(Presence.Status status) {
398 this.presenceStatus = status;
399 }
400
401 public Presence.Status getPresenceStatus() {
402 return this.presenceStatus;
403 }
404
405 public void setPresenceStatusMessage(String message) {
406 this.presenceStatusMessage = message;
407 }
408
409 public String getPresenceStatusMessage() {
410 return this.presenceStatusMessage;
411 }
412
413 public String getResource() {
414 return jid.getResourcepart();
415 }
416
417 public boolean setResource(final String resource) {
418 final String oldResource = jid.getResourcepart();
419 if (oldResource == null || !oldResource.equals(resource)) {
420 try {
421 jid = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), resource);
422 return true;
423 } catch (final InvalidJidException ignored) {
424 return true;
425 }
426 }
427 return false;
428 }
429
430 public Jid getJid() {
431 return jid;
432 }
433
434 public JSONObject getKeys() {
435 return keys;
436 }
437
438 public String getKey(final String name) {
439 synchronized (this.keys) {
440 return this.keys.optString(name, null);
441 }
442 }
443
444 public int getKeyAsInt(final String name, int defaultValue) {
445 String key = getKey(name);
446 try {
447 return key == null ? defaultValue : Integer.parseInt(key);
448 } catch (NumberFormatException e) {
449 return defaultValue;
450 }
451 }
452
453 public boolean setKey(final String keyName, final String keyValue) {
454 synchronized (this.keys) {
455 try {
456 this.keys.put(keyName, keyValue);
457 return true;
458 } catch (final JSONException e) {
459 return false;
460 }
461 }
462 }
463
464 public boolean setPrivateKeyAlias(String alias) {
465 return setKey("private_key_alias", alias);
466 }
467
468 public String getPrivateKeyAlias() {
469 return getKey("private_key_alias");
470 }
471
472 @Override
473 public ContentValues getContentValues() {
474 final ContentValues values = new ContentValues();
475 values.put(UUID, uuid);
476 values.put(USERNAME, jid.getLocalpart());
477 values.put(SERVER, jid.getDomainpart());
478 values.put(PASSWORD, password);
479 values.put(OPTIONS, options);
480 synchronized (this.keys) {
481 values.put(KEYS, this.keys.toString());
482 }
483 values.put(ROSTERVERSION, rosterVersion);
484 values.put(AVATAR, avatar);
485 values.put(DISPLAY_NAME, displayName);
486 values.put(HOSTNAME, hostname);
487 values.put(PORT, port);
488 values.put(STATUS, presenceStatus.toShowString());
489 values.put(STATUS_MESSAGE, presenceStatusMessage);
490 return values;
491 }
492
493 public AxolotlService getAxolotlService() {
494 return axolotlService;
495 }
496
497 public void initAccountServices(final XmppConnectionService context) {
498 this.mOtrService = new OtrService(context, this);
499 this.axolotlService = new AxolotlService(this, context);
500 this.pgpDecryptionService = new PgpDecryptionService(context);
501 if (xmppConnection != null) {
502 xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
503 }
504 }
505
506 public OtrService getOtrService() {
507 return this.mOtrService;
508 }
509
510 public PgpDecryptionService getPgpDecryptionService() {
511 return this.pgpDecryptionService;
512 }
513
514 public XmppConnection getXmppConnection() {
515 return this.xmppConnection;
516 }
517
518 public void setXmppConnection(final XmppConnection connection) {
519 this.xmppConnection = connection;
520 }
521
522 public String getOtrFingerprint() {
523 if (this.otrFingerprint == null) {
524 try {
525 if (this.mOtrService == null) {
526 return null;
527 }
528 final PublicKey publicKey = this.mOtrService.getPublicKey();
529 if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
530 return null;
531 }
532 this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US);
533 return this.otrFingerprint;
534 } catch (final OtrCryptoException ignored) {
535 return null;
536 }
537 } else {
538 return this.otrFingerprint;
539 }
540 }
541
542 public String getRosterVersion() {
543 if (this.rosterVersion == null) {
544 return "";
545 } else {
546 return this.rosterVersion;
547 }
548 }
549
550 public void setRosterVersion(final String version) {
551 this.rosterVersion = version;
552 }
553
554 public int countPresences() {
555 return this.getSelfContact().getPresences().size();
556 }
557
558 public String getPgpSignature() {
559 return getKey(KEY_PGP_SIGNATURE);
560 }
561
562 public boolean setPgpSignature(String signature) {
563 return setKey(KEY_PGP_SIGNATURE, signature);
564 }
565
566 public boolean unsetPgpSignature() {
567 synchronized (this.keys) {
568 return keys.remove(KEY_PGP_SIGNATURE) != null;
569 }
570 }
571
572 public long getPgpId() {
573 synchronized (this.keys) {
574 if (keys.has(KEY_PGP_ID)) {
575 try {
576 return keys.getLong(KEY_PGP_ID);
577 } catch (JSONException e) {
578 return 0;
579 }
580 } else {
581 return 0;
582 }
583 }
584 }
585
586 public boolean setPgpSignId(long pgpID) {
587 synchronized (this.keys) {
588 try {
589 if (pgpID == 0) {
590 keys.remove(KEY_PGP_ID);
591 } else {
592 keys.put(KEY_PGP_ID, pgpID);
593 }
594 } catch (JSONException e) {
595 return false;
596 }
597 return true;
598 }
599 }
600
601 public Roster getRoster() {
602 return this.roster;
603 }
604
605 public List<Bookmark> getBookmarks() {
606 return this.bookmarks;
607 }
608
609 public void setBookmarks(final List<Bookmark> bookmarks) {
610 this.bookmarks = bookmarks;
611 }
612
613 public boolean hasBookmarkFor(final Jid conferenceJid) {
614 for (final Bookmark bookmark : this.bookmarks) {
615 final Jid jid = bookmark.getJid();
616 if (jid != null && jid.equals(conferenceJid.toBareJid())) {
617 return true;
618 }
619 }
620 return false;
621 }
622
623 public boolean setAvatar(final String filename) {
624 if (this.avatar != null && this.avatar.equals(filename)) {
625 return false;
626 } else {
627 this.avatar = filename;
628 return true;
629 }
630 }
631
632 public String getAvatar() {
633 return this.avatar;
634 }
635
636 public void activateGracePeriod(long duration) {
637 this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
638 }
639
640 public void deactivateGracePeriod() {
641 this.mEndGracePeriod = 0L;
642 }
643
644 public boolean inGracePeriod() {
645 return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
646 }
647
648 public String getShareableUri() {
649 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
650 String uri = "xmpp:"+this.getJid().toBareJid().toString();
651 if (fingerprints.size() > 0) {
652 return XmppUri.getFingerprintUri(uri,fingerprints,';');
653 } else {
654 return uri;
655 }
656 }
657
658 public String getShareableLink() {
659 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
660 String uri = "https://conversations.im/i/"+this.getJid().toBareJid().toString();
661 if (fingerprints.size() > 0) {
662 return XmppUri.getFingerprintUri(uri,fingerprints,'&');
663 } else {
664 return uri;
665 }
666 }
667
668 private List<XmppUri.Fingerprint> getFingerprints() {
669 ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
670 final String otr = this.getOtrFingerprint();
671 if (otr != null) {
672 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR,otr));
673 }
674 if (axolotlService == null) {
675 return fingerprints;
676 }
677 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,axolotlService.getOwnFingerprint().substring(2),axolotlService.getOwnDeviceId()));
678 for(XmppAxolotlSession session : axolotlService.findOwnSessions()) {
679 if (session.getTrust().isVerified() && session.getTrust().isActive()) {
680 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,session.getFingerprint().substring(2).replaceAll("\\s",""),session.getRemoteAddress().getDeviceId()));
681 }
682 }
683 return fingerprints;
684 }
685
686 public boolean isBlocked(final ListItem contact) {
687 final Jid jid = contact.getJid();
688 return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid()));
689 }
690
691 public boolean isBlocked(final Jid jid) {
692 return jid != null && blocklist.contains(jid.toBareJid());
693 }
694
695 public Collection<Jid> getBlocklist() {
696 return this.blocklist;
697 }
698
699 public void clearBlocklist() {
700 getBlocklist().clear();
701 }
702
703 public boolean isOnlineAndConnected() {
704 return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
705 }
706}