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