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