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