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