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