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