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