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