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