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