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