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 void setStatus(final State status) {
345 this.status = status;
346 }
347
348 public boolean errorStatus() {
349 return getStatus().isError();
350 }
351
352 public boolean hasErrorStatus() {
353 return getXmppConnection() != null
354 && (getStatus().isError() || getStatus() == State.CONNECTING)
355 && getXmppConnection().getAttempt() >= 3;
356 }
357
358 public void setPresenceStatus(Presence.Status status) {
359 this.presenceStatus = status;
360 }
361
362 public Presence.Status getPresenceStatus() {
363 return this.presenceStatus;
364 }
365
366 public void setPresenceStatusMessage(String message) {
367 this.presenceStatusMessage = message;
368 }
369
370 public String getPresenceStatusMessage() {
371 return this.presenceStatusMessage;
372 }
373
374 public String getResource() {
375 return jid.getResourcepart();
376 }
377
378 public boolean setResource(final String resource) {
379 final String oldResource = jid.getResourcepart();
380 if (oldResource == null || !oldResource.equals(resource)) {
381 try {
382 jid = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), resource);
383 return true;
384 } catch (final InvalidJidException ignored) {
385 return true;
386 }
387 }
388 return false;
389 }
390
391 public Jid getJid() {
392 return jid;
393 }
394
395 public JSONObject getKeys() {
396 return keys;
397 }
398
399 public String getKey(final String name) {
400 synchronized (this.keys) {
401 return this.keys.optString(name, null);
402 }
403 }
404
405 public int getKeyAsInt(final String name, int defaultValue) {
406 String key = getKey(name);
407 try {
408 return key == null ? defaultValue : Integer.parseInt(key);
409 } catch (NumberFormatException e) {
410 return defaultValue;
411 }
412 }
413
414 public boolean setKey(final String keyName, final String keyValue) {
415 synchronized (this.keys) {
416 try {
417 this.keys.put(keyName, keyValue);
418 return true;
419 } catch (final JSONException e) {
420 return false;
421 }
422 }
423 }
424
425 public boolean setPrivateKeyAlias(String alias) {
426 return setKey("private_key_alias", alias);
427 }
428
429 public String getPrivateKeyAlias() {
430 return getKey("private_key_alias");
431 }
432
433 @Override
434 public ContentValues getContentValues() {
435 final ContentValues values = new ContentValues();
436 values.put(UUID, uuid);
437 values.put(USERNAME, jid.getLocalpart());
438 values.put(SERVER, jid.getDomainpart());
439 values.put(PASSWORD, password);
440 values.put(OPTIONS, options);
441 synchronized (this.keys) {
442 values.put(KEYS, this.keys.toString());
443 }
444 values.put(ROSTERVERSION, rosterVersion);
445 values.put(AVATAR, avatar);
446 values.put(DISPLAY_NAME, displayName);
447 values.put(HOSTNAME, hostname);
448 values.put(PORT, port);
449 values.put(STATUS, presenceStatus.toShowString());
450 values.put(STATUS_MESSAGE, presenceStatusMessage);
451 return values;
452 }
453
454 public AxolotlService getAxolotlService() {
455 return axolotlService;
456 }
457
458 public void initAccountServices(final XmppConnectionService context) {
459 this.mOtrService = new OtrService(context, this);
460 this.axolotlService = new AxolotlService(this, context);
461 this.pgpDecryptionService = new PgpDecryptionService(context);
462 if (xmppConnection != null) {
463 xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
464 }
465 }
466
467 public OtrService getOtrService() {
468 return this.mOtrService;
469 }
470
471 public PgpDecryptionService getPgpDecryptionService() {
472 return this.pgpDecryptionService;
473 }
474
475 public XmppConnection getXmppConnection() {
476 return this.xmppConnection;
477 }
478
479 public void setXmppConnection(final XmppConnection connection) {
480 this.xmppConnection = connection;
481 }
482
483 public String getOtrFingerprint() {
484 if (this.otrFingerprint == null) {
485 try {
486 if (this.mOtrService == null) {
487 return null;
488 }
489 final PublicKey publicKey = this.mOtrService.getPublicKey();
490 if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
491 return null;
492 }
493 this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US);
494 return this.otrFingerprint;
495 } catch (final OtrCryptoException ignored) {
496 return null;
497 }
498 } else {
499 return this.otrFingerprint;
500 }
501 }
502
503 public String getRosterVersion() {
504 if (this.rosterVersion == null) {
505 return "";
506 } else {
507 return this.rosterVersion;
508 }
509 }
510
511 public void setRosterVersion(final String version) {
512 this.rosterVersion = version;
513 }
514
515 public int countPresences() {
516 return this.getSelfContact().getPresences().size();
517 }
518
519 public String getPgpSignature() {
520 return getKey(KEY_PGP_SIGNATURE);
521 }
522
523 public boolean setPgpSignature(String signature) {
524 return setKey(KEY_PGP_SIGNATURE, signature);
525 }
526
527 public boolean unsetPgpSignature() {
528 synchronized (this.keys) {
529 return keys.remove(KEY_PGP_SIGNATURE) != null;
530 }
531 }
532
533 public long getPgpId() {
534 synchronized (this.keys) {
535 if (keys.has(KEY_PGP_ID)) {
536 try {
537 return keys.getLong(KEY_PGP_ID);
538 } catch (JSONException e) {
539 return 0;
540 }
541 } else {
542 return 0;
543 }
544 }
545 }
546
547 public boolean setPgpSignId(long pgpID) {
548 synchronized (this.keys) {
549 try {
550 keys.put(KEY_PGP_ID, pgpID);
551 } catch (JSONException e) {
552 return false;
553 }
554 return true;
555 }
556 }
557
558 public Roster getRoster() {
559 return this.roster;
560 }
561
562 public List<Bookmark> getBookmarks() {
563 return this.bookmarks;
564 }
565
566 public void setBookmarks(final List<Bookmark> bookmarks) {
567 this.bookmarks = bookmarks;
568 }
569
570 public boolean hasBookmarkFor(final Jid conferenceJid) {
571 for (final Bookmark bookmark : this.bookmarks) {
572 final Jid jid = bookmark.getJid();
573 if (jid != null && jid.equals(conferenceJid.toBareJid())) {
574 return true;
575 }
576 }
577 return false;
578 }
579
580 public boolean setAvatar(final String filename) {
581 if (this.avatar != null && this.avatar.equals(filename)) {
582 return false;
583 } else {
584 this.avatar = filename;
585 return true;
586 }
587 }
588
589 public String getAvatar() {
590 return this.avatar;
591 }
592
593 public void activateGracePeriod(long duration) {
594 this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
595 }
596
597 public void deactivateGracePeriod() {
598 this.mEndGracePeriod = 0L;
599 }
600
601 public boolean inGracePeriod() {
602 return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
603 }
604
605 public String getShareableUri() {
606 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
607 String uri = "xmpp:"+this.getJid().toBareJid().toString();
608 if (fingerprints.size() > 0) {
609 StringBuilder builder = new StringBuilder(uri);
610 builder.append('?');
611 for(int i = 0; i < fingerprints.size(); ++i) {
612 XmppUri.FingerprintType type = fingerprints.get(i).type;
613 if (type == XmppUri.FingerprintType.OMEMO) {
614 builder.append(XmppUri.OMEMO_URI_PARAM);
615 builder.append(fingerprints.get(i).deviceId);
616 } else if (type == XmppUri.FingerprintType.OTR) {
617 builder.append(XmppUri.OTR_URI_PARAM);
618 }
619 builder.append('=');
620 builder.append(fingerprints.get(i).fingerprint);
621 if (i != fingerprints.size() -1) {
622 builder.append(';');
623 }
624 }
625 return builder.toString();
626 } else {
627 return uri;
628 }
629 }
630
631 private List<XmppUri.Fingerprint> getFingerprints() {
632 ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
633 final String otr = this.getOtrFingerprint();
634 if (otr != null) {
635 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR,otr));
636 }
637 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,axolotlService.getOwnFingerprint().substring(2),axolotlService.getOwnDeviceId()));
638 for(XmppAxolotlSession session : axolotlService.findOwnSessions()) {
639 if (session.getTrust().isVerified() && session.getTrust().isActive()) {
640 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,session.getFingerprint().substring(2).replaceAll("\\s",""),session.getRemoteAddress().getDeviceId()));
641 }
642 }
643 return fingerprints;
644 }
645
646 public boolean isBlocked(final ListItem contact) {
647 final Jid jid = contact.getJid();
648 return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid()));
649 }
650
651 public boolean isBlocked(final Jid jid) {
652 return jid != null && blocklist.contains(jid.toBareJid());
653 }
654
655 public Collection<Jid> getBlocklist() {
656 return this.blocklist;
657 }
658
659 public void clearBlocklist() {
660 getBlocklist().clear();
661 }
662
663 public boolean isOnlineAndConnected() {
664 return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
665 }
666}