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