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