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