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