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