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 boolean setJid(final Jid next) {
281 final Jid prev = this.jid != null ? this.jid.toBareJid() : null;
282 this.jid = next;
283 return prev == null || (next != null && !prev.equals(next.toBareJid()));
284 }
285
286 public Jid getServer() {
287 return jid.toDomainJid();
288 }
289
290 public String getPassword() {
291 return password;
292 }
293
294 public void setPassword(final String password) {
295 this.password = password;
296 }
297
298 public void setHostname(String hostname) {
299 this.hostname = hostname;
300 }
301
302 public String getHostname() {
303 return this.hostname == null ? "" : this.hostname;
304 }
305
306 public boolean isOnion() {
307 final Jid server = getServer();
308 return server != null && server.toString().toLowerCase().endsWith(".onion");
309 }
310
311 public void setPort(int port) {
312 this.port = port;
313 }
314
315 public int getPort() {
316 return this.port;
317 }
318
319 public State getStatus() {
320 if (isOptionSet(OPTION_DISABLED)) {
321 return State.DISABLED;
322 } else {
323 return this.status;
324 }
325 }
326
327 public void setStatus(final State status) {
328 this.status = status;
329 }
330
331 public boolean errorStatus() {
332 return getStatus().isError();
333 }
334
335 public boolean hasErrorStatus() {
336 return getXmppConnection() != null
337 && (getStatus().isError() || getStatus() == State.CONNECTING)
338 && getXmppConnection().getAttempt() >= 3;
339 }
340
341 public void setPresenceStatus(Presence.Status status) {
342 this.presenceStatus = status;
343 }
344
345 public Presence.Status getPresenceStatus() {
346 return this.presenceStatus;
347 }
348
349 public void setPresenceStatusMessage(String message) {
350 this.presenceStatusMessage = message;
351 }
352
353 public String getPresenceStatusMessage() {
354 return this.presenceStatusMessage;
355 }
356
357 public String getResource() {
358 return jid.getResourcepart();
359 }
360
361 public boolean setResource(final String resource) {
362 final String oldResource = jid.getResourcepart();
363 if (oldResource == null || !oldResource.equals(resource)) {
364 try {
365 jid = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), resource);
366 return true;
367 } catch (final InvalidJidException ignored) {
368 return true;
369 }
370 }
371 return false;
372 }
373
374 public Jid getJid() {
375 return jid;
376 }
377
378 public JSONObject getKeys() {
379 return keys;
380 }
381
382 public String getKey(final String name) {
383 return this.keys.optString(name, null);
384 }
385
386 public boolean setKey(final String keyName, final String keyValue) {
387 try {
388 this.keys.put(keyName, keyValue);
389 return true;
390 } catch (final JSONException e) {
391 return false;
392 }
393 }
394
395 public boolean setPrivateKeyAlias(String alias) {
396 return setKey("private_key_alias", alias);
397 }
398
399 public String getPrivateKeyAlias() {
400 return getKey("private_key_alias");
401 }
402
403 @Override
404 public ContentValues getContentValues() {
405 final ContentValues values = new ContentValues();
406 values.put(UUID, uuid);
407 values.put(USERNAME, jid.getLocalpart());
408 values.put(SERVER, jid.getDomainpart());
409 values.put(PASSWORD, password);
410 values.put(OPTIONS, options);
411 values.put(KEYS, this.keys.toString());
412 values.put(ROSTERVERSION, rosterVersion);
413 values.put(AVATAR, avatar);
414 values.put(DISPLAY_NAME, displayName);
415 values.put(HOSTNAME, hostname);
416 values.put(PORT, port);
417 values.put(STATUS, presenceStatus.toShowString());
418 values.put(STATUS_MESSAGE, presenceStatusMessage);
419 return values;
420 }
421
422 public AxolotlService getAxolotlService() {
423 return axolotlService;
424 }
425
426 public void initAccountServices(final XmppConnectionService context) {
427 this.mOtrService = new OtrService(context, this);
428 this.axolotlService = new AxolotlService(this, context);
429 this.pgpDecryptionService = new PgpDecryptionService(context);
430 if (xmppConnection != null) {
431 xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
432 }
433 }
434
435 public OtrService getOtrService() {
436 return this.mOtrService;
437 }
438
439 public PgpDecryptionService getPgpDecryptionService() {
440 return this.pgpDecryptionService;
441 }
442
443 public XmppConnection getXmppConnection() {
444 return this.xmppConnection;
445 }
446
447 public void setXmppConnection(final XmppConnection connection) {
448 this.xmppConnection = connection;
449 }
450
451 public String getOtrFingerprint() {
452 if (this.otrFingerprint == null) {
453 try {
454 if (this.mOtrService == null) {
455 return null;
456 }
457 final PublicKey publicKey = this.mOtrService.getPublicKey();
458 if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
459 return null;
460 }
461 this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey);
462 return this.otrFingerprint;
463 } catch (final OtrCryptoException ignored) {
464 return null;
465 }
466 } else {
467 return this.otrFingerprint;
468 }
469 }
470
471 public String getRosterVersion() {
472 if (this.rosterVersion == null) {
473 return "";
474 } else {
475 return this.rosterVersion;
476 }
477 }
478
479 public void setRosterVersion(final String version) {
480 this.rosterVersion = version;
481 }
482
483 public int countPresences() {
484 return this.getSelfContact().getPresences().size();
485 }
486
487 public String getPgpSignature() {
488 try {
489 if (keys.has(KEY_PGP_SIGNATURE) && !"null".equals(keys.getString(KEY_PGP_SIGNATURE))) {
490 return keys.getString(KEY_PGP_SIGNATURE);
491 } else {
492 return null;
493 }
494 } catch (final JSONException e) {
495 return null;
496 }
497 }
498
499 public boolean setPgpSignature(String signature) {
500 try {
501 keys.put(KEY_PGP_SIGNATURE, signature);
502 } catch (JSONException e) {
503 return false;
504 }
505 return true;
506 }
507
508 public boolean unsetPgpSignature() {
509 try {
510 keys.put(KEY_PGP_SIGNATURE, JSONObject.NULL);
511 } catch (JSONException e) {
512 return false;
513 }
514 return true;
515 }
516
517 public long getPgpId() {
518 if (keys.has(KEY_PGP_ID)) {
519 try {
520 return keys.getLong(KEY_PGP_ID);
521 } catch (JSONException e) {
522 return 0;
523 }
524 } else {
525 return 0;
526 }
527 }
528
529 public boolean setPgpSignId(long pgpID) {
530 try {
531 keys.put(KEY_PGP_ID, pgpID);
532 } catch (JSONException e) {
533 return false;
534 }
535 return true;
536 }
537
538 public Roster getRoster() {
539 return this.roster;
540 }
541
542 public List<Bookmark> getBookmarks() {
543 return this.bookmarks;
544 }
545
546 public void setBookmarks(final List<Bookmark> bookmarks) {
547 this.bookmarks = bookmarks;
548 }
549
550 public boolean hasBookmarkFor(final Jid conferenceJid) {
551 for (final Bookmark bookmark : this.bookmarks) {
552 final Jid jid = bookmark.getJid();
553 if (jid != null && jid.equals(conferenceJid.toBareJid())) {
554 return true;
555 }
556 }
557 return false;
558 }
559
560 public boolean setAvatar(final String filename) {
561 if (this.avatar != null && this.avatar.equals(filename)) {
562 return false;
563 } else {
564 this.avatar = filename;
565 return true;
566 }
567 }
568
569 public String getAvatar() {
570 return this.avatar;
571 }
572
573 public void activateGracePeriod(long duration) {
574 this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
575 }
576
577 public void deactivateGracePeriod() {
578 this.mEndGracePeriod = 0L;
579 }
580
581 public boolean inGracePeriod() {
582 return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
583 }
584
585 public String getShareableUri() {
586 final String fingerprint = this.getOtrFingerprint();
587 if (fingerprint != null) {
588 return "xmpp:" + this.getJid().toBareJid().toString() + "?otr-fingerprint="+fingerprint;
589 } else {
590 return "xmpp:" + this.getJid().toBareJid().toString();
591 }
592 }
593
594 public boolean isBlocked(final ListItem contact) {
595 final Jid jid = contact.getJid();
596 return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid()));
597 }
598
599 public boolean isBlocked(final Jid jid) {
600 return jid != null && blocklist.contains(jid.toBareJid());
601 }
602
603 public Collection<Jid> getBlocklist() {
604 return this.blocklist;
605 }
606
607 public void clearBlocklist() {
608 getBlocklist().clear();
609 }
610
611 public boolean isOnlineAndConnected() {
612 return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
613 }
614}