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