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