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